Browse Source

Merge pull request #8456 from lindenlab/cleanup-repository-info

Cleanup: Replace ResolveRepositoryName with RepositoryInfo{}
Tibor Vass 10 years ago
parent
commit
6870bde584

+ 20 - 20
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 = "<user>"
 		}
-		return fmt.Errorf("You cannot push a \"root\" repository. Please rename your repository in <user>/<repo> (ex: %s/%s)", username, name)
+		return fmt.Errorf("You cannot push a \"root\" repository. Please rename your repository to <user>/<repo> (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

+ 5 - 2
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 {
@@ -260,7 +260,10 @@ func (cli *DockerCli) monitorTtySize(id string, isExec bool) error {
 	sigchan := make(chan os.Signal, 1)
 	gosignal.Notify(sigchan, signal.SIGWINCH)
 	go func() {
-		for _ = range sigchan {
+		// This tmp := range..., _ = tmp workaround is needed to
+		// suppress gofmt warnings while still preserve go1.3 compatibility
+		for tmp := range sigchan {
+			_ = tmp
 			cli.resizeTty(id, isExec)
 		}
 	}()

+ 3 - 3
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)

+ 1 - 1
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 {

+ 0 - 12
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 {

+ 1 - 1
daemon/daemon.go

@@ -898,7 +898,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)
 	}

+ 10 - 0
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", &registryConfig); 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())

+ 4 - 2
docker/daemon.go

@@ -19,11 +19,13 @@ import (
 const CanDaemon = true
 
 var (
-	daemonCfg = &daemon.Config{}
+	daemonCfg   = &daemon.Config{}
+	registryCfg = &registry.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)
 	}
 

+ 2 - 0
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 {

+ 49 - 66
graph/pull.go

@@ -85,9 +85,14 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status {
 		sf          = utils.NewStreamFormatter(job.GetenvBool("json"))
 		authConfig  = &registry.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)
-
-	// Resolve the Repository name from fqn to endpoint + name
-	hostname, remoteName, err := registry.ResolveRepositoryName(localName)
-	if err != nil {
-		return job.Error(err)
-	}
+	defer s.poolRemove("pull", repoInfo.LocalName+":"+tag)
 
-	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
 	}
 

+ 24 - 23
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 {
-		return job.Error(err)
-	}
-	defer s.poolRemove("push", localName)
 
-	// Resolve the Repository name from fqn to endpoint + name
-	hostname, remoteName, err := registry.ResolveRepositoryName(localName)
-	if err != nil {
+	if _, err := s.poolAdd("push", repoInfo.LocalName); err != nil {
 		return job.Error(err)
 	}
+	defer s.poolRemove("push", repoInfo.LocalName)
 
-	endpoint, err := registry.NewEndpoint(hostname, s.insecureRegistries)
+	endpoint, err := repoInfo.GetEndpoint()
 	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

+ 13 - 27
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 == "" {

+ 77 - 51
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")
-	}
-
-	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")
-	}
-
-	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")
+	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,
+	}
+
+	privateLookups := []string{
+		testPrivateImageID,
+		testPrivateImageIDShort,
+		testPrivateImageName + ":" + testPrivateImageID,
+		testPrivateImageName + ":" + testPrivateImageIDShort,
+		testPrivateImageName,
+		testPrivateImageName + ":" + DEFAULTTAG,
+	}
+
+	invalidLookups := []string{
+		testOfficialImageName + ":" + "fail",
+		"fail:fail",
+	}
+
+	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(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)
+		}
 	}
 
-	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 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)
-		}
-	}
-}

+ 21 - 0
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")
+}

+ 31 - 0
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")
+}

+ 46 - 0
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)
+	}
+}

+ 9 - 1
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,

+ 1 - 23
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)

+ 17 - 1
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) {

+ 8 - 30
registry/auth.go

@@ -7,7 +7,6 @@ import (
 	"fmt"
 	"io/ioutil"
 	"net/http"
-	"net/url"
 	"os"
 	"path"
 	"strings"
@@ -18,27 +17,12 @@ import (
 const (
 	// Where we store the config file
 	CONFIGFILE = ".dockercfg"
-
-	// Only used for user auth + account creation
-	INDEXSERVER    = "https://index.docker.io/v1/"
-	REGISTRYSERVER = "https://registry-1.docker.io/v1/"
-
-	// 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"`
@@ -52,10 +36,6 @@ type ConfigFile struct {
 	rootPath string
 }
 
-func IndexServerAddress() string {
-	return INDEXSERVER
-}
-
 // create a base64 encoded auth string to store in config
 func encodeAuth(authConfig *AuthConfig) string {
 	authStr := authConfig.Username + ":" + authConfig.Password
@@ -118,6 +98,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 +162,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 +194,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 +218,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 +254,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 +276,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
 		}
 	}

+ 41 - 18
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)
+			}
 		}
 	}
 }

