From 568f86eb186731b907b659e4ec64bda21c2fe31d Mon Sep 17 00:00:00 2001 From: Don Kjer Date: Tue, 7 Oct 2014 01:54:52 +0000 Subject: [PATCH] Deprecating ResolveRepositoryName Passing RepositoryInfo to ResolveAuthConfig, pullRepository, and pushRepository Moving --registry-mirror configuration to registry config Created resolve_repository job Repo names with 'index.docker.io' or 'docker.io' are now synonymous with omitting an index name. Adding test for RepositoryInfo Adding tests for opts.StringSetOpts and registry.ValidateMirror Fixing search term use of repoInfo Adding integration tests for registry mirror configuration Normalizing LookupImage image name to match LocalName parsing rules Normalizing repository LocalName to avoid multiple references to an official image Removing errorOut use in tests Removing TODO comment gofmt changes golint comments cleanup. renaming RegistryOptions => registry.Options, and RegistryServiceConfig => registry.ServiceConfig Splitting out builtins.Registry and registry.NewService calls Stray whitespace cleanup Moving integration tests for Mirrors and InsecureRegistries into TestNewIndexInfo unit test Factoring out ValidateRepositoryName from NewRepositoryInfo Removing unused IndexServerURL Allowing json marshaling of ServiceConfig. Exposing ServiceConfig in /info Switching to CamelCase for json marshaling PR cleanup; removing 'Is' prefix from boolean members. Removing unneeded json tags. Removing non-cleanup related fix for 'localhost:[port]' in splitReposName Merge fixes for gh9735 Fixing integration test Reapplying #9754 Adding comment on config.IndexConfigs use from isSecureIndex Remove unused error return value from isSecureIndex Signed-off-by: Don Kjer Adding back comment in isSecureIndex Signed-off-by: Don Kjer --- api/client/commands.go | 40 +- api/client/utils.go | 2 +- builder/internals.go | 6 +- builder/job.go | 2 +- daemon/config.go | 12 - daemon/daemon.go | 2 +- daemon/info.go | 10 + docker/daemon.go | 6 +- graph/export.go | 2 + graph/pull.go | 115 ++--- graph/push.go | 49 +- graph/tags.go | 40 +- graph/tags_unit_test.go | 120 +++-- integration-cli/docker_cli_build_test.go | 21 + integration-cli/docker_cli_pull_test.go | 31 ++ integration-cli/docker_cli_tag_test.go | 46 ++ integration/utils_test.go | 10 +- opts/opts.go | 24 +- opts/opts_test.go | 18 +- registry/auth.go | 33 +- registry/auth_test.go | 59 ++- registry/config.go | 126 +++++ registry/config_test.go | 49 ++ registry/endpoint.go | 67 ++- registry/endpoint_test.go | 2 +- registry/registry.go | 165 ++++++- registry/registry_mock_test.go | 97 +++- registry/registry_test.go | 577 +++++++++++++++++++++-- registry/service.go | 119 ++++- registry/types.go | 41 ++ utils/http.go | 4 + 31 files changed, 1510 insertions(+), 385 deletions(-) create mode 100644 registry/config.go create mode 100644 registry/config_test.go 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.