diff --git a/api/client/commands.go b/api/client/commands.go index 65f975e8d6..d6e2c94f3d 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -222,7 +222,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error { //Check if the given image name can be resolved if *tag != "" { repository, tag := parsers.ParseRepositoryTag(*tag) - if _, _, err := registry.ResolveRepositoryName(repository); err != nil { + if err := registry.ValidateRepositoryName(repository); err != nil { return err } if len(tag) > 0 { @@ -1148,7 +1148,7 @@ func (cli *DockerCli) CmdImport(args ...string) error { if repository != "" { //Check if the given image name can be resolved repo, _ := parsers.ParseRepositoryTag(repository) - if _, _, err := registry.ResolveRepositoryName(repo); err != nil { + if err := registry.ValidateRepositoryName(repo); err != nil { return err } } @@ -1174,23 +1174,23 @@ func (cli *DockerCli) CmdPush(args ...string) error { remote, tag := parsers.ParseRepositoryTag(name) - // Resolve the Repository name from fqn to hostname + name - hostname, _, err := registry.ResolveRepositoryName(remote) + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ParseRepositoryInfo(remote) if err != nil { return err } // Resolve the Auth config relevant for this server - authConfig := cli.configFile.ResolveAuthConfig(hostname) + authConfig := cli.configFile.ResolveAuthConfig(repoInfo.Index) // If we're not using a custom registry, we know the restrictions // applied to repository names and can warn the user in advance. // Custom repositories can have different rules, and we must also // allow pushing by image ID. - if len(strings.SplitN(name, "/", 2)) == 1 { - username := cli.configFile.Configs[registry.IndexServerAddress()].Username + if repoInfo.Official { + username := authConfig.Username if username == "" { username = "" } - return fmt.Errorf("You cannot push a \"root\" repository. Please rename your repository in / (ex: %s/%s)", username, name) + return fmt.Errorf("You cannot push a \"root\" repository. Please rename your repository to / (ex: %s/%s)", username, repoInfo.LocalName) } v := url.Values{} @@ -1212,10 +1212,10 @@ func (cli *DockerCli) CmdPush(args ...string) error { if err := push(authConfig); err != nil { if strings.Contains(err.Error(), "Status 401") { fmt.Fprintln(cli.out, "\nPlease login prior to push:") - if err := cli.CmdLogin(hostname); err != nil { + if err := cli.CmdLogin(repoInfo.Index.GetAuthConfigKey()); err != nil { return err } - authConfig := cli.configFile.ResolveAuthConfig(hostname) + authConfig := cli.configFile.ResolveAuthConfig(repoInfo.Index) return push(authConfig) } return err @@ -1245,8 +1245,8 @@ func (cli *DockerCli) CmdPull(args ...string) error { v.Set("fromImage", newRemote) - // Resolve the Repository name from fqn to hostname + name - hostname, _, err := registry.ResolveRepositoryName(taglessRemote) + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ParseRepositoryInfo(taglessRemote) if err != nil { return err } @@ -1254,7 +1254,7 @@ func (cli *DockerCli) CmdPull(args ...string) error { cli.LoadConfigFile() // Resolve the Auth config relevant for this server - authConfig := cli.configFile.ResolveAuthConfig(hostname) + authConfig := cli.configFile.ResolveAuthConfig(repoInfo.Index) pull := func(authConfig registry.AuthConfig) error { buf, err := json.Marshal(authConfig) @@ -1273,10 +1273,10 @@ func (cli *DockerCli) CmdPull(args ...string) error { if err := pull(authConfig); err != nil { if strings.Contains(err.Error(), "Status 401") { fmt.Fprintln(cli.out, "\nPlease login prior to pull:") - if err := cli.CmdLogin(hostname); err != nil { + if err := cli.CmdLogin(repoInfo.Index.GetAuthConfigKey()); err != nil { return err } - authConfig := cli.configFile.ResolveAuthConfig(hostname) + authConfig := cli.configFile.ResolveAuthConfig(repoInfo.Index) return pull(authConfig) } return err @@ -1691,7 +1691,7 @@ func (cli *DockerCli) CmdCommit(args ...string) error { //Check if the given image name can be resolved if repository != "" { - if _, _, err := registry.ResolveRepositoryName(repository); err != nil { + if err := registry.ValidateRepositoryName(repository); err != nil { return err } } @@ -2002,7 +2002,7 @@ func (cli *DockerCli) CmdTag(args ...string) error { ) //Check if the given image name can be resolved - if _, _, err := registry.ResolveRepositoryName(repository); err != nil { + if err := registry.ValidateRepositoryName(repository); err != nil { return err } v.Set("repo", repository) @@ -2032,8 +2032,8 @@ func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error { v.Set("fromImage", repos) v.Set("tag", tag) - // Resolve the Repository name from fqn to hostname + name - hostname, _, err := registry.ResolveRepositoryName(repos) + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ParseRepositoryInfo(repos) if err != nil { return err } @@ -2042,7 +2042,7 @@ func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error { cli.LoadConfigFile() // Resolve the Auth config relevant for this server - authConfig := cli.configFile.ResolveAuthConfig(hostname) + authConfig := cli.configFile.ResolveAuthConfig(repoInfo.Index) buf, err := json.Marshal(authConfig) if err != nil { return err diff --git a/api/client/utils.go b/api/client/utils.go index 8de571bf4d..6ebe448062 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -66,7 +66,7 @@ func (cli *DockerCli) call(method, path string, data interface{}, passAuthInfo b if passAuthInfo { cli.LoadConfigFile() // Resolve the Auth config relevant for this server - authConfig := cli.configFile.ResolveAuthConfig(registry.IndexServerAddress()) + authConfig := cli.configFile.Configs[registry.IndexServerAddress()] getHeaders := func(authConfig registry.AuthConfig) (map[string][]string, error) { buf, err := json.Marshal(authConfig) if err != nil { diff --git a/builder/internals.go b/builder/internals.go index 909e7a8d10..9d9a65f9e8 100644 --- a/builder/internals.go +++ b/builder/internals.go @@ -427,17 +427,17 @@ func (b *Builder) pullImage(name string) (*imagepkg.Image, error) { if tag == "" { tag = "latest" } + job := b.Engine.Job("pull", remote, tag) pullRegistryAuth := b.AuthConfig if len(b.AuthConfigFile.Configs) > 0 { // The request came with a full auth config file, we prefer to use that - endpoint, _, err := registry.ResolveRepositoryName(remote) + repoInfo, err := registry.ResolveRepositoryInfo(job, remote) if err != nil { return nil, err } - resolvedAuth := b.AuthConfigFile.ResolveAuthConfig(endpoint) + resolvedAuth := b.AuthConfigFile.ResolveAuthConfig(repoInfo.Index) pullRegistryAuth = &resolvedAuth } - job := b.Engine.Job("pull", remote, tag) job.SetenvBool("json", b.StreamFormatter.Json()) job.SetenvBool("parallel", true) job.SetenvJson("authConfig", pullRegistryAuth) diff --git a/builder/job.go b/builder/job.go index 905a8cc998..53490b7e57 100644 --- a/builder/job.go +++ b/builder/job.go @@ -50,7 +50,7 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) engine.Status { repoName, tag = parsers.ParseRepositoryTag(repoName) if repoName != "" { - if _, _, err := registry.ResolveRepositoryName(repoName); err != nil { + if err := registry.ValidateRepositoryName(repoName); err != nil { return job.Error(err) } if len(tag) > 0 { diff --git a/daemon/config.go b/daemon/config.go index 4d9041e895..c5ac056d21 100644 --- a/daemon/config.go +++ b/daemon/config.go @@ -23,7 +23,6 @@ type Config struct { AutoRestart bool Dns []string DnsSearch []string - Mirrors []string EnableIptables bool EnableIpForward bool EnableIpMasq bool @@ -31,7 +30,6 @@ type Config struct { BridgeIface string BridgeIP string FixedCIDR string - InsecureRegistries []string InterContainerCommunication bool GraphDriver string GraphOptions []string @@ -58,7 +56,6 @@ func (config *Config) InstallFlags() { flag.StringVar(&config.BridgeIP, []string{"#bip", "-bip"}, "", "Use this CIDR notation address for the network bridge's IP, not compatible with -b") flag.StringVar(&config.BridgeIface, []string{"b", "-bridge"}, "", "Attach containers to a pre-existing network bridge\nuse 'none' to disable container networking") flag.StringVar(&config.FixedCIDR, []string{"-fixed-cidr"}, "", "IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)\nthis subnet must be nested in the bridge subnet (which is defined by -b or --bip)") - opts.ListVar(&config.InsecureRegistries, []string{"-insecure-registry"}, "Enable insecure communication with specified registries (no certificate verification for HTTPS and enable HTTP fallback) (e.g., localhost:5000 or 10.20.0.0/16)") flag.BoolVar(&config.InterContainerCommunication, []string{"#icc", "-icc"}, true, "Allow unrestricted inter-container and Docker daemon host communication") flag.StringVar(&config.GraphDriver, []string{"s", "-storage-driver"}, "", "Force the Docker runtime to use a specific storage driver") flag.StringVar(&config.ExecDriver, []string{"e", "-exec-driver"}, "native", "Force the Docker runtime to use a specific exec driver") @@ -69,16 +66,7 @@ func (config *Config) InstallFlags() { // FIXME: why the inconsistency between "hosts" and "sockets"? opts.IPListVar(&config.Dns, []string{"#dns", "-dns"}, "Force Docker to use specific DNS servers") opts.DnsSearchListVar(&config.DnsSearch, []string{"-dns-search"}, "Force Docker to use specific DNS search domains") - opts.MirrorListVar(&config.Mirrors, []string{"-registry-mirror"}, "Specify a preferred Docker registry mirror") opts.LabelListVar(&config.Labels, []string{"-label"}, "Set key=value labels to the daemon (displayed in `docker info`)") - - // Localhost is by default considered as an insecure registry - // This is a stop-gap for people who are running a private registry on localhost (especially on Boot2docker). - // - // TODO: should we deprecate this once it is easier for people to set up a TLS registry or change - // daemon flags on boot2docker? - // If so, do not forget to check the TODO in TestIsSecure - config.InsecureRegistries = append(config.InsecureRegistries, "127.0.0.0/8") } func getDefaultNetworkMtu() int { diff --git a/daemon/daemon.go b/daemon/daemon.go index 8ad677bed0..cb162780d3 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -841,7 +841,7 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error) } log.Debugf("Creating repository list") - repositories, err := graph.NewTagStore(path.Join(config.Root, "repositories-"+driver.String()), g, config.Mirrors, config.InsecureRegistries) + repositories, err := graph.NewTagStore(path.Join(config.Root, "repositories-"+driver.String()), g) if err != nil { return nil, fmt.Errorf("Couldn't create Tag store: %s", err) } diff --git a/daemon/info.go b/daemon/info.go index bf7ec99680..8eb4358f4a 100644 --- a/daemon/info.go +++ b/daemon/info.go @@ -55,6 +55,15 @@ func (daemon *Daemon) CmdInfo(job *engine.Job) engine.Status { if err := cjob.Run(); err != nil { return job.Error(err) } + registryJob := job.Eng.Job("registry_config") + registryEnv, _ := registryJob.Stdout.AddEnv() + if err := registryJob.Run(); err != nil { + return job.Error(err) + } + registryConfig := registry.ServiceConfig{} + if err := registryEnv.GetJson("config", ®istryConfig); err != nil { + return job.Error(err) + } v := &engine.Env{} v.SetJson("ID", daemon.ID) v.SetInt("Containers", len(daemon.List())) @@ -72,6 +81,7 @@ func (daemon *Daemon) CmdInfo(job *engine.Job) engine.Status { v.Set("KernelVersion", kernelVersion) v.Set("OperatingSystem", operatingSystem) v.Set("IndexServerAddress", registry.IndexServerAddress()) + v.SetJson("RegistryConfig", registryConfig) v.Set("InitSha1", dockerversion.INITSHA1) v.Set("InitPath", initPath) v.SetInt("NCPU", runtime.NumCPU()) diff --git a/docker/daemon.go b/docker/daemon.go index 3128f7ee55..508a75bd86 100644 --- a/docker/daemon.go +++ b/docker/daemon.go @@ -19,11 +19,13 @@ import ( const CanDaemon = true var ( - daemonCfg = &daemon.Config{} + daemonCfg = &daemon.Config{} + registryCfg = ®istry.Options{} ) func init() { daemonCfg.InstallFlags() + registryCfg.InstallFlags() } func mainDaemon() { @@ -42,7 +44,7 @@ func mainDaemon() { } // load registry service - if err := registry.NewService(daemonCfg.InsecureRegistries).Install(eng); err != nil { + if err := registry.NewService(registryCfg).Install(eng); err != nil { log.Fatal(err) } diff --git a/graph/export.go b/graph/export.go index 7a8054010e..3f7ecd3c4e 100644 --- a/graph/export.go +++ b/graph/export.go @@ -11,6 +11,7 @@ import ( "github.com/docker/docker/engine" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/registry" ) // CmdImageExport exports all images with the given tag. All versions @@ -39,6 +40,7 @@ func (s *TagStore) CmdImageExport(job *engine.Job) engine.Status { } } for _, name := range job.Args { + name = registry.NormalizeLocalName(name) log.Debugf("Serializing %s", name) rootRepo := s.Repositories[name] if rootRepo != nil { diff --git a/graph/pull.go b/graph/pull.go index 716a27c909..587eb5f500 100644 --- a/graph/pull.go +++ b/graph/pull.go @@ -85,9 +85,14 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { sf = utils.NewStreamFormatter(job.GetenvBool("json")) authConfig = ®istry.AuthConfig{} metaHeaders map[string][]string - mirrors []string ) + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ResolveRepositoryInfo(job, localName) + if err != nil { + return job.Error(err) + } + if len(job.Args) > 1 { tag = job.Args[1] } @@ -95,25 +100,19 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { job.GetenvJson("authConfig", authConfig) job.GetenvJson("metaHeaders", &metaHeaders) - c, err := s.poolAdd("pull", localName+":"+tag) + c, err := s.poolAdd("pull", repoInfo.LocalName+":"+tag) if err != nil { if c != nil { // Another pull of the same repository is already taking place; just wait for it to finish - job.Stdout.Write(sf.FormatStatus("", "Repository %s already being pulled by another client. Waiting.", localName)) + job.Stdout.Write(sf.FormatStatus("", "Repository %s already being pulled by another client. Waiting.", repoInfo.LocalName)) <-c return engine.StatusOK } return job.Error(err) } - defer s.poolRemove("pull", localName+":"+tag) + defer s.poolRemove("pull", repoInfo.LocalName+":"+tag) - // Resolve the Repository name from fqn to endpoint + name - hostname, remoteName, err := registry.ResolveRepositoryName(localName) - if err != nil { - return job.Error(err) - } - - endpoint, err := registry.NewEndpoint(hostname, s.insecureRegistries) + endpoint, err := repoInfo.GetEndpoint() if err != nil { return job.Error(err) } @@ -123,32 +122,18 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { return job.Error(err) } - var isOfficial bool - if endpoint.VersionString(1) == registry.IndexServerAddress() { - // If pull "index.docker.io/foo/bar", it's stored locally under "foo/bar" - localName = remoteName - - isOfficial = isOfficialName(remoteName) - if isOfficial && strings.IndexRune(remoteName, '/') == -1 { - remoteName = "library/" + remoteName - } - - // Use provided mirrors, if any - mirrors = s.mirrors - } - - logName := localName + logName := repoInfo.LocalName if tag != "" { logName += ":" + tag } - if len(mirrors) == 0 && (isOfficial || endpoint.Version == registry.APIVersion2) { + if len(repoInfo.Index.Mirrors) == 0 && (repoInfo.Official || endpoint.Version == registry.APIVersion2) { j := job.Eng.Job("trust_update_base") if err = j.Run(); err != nil { return job.Errorf("error updating trust base graph: %s", err) } - if err := s.pullV2Repository(job.Eng, r, job.Stdout, localName, remoteName, tag, sf, job.GetenvBool("parallel")); err == nil { + if err := s.pullV2Repository(job.Eng, r, job.Stdout, repoInfo, tag, sf, job.GetenvBool("parallel")); err == nil { if err = job.Eng.Job("log", "pull", logName, "").Run(); err != nil { log.Errorf("Error logging event 'pull' for %s: %s", logName, err) } @@ -158,7 +143,7 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { } } - if err = s.pullRepository(r, job.Stdout, localName, remoteName, tag, sf, job.GetenvBool("parallel"), mirrors); err != nil { + if err = s.pullRepository(r, job.Stdout, repoInfo, tag, sf, job.GetenvBool("parallel")); err != nil { return job.Error(err) } @@ -169,20 +154,20 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { return engine.StatusOK } -func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, localName, remoteName, askedTag string, sf *utils.StreamFormatter, parallel bool, mirrors []string) error { - out.Write(sf.FormatStatus("", "Pulling repository %s", localName)) +func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, askedTag string, sf *utils.StreamFormatter, parallel bool) error { + out.Write(sf.FormatStatus("", "Pulling repository %s", repoInfo.CanonicalName)) - repoData, err := r.GetRepositoryData(remoteName) + repoData, err := r.GetRepositoryData(repoInfo.RemoteName) if err != nil { if strings.Contains(err.Error(), "HTTP code: 404") { - return fmt.Errorf("Error: image %s:%s not found", remoteName, askedTag) + return fmt.Errorf("Error: image %s:%s not found", repoInfo.RemoteName, askedTag) } // Unexpected HTTP error return err } log.Debugf("Retrieving the tag list") - tagsList, err := r.GetRemoteTags(repoData.Endpoints, remoteName, repoData.Tokens) + tagsList, err := r.GetRemoteTags(repoData.Endpoints, repoInfo.RemoteName, repoData.Tokens) if err != nil { log.Errorf("%v", err) return err @@ -207,7 +192,7 @@ func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, localName, // Otherwise, check that the tag exists and use only that one id, exists := tagsList[askedTag] if !exists { - return fmt.Errorf("Tag %s not found in repository %s", askedTag, localName) + return fmt.Errorf("Tag %s not found in repository %s", askedTag, repoInfo.CanonicalName) } imageId = id repoData.ImgList[id].Tag = askedTag @@ -250,31 +235,29 @@ func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, localName, } defer s.poolRemove("pull", "img:"+img.ID) - out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s", img.Tag, localName), nil)) + out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s", img.Tag, repoInfo.CanonicalName), nil)) success := false var lastErr, err error var is_downloaded bool - if mirrors != nil { - for _, ep := range mirrors { - out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, mirror: %s", img.Tag, localName, ep), nil)) - if is_downloaded, err = s.pullImage(r, out, img.ID, ep, repoData.Tokens, sf); err != nil { - // Don't report errors when pulling from mirrors. - log.Debugf("Error pulling image (%s) from %s, mirror: %s, %s", img.Tag, localName, ep, err) - continue - } - layers_downloaded = layers_downloaded || is_downloaded - success = true - break + for _, ep := range repoInfo.Index.Mirrors { + out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, mirror: %s", img.Tag, repoInfo.CanonicalName, ep), nil)) + if is_downloaded, err = s.pullImage(r, out, img.ID, ep, repoData.Tokens, sf); err != nil { + // Don't report errors when pulling from mirrors. + log.Debugf("Error pulling image (%s) from %s, mirror: %s, %s", img.Tag, repoInfo.CanonicalName, ep, err) + continue } + layers_downloaded = layers_downloaded || is_downloaded + success = true + break } if !success { for _, ep := range repoData.Endpoints { - out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, endpoint: %s", img.Tag, localName, ep), nil)) + out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, endpoint: %s", img.Tag, repoInfo.CanonicalName, ep), nil)) if is_downloaded, err = s.pullImage(r, out, img.ID, ep, repoData.Tokens, sf); err != nil { // It's not ideal that only the last error is returned, it would be better to concatenate the errors. // As the error is also given to the output stream the user will see the error. lastErr = err - out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Error pulling image (%s) from %s, endpoint: %s, %s", img.Tag, localName, ep, err), nil)) + out.Write(sf.FormatProgress(utils.TruncateID(img.ID), fmt.Sprintf("Error pulling image (%s) from %s, endpoint: %s, %s", img.Tag, repoInfo.CanonicalName, ep, err), nil)) continue } layers_downloaded = layers_downloaded || is_downloaded @@ -283,7 +266,7 @@ func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, localName, } } if !success { - err := fmt.Errorf("Error pulling image (%s) from %s, %v", img.Tag, localName, lastErr) + err := fmt.Errorf("Error pulling image (%s) from %s, %v", img.Tag, repoInfo.CanonicalName, lastErr) out.Write(sf.FormatProgress(utils.TruncateID(img.ID), err.Error(), nil)) if parallel { errors <- err @@ -319,14 +302,14 @@ func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, localName, if askedTag != "" && id != imageId { continue } - if err := s.Set(localName, tag, id, true); err != nil { + if err := s.Set(repoInfo.LocalName, tag, id, true); err != nil { return err } } - requestedTag := localName + requestedTag := repoInfo.CanonicalName if len(askedTag) > 0 { - requestedTag = localName + ":" + askedTag + requestedTag = repoInfo.CanonicalName + ":" + askedTag } WriteStatus(requestedTag, out, sf, layers_downloaded) return nil @@ -440,40 +423,40 @@ type downloadInfo struct { err chan error } -func (s *TagStore) pullV2Repository(eng *engine.Engine, r *registry.Session, out io.Writer, localName, remoteName, tag string, sf *utils.StreamFormatter, parallel bool) error { +func (s *TagStore) pullV2Repository(eng *engine.Engine, r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter, parallel bool) error { var layersDownloaded bool if tag == "" { - log.Debugf("Pulling tag list from V2 registry for %s", remoteName) - tags, err := r.GetV2RemoteTags(remoteName, nil) + log.Debugf("Pulling tag list from V2 registry for %s", repoInfo.CanonicalName) + tags, err := r.GetV2RemoteTags(repoInfo.RemoteName, nil) if err != nil { return err } for _, t := range tags { - if downloaded, err := s.pullV2Tag(eng, r, out, localName, remoteName, t, sf, parallel); err != nil { + if downloaded, err := s.pullV2Tag(eng, r, out, repoInfo, t, sf, parallel); err != nil { return err } else if downloaded { layersDownloaded = true } } } else { - if downloaded, err := s.pullV2Tag(eng, r, out, localName, remoteName, tag, sf, parallel); err != nil { + if downloaded, err := s.pullV2Tag(eng, r, out, repoInfo, tag, sf, parallel); err != nil { return err } else if downloaded { layersDownloaded = true } } - requestedTag := localName + requestedTag := repoInfo.CanonicalName if len(tag) > 0 { - requestedTag = localName + ":" + tag + requestedTag = repoInfo.CanonicalName + ":" + tag } WriteStatus(requestedTag, out, sf, layersDownloaded) return nil } -func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Writer, localName, remoteName, tag string, sf *utils.StreamFormatter, parallel bool) (bool, error) { +func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter, parallel bool) (bool, error) { log.Debugf("Pulling tag from V2 registry: %q", tag) - manifestBytes, err := r.GetV2ImageManifest(remoteName, tag, nil) + manifestBytes, err := r.GetV2ImageManifest(repoInfo.RemoteName, tag, nil) if err != nil { return false, err } @@ -488,9 +471,9 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri } if verified { - out.Write(sf.FormatStatus(localName+":"+tag, "The image you are pulling has been verified")) + out.Write(sf.FormatStatus(repoInfo.CanonicalName+":"+tag, "The image you are pulling has been verified")) } else { - out.Write(sf.FormatStatus(tag, "Pulling from %s", localName)) + out.Write(sf.FormatStatus(tag, "Pulling from %s", repoInfo.CanonicalName)) } if len(manifest.FSLayers) == 0 { @@ -542,7 +525,7 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri return err } - r, l, err := r.GetV2ImageBlobReader(remoteName, sumType, checksum, nil) + r, l, err := r.GetV2ImageBlobReader(repoInfo.RemoteName, sumType, checksum, nil) if err != nil { return err } @@ -605,7 +588,7 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri } - if err = s.Set(localName, tag, downloads[0].img.ID, true); err != nil { + if err = s.Set(repoInfo.LocalName, tag, downloads[0].img.ID, true); err != nil { return false, err } diff --git a/graph/push.go b/graph/push.go index 77db243817..09e13a5cff 100644 --- a/graph/push.go +++ b/graph/push.go @@ -61,7 +61,7 @@ func (s *TagStore) getImageList(localRepo map[string]string, requestedTag string return imageList, tagsByImage, nil } -func (s *TagStore) pushRepository(r *registry.Session, out io.Writer, localName, remoteName string, localRepo map[string]string, tag string, sf *utils.StreamFormatter) error { +func (s *TagStore) pushRepository(r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, localRepo map[string]string, tag string, sf *utils.StreamFormatter) error { out = utils.NewWriteFlusher(out) log.Debugf("Local repo: %s", localRepo) imgList, tagsByImage, err := s.getImageList(localRepo, tag) @@ -104,7 +104,7 @@ func (s *TagStore) pushRepository(r *registry.Session, out io.Writer, localName, // Register all the images in a repository with the registry // If an image is not in this list it will not be associated with the repository - repoData, err = r.PushImageJSONIndex(remoteName, imageIndex, false, nil) + repoData, err = r.PushImageJSONIndex(repoInfo.RemoteName, imageIndex, false, nil) if err != nil { return err } @@ -114,11 +114,11 @@ func (s *TagStore) pushRepository(r *registry.Session, out io.Writer, localName, nTag = len(localRepo) } for _, ep := range repoData.Endpoints { - out.Write(sf.FormatStatus("", "Pushing repository %s (%d tags)", localName, nTag)) + out.Write(sf.FormatStatus("", "Pushing repository %s (%d tags)", repoInfo.CanonicalName, nTag)) for _, imgId := range imgList { if err := r.LookupRemoteImage(imgId, ep, repoData.Tokens); err != nil { log.Errorf("Error in LookupRemoteImage: %s", err) - if _, err := s.pushImage(r, out, remoteName, imgId, ep, repoData.Tokens, sf); err != nil { + if _, err := s.pushImage(r, out, imgId, ep, repoData.Tokens, sf); err != nil { // FIXME: Continue on error? return err } @@ -126,23 +126,23 @@ func (s *TagStore) pushRepository(r *registry.Session, out io.Writer, localName, out.Write(sf.FormatStatus("", "Image %s already pushed, skipping", utils.TruncateID(imgId))) } for _, tag := range tagsByImage[imgId] { - out.Write(sf.FormatStatus("", "Pushing tag for rev [%s] on {%s}", utils.TruncateID(imgId), ep+"repositories/"+remoteName+"/tags/"+tag)) + out.Write(sf.FormatStatus("", "Pushing tag for rev [%s] on {%s}", utils.TruncateID(imgId), ep+"repositories/"+repoInfo.RemoteName+"/tags/"+tag)) - if err := r.PushRegistryTag(remoteName, imgId, tag, ep, repoData.Tokens); err != nil { + if err := r.PushRegistryTag(repoInfo.RemoteName, imgId, tag, ep, repoData.Tokens); err != nil { return err } } } } - if _, err := r.PushImageJSONIndex(remoteName, imageIndex, true, repoData.Endpoints); err != nil { + if _, err := r.PushImageJSONIndex(repoInfo.RemoteName, imageIndex, true, repoData.Endpoints); err != nil { return err } return nil } -func (s *TagStore) pushImage(r *registry.Session, out io.Writer, remote, imgID, ep string, token []string, sf *utils.StreamFormatter) (checksum string, err error) { +func (s *TagStore) pushImage(r *registry.Session, out io.Writer, imgID, ep string, token []string, sf *utils.StreamFormatter) (checksum string, err error) { out = utils.NewWriteFlusher(out) jsonRaw, err := ioutil.ReadFile(path.Join(s.graph.Root, imgID, "json")) if err != nil { @@ -199,26 +199,27 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { metaHeaders map[string][]string ) + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ResolveRepositoryInfo(job, localName) + if err != nil { + return job.Error(err) + } + tag := job.Getenv("tag") job.GetenvJson("authConfig", authConfig) job.GetenvJson("metaHeaders", &metaHeaders) - if _, err := s.poolAdd("push", localName); err != nil { + + if _, err := s.poolAdd("push", repoInfo.LocalName); err != nil { return job.Error(err) } - defer s.poolRemove("push", localName) + defer s.poolRemove("push", repoInfo.LocalName) - // Resolve the Repository name from fqn to endpoint + name - hostname, remoteName, err := registry.ResolveRepositoryName(localName) + endpoint, err := repoInfo.GetEndpoint() if err != nil { return job.Error(err) } - endpoint, err := registry.NewEndpoint(hostname, s.insecureRegistries) - if err != nil { - return job.Error(err) - } - - img, err := s.graph.Get(localName) + img, err := s.graph.Get(repoInfo.LocalName) r, err2 := registry.NewSession(authConfig, registry.HTTPRequestFactory(metaHeaders), endpoint, false) if err2 != nil { return job.Error(err2) @@ -227,12 +228,12 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { if err != nil { reposLen := 1 if tag == "" { - reposLen = len(s.Repositories[localName]) + reposLen = len(s.Repositories[repoInfo.LocalName]) } - job.Stdout.Write(sf.FormatStatus("", "The push refers to a repository [%s] (len: %d)", localName, reposLen)) + job.Stdout.Write(sf.FormatStatus("", "The push refers to a repository [%s] (len: %d)", repoInfo.CanonicalName, reposLen)) // If it fails, try to get the repository - if localRepo, exists := s.Repositories[localName]; exists { - if err := s.pushRepository(r, job.Stdout, localName, remoteName, localRepo, tag, sf); err != nil { + if localRepo, exists := s.Repositories[repoInfo.LocalName]; exists { + if err := s.pushRepository(r, job.Stdout, repoInfo, localRepo, tag, sf); err != nil { return job.Error(err) } return engine.StatusOK @@ -241,8 +242,8 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { } var token []string - job.Stdout.Write(sf.FormatStatus("", "The push refers to an image: [%s]", localName)) - if _, err := s.pushImage(r, job.Stdout, remoteName, img.ID, endpoint.String(), token, sf); err != nil { + job.Stdout.Write(sf.FormatStatus("", "The push refers to an image: [%s]", repoInfo.CanonicalName)) + if _, err := s.pushImage(r, job.Stdout, img.ID, endpoint.String(), token, sf); err != nil { return job.Error(err) } return engine.StatusOK diff --git a/graph/tags.go b/graph/tags.go index d584ac2a03..998b447e6c 100644 --- a/graph/tags.go +++ b/graph/tags.go @@ -13,6 +13,7 @@ import ( "github.com/docker/docker/image" "github.com/docker/docker/pkg/parsers" + "github.com/docker/docker/registry" "github.com/docker/docker/utils" ) @@ -23,11 +24,9 @@ var ( ) type TagStore struct { - path string - graph *Graph - mirrors []string - insecureRegistries []string - Repositories map[string]Repository + path string + graph *Graph + Repositories map[string]Repository sync.Mutex // FIXME: move push/pull-related fields // to a helper type @@ -55,20 +54,18 @@ func (r Repository) Contains(u Repository) bool { return true } -func NewTagStore(path string, graph *Graph, mirrors []string, insecureRegistries []string) (*TagStore, error) { +func NewTagStore(path string, graph *Graph) (*TagStore, error) { abspath, err := filepath.Abs(path) if err != nil { return nil, err } store := &TagStore{ - path: abspath, - graph: graph, - mirrors: mirrors, - insecureRegistries: insecureRegistries, - Repositories: make(map[string]Repository), - pullingPool: make(map[string]chan struct{}), - pushingPool: make(map[string]chan struct{}), + path: abspath, + graph: graph, + Repositories: make(map[string]Repository), + pullingPool: make(map[string]chan struct{}), + pushingPool: make(map[string]chan struct{}), } // Load the json file if it exists, otherwise create it. if err := store.reload(); os.IsNotExist(err) { @@ -178,6 +175,7 @@ func (store *TagStore) Delete(repoName, tag string) (bool, error) { if err := store.reload(); err != nil { return false, err } + repoName = registry.NormalizeLocalName(repoName) if r, exists := store.Repositories[repoName]; exists { if tag != "" { if _, exists2 := r[tag]; exists2 { @@ -219,6 +217,7 @@ func (store *TagStore) Set(repoName, tag, imageName string, force bool) error { return err } var repo Repository + repoName = registry.NormalizeLocalName(repoName) if r, exists := store.Repositories[repoName]; exists { repo = r if old, exists := store.Repositories[repoName][tag]; exists && !force { @@ -238,6 +237,7 @@ func (store *TagStore) Get(repoName string) (Repository, error) { if err := store.reload(); err != nil { return nil, err } + repoName = registry.NormalizeLocalName(repoName) if r, exists := store.Repositories[repoName]; exists { return r, nil } @@ -279,20 +279,6 @@ func (store *TagStore) GetRepoRefs() map[string][]string { return reporefs } -// isOfficialName returns whether a repo name is considered an official -// repository. Official repositories are repos with names within -// the library namespace or which default to the library namespace -// by not providing one. -func isOfficialName(name string) bool { - if strings.HasPrefix(name, "library/") { - return true - } - if strings.IndexRune(name, '/') == -1 { - return true - } - return false -} - // Validate the name of a repository func validateRepoName(name string) error { if name == "" { diff --git a/graph/tags_unit_test.go b/graph/tags_unit_test.go index 339fb51fc9..45dad62951 100644 --- a/graph/tags_unit_test.go +++ b/graph/tags_unit_test.go @@ -15,8 +15,12 @@ import ( ) const ( - testImageName = "myapp" - testImageID = "1a2d3c4d4e5fa2d2a21acea242a5e2345d3aefc3e7dfa2a2a2a21a2a2ad2d234" + testOfficialImageName = "myapp" + testOfficialImageID = "1a2d3c4d4e5fa2d2a21acea242a5e2345d3aefc3e7dfa2a2a2a21a2a2ad2d234" + testOfficialImageIDShort = "1a2d3c4d4e5f" + testPrivateImageName = "127.0.0.1:8000/privateapp" + testPrivateImageID = "5bc255f8699e4ee89ac4469266c3d11515da88fdcbde45d7b069b636ff4efd81" + testPrivateImageIDShort = "5bc255f8699e" ) func fakeTar() (io.Reader, error) { @@ -53,19 +57,30 @@ func mkTestTagStore(root string, t *testing.T) *TagStore { if err != nil { t.Fatal(err) } - store, err := NewTagStore(path.Join(root, "tags"), graph, nil, nil) + store, err := NewTagStore(path.Join(root, "tags"), graph) if err != nil { t.Fatal(err) } - archive, err := fakeTar() + officialArchive, err := fakeTar() if err != nil { t.Fatal(err) } - img := &image.Image{ID: testImageID} - if err := graph.Register(img, archive); err != nil { + img := &image.Image{ID: testOfficialImageID} + if err := graph.Register(img, officialArchive); err != nil { t.Fatal(err) } - if err := store.Set(testImageName, "", testImageID, false); err != nil { + if err := store.Set(testOfficialImageName, "", testOfficialImageID, false); err != nil { + t.Fatal(err) + } + privateArchive, err := fakeTar() + if err != nil { + t.Fatal(err) + } + img = &image.Image{ID: testPrivateImageID} + if err := graph.Register(img, privateArchive); err != nil { + t.Fatal(err) + } + if err := store.Set(testPrivateImageName, "", testPrivateImageID, false); err != nil { t.Fatal(err) } return store @@ -80,39 +95,65 @@ func TestLookupImage(t *testing.T) { store := mkTestTagStore(tmp, t) defer store.graph.driver.Cleanup() - if img, err := store.LookupImage(testImageName); err != nil { - t.Fatal(err) - } else if img == nil { - t.Errorf("Expected 1 image, none found") - } - if img, err := store.LookupImage(testImageName + ":" + DEFAULTTAG); err != nil { - t.Fatal(err) - } else if img == nil { - t.Errorf("Expected 1 image, none found") + officialLookups := []string{ + testOfficialImageID, + testOfficialImageIDShort, + testOfficialImageName + ":" + testOfficialImageID, + testOfficialImageName + ":" + testOfficialImageIDShort, + testOfficialImageName, + testOfficialImageName + ":" + DEFAULTTAG, + "docker.io/" + testOfficialImageName, + "docker.io/" + testOfficialImageName + ":" + DEFAULTTAG, + "index.docker.io/" + testOfficialImageName, + "index.docker.io/" + testOfficialImageName + ":" + DEFAULTTAG, + "library/" + testOfficialImageName, + "library/" + testOfficialImageName + ":" + DEFAULTTAG, + "docker.io/library/" + testOfficialImageName, + "docker.io/library/" + testOfficialImageName + ":" + DEFAULTTAG, + "index.docker.io/library/" + testOfficialImageName, + "index.docker.io/library/" + testOfficialImageName + ":" + DEFAULTTAG, } - if img, err := store.LookupImage(testImageName + ":" + "fail"); err == nil { - t.Errorf("Expected error, none found") - } else if img != nil { - t.Errorf("Expected 0 image, 1 found") + privateLookups := []string{ + testPrivateImageID, + testPrivateImageIDShort, + testPrivateImageName + ":" + testPrivateImageID, + testPrivateImageName + ":" + testPrivateImageIDShort, + testPrivateImageName, + testPrivateImageName + ":" + DEFAULTTAG, } - if img, err := store.LookupImage("fail:fail"); err == nil { - t.Errorf("Expected error, none found") - } else if img != nil { - t.Errorf("Expected 0 image, 1 found") + invalidLookups := []string{ + testOfficialImageName + ":" + "fail", + "fail:fail", } - if img, err := store.LookupImage(testImageID); err != nil { - t.Fatal(err) - } else if img == nil { - t.Errorf("Expected 1 image, none found") + for _, name := range officialLookups { + if img, err := store.LookupImage(name); err != nil { + t.Errorf("Error looking up %s: %s", name, err) + } else if img == nil { + t.Errorf("Expected 1 image, none found: %s", name) + } else if img.ID != testOfficialImageID { + t.Errorf("Expected ID '%s' found '%s'", testOfficialImageID, img.ID) + } } - if img, err := store.LookupImage(testImageName + ":" + testImageID); err != nil { - t.Fatal(err) - } else if img == nil { - t.Errorf("Expected 1 image, none found") + for _, name := range privateLookups { + if img, err := store.LookupImage(name); err != nil { + t.Errorf("Error looking up %s: %s", name, err) + } else if img == nil { + t.Errorf("Expected 1 image, none found: %s", name) + } else if img.ID != testPrivateImageID { + t.Errorf("Expected ID '%s' found '%s'", testPrivateImageID, img.ID) + } + } + + for _, name := range invalidLookups { + if img, err := store.LookupImage(name); err == nil { + t.Errorf("Expected error, none found: %s", name) + } else if img != nil { + t.Errorf("Expected 0 image, 1 found: %s", name) + } } } @@ -133,18 +174,3 @@ func TestInvalidTagName(t *testing.T) { } } } - -func TestOfficialName(t *testing.T) { - names := map[string]bool{ - "library/ubuntu": true, - "nonlibrary/ubuntu": false, - "ubuntu": true, - "other/library": false, - } - for name, isOfficial := range names { - result := isOfficialName(name) - if result != isOfficial { - t.Errorf("Unexpected result for %s\n\tExpecting: %v\n\tActual: %v", name, isOfficial, result) - } - } -} diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index a27aecc56f..189b6a7ba1 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -4301,3 +4301,24 @@ func TestBuildRenamedDockerfile(t *testing.T) { logDone("build - rename dockerfile") } + +func TestBuildFromOfficialNames(t *testing.T) { + name := "testbuildfromofficial" + fromNames := []string{ + "busybox", + "docker.io/busybox", + "index.docker.io/busybox", + "library/busybox", + "docker.io/library/busybox", + "index.docker.io/library/busybox", + } + for idx, fromName := range fromNames { + imgName := fmt.Sprintf("%s%d", name, idx) + _, err := buildImage(imgName, "FROM "+fromName, true) + if err != nil { + t.Errorf("Build failed using FROM %s: %s", fromName, err) + } + deleteImages(imgName) + } + logDone("build - from official names") +} diff --git a/integration-cli/docker_cli_pull_test.go b/integration-cli/docker_cli_pull_test.go index 5b3324c771..bed015be0e 100644 --- a/integration-cli/docker_cli_pull_test.go +++ b/integration-cli/docker_cli_pull_test.go @@ -2,6 +2,7 @@ package main import ( "os/exec" + "strings" "testing" ) @@ -24,3 +25,33 @@ func TestPullNonExistingImage(t *testing.T) { } logDone("pull - pull fooblahblah1234 (non-existing image)") } + +// pulling an image from the central registry using official names should work +// ensure all pulls result in the same image +func TestPullImageOfficialNames(t *testing.T) { + names := []string{ + "docker.io/hello-world", + "index.docker.io/hello-world", + "library/hello-world", + "docker.io/library/hello-world", + "index.docker.io/library/hello-world", + } + for _, name := range names { + pullCmd := exec.Command(dockerBinary, "pull", name) + out, exitCode, err := runCommandWithOutput(pullCmd) + if err != nil || exitCode != 0 { + t.Errorf("pulling the '%s' image from the registry has failed: %s", name, err) + continue + } + + // ensure we don't have multiple image names. + imagesCmd := exec.Command(dockerBinary, "images") + out, _, err = runCommandWithOutput(imagesCmd) + if err != nil { + t.Errorf("listing images failed with errors: %v", err) + } else if strings.Contains(out, name) { + t.Errorf("images should not have listed '%s'", name) + } + } + logDone("pull - pull official names") +} diff --git a/integration-cli/docker_cli_tag_test.go b/integration-cli/docker_cli_tag_test.go index bfab851115..4d5a394e4c 100644 --- a/integration-cli/docker_cli_tag_test.go +++ b/integration-cli/docker_cli_tag_test.go @@ -132,3 +132,49 @@ func TestTagExistedNameWithForce(t *testing.T) { logDone("tag - busybox with an existed tag name with -f option work") } + +// ensure tagging using official names works +// ensure all tags result in the same name +func TestTagOfficialNames(t *testing.T) { + names := []string{ + "docker.io/busybox", + "index.docker.io/busybox", + "library/busybox", + "docker.io/library/busybox", + "index.docker.io/library/busybox", + } + + for _, name := range names { + tagCmd := exec.Command(dockerBinary, "tag", "-f", "busybox:latest", name+":latest") + out, exitCode, err := runCommandWithOutput(tagCmd) + if err != nil || exitCode != 0 { + t.Errorf("tag busybox %v should have worked: %s, %s", name, err, out) + continue + } + + // ensure we don't have multiple tag names. + imagesCmd := exec.Command(dockerBinary, "images") + out, _, err = runCommandWithOutput(imagesCmd) + if err != nil { + t.Errorf("listing images failed with errors: %v, %s", err, out) + } else if strings.Contains(out, name) { + t.Errorf("images should not have listed '%s'", name) + deleteImages(name + ":latest") + } else { + logMessage := fmt.Sprintf("tag official name - busybox %v", name) + logDone(logMessage) + } + } + + for _, name := range names { + tagCmd := exec.Command(dockerBinary, "tag", "-f", name+":latest", "fooo/bar:latest") + _, exitCode, err := runCommandWithOutput(tagCmd) + if err != nil || exitCode != 0 { + t.Errorf("tag %v fooo/bar should have worked: %s", name, err) + continue + } + deleteImages("fooo/bar:latest") + logMessage := fmt.Sprintf("tag official name - %v fooo/bar", name) + logDone(logMessage) + } +} diff --git a/integration/utils_test.go b/integration/utils_test.go index 0cb22ee1c2..32ca8e0d62 100644 --- a/integration/utils_test.go +++ b/integration/utils_test.go @@ -20,6 +20,7 @@ import ( "github.com/docker/docker/daemon" "github.com/docker/docker/engine" flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/registry" "github.com/docker/docker/runconfig" "github.com/docker/docker/utils" ) @@ -173,7 +174,14 @@ func newTestEngine(t Fataler, autorestart bool, root string) *engine.Engine { eng := engine.New() eng.Logging = false // Load default plugins - builtins.Register(eng) + if err := builtins.Register(eng); err != nil { + t.Fatal(err) + } + // load registry service + if err := registry.NewService(nil).Install(eng); err != nil { + t.Fatal(err) + } + // (This is manually copied and modified from main() until we have a more generic plugin system) cfg := &daemon.Config{ Root: root, diff --git a/opts/opts.go b/opts/opts.go index f15064ac69..3d8c23ff77 100644 --- a/opts/opts.go +++ b/opts/opts.go @@ -3,7 +3,6 @@ package opts import ( "fmt" "net" - "net/url" "os" "path" "regexp" @@ -39,10 +38,6 @@ func IPVar(value *net.IP, names []string, defaultValue, usage string) { flag.Var(NewIpOpt(value, defaultValue), names, usage) } -func MirrorListVar(values *[]string, names []string, usage string) { - flag.Var(newListOptsRef(values, ValidateMirror), names, usage) -} - func LabelListVar(values *[]string, names []string, usage string) { flag.Var(newListOptsRef(values, ValidateLabel), names, usage) } @@ -127,6 +122,7 @@ func (opts *ListOpts) Len() int { // Validators type ValidatorFctType func(val string) (string, error) +type ValidatorFctListType func(val string) ([]string, error) func ValidateAttach(val string) (string, error) { s := strings.ToLower(val) @@ -214,24 +210,6 @@ func ValidateExtraHost(val string) (string, error) { return val, nil } -// Validates an HTTP(S) registry mirror -func ValidateMirror(val string) (string, error) { - uri, err := url.Parse(val) - if err != nil { - return "", fmt.Errorf("%s is not a valid URI", val) - } - - if uri.Scheme != "http" && uri.Scheme != "https" { - return "", fmt.Errorf("Unsupported scheme %s", uri.Scheme) - } - - if uri.Path != "" || uri.RawQuery != "" || uri.Fragment != "" { - return "", fmt.Errorf("Unsupported path/query/fragment at end of the URI") - } - - return fmt.Sprintf("%s://%s/v1/", uri.Scheme, uri.Host), nil -} - func ValidateLabel(val string) (string, error) { if strings.Count(val, "=") != 1 { return "", fmt.Errorf("bad attribute format: %s", val) diff --git a/opts/opts_test.go b/opts/opts_test.go index 09b5aa780b..e813c44326 100644 --- a/opts/opts_test.go +++ b/opts/opts_test.go @@ -30,7 +30,23 @@ func TestValidateIPAddress(t *testing.T) { func TestListOpts(t *testing.T) { o := NewListOpts(nil) o.Set("foo") - o.String() + if o.String() != "[foo]" { + t.Errorf("%s != [foo]", o.String()) + } + o.Set("bar") + if o.Len() != 2 { + t.Errorf("%d != 2", o.Len()) + } + if !o.Get("bar") { + t.Error("o.Get(\"bar\") == false") + } + if o.Get("baz") { + t.Error("o.Get(\"baz\") == true") + } + o.Delete("foo") + if o.String() != "[bar]" { + t.Errorf("%s != [bar]", o.String()) + } } func TestValidateDnsSearch(t *testing.T) { diff --git a/registry/auth.go b/registry/auth.go index 4276064083..8382869b37 100644 --- a/registry/auth.go +++ b/registry/auth.go @@ -7,7 +7,6 @@ import ( "fmt" "io/ioutil" "net/http" - "net/url" "os" "path" "strings" @@ -22,23 +21,15 @@ const ( // Only used for user auth + account creation INDEXSERVER = "https://index.docker.io/v1/" REGISTRYSERVER = "https://registry-1.docker.io/v1/" + INDEXNAME = "docker.io" // INDEXSERVER = "https://registry-stage.hub.docker.com/v1/" ) var ( ErrConfigFileMissing = errors.New("The Auth config file is missing") - IndexServerURL *url.URL ) -func init() { - url, err := url.Parse(INDEXSERVER) - if err != nil { - panic(err) - } - IndexServerURL = url -} - type AuthConfig struct { Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` @@ -56,6 +47,10 @@ func IndexServerAddress() string { return INDEXSERVER } +func IndexServerName() string { + return INDEXNAME +} + // create a base64 encoded auth string to store in config func encodeAuth(authConfig *AuthConfig) string { authStr := authConfig.Username + ":" + authConfig.Password @@ -118,6 +113,7 @@ func LoadConfig(rootPath string) (*ConfigFile, error) { } authConfig.Email = origEmail[1] authConfig.ServerAddress = IndexServerAddress() + // *TODO: Switch to using IndexServerName() instead? configFile.Configs[IndexServerAddress()] = authConfig } else { for k, authConfig := range configFile.Configs { @@ -181,7 +177,7 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e ) if serverAddress == "" { - serverAddress = IndexServerAddress() + return "", fmt.Errorf("Server Error: Server Address not set.") } loginAgainstOfficialIndex := serverAddress == IndexServerAddress() @@ -213,6 +209,7 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e status = "Account created. Please use the confirmation link we sent" + " to your e-mail to activate it." } else { + // *TODO: Use registry configuration to determine what this says, if anything? status = "Account created. Please see the documentation of the registry " + serverAddress + " for instructions how to activate it." } } else if reqStatusCode == 400 { @@ -236,6 +233,7 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e if loginAgainstOfficialIndex { return "", fmt.Errorf("Login: Account is not Active. Please check your e-mail for a confirmation link.") } + // *TODO: Use registry configuration to determine what this says, if anything? return "", fmt.Errorf("Login: Account is not Active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress) } return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, resp.StatusCode, resp.Header) @@ -271,14 +269,10 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e } // this method matches a auth configuration to a server address or a url -func (config *ConfigFile) ResolveAuthConfig(hostname string) AuthConfig { - if hostname == IndexServerAddress() || len(hostname) == 0 { - // default to the index server - return config.Configs[IndexServerAddress()] - } - +func (config *ConfigFile) ResolveAuthConfig(index *IndexInfo) AuthConfig { + configKey := index.GetAuthConfigKey() // First try the happy case - if c, found := config.Configs[hostname]; found { + if c, found := config.Configs[configKey]; found || index.Official { return c } @@ -297,9 +291,8 @@ func (config *ConfigFile) ResolveAuthConfig(hostname string) AuthConfig { // Maybe they have a legacy config file, we will iterate the keys converting // them to the new format and testing - normalizedHostename := convertToHostname(hostname) for registry, config := range config.Configs { - if registryHostname := convertToHostname(registry); registryHostname == normalizedHostename { + if configKey == convertToHostname(registry) { return config } } diff --git a/registry/auth_test.go b/registry/auth_test.go index 3cb1a9ac4b..22f879946a 100644 --- a/registry/auth_test.go +++ b/registry/auth_test.go @@ -81,12 +81,20 @@ func TestResolveAuthConfigIndexServer(t *testing.T) { } defer os.RemoveAll(configFile.rootPath) - for _, registry := range []string{"", IndexServerAddress()} { - resolved := configFile.ResolveAuthConfig(registry) - if resolved != configFile.Configs[IndexServerAddress()] { - t.Fail() - } + indexConfig := configFile.Configs[IndexServerAddress()] + + officialIndex := &IndexInfo{ + Official: true, } + privateIndex := &IndexInfo{ + Official: false, + } + + resolved := configFile.ResolveAuthConfig(officialIndex) + assertEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServerAddress()") + + resolved = configFile.ResolveAuthConfig(privateIndex) + assertNotEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to not return IndexServerAddress()") } func TestResolveAuthConfigFullURL(t *testing.T) { @@ -106,18 +114,27 @@ func TestResolveAuthConfigFullURL(t *testing.T) { Password: "bar-pass", Email: "bar@example.com", } - configFile.Configs["https://registry.example.com/v1/"] = registryAuth - configFile.Configs["http://localhost:8000/v1/"] = localAuth - configFile.Configs["registry.com"] = registryAuth + officialAuth := AuthConfig{ + Username: "baz-user", + Password: "baz-pass", + Email: "baz@example.com", + } + configFile.Configs[IndexServerAddress()] = officialAuth + + expectedAuths := map[string]AuthConfig{ + "registry.example.com": registryAuth, + "localhost:8000": localAuth, + "registry.com": localAuth, + } validRegistries := map[string][]string{ - "https://registry.example.com/v1/": { + "registry.example.com": { "https://registry.example.com/v1/", "http://registry.example.com/v1/", "registry.example.com", "registry.example.com/v1/", }, - "http://localhost:8000/v1/": { + "localhost:8000": { "https://localhost:8000/v1/", "http://localhost:8000/v1/", "localhost:8000", @@ -132,18 +149,24 @@ func TestResolveAuthConfigFullURL(t *testing.T) { } for configKey, registries := range validRegistries { + configured, ok := expectedAuths[configKey] + if !ok || configured.Email == "" { + t.Fatal() + } + index := &IndexInfo{ + Name: configKey, + } for _, registry := range registries { - var ( - configured AuthConfig - ok bool - ) - resolved := configFile.ResolveAuthConfig(registry) - if configured, ok = configFile.Configs[configKey]; !ok { - t.Fail() - } + configFile.Configs[registry] = configured + resolved := configFile.ResolveAuthConfig(index) if resolved.Email != configured.Email { t.Errorf("%s -> %q != %q\n", registry, resolved.Email, configured.Email) } + delete(configFile.Configs, registry) + resolved = configFile.ResolveAuthConfig(index) + if resolved.Email == configured.Email { + t.Errorf("%s -> %q == %q\n", registry, resolved.Email, configured.Email) + } } } } diff --git a/registry/config.go b/registry/config.go new file mode 100644 index 0000000000..bd993edd50 --- /dev/null +++ b/registry/config.go @@ -0,0 +1,126 @@ +package registry + +import ( + "encoding/json" + "fmt" + "net" + "net/url" + + "github.com/docker/docker/opts" + flag "github.com/docker/docker/pkg/mflag" +) + +// Options holds command line options. +type Options struct { + Mirrors opts.ListOpts + InsecureRegistries opts.ListOpts +} + +// InstallFlags adds command-line options to the top-level flag parser for +// the current process. +func (options *Options) InstallFlags() { + options.Mirrors = opts.NewListOpts(ValidateMirror) + flag.Var(&options.Mirrors, []string{"-registry-mirror"}, "Specify a preferred Docker registry mirror") + options.InsecureRegistries = opts.NewListOpts(ValidateIndexName) + flag.Var(&options.InsecureRegistries, []string{"-insecure-registry"}, "Enable insecure communication with specified registries (no certificate verification for HTTPS and enable HTTP fallback) (e.g., localhost:5000 or 10.20.0.0/16)") +} + +// ValidateMirror validates an HTTP(S) registry mirror +func ValidateMirror(val string) (string, error) { + uri, err := url.Parse(val) + if err != nil { + return "", fmt.Errorf("%s is not a valid URI", val) + } + + if uri.Scheme != "http" && uri.Scheme != "https" { + return "", fmt.Errorf("Unsupported scheme %s", uri.Scheme) + } + + if uri.Path != "" || uri.RawQuery != "" || uri.Fragment != "" { + return "", fmt.Errorf("Unsupported path/query/fragment at end of the URI") + } + + return fmt.Sprintf("%s://%s/v1/", uri.Scheme, uri.Host), nil +} + +// ValidateIndexName validates an index name. +func ValidateIndexName(val string) (string, error) { + // 'index.docker.io' => 'docker.io' + if val == "index."+IndexServerName() { + val = IndexServerName() + } + // *TODO: Check if valid hostname[:port]/ip[:port]? + return val, nil +} + +type netIPNet net.IPNet + +func (ipnet *netIPNet) MarshalJSON() ([]byte, error) { + return json.Marshal((*net.IPNet)(ipnet).String()) +} + +func (ipnet *netIPNet) UnmarshalJSON(b []byte) (err error) { + var ipnet_str string + if err = json.Unmarshal(b, &ipnet_str); err == nil { + var cidr *net.IPNet + if _, cidr, err = net.ParseCIDR(ipnet_str); err == nil { + *ipnet = netIPNet(*cidr) + } + } + return +} + +// ServiceConfig stores daemon registry services configuration. +type ServiceConfig struct { + InsecureRegistryCIDRs []*netIPNet `json:"InsecureRegistryCIDRs"` + IndexConfigs map[string]*IndexInfo `json:"IndexConfigs"` +} + +// NewServiceConfig returns a new instance of ServiceConfig +func NewServiceConfig(options *Options) *ServiceConfig { + if options == nil { + options = &Options{ + Mirrors: opts.NewListOpts(nil), + InsecureRegistries: opts.NewListOpts(nil), + } + } + + // Localhost is by default considered as an insecure registry + // This is a stop-gap for people who are running a private registry on localhost (especially on Boot2docker). + // + // TODO: should we deprecate this once it is easier for people to set up a TLS registry or change + // daemon flags on boot2docker? + options.InsecureRegistries.Set("127.0.0.0/8") + + config := &ServiceConfig{ + InsecureRegistryCIDRs: make([]*netIPNet, 0), + IndexConfigs: make(map[string]*IndexInfo, 0), + } + // Split --insecure-registry into CIDR and registry-specific settings. + for _, r := range options.InsecureRegistries.GetAll() { + // Check if CIDR was passed to --insecure-registry + _, ipnet, err := net.ParseCIDR(r) + if err == nil { + // Valid CIDR. + config.InsecureRegistryCIDRs = append(config.InsecureRegistryCIDRs, (*netIPNet)(ipnet)) + } else { + // Assume `host:port` if not CIDR. + config.IndexConfigs[r] = &IndexInfo{ + Name: r, + Mirrors: make([]string, 0), + Secure: false, + Official: false, + } + } + } + + // Configure public registry. + config.IndexConfigs[IndexServerName()] = &IndexInfo{ + Name: IndexServerName(), + Mirrors: options.Mirrors.GetAll(), + Secure: true, + Official: true, + } + + return config +} diff --git a/registry/config_test.go b/registry/config_test.go new file mode 100644 index 0000000000..25578a7f2b --- /dev/null +++ b/registry/config_test.go @@ -0,0 +1,49 @@ +package registry + +import ( + "testing" +) + +func TestValidateMirror(t *testing.T) { + valid := []string{ + "http://mirror-1.com", + "https://mirror-1.com", + "http://localhost", + "https://localhost", + "http://localhost:5000", + "https://localhost:5000", + "http://127.0.0.1", + "https://127.0.0.1", + "http://127.0.0.1:5000", + "https://127.0.0.1:5000", + } + + invalid := []string{ + "!invalid!://%as%", + "ftp://mirror-1.com", + "http://mirror-1.com/", + "http://mirror-1.com/?q=foo", + "http://mirror-1.com/v1/", + "http://mirror-1.com/v1/?q=foo", + "http://mirror-1.com/v1/?q=foo#frag", + "http://mirror-1.com?q=foo", + "https://mirror-1.com#frag", + "https://mirror-1.com/", + "https://mirror-1.com/#frag", + "https://mirror-1.com/v1/", + "https://mirror-1.com/v1/#", + "https://mirror-1.com?q", + } + + for _, address := range valid { + if ret, err := ValidateMirror(address); err != nil || ret == "" { + t.Errorf("ValidateMirror(`"+address+"`) got %s %s", ret, err) + } + } + + for _, address := range invalid { + if ret, err := ValidateMirror(address); err == nil || ret != "" { + t.Errorf("ValidateMirror(`"+address+"`) got %s %s", ret, err) + } + } +} diff --git a/registry/endpoint.go b/registry/endpoint.go index 019bccfc6d..86f53744de 100644 --- a/registry/endpoint.go +++ b/registry/endpoint.go @@ -37,8 +37,9 @@ func scanForAPIVersion(hostname string) (string, APIVersion) { return hostname, DefaultAPIVersion } -func NewEndpoint(hostname string, insecureRegistries []string) (*Endpoint, error) { - endpoint, err := newEndpoint(hostname, insecureRegistries) +func NewEndpoint(index *IndexInfo) (*Endpoint, error) { + // *TODO: Allow per-registry configuration of endpoints. + endpoint, err := newEndpoint(index.GetAuthConfigKey(), index.Secure) if err != nil { return nil, err } @@ -49,7 +50,7 @@ func NewEndpoint(hostname string, insecureRegistries []string) (*Endpoint, error //TODO: triggering highland build can be done there without "failing" - if endpoint.secure { + if index.Secure { // If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry` // in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fallback to HTTP. return nil, fmt.Errorf("Invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host) @@ -68,7 +69,7 @@ func NewEndpoint(hostname string, insecureRegistries []string) (*Endpoint, error return endpoint, nil } -func newEndpoint(hostname string, insecureRegistries []string) (*Endpoint, error) { +func newEndpoint(hostname string, secure bool) (*Endpoint, error) { var ( endpoint = Endpoint{} trimmedHostname string @@ -82,13 +83,14 @@ func newEndpoint(hostname string, insecureRegistries []string) (*Endpoint, error if err != nil { return nil, err } - endpoint.secure, err = isSecure(endpoint.URL.Host, insecureRegistries) - if err != nil { - return nil, err - } + endpoint.secure = secure return &endpoint, nil } +func (repoInfo *RepositoryInfo) GetEndpoint() (*Endpoint, error) { + return NewEndpoint(repoInfo.Index) +} + type Endpoint struct { URL *url.URL Version APIVersion @@ -156,27 +158,30 @@ func (e Endpoint) Ping() (RegistryInfo, error) { return info, nil } -// isSecure returns false if the provided hostname is part of the list of insecure registries. +// isSecureIndex returns false if the provided indexName is part of the list of insecure registries // Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs. // // The list of insecure registries can contain an element with CIDR notation to specify a whole subnet. -// If the subnet contains one of the IPs of the registry specified by hostname, the latter is considered +// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered // insecure. // -// hostname should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name +// indexName should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name // or an IP address. If it is a domain name, then it will be resolved in order to check if the IP is contained -// in a subnet. If the resolving is not successful, isSecure will only try to match hostname to any element +// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element // of insecureRegistries. -func isSecure(hostname string, insecureRegistries []string) (bool, error) { - if hostname == IndexServerURL.Host { - return true, nil +func (config *ServiceConfig) isSecureIndex(indexName string) bool { + // Check for configured index, first. This is needed in case isSecureIndex + // is called from anything besides NewIndexInfo, in order to honor per-index configurations. + if index, ok := config.IndexConfigs[indexName]; ok { + return index.Secure } - host, _, err := net.SplitHostPort(hostname) + host, _, err := net.SplitHostPort(indexName) if err != nil { - // assume hostname is of the form `host` without the port and go on. - host = hostname + // assume indexName is of the form `host` without the port and go on. + host = indexName } + addrs, err := lookupIP(host) if err != nil { ip := net.ParseIP(host) @@ -189,29 +194,15 @@ func isSecure(hostname string, insecureRegistries []string) (bool, error) { // So, len(addrs) == 0 and we're not aborting. } - for _, r := range insecureRegistries { - if hostname == r { - // hostname matches insecure registry - return false, nil - } - - // Try CIDR notation only if addrs has any elements, i.e. if `host`'s IP could be determined. - for _, addr := range addrs { - - // now assume a CIDR was passed to --insecure-registry - _, ipnet, err := net.ParseCIDR(r) - if err != nil { - // if we could not parse it as a CIDR, even after removing - // assume it's not a CIDR and go on with the next candidate - break - } - + // Try CIDR notation only if addrs has any elements, i.e. if `host`'s IP could be determined. + for _, addr := range addrs { + for _, ipnet := range config.InsecureRegistryCIDRs { // check if the addr falls in the subnet - if ipnet.Contains(addr) { - return false, nil + if (*net.IPNet)(ipnet).Contains(addr) { + return false } } } - return true, nil + return true } diff --git a/registry/endpoint_test.go b/registry/endpoint_test.go index 54105ec174..b691a4fb98 100644 --- a/registry/endpoint_test.go +++ b/registry/endpoint_test.go @@ -12,7 +12,7 @@ func TestEndpointParse(t *testing.T) { {"0.0.0.0:5000", "https://0.0.0.0:5000/v1/"}, } for _, td := range testData { - e, err := newEndpoint(td.str, insecureRegistries) + e, err := newEndpoint(td.str, false) if err != nil { t.Errorf("%q: %s", td.str, err) } diff --git a/registry/registry.go b/registry/registry.go index a122918977..de724ee20c 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -25,6 +25,7 @@ var ( errLoginRequired = errors.New("Authentication is required.") validNamespaceChars = regexp.MustCompile(`^([a-z0-9-_]*)$`) validRepo = regexp.MustCompile(`^([a-z0-9-_.]+)$`) + emptyServiceConfig = NewServiceConfig(nil) ) type TimeoutType uint32 @@ -160,12 +161,12 @@ func doRequest(req *http.Request, jar http.CookieJar, timeout TimeoutType, secur return res, client, err } -func validateRepositoryName(repositoryName string) error { +func validateRemoteName(remoteName string) error { var ( namespace string name string ) - nameParts := strings.SplitN(repositoryName, "/", 2) + nameParts := strings.SplitN(remoteName, "/", 2) if len(nameParts) < 2 { namespace = "library" name = nameParts[0] @@ -196,29 +197,147 @@ func validateRepositoryName(repositoryName string) error { return nil } -// Resolves a repository name to a hostname + name -func ResolveRepositoryName(reposName string) (string, string, error) { - if strings.Contains(reposName, "://") { - // It cannot contain a scheme! - return "", "", ErrInvalidRepositoryName - } - nameParts := strings.SplitN(reposName, "/", 2) - if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") && !strings.Contains(nameParts[0], ":") && - nameParts[0] != "localhost") { - // This is a Docker Index repos (ex: samalba/hipache or ubuntu) - err := validateRepositoryName(reposName) - return IndexServerAddress(), reposName, err - } - hostname := nameParts[0] - reposName = nameParts[1] - if strings.Contains(hostname, "index.docker.io") { - return "", "", fmt.Errorf("Invalid repository name, try \"%s\" instead", reposName) - } - if err := validateRepositoryName(reposName); err != nil { - return "", "", err +// NewIndexInfo returns IndexInfo configuration from indexName +func NewIndexInfo(config *ServiceConfig, indexName string) (*IndexInfo, error) { + var err error + indexName, err = ValidateIndexName(indexName) + if err != nil { + return nil, err } - return hostname, reposName, nil + // Return any configured index info, first. + if index, ok := config.IndexConfigs[indexName]; ok { + return index, nil + } + + // Construct a non-configured index info. + index := &IndexInfo{ + Name: indexName, + Mirrors: make([]string, 0), + Official: false, + } + index.Secure = config.isSecureIndex(indexName) + return index, nil +} + +func validateNoSchema(reposName string) error { + if strings.Contains(reposName, "://") { + // It cannot contain a scheme! + return ErrInvalidRepositoryName + } + return nil +} + +// splitReposName breaks a reposName into an index name and remote name +func splitReposName(reposName string) (string, string) { + nameParts := strings.SplitN(reposName, "/", 2) + var indexName, remoteName string + if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") && + !strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") { + // This is a Docker Index repos (ex: samalba/hipache or ubuntu) + // 'docker.io' + indexName = IndexServerName() + remoteName = reposName + } else { + indexName = nameParts[0] + remoteName = nameParts[1] + } + return indexName, remoteName +} + +// NewRepositoryInfo validates and breaks down a repository name into a RepositoryInfo +func NewRepositoryInfo(config *ServiceConfig, reposName string) (*RepositoryInfo, error) { + if err := validateNoSchema(reposName); err != nil { + return nil, err + } + + indexName, remoteName := splitReposName(reposName) + if err := validateRemoteName(remoteName); err != nil { + return nil, err + } + + repoInfo := &RepositoryInfo{ + RemoteName: remoteName, + } + + var err error + repoInfo.Index, err = NewIndexInfo(config, indexName) + if err != nil { + return nil, err + } + + if repoInfo.Index.Official { + normalizedName := repoInfo.RemoteName + if strings.HasPrefix(normalizedName, "library/") { + // If pull "library/foo", it's stored locally under "foo" + normalizedName = strings.SplitN(normalizedName, "/", 2)[1] + } + + repoInfo.LocalName = normalizedName + repoInfo.RemoteName = normalizedName + // If the normalized name does not contain a '/' (e.g. "foo") + // then it is an official repo. + if strings.IndexRune(normalizedName, '/') == -1 { + repoInfo.Official = true + // Fix up remote name for official repos. + repoInfo.RemoteName = "library/" + normalizedName + } + + // *TODO: Prefix this with 'docker.io/'. + repoInfo.CanonicalName = repoInfo.LocalName + } else { + // *TODO: Decouple index name from hostname (via registry configuration?) + repoInfo.LocalName = repoInfo.Index.Name + "/" + repoInfo.RemoteName + repoInfo.CanonicalName = repoInfo.LocalName + } + return repoInfo, nil +} + +// ValidateRepositoryName validates a repository name +func ValidateRepositoryName(reposName string) error { + var err error + if err = validateNoSchema(reposName); err != nil { + return err + } + indexName, remoteName := splitReposName(reposName) + if _, err = ValidateIndexName(indexName); err != nil { + return err + } + return validateRemoteName(remoteName) +} + +// ParseRepositoryInfo performs the breakdown of a repository name into a RepositoryInfo, but +// lacks registry configuration. +func ParseRepositoryInfo(reposName string) (*RepositoryInfo, error) { + return NewRepositoryInfo(emptyServiceConfig, reposName) +} + +// NormalizeLocalName transforms a repository name into a normalize LocalName +// Passes through the name without transformation on error (image id, etc) +func NormalizeLocalName(name string) string { + repoInfo, err := ParseRepositoryInfo(name) + if err != nil { + return name + } + return repoInfo.LocalName +} + +// GetAuthConfigKey special-cases using the full index address of the official +// index as the AuthConfig key, and uses the (host)name[:port] for private indexes. +func (index *IndexInfo) GetAuthConfigKey() string { + if index.Official { + return IndexServerAddress() + } + return index.Name +} + +// GetSearchTerm special-cases using local name for official index, and +// remote name for private indexes. +func (repoInfo *RepositoryInfo) GetSearchTerm() string { + if repoInfo.Index.Official { + return repoInfo.LocalName + } + return repoInfo.RemoteName } func trustedLocation(req *http.Request) bool { diff --git a/registry/registry_mock_test.go b/registry/registry_mock_test.go index 887d2ef6f2..57233d7c79 100644 --- a/registry/registry_mock_test.go +++ b/registry/registry_mock_test.go @@ -15,15 +15,16 @@ import ( "testing" "time" + "github.com/docker/docker/opts" "github.com/gorilla/mux" log "github.com/Sirupsen/logrus" ) var ( - testHTTPServer *httptest.Server - insecureRegistries []string - testLayers = map[string]map[string]string{ + testHTTPServer *httptest.Server + testHTTPSServer *httptest.Server + testLayers = map[string]map[string]string{ "77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20": { "json": `{"id":"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20", "comment":"test base image","created":"2013-03-23T12:53:11.10432-07:00", @@ -86,6 +87,7 @@ var ( "": {net.ParseIP("0.0.0.0")}, "localhost": {net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, "example.com": {net.ParseIP("42.42.42.42")}, + "other.com": {net.ParseIP("43.43.43.43")}, } ) @@ -108,11 +110,7 @@ func init() { r.HandleFunc("/v2/version", handlerGetPing).Methods("GET") testHTTPServer = httptest.NewServer(handlerAccessLog(r)) - URL, err := url.Parse(testHTTPServer.URL) - if err != nil { - panic(err) - } - insecureRegistries = []string{URL.Host} + testHTTPSServer = httptest.NewTLSServer(handlerAccessLog(r)) // override net.LookupIP lookupIP = func(host string) ([]net.IP, error) { @@ -146,6 +144,52 @@ func makeURL(req string) string { return testHTTPServer.URL + req } +func makeHttpsURL(req string) string { + return testHTTPSServer.URL + req +} + +func makeIndex(req string) *IndexInfo { + index := &IndexInfo{ + Name: makeURL(req), + } + return index +} + +func makeHttpsIndex(req string) *IndexInfo { + index := &IndexInfo{ + Name: makeHttpsURL(req), + } + return index +} + +func makePublicIndex() *IndexInfo { + index := &IndexInfo{ + Name: IndexServerAddress(), + Secure: true, + Official: true, + } + return index +} + +func makeServiceConfig(mirrors []string, insecure_registries []string) *ServiceConfig { + options := &Options{ + Mirrors: opts.NewListOpts(nil), + InsecureRegistries: opts.NewListOpts(nil), + } + if mirrors != nil { + for _, mirror := range mirrors { + options.Mirrors.Set(mirror) + } + } + if insecure_registries != nil { + for _, insecure_registries := range insecure_registries { + options.InsecureRegistries.Set(insecure_registries) + } + } + + return NewServiceConfig(options) +} + func writeHeaders(w http.ResponseWriter) { h := w.Header() h.Add("Server", "docker-tests/mock") @@ -193,6 +237,40 @@ func assertEqual(t *testing.T, a interface{}, b interface{}, message string) { t.Fatal(message) } +func assertNotEqual(t *testing.T, a interface{}, b interface{}, message string) { + if a != b { + return + } + if len(message) == 0 { + message = fmt.Sprintf("%v == %v", a, b) + } + t.Fatal(message) +} + +// Similar to assertEqual, but does not stop test +func checkEqual(t *testing.T, a interface{}, b interface{}, messagePrefix string) { + if a == b { + return + } + message := fmt.Sprintf("%v != %v", a, b) + if len(messagePrefix) != 0 { + message = messagePrefix + ": " + message + } + t.Error(message) +} + +// Similar to assertNotEqual, but does not stop test +func checkNotEqual(t *testing.T, a interface{}, b interface{}, messagePrefix string) { + if a != b { + return + } + message := fmt.Sprintf("%v == %v", a, b) + if len(messagePrefix) != 0 { + message = messagePrefix + ": " + message + } + t.Error(message) +} + func requiresAuth(w http.ResponseWriter, r *http.Request) bool { writeCookie := func() { value := fmt.Sprintf("FAKE-SESSION-%d", time.Now().UnixNano()) @@ -271,6 +349,7 @@ func handlerGetDeleteTags(w http.ResponseWriter, r *http.Request) { return } repositoryName := mux.Vars(r)["repository"] + repositoryName = NormalizeLocalName(repositoryName) tags, exists := testRepositories[repositoryName] if !exists { apiError(w, "Repository not found", 404) @@ -290,6 +369,7 @@ func handlerGetTag(w http.ResponseWriter, r *http.Request) { } vars := mux.Vars(r) repositoryName := vars["repository"] + repositoryName = NormalizeLocalName(repositoryName) tagName := vars["tag"] tags, exists := testRepositories[repositoryName] if !exists { @@ -310,6 +390,7 @@ func handlerPutTag(w http.ResponseWriter, r *http.Request) { } vars := mux.Vars(r) repositoryName := vars["repository"] + repositoryName = NormalizeLocalName(repositoryName) tagName := vars["tag"] tags, exists := testRepositories[repositoryName] if !exists { diff --git a/registry/registry_test.go b/registry/registry_test.go index c1bb97d657..511d7eb17a 100644 --- a/registry/registry_test.go +++ b/registry/registry_test.go @@ -21,7 +21,7 @@ const ( func spawnTestRegistrySession(t *testing.T) *Session { authConfig := &AuthConfig{} - endpoint, err := NewEndpoint(makeURL("/v1/"), insecureRegistries) + endpoint, err := NewEndpoint(makeIndex("/v1/")) if err != nil { t.Fatal(err) } @@ -32,16 +32,139 @@ func spawnTestRegistrySession(t *testing.T) *Session { return r } +func TestPublicSession(t *testing.T) { + authConfig := &AuthConfig{} + + getSessionDecorators := func(index *IndexInfo) int { + endpoint, err := NewEndpoint(index) + if err != nil { + t.Fatal(err) + } + r, err := NewSession(authConfig, utils.NewHTTPRequestFactory(), endpoint, true) + if err != nil { + t.Fatal(err) + } + return len(r.reqFactory.GetDecorators()) + } + + decorators := getSessionDecorators(makeIndex("/v1/")) + assertEqual(t, decorators, 0, "Expected no decorator on http session") + + decorators = getSessionDecorators(makeHttpsIndex("/v1/")) + assertNotEqual(t, decorators, 0, "Expected decorator on https session") + + decorators = getSessionDecorators(makePublicIndex()) + assertEqual(t, decorators, 0, "Expected no decorator on public session") +} + func TestPingRegistryEndpoint(t *testing.T) { - ep, err := NewEndpoint(makeURL("/v1/"), insecureRegistries) - if err != nil { - t.Fatal(err) + testPing := func(index *IndexInfo, expectedStandalone bool, assertMessage string) { + ep, err := NewEndpoint(index) + if err != nil { + t.Fatal(err) + } + regInfo, err := ep.Ping() + if err != nil { + t.Fatal(err) + } + + assertEqual(t, regInfo.Standalone, expectedStandalone, assertMessage) } - regInfo, err := ep.Ping() - if err != nil { - t.Fatal(err) + + testPing(makeIndex("/v1/"), true, "Expected standalone to be true (default)") + testPing(makeHttpsIndex("/v1/"), true, "Expected standalone to be true (default)") + testPing(makePublicIndex(), false, "Expected standalone to be false for public index") +} + +func TestEndpoint(t *testing.T) { + // Simple wrapper to fail test if err != nil + expandEndpoint := func(index *IndexInfo) *Endpoint { + endpoint, err := NewEndpoint(index) + if err != nil { + t.Fatal(err) + } + return endpoint + } + + assertInsecureIndex := func(index *IndexInfo) { + index.Secure = true + _, err := NewEndpoint(index) + assertNotEqual(t, err, nil, index.Name+": Expected error for insecure index") + assertEqual(t, strings.Contains(err.Error(), "insecure-registry"), true, index.Name+": Expected insecure-registry error for insecure index") + index.Secure = false + } + + assertSecureIndex := func(index *IndexInfo) { + index.Secure = true + _, err := NewEndpoint(index) + assertNotEqual(t, err, nil, index.Name+": Expected cert error for secure index") + assertEqual(t, strings.Contains(err.Error(), "certificate signed by unknown authority"), true, index.Name+": Expected cert error for secure index") + index.Secure = false + } + + index := &IndexInfo{} + index.Name = makeURL("/v1/") + endpoint := expandEndpoint(index) + assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) + if endpoint.Version != APIVersion1 { + t.Fatal("Expected endpoint to be v1") + } + assertInsecureIndex(index) + + index.Name = makeURL("") + endpoint = expandEndpoint(index) + assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/") + if endpoint.Version != APIVersion1 { + t.Fatal("Expected endpoint to be v1") + } + assertInsecureIndex(index) + + httpURL := makeURL("") + index.Name = strings.SplitN(httpURL, "://", 2)[1] + endpoint = expandEndpoint(index) + assertEqual(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/") + if endpoint.Version != APIVersion1 { + t.Fatal("Expected endpoint to be v1") + } + assertInsecureIndex(index) + + index.Name = makeHttpsURL("/v1/") + endpoint = expandEndpoint(index) + assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) + if endpoint.Version != APIVersion1 { + t.Fatal("Expected endpoint to be v1") + } + assertSecureIndex(index) + + index.Name = makeHttpsURL("") + endpoint = expandEndpoint(index) + assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/") + if endpoint.Version != APIVersion1 { + t.Fatal("Expected endpoint to be v1") + } + assertSecureIndex(index) + + httpsURL := makeHttpsURL("") + index.Name = strings.SplitN(httpsURL, "://", 2)[1] + endpoint = expandEndpoint(index) + assertEqual(t, endpoint.String(), httpsURL+"/v1/", index.Name+": Expected endpoint to be "+httpsURL+"/v1/") + if endpoint.Version != APIVersion1 { + t.Fatal("Expected endpoint to be v1") + } + assertSecureIndex(index) + + badEndpoints := []string{ + "http://127.0.0.1/v1/", + "https://127.0.0.1/v1/", + "http://127.0.0.1", + "https://127.0.0.1", + "127.0.0.1", + } + for _, address := range badEndpoints { + index.Name = address + _, err := NewEndpoint(index) + checkNotEqual(t, err, nil, "Expected error while expanding bad endpoint") } - assertEqual(t, regInfo.Standalone, true, "Expected standalone to be true (default)") } func TestGetRemoteHistory(t *testing.T) { @@ -156,30 +279,413 @@ func TestPushImageLayerRegistry(t *testing.T) { } } -func TestResolveRepositoryName(t *testing.T) { - _, _, err := ResolveRepositoryName("https://github.com/docker/docker") - assertEqual(t, err, ErrInvalidRepositoryName, "Expected error invalid repo name") - ep, repo, err := ResolveRepositoryName("fooo/bar") - if err != nil { - t.Fatal(err) +func TestValidateRepositoryName(t *testing.T) { + validRepoNames := []string{ + "docker/docker", + "library/debian", + "debian", + "docker.io/docker/docker", + "docker.io/library/debian", + "docker.io/debian", + "index.docker.io/docker/docker", + "index.docker.io/library/debian", + "index.docker.io/debian", + "127.0.0.1:5000/docker/docker", + "127.0.0.1:5000/library/debian", + "127.0.0.1:5000/debian", + "thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev", + } + invalidRepoNames := []string{ + "https://github.com/docker/docker", + "docker/Docker", + "docker///docker", + "docker.io/docker/Docker", + "docker.io/docker///docker", + "1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", + "docker.io/1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", } - assertEqual(t, ep, IndexServerAddress(), "Expected endpoint to be index server address") - assertEqual(t, repo, "fooo/bar", "Expected resolved repo to be foo/bar") - u := makeURL("")[7:] - ep, repo, err = ResolveRepositoryName(u + "/private/moonbase") - if err != nil { - t.Fatal(err) + for _, name := range invalidRepoNames { + err := ValidateRepositoryName(name) + assertNotEqual(t, err, nil, "Expected invalid repo name: "+name) } - assertEqual(t, ep, u, "Expected endpoint to be "+u) - assertEqual(t, repo, "private/moonbase", "Expected endpoint to be private/moonbase") - ep, repo, err = ResolveRepositoryName("ubuntu-12.04-base") - if err != nil { - t.Fatal(err) + for _, name := range validRepoNames { + err := ValidateRepositoryName(name) + assertEqual(t, err, nil, "Expected valid repo name: "+name) } - assertEqual(t, ep, IndexServerAddress(), "Expected endpoint to be "+IndexServerAddress()) - assertEqual(t, repo, "ubuntu-12.04-base", "Expected endpoint to be ubuntu-12.04-base") + + err := ValidateRepositoryName(invalidRepoNames[0]) + assertEqual(t, err, ErrInvalidRepositoryName, "Expected ErrInvalidRepositoryName: "+invalidRepoNames[0]) +} + +func TestParseRepositoryInfo(t *testing.T) { + expectedRepoInfos := map[string]RepositoryInfo{ + "fooo/bar": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "fooo/bar", + LocalName: "fooo/bar", + CanonicalName: "fooo/bar", + Official: false, + }, + "library/ubuntu": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "library/ubuntu", + LocalName: "ubuntu", + CanonicalName: "ubuntu", + Official: true, + }, + "nonlibrary/ubuntu": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "nonlibrary/ubuntu", + LocalName: "nonlibrary/ubuntu", + CanonicalName: "nonlibrary/ubuntu", + Official: false, + }, + "ubuntu": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "library/ubuntu", + LocalName: "ubuntu", + CanonicalName: "ubuntu", + Official: true, + }, + "other/library": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "other/library", + LocalName: "other/library", + CanonicalName: "other/library", + Official: false, + }, + "127.0.0.1:8000/private/moonbase": { + Index: &IndexInfo{ + Name: "127.0.0.1:8000", + Official: false, + }, + RemoteName: "private/moonbase", + LocalName: "127.0.0.1:8000/private/moonbase", + CanonicalName: "127.0.0.1:8000/private/moonbase", + Official: false, + }, + "127.0.0.1:8000/privatebase": { + Index: &IndexInfo{ + Name: "127.0.0.1:8000", + Official: false, + }, + RemoteName: "privatebase", + LocalName: "127.0.0.1:8000/privatebase", + CanonicalName: "127.0.0.1:8000/privatebase", + Official: false, + }, + "localhost:8000/private/moonbase": { + Index: &IndexInfo{ + Name: "localhost:8000", + Official: false, + }, + RemoteName: "private/moonbase", + LocalName: "localhost:8000/private/moonbase", + CanonicalName: "localhost:8000/private/moonbase", + Official: false, + }, + "localhost:8000/privatebase": { + Index: &IndexInfo{ + Name: "localhost:8000", + Official: false, + }, + RemoteName: "privatebase", + LocalName: "localhost:8000/privatebase", + CanonicalName: "localhost:8000/privatebase", + Official: false, + }, + "example.com/private/moonbase": { + Index: &IndexInfo{ + Name: "example.com", + Official: false, + }, + RemoteName: "private/moonbase", + LocalName: "example.com/private/moonbase", + CanonicalName: "example.com/private/moonbase", + Official: false, + }, + "example.com/privatebase": { + Index: &IndexInfo{ + Name: "example.com", + Official: false, + }, + RemoteName: "privatebase", + LocalName: "example.com/privatebase", + CanonicalName: "example.com/privatebase", + Official: false, + }, + "example.com:8000/private/moonbase": { + Index: &IndexInfo{ + Name: "example.com:8000", + Official: false, + }, + RemoteName: "private/moonbase", + LocalName: "example.com:8000/private/moonbase", + CanonicalName: "example.com:8000/private/moonbase", + Official: false, + }, + "example.com:8000/privatebase": { + Index: &IndexInfo{ + Name: "example.com:8000", + Official: false, + }, + RemoteName: "privatebase", + LocalName: "example.com:8000/privatebase", + CanonicalName: "example.com:8000/privatebase", + Official: false, + }, + "localhost/private/moonbase": { + Index: &IndexInfo{ + Name: "localhost", + Official: false, + }, + RemoteName: "private/moonbase", + LocalName: "localhost/private/moonbase", + CanonicalName: "localhost/private/moonbase", + Official: false, + }, + "localhost/privatebase": { + Index: &IndexInfo{ + Name: "localhost", + Official: false, + }, + RemoteName: "privatebase", + LocalName: "localhost/privatebase", + CanonicalName: "localhost/privatebase", + Official: false, + }, + IndexServerName() + "/public/moonbase": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "public/moonbase", + LocalName: "public/moonbase", + CanonicalName: "public/moonbase", + Official: false, + }, + "index." + IndexServerName() + "/public/moonbase": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "public/moonbase", + LocalName: "public/moonbase", + CanonicalName: "public/moonbase", + Official: false, + }, + IndexServerName() + "/public/moonbase": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "public/moonbase", + LocalName: "public/moonbase", + CanonicalName: "public/moonbase", + Official: false, + }, + "ubuntu-12.04-base": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "ubuntu-12.04-base", + Official: true, + }, + IndexServerName() + "/ubuntu-12.04-base": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "ubuntu-12.04-base", + Official: true, + }, + IndexServerName() + "/ubuntu-12.04-base": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "ubuntu-12.04-base", + Official: true, + }, + "index." + IndexServerName() + "/ubuntu-12.04-base": { + Index: &IndexInfo{ + Name: IndexServerName(), + Official: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "ubuntu-12.04-base", + Official: true, + }, + } + + for reposName, expectedRepoInfo := range expectedRepoInfos { + repoInfo, err := ParseRepositoryInfo(reposName) + if err != nil { + t.Error(err) + } else { + checkEqual(t, repoInfo.Index.Name, expectedRepoInfo.Index.Name, reposName) + checkEqual(t, repoInfo.RemoteName, expectedRepoInfo.RemoteName, reposName) + checkEqual(t, repoInfo.LocalName, expectedRepoInfo.LocalName, reposName) + checkEqual(t, repoInfo.CanonicalName, expectedRepoInfo.CanonicalName, reposName) + checkEqual(t, repoInfo.Index.Official, expectedRepoInfo.Index.Official, reposName) + checkEqual(t, repoInfo.Official, expectedRepoInfo.Official, reposName) + } + } +} + +func TestNewIndexInfo(t *testing.T) { + testIndexInfo := func(config *ServiceConfig, expectedIndexInfos map[string]*IndexInfo) { + for indexName, expectedIndexInfo := range expectedIndexInfos { + index, err := NewIndexInfo(config, indexName) + if err != nil { + t.Fatal(err) + } else { + checkEqual(t, index.Name, expectedIndexInfo.Name, indexName+" name") + checkEqual(t, index.Official, expectedIndexInfo.Official, indexName+" is official") + checkEqual(t, index.Secure, expectedIndexInfo.Secure, indexName+" is secure") + checkEqual(t, len(index.Mirrors), len(expectedIndexInfo.Mirrors), indexName+" mirrors") + } + } + } + + config := NewServiceConfig(nil) + noMirrors := make([]string, 0) + expectedIndexInfos := map[string]*IndexInfo{ + IndexServerName(): { + Name: IndexServerName(), + Official: true, + Secure: true, + Mirrors: noMirrors, + }, + "index." + IndexServerName(): { + Name: IndexServerName(), + Official: true, + Secure: true, + Mirrors: noMirrors, + }, + "example.com": { + Name: "example.com", + Official: false, + Secure: true, + Mirrors: noMirrors, + }, + "127.0.0.1:5000": { + Name: "127.0.0.1:5000", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + } + testIndexInfo(config, expectedIndexInfos) + + publicMirrors := []string{"http://mirror1.local", "http://mirror2.local"} + config = makeServiceConfig(publicMirrors, []string{"example.com"}) + + expectedIndexInfos = map[string]*IndexInfo{ + IndexServerName(): { + Name: IndexServerName(), + Official: true, + Secure: true, + Mirrors: publicMirrors, + }, + "index." + IndexServerName(): { + Name: IndexServerName(), + Official: true, + Secure: true, + Mirrors: publicMirrors, + }, + "example.com": { + Name: "example.com", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "example.com:5000": { + Name: "example.com:5000", + Official: false, + Secure: true, + Mirrors: noMirrors, + }, + "127.0.0.1": { + Name: "127.0.0.1", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "127.0.0.1:5000": { + Name: "127.0.0.1:5000", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "other.com": { + Name: "other.com", + Official: false, + Secure: true, + Mirrors: noMirrors, + }, + } + testIndexInfo(config, expectedIndexInfos) + + config = makeServiceConfig(nil, []string{"42.42.0.0/16"}) + expectedIndexInfos = map[string]*IndexInfo{ + "example.com": { + Name: "example.com", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "example.com:5000": { + Name: "example.com:5000", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "127.0.0.1": { + Name: "127.0.0.1", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "127.0.0.1:5000": { + Name: "127.0.0.1:5000", + Official: false, + Secure: false, + Mirrors: noMirrors, + }, + "other.com": { + Name: "other.com", + Official: false, + Secure: true, + Mirrors: noMirrors, + }, + } + testIndexInfo(config, expectedIndexInfos) } func TestPushRegistryTag(t *testing.T) { @@ -232,7 +738,7 @@ func TestSearchRepositories(t *testing.T) { assertEqual(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' a ot hae 42 stars") } -func TestValidRepositoryName(t *testing.T) { +func TestValidRemoteName(t *testing.T) { validRepositoryNames := []string{ // Sanity check. "docker/docker", @@ -247,7 +753,7 @@ func TestValidRepositoryName(t *testing.T) { "____/____", } for _, repositoryName := range validRepositoryNames { - if err := validateRepositoryName(repositoryName); err != nil { + if err := validateRemoteName(repositoryName); err != nil { t.Errorf("Repository name should be valid: %v. Error: %v", repositoryName, err) } } @@ -277,7 +783,7 @@ func TestValidRepositoryName(t *testing.T) { "docker/", } for _, repositoryName := range invalidRepositoryNames { - if err := validateRepositoryName(repositoryName); err == nil { + if err := validateRemoteName(repositoryName); err == nil { t.Errorf("Repository name should be invalid: %v", repositoryName) } } @@ -350,13 +856,13 @@ func TestAddRequiredHeadersToRedirectedRequests(t *testing.T) { } } -func TestIsSecure(t *testing.T) { +func TestIsSecureIndex(t *testing.T) { tests := []struct { addr string insecureRegistries []string expected bool }{ - {IndexServerURL.Host, nil, true}, + {IndexServerName(), nil, true}, {"example.com", []string{}, true}, {"example.com", []string{"example.com"}, false}, {"localhost", []string{"localhost:5000"}, false}, @@ -383,10 +889,9 @@ func TestIsSecure(t *testing.T) { {"invalid.domain.com:5000", []string{"invalid.domain.com:5000"}, false}, } for _, tt := range tests { - // TODO: remove this once we remove localhost insecure by default - insecureRegistries := append(tt.insecureRegistries, "127.0.0.0/8") - if sec, err := isSecure(tt.addr, insecureRegistries); err != nil || sec != tt.expected { - t.Fatalf("isSecure failed for %q %v, expected %v got %v. Error: %v", tt.addr, insecureRegistries, tt.expected, sec, err) + config := makeServiceConfig(nil, tt.insecureRegistries) + if sec := config.isSecureIndex(tt.addr); sec != tt.expected { + t.Errorf("isSecureIndex failed for %q %v, expected %v got %v", tt.addr, tt.insecureRegistries, tt.expected, sec) } } } diff --git a/registry/service.go b/registry/service.go index 53e8278b04..310539c4f5 100644 --- a/registry/service.go +++ b/registry/service.go @@ -13,14 +13,14 @@ import ( // 'pull': Download images from any registry (TODO) // 'push': Upload images to any registry (TODO) type Service struct { - insecureRegistries []string + Config *ServiceConfig } // NewService returns a new instance of Service ready to be // installed no an engine. -func NewService(insecureRegistries []string) *Service { +func NewService(options *Options) *Service { return &Service{ - insecureRegistries: insecureRegistries, + Config: NewServiceConfig(options), } } @@ -28,6 +28,9 @@ func NewService(insecureRegistries []string) *Service { func (s *Service) Install(eng *engine.Engine) error { eng.Register("auth", s.Auth) eng.Register("search", s.Search) + eng.Register("resolve_repository", s.ResolveRepository) + eng.Register("resolve_index", s.ResolveIndex) + eng.Register("registry_config", s.GetRegistryConfig) return nil } @@ -39,15 +42,18 @@ func (s *Service) Auth(job *engine.Job) engine.Status { job.GetenvJson("authConfig", authConfig) - if addr := authConfig.ServerAddress; addr != "" && addr != IndexServerAddress() { - endpoint, err := NewEndpoint(addr, s.insecureRegistries) + if authConfig.ServerAddress != "" { + index, err := ResolveIndexInfo(job, authConfig.ServerAddress) if err != nil { return job.Error(err) } - if _, err := endpoint.Ping(); err != nil { - return job.Error(err) + if !index.Official { + endpoint, err := NewEndpoint(index) + if err != nil { + return job.Error(err) + } + authConfig.ServerAddress = endpoint.String() } - authConfig.ServerAddress = endpoint.String() } status, err := Login(authConfig, HTTPRequestFactory(nil)) @@ -87,12 +93,12 @@ func (s *Service) Search(job *engine.Job) engine.Status { job.GetenvJson("authConfig", authConfig) job.GetenvJson("metaHeaders", metaHeaders) - hostname, term, err := ResolveRepositoryName(term) + repoInfo, err := ResolveRepositoryInfo(job, term) if err != nil { return job.Error(err) } - - endpoint, err := NewEndpoint(hostname, s.insecureRegistries) + // *TODO: Search multiple indexes. + endpoint, err := repoInfo.GetEndpoint() if err != nil { return job.Error(err) } @@ -100,7 +106,7 @@ func (s *Service) Search(job *engine.Job) engine.Status { if err != nil { return job.Error(err) } - results, err := r.SearchRepositories(term) + results, err := r.SearchRepositories(repoInfo.GetSearchTerm()) if err != nil { return job.Error(err) } @@ -116,3 +122,92 @@ func (s *Service) Search(job *engine.Job) engine.Status { } return engine.StatusOK } + +// ResolveRepository splits a repository name into its components +// and configuration of the associated registry. +func (s *Service) ResolveRepository(job *engine.Job) engine.Status { + var ( + reposName = job.Args[0] + ) + + repoInfo, err := NewRepositoryInfo(s.Config, reposName) + if err != nil { + return job.Error(err) + } + + out := engine.Env{} + err = out.SetJson("repository", repoInfo) + if err != nil { + return job.Error(err) + } + out.WriteTo(job.Stdout) + + return engine.StatusOK +} + +// Convenience wrapper for calling resolve_repository Job from a running job. +func ResolveRepositoryInfo(jobContext *engine.Job, reposName string) (*RepositoryInfo, error) { + job := jobContext.Eng.Job("resolve_repository", reposName) + env, err := job.Stdout.AddEnv() + if err != nil { + return nil, err + } + if err := job.Run(); err != nil { + return nil, err + } + info := RepositoryInfo{} + if err := env.GetJson("repository", &info); err != nil { + return nil, err + } + return &info, nil +} + +// ResolveIndex takes indexName and returns index info +func (s *Service) ResolveIndex(job *engine.Job) engine.Status { + var ( + indexName = job.Args[0] + ) + + index, err := NewIndexInfo(s.Config, indexName) + if err != nil { + return job.Error(err) + } + + out := engine.Env{} + err = out.SetJson("index", index) + if err != nil { + return job.Error(err) + } + out.WriteTo(job.Stdout) + + return engine.StatusOK +} + +// Convenience wrapper for calling resolve_index Job from a running job. +func ResolveIndexInfo(jobContext *engine.Job, indexName string) (*IndexInfo, error) { + job := jobContext.Eng.Job("resolve_index", indexName) + env, err := job.Stdout.AddEnv() + if err != nil { + return nil, err + } + if err := job.Run(); err != nil { + return nil, err + } + info := IndexInfo{} + if err := env.GetJson("index", &info); err != nil { + return nil, err + } + return &info, nil +} + +// GetRegistryConfig returns current registry configuration. +func (s *Service) GetRegistryConfig(job *engine.Job) engine.Status { + out := engine.Env{} + err := out.SetJson("config", s.Config) + if err != nil { + return job.Error(err) + } + out.WriteTo(job.Stdout) + + return engine.StatusOK +} diff --git a/registry/types.go b/registry/types.go index 3b429f19af..fbbc0e7098 100644 --- a/registry/types.go +++ b/registry/types.go @@ -65,3 +65,44 @@ const ( APIVersion1 = iota + 1 APIVersion2 ) + +// RepositoryInfo Examples: +// { +// "Index" : { +// "Name" : "docker.io", +// "Mirrors" : ["https://registry-2.docker.io/v1/", "https://registry-3.docker.io/v1/"], +// "Secure" : true, +// "Official" : true, +// }, +// "RemoteName" : "library/debian", +// "LocalName" : "debian", +// "CanonicalName" : "docker.io/debian" +// "Official" : true, +// } + +// { +// "Index" : { +// "Name" : "127.0.0.1:5000", +// "Mirrors" : [], +// "Secure" : false, +// "Official" : false, +// }, +// "RemoteName" : "user/repo", +// "LocalName" : "127.0.0.1:5000/user/repo", +// "CanonicalName" : "127.0.0.1:5000/user/repo", +// "Official" : false, +// } +type IndexInfo struct { + Name string + Mirrors []string + Secure bool + Official bool +} + +type RepositoryInfo struct { + Index *IndexInfo + RemoteName string + LocalName string + CanonicalName string + Official bool +} diff --git a/utils/http.go b/utils/http.go index bcf1865e2e..24eaea56bc 100644 --- a/utils/http.go +++ b/utils/http.go @@ -134,6 +134,10 @@ func (self *HTTPRequestFactory) AddDecorator(d ...HTTPRequestDecorator) { self.decorators = append(self.decorators, d...) } +func (self *HTTPRequestFactory) GetDecorators() []HTTPRequestDecorator { + return self.decorators +} + // NewRequest() creates a new *http.Request, // applies all decorators in the HTTPRequestFactory on the request, // then applies decorators provided by d on the request.