+ 382 - 0
registry/config.go

@@ -0,0 +1,382 @@
+package registry
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net"
+	"net/url"
+	"regexp"
+	"strings"
+
+	"github.com/docker/docker/opts"
+	flag "github.com/docker/docker/pkg/mflag"
+	"github.com/docker/docker/utils"
+)
+
+// Options holds command line options.
+type Options struct {
+	Mirrors            opts.ListOpts
+	InsecureRegistries opts.ListOpts
+}
+
+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 (
+	ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")")
+	emptyServiceConfig       = NewServiceConfig(nil)
+	validNamespaceChars      = regexp.MustCompile(`^([a-z0-9-_]*)$`)
+	validRepo                = regexp.MustCompile(`^([a-z0-9-_.]+)$`)
+)
+
+func IndexServerAddress() string {
+	return INDEXSERVER
+}
+
+func IndexServerName() string {
+	return INDEXNAME
+}
+
+// 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)")
+}
+
+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
+}
+
+// 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 indexName, the latter is considered
+// insecure.
+//
+// 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, isSecureIndex will only try to match hostname to any element
+// of insecureRegistries.
+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(indexName)
+	if err != nil {
+		// 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)
+		if ip != nil {
+			addrs = []net.IP{ip}
+		}
+
+		// if ip == nil, then `host` is neither an IP nor it could be looked up,
+		// either because the index is unreachable, or because the index is behind an HTTP proxy.
+		// So, len(addrs) == 0 and we're not aborting.
+	}
+
+	// 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 (*net.IPNet)(ipnet).Contains(addr) {
+				return false
+			}
+		}
+	}
+
+	return true
+}
+
+// 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
+}
+
+func validateRemoteName(remoteName string) error {
+	var (
+		namespace string
+		name      string
+	)
+	nameParts := strings.SplitN(remoteName, "/", 2)
+	if len(nameParts) < 2 {
+		namespace = "library"
+		name = nameParts[0]
+
+		// the repository name must not be a valid image ID
+		if err := utils.ValidateID(name); err == nil {
+			return fmt.Errorf("Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", name)
+		}
+	} else {
+		namespace = nameParts[0]
+		name = nameParts[1]
+	}
+	if !validNamespaceChars.MatchString(namespace) {
+		return fmt.Errorf("Invalid namespace name (%s). Only [a-z0-9-_] are allowed.", namespace)
+	}
+	if len(namespace) < 4 || len(namespace) > 30 {
+		return fmt.Errorf("Invalid namespace name (%s). Cannot be fewer than 4 or more than 30 characters.", namespace)
+	}
+	if strings.HasPrefix(namespace, "-") || strings.HasSuffix(namespace, "-") {
+		return fmt.Errorf("Invalid namespace name (%s). Cannot begin or end with a hyphen.", namespace)
+	}
+	if strings.Contains(namespace, "--") {
+		return fmt.Errorf("Invalid namespace name (%s). Cannot contain consecutive hyphens.", namespace)
+	}
+	if !validRepo.MatchString(name) {
+		return fmt.Errorf("Invalid repository name (%s), only [a-z0-9-_.] are allowed", name)
+	}
+	return nil
+}
+
+func validateNoSchema(reposName string) error {
+	if strings.Contains(reposName, "://") {
+		// It cannot contain a scheme!
+		return ErrInvalidRepositoryName
+	}
+	return 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)
+}
+
+// NewIndexInfo returns IndexInfo configuration from indexName
+func (config *ServiceConfig) NewIndexInfo(indexName string) (*IndexInfo, error) {
+	var err error
+	indexName, err = ValidateIndexName(indexName)
+	if err != nil {
+		return nil, err
+	}
+
+	// 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
+}
+
+// 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
+}
+
+// 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 (config *ServiceConfig) NewRepositoryInfo(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 = config.NewIndexInfo(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
+}
+
+// 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
+}
+
+// ParseRepositoryInfo performs the breakdown of a repository name into a RepositoryInfo, but
+// lacks registry configuration.
+func ParseRepositoryInfo(reposName string) (*RepositoryInfo, error) {
+	return emptyServiceConfig.NewRepositoryInfo(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
+}

+ 49 - 0
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)
+		}
+	}
+}

