diff --git a/api/agent/drivers/docker/docker.go b/api/agent/drivers/docker/docker.go index f9ee3ec92..c5f711f9b 100644 --- a/api/agent/drivers/docker/docker.go +++ b/api/agent/drivers/docker/docker.go @@ -45,6 +45,11 @@ type runResult struct { status string } +type driverAuthConfig struct { + auth docker.AuthConfiguration + subdomains map[string]bool +} + func (r *runResult) Error() error { return r.err } func (r *runResult) Status() string { return r.status } @@ -52,7 +57,7 @@ type DockerDriver struct { conf drivers.Config docker dockerClient // retries on *docker.Client, restricts ad hoc *docker.Client usage / retries hostname string - auths map[string]docker.AuthConfiguration + auths map[string]driverAuthConfig pool DockerPool // protects networks map networksLock sync.Mutex @@ -66,11 +71,16 @@ func NewDocker(conf drivers.Config) *DockerDriver { logrus.WithError(err).Fatal("couldn't resolve hostname") } + auths, err := registryFromEnv() + if err != nil { + logrus.WithError(err).Fatal("couldn't initialize registry") + } + driver := &DockerDriver{ conf: conf, docker: newClient(), hostname: hostname, - auths: registryFromEnv(), + auths: auths, } if conf.ServerVersion != "" { @@ -131,23 +141,6 @@ func loadDockerImages(driver *DockerDriver, filePath string) error { return driver.docker.LoadImages(ctx, filePath) } -func registryFromEnv() map[string]docker.AuthConfiguration { - var auths *docker.AuthConfigurations - var err error - if reg := os.Getenv("DOCKER_AUTH"); reg != "" { - // TODO docker does not use this itself, we should get rid of env docker config (nor is this documented..) - auths, err = docker.NewAuthConfigurations(strings.NewReader(reg)) - } else { - auths, err = docker.NewAuthConfigurationsFromDockerCfg() - } - - if err != nil { - logrus.WithError(err).Info("no docker auths from config files found (this is fine)") - return nil - } - return auths.Configs -} - func (drv *DockerDriver) Close() error { var err error if drv.pool != nil { @@ -304,63 +297,41 @@ func (drv *DockerDriver) removeContainer(ctx context.Context, container string) } func (drv *DockerDriver) ensureImage(ctx context.Context, task drivers.ContainerTask) error { - reg, _, _ := drivers.ParseImage(task.Image()) + reg, repo, tag := drivers.ParseImage(task.Image()) // ask for docker creds before looking for image, as the tasker may need to // validate creds even if the image is downloaded. - var config docker.AuthConfiguration // default, tries docker hub w/o user/pass - - // if any configured host auths match task registry, try them (task docker auth can override) - // TODO this is still a little hairy using suffix, we should probably try to parse it as a - // url and extract the host (from both the config file & image) - for _, v := range drv.auths { - if reg != "" && strings.HasSuffix(v.ServerAddress, reg) { - config = v - break - } - } + config := findRegistryConfig(reg, drv.auths) if task, ok := task.(Auther); ok { var err error _, span := trace.StartSpan(ctx, "docker_auth") - config, err = task.DockerAuth() + authConfig, err := task.DockerAuth() span.End() if err != nil { return err } + config = &authConfig } - if reg != "" { - config.ServerAddress = reg - } + globalRepo := path.Join(reg, repo) // see if we already have it, if not, pull it _, err := drv.docker.InspectImage(ctx, task.Image()) if err == docker.ErrNoSuchImage { - err = drv.pullImage(ctx, task, config) + err = drv.pullImage(ctx, task, *config, globalRepo, tag) } return err } -func (drv *DockerDriver) pullImage(ctx context.Context, task drivers.ContainerTask, config docker.AuthConfiguration) error { +func (drv *DockerDriver) pullImage(ctx context.Context, task drivers.ContainerTask, config docker.AuthConfiguration, globalRepo, tag string) error { log := common.Logger(ctx) - reg, repo, tag := drivers.ParseImage(task.Image()) - globalRepo := path.Join(reg, repo) - if reg != "" { - config.ServerAddress = reg - } - - var err error - config.ServerAddress, err = registryURL(config.ServerAddress) - if err != nil { - return err - } log.WithFields(logrus.Fields{"registry": config.ServerAddress, "username": config.Username, "image": task.Image()}).Info("Pulling image") - err = drv.docker.PullImage(docker.PullImageOptions{Repository: globalRepo, Tag: tag, Context: ctx}, config) + err := drv.docker.PullImage(docker.PullImageOptions{Repository: globalRepo, Tag: tag, Context: ctx}, config) if err != nil { log.WithFields(logrus.Fields{"registry": config.ServerAddress, "username": config.Username, "image": task.Image()}).WithError(err).Error("Failed to pull image") diff --git a/api/agent/drivers/docker/registry.go b/api/agent/drivers/docker/registry.go index f7e85fd80..e55555912 100644 --- a/api/agent/drivers/docker/registry.go +++ b/api/agent/drivers/docker/registry.go @@ -2,26 +2,96 @@ package docker import ( "net/url" + "os" "strings" + + "github.com/fsouza/go-dockerclient" + "github.com/sirupsen/logrus" ) -const hubURL = "https://registry.hub.docker.com" +var ( + defaultPrivateRegistries = []string{"hub.docker.com", "index.docker.io"} +) -func registryURL(addr string) (string, error) { - if addr == "" || strings.Contains(addr, "hub.docker.com") || strings.Contains(addr, "index.docker.io") { - return hubURL, nil +func registryFromEnv() (map[string]driverAuthConfig, error) { + drvAuths := make(map[string]driverAuthConfig) + + var auths *docker.AuthConfigurations + var err error + if reg := os.Getenv("DOCKER_AUTH"); reg != "" { + // TODO docker does not use this itself, we should get rid of env docker config (nor is this documented..) + auths, err = docker.NewAuthConfigurations(strings.NewReader(reg)) + } else { + auths, err = docker.NewAuthConfigurationsFromDockerCfg() } - uri, err := url.Parse(addr) if err != nil { - return "", err + logrus.WithError(err).Info("no docker auths from config files found (this is fine)") + return drvAuths, nil } - if uri.Scheme == "" { - uri.Scheme = "https" + for key, v := range auths.Configs { + + u, err := url.Parse(v.ServerAddress) + if err != nil { + return drvAuths, err + } + + drvAuths[key] = driverAuthConfig{ + auth: v, + subdomains: getSubdomains(u.Host), + } } - uri.Path = strings.TrimSuffix(uri.Path, "/") - uri.Path = strings.TrimPrefix(uri.Path, "/v2") - uri.Path = strings.TrimPrefix(uri.Path, "/v1") // just try this, if it fails it fails, not supporting v1 - return uri.String(), nil + + return drvAuths, nil +} + +func getSubdomains(hostname string) map[string]bool { + + subdomains := make(map[string]bool) + tokens := strings.Split(hostname, ".") + + if len(tokens) <= 2 { + subdomains[hostname] = true + } else { + for i := 0; i <= len(tokens)-2; i++ { + joined := strings.Join(tokens[i:], ".") + subdomains[joined] = true + } + } + + return subdomains +} + +func findRegistryConfig(reg string, configs map[string]driverAuthConfig) *docker.AuthConfiguration { + var config docker.AuthConfiguration + + if reg != "" { + res := lookupRegistryConfig(reg, configs) + if res != nil { + return res + } + } else { + for _, reg := range defaultPrivateRegistries { + res := lookupRegistryConfig(reg, configs) + if res != nil { + return res + } + } + } + + return &config +} + +func lookupRegistryConfig(reg string, configs map[string]driverAuthConfig) *docker.AuthConfiguration { + + // if any configured host auths match task registry, try them (task docker auth can override) + for _, v := range configs { + _, ok := v.subdomains[reg] + if ok { + return &v.auth + } + } + + return nil } diff --git a/api/agent/drivers/docker/registry_test.go b/api/agent/drivers/docker/registry_test.go new file mode 100644 index 000000000..ba5104f70 --- /dev/null +++ b/api/agent/drivers/docker/registry_test.go @@ -0,0 +1,59 @@ +package docker + +import ( + "testing" +) + +func verify(expected []string, checks map[string]bool) bool { + if len(expected) != len(checks) { + return false + } + for _, v := range expected { + _, ok := checks[v] + if !ok { + return false + } + } + return true +} + +func TestRegistrySubDomains(t *testing.T) { + var exp []string + var res map[string]bool + + exp = []string{"google.com"} + res = getSubdomains("google.com") + if !verify(exp, res) { + t.Fatalf("subdomain results failed expected[%+v] != results[%+v]", exp, res) + } + + exp = []string{"top.google.com", "google.com"} + res = getSubdomains("top.google.com") + if !verify(exp, res) { + t.Fatalf("subdomain results failed expected[%+v] != results[%+v]", exp, res) + } + + exp = []string{"top.google.com:443", "google.com:443"} + res = getSubdomains("top.google.com:443") + if !verify(exp, res) { + t.Fatalf("subdomain results failed expected[%+v] != results[%+v]", exp, res) + } + + exp = []string{"top.top.google.com", "top.google.com", "google.com"} + res = getSubdomains("top.top.google.com") + if !verify(exp, res) { + t.Fatalf("subdomain results failed expected[%+v] != results[%+v]", exp, res) + } + + exp = []string{"docker"} + res = getSubdomains("docker") + if !verify(exp, res) { + t.Fatalf("subdomain results failed expected[%+v] != results[%+v]", exp, res) + } + + exp = []string{""} + res = getSubdomains("") + if !verify(exp, res) { + t.Fatalf("subdomain results failed expected[%+v] != results[%+v]", exp, res) + } +}