diff --git a/docs/config.go b/docs/config.go index 7cac71584..8d7962f8d 100644 --- a/docs/config.go +++ b/docs/config.go @@ -9,7 +9,7 @@ import ( "strings" "github.com/docker/distribution/reference" - "github.com/docker/docker/image" + "github.com/docker/docker/image/v1" "github.com/docker/docker/opts" flag "github.com/docker/docker/pkg/mflag" ) @@ -216,18 +216,15 @@ func ValidateIndexName(val string) (string, error) { return val, nil } -func validateRemoteName(remoteName string) error { - - if !strings.Contains(remoteName, "/") { - +func validateRemoteName(remoteName reference.Named) error { + remoteNameStr := remoteName.Name() + if !strings.Contains(remoteNameStr, "/") { // the repository name must not be a valid image ID - if err := image.ValidateID(remoteName); err == nil { + if err := v1.ValidateID(remoteNameStr); err == nil { return fmt.Errorf("Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", remoteName) } } - - _, err := reference.WithName(remoteName) - return err + return nil } func validateNoSchema(reposName string) error { @@ -239,27 +236,24 @@ func validateNoSchema(reposName string) error { } // ValidateRepositoryName validates a repository name -func ValidateRepositoryName(reposName string) error { - _, _, err := loadRepositoryName(reposName, true) +func ValidateRepositoryName(reposName reference.Named) error { + _, _, err := loadRepositoryName(reposName) return err } // loadRepositoryName returns the repo name splitted into index name // and remote repo name. It returns an error if the name is not valid. -func loadRepositoryName(reposName string, checkRemoteName bool) (string, string, error) { - if err := validateNoSchema(reposName); err != nil { - return "", "", err +func loadRepositoryName(reposName reference.Named) (string, reference.Named, error) { + if err := validateNoSchema(reposName.Name()); err != nil { + return "", nil, err } - indexName, remoteName := splitReposName(reposName) + indexName, remoteName, err := splitReposName(reposName) - var err error if indexName, err = ValidateIndexName(indexName); err != nil { - return "", "", err + return "", nil, err } - if checkRemoteName { - if err = validateRemoteName(remoteName); err != nil { - return "", "", err - } + if err = validateRemoteName(remoteName); err != nil { + return "", nil, err } return indexName, remoteName, nil } @@ -297,31 +291,36 @@ func (index *IndexInfo) GetAuthConfigKey() string { } // 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") { +func splitReposName(reposName reference.Named) (indexName string, remoteName reference.Named, err error) { + var remoteNameStr string + indexName, remoteNameStr = reference.SplitHostname(reposName) + if indexName == "" || (!strings.Contains(indexName, ".") && + !strings.Contains(indexName, ":") && indexName != "localhost") { // This is a Docker Index repos (ex: samalba/hipache or ubuntu) // 'docker.io' indexName = IndexName remoteName = reposName } else { - indexName = nameParts[0] - remoteName = nameParts[1] + remoteName, err = reference.WithName(remoteNameStr) } - return indexName, remoteName + return } // NewRepositoryInfo validates and breaks down a repository name into a RepositoryInfo -func (config *ServiceConfig) NewRepositoryInfo(reposName string, bySearch bool) (*RepositoryInfo, error) { - indexName, remoteName, err := loadRepositoryName(reposName, !bySearch) - if err != nil { +func (config *ServiceConfig) NewRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) { + if err := validateNoSchema(reposName.Name()); err != nil { return nil, err } - repoInfo := &RepositoryInfo{ - RemoteName: remoteName, + repoInfo := &RepositoryInfo{} + var ( + indexName string + err error + ) + + indexName, repoInfo.RemoteName, err = loadRepositoryName(reposName) + if err != nil { + return nil, err } repoInfo.Index, err = config.NewIndexInfo(indexName) @@ -330,46 +329,47 @@ func (config *ServiceConfig) NewRepositoryInfo(reposName string, bySearch bool) } if repoInfo.Index.Official { - normalizedName := normalizeLibraryRepoName(repoInfo.RemoteName) + repoInfo.LocalName, err = normalizeLibraryRepoName(repoInfo.RemoteName) + if err != nil { + return nil, err + } + repoInfo.RemoteName = repoInfo.LocalName - 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 { + if strings.IndexRune(repoInfo.RemoteName.Name(), '/') == -1 { repoInfo.Official = true // Fix up remote name for official repos. - repoInfo.RemoteName = "library/" + normalizedName + repoInfo.RemoteName, err = reference.WithName("library/" + repoInfo.RemoteName.Name()) + if err != nil { + return nil, err + } } - repoInfo.CanonicalName = "docker.io/" + repoInfo.RemoteName + repoInfo.CanonicalName, err = reference.WithName("docker.io/" + repoInfo.RemoteName.Name()) + if err != nil { + return nil, err + } } else { - repoInfo.LocalName = localNameFromRemote(repoInfo.Index.Name, repoInfo.RemoteName) + repoInfo.LocalName, err = localNameFromRemote(repoInfo.Index.Name, repoInfo.RemoteName) + if err != nil { + return nil, err + } 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, false) +func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) { + return emptyServiceConfig.NewRepositoryInfo(reposName) } -// ParseIndexInfo will use repository name to get back an indexInfo. -func ParseIndexInfo(reposName string) (*IndexInfo, error) { - indexName, _ := splitReposName(reposName) +// ParseSearchIndexInfo will use repository name to get back an indexInfo. +func ParseSearchIndexInfo(reposName string) (*IndexInfo, error) { + indexName, _ := splitReposSearchTerm(reposName) indexInfo, err := emptyServiceConfig.NewIndexInfo(indexName) if err != nil { @@ -378,12 +378,12 @@ func ParseIndexInfo(reposName string) (*IndexInfo, error) { return indexInfo, nil } -// NormalizeLocalName transforms a repository name into a normalize LocalName +// NormalizeLocalName transforms a repository name into a normalized LocalName // Passes through the name without transformation on error (image id, etc) // It does not use the repository info because we don't want to load // the repository index and do request over the network. -func NormalizeLocalName(name string) string { - indexName, remoteName, err := loadRepositoryName(name, true) +func NormalizeLocalName(name reference.Named) reference.Named { + indexName, remoteName, err := loadRepositoryName(name) if err != nil { return name } @@ -395,23 +395,52 @@ func NormalizeLocalName(name string) string { } if officialIndex { - return normalizeLibraryRepoName(remoteName) + localName, err := normalizeLibraryRepoName(remoteName) + if err != nil { + return name + } + return localName } - return localNameFromRemote(indexName, remoteName) + localName, err := localNameFromRemote(indexName, remoteName) + if err != nil { + return name + } + return localName } // normalizeLibraryRepoName removes the library prefix from // the repository name for official repos. -func normalizeLibraryRepoName(name string) string { - if strings.HasPrefix(name, "library/") { +func normalizeLibraryRepoName(name reference.Named) (reference.Named, error) { + if strings.HasPrefix(name.Name(), "library/") { // If pull "library/foo", it's stored locally under "foo" - name = strings.SplitN(name, "/", 2)[1] + return reference.WithName(strings.SplitN(name.Name(), "/", 2)[1]) } - return name + return name, nil } // localNameFromRemote combines the index name and the repo remote name // to generate a repo local name. -func localNameFromRemote(indexName, remoteName string) string { - return indexName + "/" + remoteName +func localNameFromRemote(indexName string, remoteName reference.Named) (reference.Named, error) { + return reference.WithName(indexName + "/" + remoteName.Name()) +} + +// NormalizeLocalReference transforms a reference to use a normalized LocalName +// for the name poriton. Passes through the reference without transformation on +// error. +func NormalizeLocalReference(ref reference.Named) reference.Named { + localName := NormalizeLocalName(ref) + if tagged, isTagged := ref.(reference.Tagged); isTagged { + newRef, err := reference.WithTag(localName, tagged.Tag()) + if err != nil { + return ref + } + return newRef + } else if digested, isDigested := ref.(reference.Digested); isDigested { + newRef, err := reference.WithDigest(localName, digested.Digest()) + if err != nil { + return ref + } + return newRef + } + return localName } diff --git a/docs/registry_mock_test.go b/docs/registry_mock_test.go index fb19e577d..3c75dea6d 100644 --- a/docs/registry_mock_test.go +++ b/docs/registry_mock_test.go @@ -15,6 +15,7 @@ import ( "testing" "time" + "github.com/docker/distribution/reference" "github.com/docker/docker/opts" "github.com/gorilla/mux" @@ -349,15 +350,19 @@ func handlerGetDeleteTags(w http.ResponseWriter, r *http.Request) { if !requiresAuth(w, r) { return } - repositoryName := mux.Vars(r)["repository"] + repositoryName, err := reference.WithName(mux.Vars(r)["repository"]) + if err != nil { + apiError(w, "Could not parse repository", 400) + return + } repositoryName = NormalizeLocalName(repositoryName) - tags, exists := testRepositories[repositoryName] + tags, exists := testRepositories[repositoryName.String()] if !exists { apiError(w, "Repository not found", 404) return } if r.Method == "DELETE" { - delete(testRepositories, repositoryName) + delete(testRepositories, repositoryName.String()) writeResponse(w, true, 200) return } @@ -369,10 +374,14 @@ func handlerGetTag(w http.ResponseWriter, r *http.Request) { return } vars := mux.Vars(r) - repositoryName := vars["repository"] + repositoryName, err := reference.WithName(vars["repository"]) + if err != nil { + apiError(w, "Could not parse repository", 400) + return + } repositoryName = NormalizeLocalName(repositoryName) tagName := vars["tag"] - tags, exists := testRepositories[repositoryName] + tags, exists := testRepositories[repositoryName.String()] if !exists { apiError(w, "Repository not found", 404) return @@ -390,13 +399,17 @@ func handlerPutTag(w http.ResponseWriter, r *http.Request) { return } vars := mux.Vars(r) - repositoryName := vars["repository"] + repositoryName, err := reference.WithName(vars["repository"]) + if err != nil { + apiError(w, "Could not parse repository", 400) + return + } repositoryName = NormalizeLocalName(repositoryName) tagName := vars["tag"] - tags, exists := testRepositories[repositoryName] + tags, exists := testRepositories[repositoryName.String()] if !exists { - tags := make(map[string]string) - testRepositories[repositoryName] = tags + tags = make(map[string]string) + testRepositories[repositoryName.String()] = tags } tagValue := "" readJSON(r, tagValue) diff --git a/docs/registry_test.go b/docs/registry_test.go index 7714310d9..2bc1edff7 100644 --- a/docs/registry_test.go +++ b/docs/registry_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/client/transport" "github.com/docker/docker/cliconfig" ) @@ -214,13 +215,21 @@ func TestGetRemoteImageLayer(t *testing.T) { func TestGetRemoteTag(t *testing.T) { r := spawnTestRegistrySession(t) - tag, err := r.GetRemoteTag([]string{makeURL("/v1/")}, REPO, "test") + repoRef, err := reference.ParseNamed(REPO) + if err != nil { + t.Fatal(err) + } + tag, err := r.GetRemoteTag([]string{makeURL("/v1/")}, repoRef, "test") if err != nil { t.Fatal(err) } assertEqual(t, tag, imageID, "Expected tag test to map to "+imageID) - _, err = r.GetRemoteTag([]string{makeURL("/v1/")}, "foo42/baz", "foo") + bazRef, err := reference.ParseNamed("foo42/baz") + if err != nil { + t.Fatal(err) + } + _, err = r.GetRemoteTag([]string{makeURL("/v1/")}, bazRef, "foo") if err != ErrRepoNotFound { t.Fatal("Expected ErrRepoNotFound error when fetching tag for bogus repo") } @@ -228,7 +237,11 @@ func TestGetRemoteTag(t *testing.T) { func TestGetRemoteTags(t *testing.T) { r := spawnTestRegistrySession(t) - tags, err := r.GetRemoteTags([]string{makeURL("/v1/")}, REPO) + repoRef, err := reference.ParseNamed(REPO) + if err != nil { + t.Fatal(err) + } + tags, err := r.GetRemoteTags([]string{makeURL("/v1/")}, repoRef) if err != nil { t.Fatal(err) } @@ -236,7 +249,11 @@ func TestGetRemoteTags(t *testing.T) { assertEqual(t, tags["latest"], imageID, "Expected tag latest to map to "+imageID) assertEqual(t, tags["test"], imageID, "Expected tag test to map to "+imageID) - _, err = r.GetRemoteTags([]string{makeURL("/v1/")}, "foo42/baz") + bazRef, err := reference.ParseNamed("foo42/baz") + if err != nil { + t.Fatal(err) + } + _, err = r.GetRemoteTags([]string{makeURL("/v1/")}, bazRef) if err != ErrRepoNotFound { t.Fatal("Expected ErrRepoNotFound error when fetching tags for bogus repo") } @@ -249,7 +266,11 @@ func TestGetRepositoryData(t *testing.T) { t.Fatal(err) } host := "http://" + parsedURL.Host + "/v1/" - data, err := r.GetRepositoryData("foo42/bar") + repoRef, err := reference.ParseNamed(REPO) + if err != nil { + t.Fatal(err) + } + data, err := r.GetRepositoryData(repoRef) if err != nil { t.Fatal(err) } @@ -315,29 +336,41 @@ func TestValidateRepositoryName(t *testing.T) { } for _, name := range invalidRepoNames { - err := ValidateRepositoryName(name) - assertNotEqual(t, err, nil, "Expected invalid repo name: "+name) + named, err := reference.WithName(name) + if err == nil { + err := ValidateRepositoryName(named) + assertNotEqual(t, err, nil, "Expected invalid repo name: "+name) + } } for _, name := range validRepoNames { - err := ValidateRepositoryName(name) + named, err := reference.WithName(name) + if err != nil { + t.Fatalf("could not parse valid name: %s", name) + } + err = ValidateRepositoryName(named) 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) { + withName := func(name string) reference.Named { + named, err := reference.WithName(name) + if err != nil { + t.Fatalf("could not parse reference %s", name) + } + return named + } + expectedRepoInfos := map[string]RepositoryInfo{ "fooo/bar": { Index: &IndexInfo{ Name: IndexName, Official: true, }, - RemoteName: "fooo/bar", - LocalName: "fooo/bar", - CanonicalName: "docker.io/fooo/bar", + RemoteName: withName("fooo/bar"), + LocalName: withName("fooo/bar"), + CanonicalName: withName("docker.io/fooo/bar"), Official: false, }, "library/ubuntu": { @@ -345,9 +378,9 @@ func TestParseRepositoryInfo(t *testing.T) { Name: IndexName, Official: true, }, - RemoteName: "library/ubuntu", - LocalName: "ubuntu", - CanonicalName: "docker.io/library/ubuntu", + RemoteName: withName("library/ubuntu"), + LocalName: withName("ubuntu"), + CanonicalName: withName("docker.io/library/ubuntu"), Official: true, }, "nonlibrary/ubuntu": { @@ -355,9 +388,9 @@ func TestParseRepositoryInfo(t *testing.T) { Name: IndexName, Official: true, }, - RemoteName: "nonlibrary/ubuntu", - LocalName: "nonlibrary/ubuntu", - CanonicalName: "docker.io/nonlibrary/ubuntu", + RemoteName: withName("nonlibrary/ubuntu"), + LocalName: withName("nonlibrary/ubuntu"), + CanonicalName: withName("docker.io/nonlibrary/ubuntu"), Official: false, }, "ubuntu": { @@ -365,9 +398,9 @@ func TestParseRepositoryInfo(t *testing.T) { Name: IndexName, Official: true, }, - RemoteName: "library/ubuntu", - LocalName: "ubuntu", - CanonicalName: "docker.io/library/ubuntu", + RemoteName: withName("library/ubuntu"), + LocalName: withName("ubuntu"), + CanonicalName: withName("docker.io/library/ubuntu"), Official: true, }, "other/library": { @@ -375,9 +408,9 @@ func TestParseRepositoryInfo(t *testing.T) { Name: IndexName, Official: true, }, - RemoteName: "other/library", - LocalName: "other/library", - CanonicalName: "docker.io/other/library", + RemoteName: withName("other/library"), + LocalName: withName("other/library"), + CanonicalName: withName("docker.io/other/library"), Official: false, }, "127.0.0.1:8000/private/moonbase": { @@ -385,9 +418,9 @@ func TestParseRepositoryInfo(t *testing.T) { 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", + RemoteName: withName("private/moonbase"), + LocalName: withName("127.0.0.1:8000/private/moonbase"), + CanonicalName: withName("127.0.0.1:8000/private/moonbase"), Official: false, }, "127.0.0.1:8000/privatebase": { @@ -395,9 +428,9 @@ func TestParseRepositoryInfo(t *testing.T) { Name: "127.0.0.1:8000", Official: false, }, - RemoteName: "privatebase", - LocalName: "127.0.0.1:8000/privatebase", - CanonicalName: "127.0.0.1:8000/privatebase", + RemoteName: withName("privatebase"), + LocalName: withName("127.0.0.1:8000/privatebase"), + CanonicalName: withName("127.0.0.1:8000/privatebase"), Official: false, }, "localhost:8000/private/moonbase": { @@ -405,9 +438,9 @@ func TestParseRepositoryInfo(t *testing.T) { Name: "localhost:8000", Official: false, }, - RemoteName: "private/moonbase", - LocalName: "localhost:8000/private/moonbase", - CanonicalName: "localhost:8000/private/moonbase", + RemoteName: withName("private/moonbase"), + LocalName: withName("localhost:8000/private/moonbase"), + CanonicalName: withName("localhost:8000/private/moonbase"), Official: false, }, "localhost:8000/privatebase": { @@ -415,9 +448,9 @@ func TestParseRepositoryInfo(t *testing.T) { Name: "localhost:8000", Official: false, }, - RemoteName: "privatebase", - LocalName: "localhost:8000/privatebase", - CanonicalName: "localhost:8000/privatebase", + RemoteName: withName("privatebase"), + LocalName: withName("localhost:8000/privatebase"), + CanonicalName: withName("localhost:8000/privatebase"), Official: false, }, "example.com/private/moonbase": { @@ -425,9 +458,9 @@ func TestParseRepositoryInfo(t *testing.T) { Name: "example.com", Official: false, }, - RemoteName: "private/moonbase", - LocalName: "example.com/private/moonbase", - CanonicalName: "example.com/private/moonbase", + RemoteName: withName("private/moonbase"), + LocalName: withName("example.com/private/moonbase"), + CanonicalName: withName("example.com/private/moonbase"), Official: false, }, "example.com/privatebase": { @@ -435,9 +468,9 @@ func TestParseRepositoryInfo(t *testing.T) { Name: "example.com", Official: false, }, - RemoteName: "privatebase", - LocalName: "example.com/privatebase", - CanonicalName: "example.com/privatebase", + RemoteName: withName("privatebase"), + LocalName: withName("example.com/privatebase"), + CanonicalName: withName("example.com/privatebase"), Official: false, }, "example.com:8000/private/moonbase": { @@ -445,9 +478,9 @@ func TestParseRepositoryInfo(t *testing.T) { Name: "example.com:8000", Official: false, }, - RemoteName: "private/moonbase", - LocalName: "example.com:8000/private/moonbase", - CanonicalName: "example.com:8000/private/moonbase", + RemoteName: withName("private/moonbase"), + LocalName: withName("example.com:8000/private/moonbase"), + CanonicalName: withName("example.com:8000/private/moonbase"), Official: false, }, "example.com:8000/privatebase": { @@ -455,9 +488,9 @@ func TestParseRepositoryInfo(t *testing.T) { Name: "example.com:8000", Official: false, }, - RemoteName: "privatebase", - LocalName: "example.com:8000/privatebase", - CanonicalName: "example.com:8000/privatebase", + RemoteName: withName("privatebase"), + LocalName: withName("example.com:8000/privatebase"), + CanonicalName: withName("example.com:8000/privatebase"), Official: false, }, "localhost/private/moonbase": { @@ -465,9 +498,9 @@ func TestParseRepositoryInfo(t *testing.T) { Name: "localhost", Official: false, }, - RemoteName: "private/moonbase", - LocalName: "localhost/private/moonbase", - CanonicalName: "localhost/private/moonbase", + RemoteName: withName("private/moonbase"), + LocalName: withName("localhost/private/moonbase"), + CanonicalName: withName("localhost/private/moonbase"), Official: false, }, "localhost/privatebase": { @@ -475,9 +508,9 @@ func TestParseRepositoryInfo(t *testing.T) { Name: "localhost", Official: false, }, - RemoteName: "privatebase", - LocalName: "localhost/privatebase", - CanonicalName: "localhost/privatebase", + RemoteName: withName("privatebase"), + LocalName: withName("localhost/privatebase"), + CanonicalName: withName("localhost/privatebase"), Official: false, }, IndexName + "/public/moonbase": { @@ -485,9 +518,9 @@ func TestParseRepositoryInfo(t *testing.T) { Name: IndexName, Official: true, }, - RemoteName: "public/moonbase", - LocalName: "public/moonbase", - CanonicalName: "docker.io/public/moonbase", + RemoteName: withName("public/moonbase"), + LocalName: withName("public/moonbase"), + CanonicalName: withName("docker.io/public/moonbase"), Official: false, }, "index." + IndexName + "/public/moonbase": { @@ -495,9 +528,9 @@ func TestParseRepositoryInfo(t *testing.T) { Name: IndexName, Official: true, }, - RemoteName: "public/moonbase", - LocalName: "public/moonbase", - CanonicalName: "docker.io/public/moonbase", + RemoteName: withName("public/moonbase"), + LocalName: withName("public/moonbase"), + CanonicalName: withName("docker.io/public/moonbase"), Official: false, }, "ubuntu-12.04-base": { @@ -505,9 +538,9 @@ func TestParseRepositoryInfo(t *testing.T) { Name: IndexName, Official: true, }, - RemoteName: "library/ubuntu-12.04-base", - LocalName: "ubuntu-12.04-base", - CanonicalName: "docker.io/library/ubuntu-12.04-base", + RemoteName: withName("library/ubuntu-12.04-base"), + LocalName: withName("ubuntu-12.04-base"), + CanonicalName: withName("docker.io/library/ubuntu-12.04-base"), Official: true, }, IndexName + "/ubuntu-12.04-base": { @@ -515,9 +548,9 @@ func TestParseRepositoryInfo(t *testing.T) { Name: IndexName, Official: true, }, - RemoteName: "library/ubuntu-12.04-base", - LocalName: "ubuntu-12.04-base", - CanonicalName: "docker.io/library/ubuntu-12.04-base", + RemoteName: withName("library/ubuntu-12.04-base"), + LocalName: withName("ubuntu-12.04-base"), + CanonicalName: withName("docker.io/library/ubuntu-12.04-base"), Official: true, }, "index." + IndexName + "/ubuntu-12.04-base": { @@ -525,22 +558,27 @@ func TestParseRepositoryInfo(t *testing.T) { Name: IndexName, Official: true, }, - RemoteName: "library/ubuntu-12.04-base", - LocalName: "ubuntu-12.04-base", - CanonicalName: "docker.io/library/ubuntu-12.04-base", + RemoteName: withName("library/ubuntu-12.04-base"), + LocalName: withName("ubuntu-12.04-base"), + CanonicalName: withName("docker.io/library/ubuntu-12.04-base"), Official: true, }, } for reposName, expectedRepoInfo := range expectedRepoInfos { - repoInfo, err := ParseRepositoryInfo(reposName) + named, err := reference.WithName(reposName) + if err != nil { + t.Error(err) + } + + repoInfo, err := ParseRepositoryInfo(named) 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.RemoteName.String(), expectedRepoInfo.RemoteName.String(), reposName) + checkEqual(t, repoInfo.LocalName.String(), expectedRepoInfo.LocalName.String(), reposName) + checkEqual(t, repoInfo.CanonicalName.String(), expectedRepoInfo.CanonicalName.String(), reposName) checkEqual(t, repoInfo.Index.Official, expectedRepoInfo.Index.Official, reposName) checkEqual(t, repoInfo.Official, expectedRepoInfo.Official, reposName) } @@ -687,8 +725,11 @@ func TestMirrorEndpointLookup(t *testing.T) { return false } s := Service{Config: makeServiceConfig([]string{"my.mirror"}, nil)} - imageName := IndexName + "/test/image" + imageName, err := reference.WithName(IndexName + "/test/image") + if err != nil { + t.Error(err) + } pushAPIEndpoints, err := s.LookupPushEndpoints(imageName) if err != nil { t.Fatal(err) @@ -708,7 +749,11 @@ func TestMirrorEndpointLookup(t *testing.T) { func TestPushRegistryTag(t *testing.T) { r := spawnTestRegistrySession(t) - err := r.PushRegistryTag("foo42/bar", imageID, "stable", makeURL("/v1/")) + repoRef, err := reference.ParseNamed(REPO) + if err != nil { + t.Fatal(err) + } + err = r.PushRegistryTag(repoRef, imageID, "stable", makeURL("/v1/")) if err != nil { t.Fatal(err) } @@ -726,14 +771,18 @@ func TestPushImageJSONIndex(t *testing.T) { Checksum: "sha256:bea7bf2e4bacd479344b737328db47b18880d09096e6674165533aa994f5e9f2", }, } - repoData, err := r.PushImageJSONIndex("foo42/bar", imgData, false, nil) + repoRef, err := reference.ParseNamed(REPO) + if err != nil { + t.Fatal(err) + } + repoData, err := r.PushImageJSONIndex(repoRef, imgData, false, nil) if err != nil { t.Fatal(err) } if repoData == nil { t.Fatal("Expected RepositoryData object") } - repoData, err = r.PushImageJSONIndex("foo42/bar", imgData, true, []string{r.indexEndpoint.String()}) + repoData, err = r.PushImageJSONIndex(repoRef, imgData, true, []string{r.indexEndpoint.String()}) if err != nil { t.Fatal(err) } @@ -781,7 +830,11 @@ func TestValidRemoteName(t *testing.T) { "dock__er/docker", } for _, repositoryName := range validRepositoryNames { - if err := validateRemoteName(repositoryName); err != nil { + repositoryRef, err := reference.WithName(repositoryName) + if err != nil { + t.Errorf("Repository name should be valid: %v. Error: %v", repositoryName, err) + } + if err := validateRemoteName(repositoryRef); err != nil { t.Errorf("Repository name should be valid: %v. Error: %v", repositoryName, err) } } @@ -818,7 +871,11 @@ func TestValidRemoteName(t *testing.T) { "this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255/docker", } for _, repositoryName := range invalidRepositoryNames { - if err := validateRemoteName(repositoryName); err == nil { + repositoryRef, err := reference.ParseNamed(repositoryName) + if err != nil { + continue + } + if err := validateRemoteName(repositoryRef); err == nil { t.Errorf("Repository name should be invalid: %v", repositoryName) } } diff --git a/docs/service.go b/docs/service.go index 6ac930d6e..1ef968278 100644 --- a/docs/service.go +++ b/docs/service.go @@ -4,7 +4,9 @@ import ( "crypto/tls" "net/http" "net/url" + "strings" + "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/client/auth" "github.com/docker/docker/cliconfig" ) @@ -51,17 +53,39 @@ func (s *Service) Auth(authConfig *cliconfig.AuthConfig) (string, error) { return Login(authConfig, endpoint) } +// splitReposSearchTerm breaks a search term into an index name and remote name +func splitReposSearchTerm(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 = IndexName + remoteName = reposName + } else { + indexName = nameParts[0] + remoteName = nameParts[1] + } + return indexName, remoteName +} + // Search queries the public registry for images matching the specified // search terms, and returns the results. func (s *Service) Search(term string, authConfig *cliconfig.AuthConfig, headers map[string][]string) (*SearchResults, error) { + if err := validateNoSchema(term); err != nil { + return nil, err + } - repoInfo, err := s.ResolveRepositoryBySearch(term) + indexName, remoteName := splitReposSearchTerm(term) + + index, err := s.Config.NewIndexInfo(indexName) if err != nil { return nil, err } // *TODO: Search multiple indexes. - endpoint, err := NewEndpoint(repoInfo.Index, http.Header(headers), APIVersionUnknown) + endpoint, err := NewEndpoint(index, http.Header(headers), APIVersionUnknown) if err != nil { return nil, err } @@ -70,19 +94,23 @@ func (s *Service) Search(term string, authConfig *cliconfig.AuthConfig, headers if err != nil { return nil, err } - return r.SearchRepositories(repoInfo.GetSearchTerm()) + + if index.Official { + localName := remoteName + if strings.HasPrefix(localName, "library/") { + // If pull "library/foo", it's stored locally under "foo" + localName = strings.SplitN(localName, "/", 2)[1] + } + + return r.SearchRepositories(localName) + } + return r.SearchRepositories(remoteName) } // ResolveRepository splits a repository name into its components // and configuration of the associated registry. -func (s *Service) ResolveRepository(name string) (*RepositoryInfo, error) { - return s.Config.NewRepositoryInfo(name, false) -} - -// ResolveRepositoryBySearch splits a repository name into its components -// and configuration of the associated registry. -func (s *Service) ResolveRepositoryBySearch(name string) (*RepositoryInfo, error) { - return s.Config.NewRepositoryInfo(name, true) +func (s *Service) ResolveRepository(name reference.Named) (*RepositoryInfo, error) { + return s.Config.NewRepositoryInfo(name) } // ResolveIndex takes indexName and returns index info @@ -123,14 +151,14 @@ func (s *Service) tlsConfigForMirror(mirror string) (*tls.Config, error) { // LookupPullEndpoints creates an list of endpoints to try to pull from, in order of preference. // It gives preference to v2 endpoints over v1, mirrors over the actual // registry, and HTTPS over plain HTTP. -func (s *Service) LookupPullEndpoints(repoName string) (endpoints []APIEndpoint, err error) { +func (s *Service) LookupPullEndpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) { return s.lookupEndpoints(repoName) } // LookupPushEndpoints creates an list of endpoints to try to push to, in order of preference. // It gives preference to v2 endpoints over v1, and HTTPS over plain HTTP. // Mirrors are not included. -func (s *Service) LookupPushEndpoints(repoName string) (endpoints []APIEndpoint, err error) { +func (s *Service) LookupPushEndpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) { allEndpoints, err := s.lookupEndpoints(repoName) if err == nil { for _, endpoint := range allEndpoints { @@ -142,7 +170,7 @@ func (s *Service) LookupPushEndpoints(repoName string) (endpoints []APIEndpoint, return endpoints, err } -func (s *Service) lookupEndpoints(repoName string) (endpoints []APIEndpoint, err error) { +func (s *Service) lookupEndpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) { endpoints, err = s.lookupV2Endpoints(repoName) if err != nil { return nil, err diff --git a/docs/service_v1.go b/docs/service_v1.go index ddb78ee60..5fdc1ecec 100644 --- a/docs/service_v1.go +++ b/docs/service_v1.go @@ -4,13 +4,15 @@ import ( "fmt" "strings" + "github.com/docker/distribution/reference" "github.com/docker/docker/pkg/tlsconfig" ) -func (s *Service) lookupV1Endpoints(repoName string) (endpoints []APIEndpoint, err error) { +func (s *Service) lookupV1Endpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) { var cfg = tlsconfig.ServerDefault tlsConfig := &cfg - if strings.HasPrefix(repoName, DefaultNamespace+"/") { + nameString := repoName.Name() + if strings.HasPrefix(nameString, DefaultNamespace+"/") { endpoints = append(endpoints, APIEndpoint{ URL: DefaultV1Registry, Version: APIVersion1, @@ -21,11 +23,11 @@ func (s *Service) lookupV1Endpoints(repoName string) (endpoints []APIEndpoint, e return endpoints, nil } - slashIndex := strings.IndexRune(repoName, '/') + slashIndex := strings.IndexRune(nameString, '/') if slashIndex <= 0 { - return nil, fmt.Errorf("invalid repo name: missing '/': %s", repoName) + return nil, fmt.Errorf("invalid repo name: missing '/': %s", nameString) } - hostname := repoName[:slashIndex] + hostname := nameString[:slashIndex] tlsConfig, err = s.TLSConfig(hostname) if err != nil { diff --git a/docs/service_v2.go b/docs/service_v2.go index 70d5fd710..56a3d2eee 100644 --- a/docs/service_v2.go +++ b/docs/service_v2.go @@ -4,14 +4,16 @@ import ( "fmt" "strings" + "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/client/auth" "github.com/docker/docker/pkg/tlsconfig" ) -func (s *Service) lookupV2Endpoints(repoName string) (endpoints []APIEndpoint, err error) { +func (s *Service) lookupV2Endpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) { var cfg = tlsconfig.ServerDefault tlsConfig := &cfg - if strings.HasPrefix(repoName, DefaultNamespace+"/") { + nameString := repoName.Name() + if strings.HasPrefix(nameString, DefaultNamespace+"/") { // v2 mirrors for _, mirror := range s.Config.Mirrors { mirrorTLSConfig, err := s.tlsConfigForMirror(mirror) @@ -39,11 +41,11 @@ func (s *Service) lookupV2Endpoints(repoName string) (endpoints []APIEndpoint, e return endpoints, nil } - slashIndex := strings.IndexRune(repoName, '/') + slashIndex := strings.IndexRune(nameString, '/') if slashIndex <= 0 { - return nil, fmt.Errorf("invalid repo name: missing '/': %s", repoName) + return nil, fmt.Errorf("invalid repo name: missing '/': %s", nameString) } - hostname := repoName[:slashIndex] + hostname := nameString[:slashIndex] tlsConfig, err = s.TLSConfig(hostname) if err != nil { diff --git a/docs/session.go b/docs/session.go index 2a20d3219..645e5d44b 100644 --- a/docs/session.go +++ b/docs/session.go @@ -20,6 +20,7 @@ import ( "time" "github.com/Sirupsen/logrus" + "github.com/docker/distribution/reference" "github.com/docker/docker/cliconfig" "github.com/docker/docker/pkg/httputils" "github.com/docker/docker/pkg/ioutils" @@ -320,7 +321,9 @@ func (r *Session) GetRemoteImageLayer(imgID, registry string, imgSize int64) (io // repository. It queries each of the registries supplied in the registries // argument, and returns data from the first one that answers the query // successfully. -func (r *Session) GetRemoteTag(registries []string, repository string, askedTag string) (string, error) { +func (r *Session) GetRemoteTag(registries []string, repositoryRef reference.Named, askedTag string) (string, error) { + repository := repositoryRef.Name() + if strings.Count(repository, "/") == 0 { // This will be removed once the registry supports auto-resolution on // the "library" namespace @@ -356,7 +359,9 @@ func (r *Session) GetRemoteTag(registries []string, repository string, askedTag // of the registries supplied in the registries argument, and returns data from // the first one that answers the query successfully. It returns a map with // tag names as the keys and image IDs as the values. -func (r *Session) GetRemoteTags(registries []string, repository string) (map[string]string, error) { +func (r *Session) GetRemoteTags(registries []string, repositoryRef reference.Named) (map[string]string, error) { + repository := repositoryRef.Name() + if strings.Count(repository, "/") == 0 { // This will be removed once the registry supports auto-resolution on // the "library" namespace @@ -408,8 +413,8 @@ func buildEndpointsList(headers []string, indexEp string) ([]string, error) { } // GetRepositoryData returns lists of images and endpoints for the repository -func (r *Session) GetRepositoryData(remote string) (*RepositoryData, error) { - repositoryTarget := fmt.Sprintf("%srepositories/%s/images", r.indexEndpoint.VersionString(1), remote) +func (r *Session) GetRepositoryData(remote reference.Named) (*RepositoryData, error) { + repositoryTarget := fmt.Sprintf("%srepositories/%s/images", r.indexEndpoint.VersionString(1), remote.Name()) logrus.Debugf("[registry] Calling GET %s", repositoryTarget) @@ -443,7 +448,7 @@ func (r *Session) GetRepositoryData(remote string) (*RepositoryData, error) { if err != nil { logrus.Debugf("Error reading response body: %s", err) } - return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to pull repository %s: %q", res.StatusCode, remote, errBody), res) + return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to pull repository %s: %q", res.StatusCode, remote.Name(), errBody), res) } var endpoints []string @@ -595,10 +600,10 @@ func (r *Session) PushImageLayerRegistry(imgID string, layer io.Reader, registry // PushRegistryTag pushes a tag on the registry. // Remote has the format '/ -func (r *Session) PushRegistryTag(remote, revision, tag, registry string) error { +func (r *Session) PushRegistryTag(remote reference.Named, revision, tag, registry string) error { // "jsonify" the string revision = "\"" + revision + "\"" - path := fmt.Sprintf("repositories/%s/tags/%s", remote, tag) + path := fmt.Sprintf("repositories/%s/tags/%s", remote.Name(), tag) req, err := http.NewRequest("PUT", registry+path, strings.NewReader(revision)) if err != nil { @@ -612,13 +617,13 @@ func (r *Session) PushRegistryTag(remote, revision, tag, registry string) error } res.Body.Close() if res.StatusCode != 200 && res.StatusCode != 201 { - return httputils.NewHTTPRequestError(fmt.Sprintf("Internal server error: %d trying to push tag %s on %s", res.StatusCode, tag, remote), res) + return httputils.NewHTTPRequestError(fmt.Sprintf("Internal server error: %d trying to push tag %s on %s", res.StatusCode, tag, remote.Name()), res) } return nil } // PushImageJSONIndex uploads an image list to the repository -func (r *Session) PushImageJSONIndex(remote string, imgList []*ImgData, validate bool, regs []string) (*RepositoryData, error) { +func (r *Session) PushImageJSONIndex(remote reference.Named, imgList []*ImgData, validate bool, regs []string) (*RepositoryData, error) { cleanImgList := []*ImgData{} if validate { for _, elem := range imgList { @@ -638,7 +643,7 @@ func (r *Session) PushImageJSONIndex(remote string, imgList []*ImgData, validate if validate { suffix = "images" } - u := fmt.Sprintf("%srepositories/%s/%s", r.indexEndpoint.VersionString(1), remote, suffix) + u := fmt.Sprintf("%srepositories/%s/%s", r.indexEndpoint.VersionString(1), remote.Name(), suffix) logrus.Debugf("[registry] PUT %s", u) logrus.Debugf("Image list pushed to index:\n%s", imgListJSON) headers := map[string][]string{ @@ -676,7 +681,7 @@ func (r *Session) PushImageJSONIndex(remote string, imgList []*ImgData, validate if err != nil { logrus.Debugf("Error reading response body: %s", err) } - return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to push repository %s: %q", res.StatusCode, remote, errBody), res) + return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to push repository %s: %q", res.StatusCode, remote.Name(), errBody), res) } tokens = res.Header["X-Docker-Token"] logrus.Debugf("Auth token: %v", tokens) @@ -694,7 +699,7 @@ func (r *Session) PushImageJSONIndex(remote string, imgList []*ImgData, validate if err != nil { logrus.Debugf("Error reading response body: %s", err) } - return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to push checksums %s: %q", res.StatusCode, remote, errBody), res) + return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to push checksums %s: %q", res.StatusCode, remote.Name(), errBody), res) } } diff --git a/docs/types.go b/docs/types.go index 09b9d5713..8a201a917 100644 --- a/docs/types.go +++ b/docs/types.go @@ -1,5 +1,9 @@ package registry +import ( + "github.com/docker/distribution/reference" +) + // SearchResult describes a search result returned from a registry type SearchResult struct { // StarCount indicates the number of stars this repository has @@ -126,13 +130,13 @@ type RepositoryInfo struct { Index *IndexInfo // RemoteName is the remote name of the repository, such as // "library/ubuntu-12.04-base" - RemoteName string + RemoteName reference.Named // LocalName is the local name of the repository, such as // "ubuntu-12.04-base" - LocalName string + LocalName reference.Named // CanonicalName is the canonical name of the repository, such as // "docker.io/library/ubuntu-12.04-base" - CanonicalName string + CanonicalName reference.Named // Official indicates whether the repository is considered official. // If the registry is official, and the normalized name does not // contain a '/' (e.g. "foo"), then it is considered an official repo.