+ 10 - 68
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
@@ -155,63 +157,3 @@ func (e Endpoint) Ping() (RegistryInfo, error) {
 	log.Debugf("RegistryInfo.Standalone: %t", info.Standalone)
 	return info, nil
 }
-
-// isSecure returns false if the provided hostname 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
-// insecure.
-//
-// hostname 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
-// of insecureRegistries.
-func isSecure(hostname string, insecureRegistries []string) (bool, error) {
-	if hostname == IndexServerURL.Host {
-		return true, nil
-	}
-
-	host, _, err := net.SplitHostPort(hostname)
-	if err != nil {
-		// assume hostname is of the form `host` without the port and go on.
-		host = hostname
-	}
-	addrs, err := lookupIP(host)
-	if err != nil {
-		ip := net.ParseIP(host)
-		if ip != nil {
-			addrs = []net.IP{ip}
-		}
-
-		// if ip == nil, then `host` is neither an IP nor it could be looked up,
-		// either because the index is unreachable, or because the index is behind an HTTP proxy.
-		// 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
-			}
-
-			// check if the addr falls in the subnet
-			if ipnet.Contains(addr) {
-				return false, nil
-			}
-		}
-	}
-
-	return true, nil
-}

+ 1 - 1
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)
 		}

+ 3 - 68
registry/registry.go

@@ -10,7 +10,6 @@ import (
 	"net/http"
 	"os"
 	"path"
-	"regexp"
 	"strings"
 	"time"
 
@@ -19,12 +18,9 @@ import (
 )
 
 var (
-	ErrAlreadyExists         = errors.New("Image already exists")
-	ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")")
-	ErrDoesNotExist          = errors.New("Image does not exist")
-	errLoginRequired         = errors.New("Authentication is required.")
-	validNamespaceChars      = regexp.MustCompile(`^([a-z0-9-_]*)$`)
-	validRepo                = regexp.MustCompile(`^([a-z0-9-_.]+)$`)
+	ErrAlreadyExists = errors.New("Image already exists")
+	ErrDoesNotExist  = errors.New("Image does not exist")
+	errLoginRequired = errors.New("Authentication is required.")
 )
 
 type TimeoutType uint32
@@ -160,67 +156,6 @@ func doRequest(req *http.Request, jar http.CookieJar, timeout TimeoutType, secur
 	return res, client, err
 }
 
-func validateRepositoryName(repositoryName string) error {
-	var (
-		namespace string
-		name      string
-	)
-	nameParts := strings.SplitN(repositoryName, "/", 2)
-	if len(nameParts) < 2 {
-		namespace = "library"
-		name = nameParts[0]
-
-		// the repository name must not be a valid image ID
-		if err := utils.ValidateID(name); err == nil {
-			return fmt.Errorf("Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", name)
-		}
-	} else {
-		namespace = nameParts[0]
-		name = nameParts[1]
-	}
-	if !validNamespaceChars.MatchString(namespace) {
-		return fmt.Errorf("Invalid namespace name (%s). Only [a-z0-9-_] are allowed.", namespace)
-	}
-	if len(namespace) < 4 || len(namespace) > 30 {
-		return fmt.Errorf("Invalid namespace name (%s). Cannot be fewer than 4 or more than 30 characters.", namespace)
-	}
-	if strings.HasPrefix(namespace, "-") || strings.HasSuffix(namespace, "-") {
-		return fmt.Errorf("Invalid namespace name (%s). Cannot begin or end with a hyphen.", namespace)
-	}
-	if strings.Contains(namespace, "--") {
-		return fmt.Errorf("Invalid namespace name (%s). Cannot contain consecutive hyphens.", namespace)
-	}
-	if !validRepo.MatchString(name) {
-		return fmt.Errorf("Invalid repository name (%s), only [a-z0-9-_.] are allowed", name)
-	}
-	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
-	}
-
-	return hostname, reposName, nil
-}
-
 func trustedLocation(req *http.Request) bool {
 	var (
 		trusteds = []string{"docker.com", "docker.io"}

+ 89 - 8
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 {

+ 541 - 36
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)
+	}
+
+	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 := config.NewIndexInfo(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,
+		},
 	}
-	assertEqual(t, ep, IndexServerAddress(), "Expected endpoint to be "+IndexServerAddress())
-	assertEqual(t, repo, "ubuntu-12.04-base", "Expected endpoint to be ubuntu-12.04-base")
+	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)
 		}
 	}
 }

+ 107 - 12
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 := s.Config.NewRepositoryInfo(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 := s.Config.NewIndexInfo(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
+}

+ 41 - 0
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
+}

+ 4 - 0
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.