diff --git a/api_test.go b/api_test.go index 41f3de695..15ba0ca6f 100644 --- a/api_test.go +++ b/api_test.go @@ -195,6 +195,32 @@ func TestManifestAPI(t *testing.T) { t.Fatalf("expected manifest unknown error: got %v", respErrs) } + tagsURL, err := builder.buildTagsURL(imageName) + if err != nil { + t.Fatalf("unexpected error building tags url: %v", err) + } + + resp, err = http.Get(tagsURL) + if err != nil { + t.Fatalf("unexpected error getting unknown tags: %v", err) + } + defer resp.Body.Close() + + // Check that we get an unknown repository error when asking for tags + checkResponse(t, "getting unknown manifest tags", resp, http.StatusNotFound) + dec = json.NewDecoder(resp.Body) + if err := dec.Decode(&respErrs); err != nil { + t.Fatalf("unexpected error decoding error response: %v", err) + } + + if len(respErrs.Errors) == 0 { + t.Fatalf("expected errors in response") + } + + if respErrs.Errors[0].Code != ErrorCodeUnknownRepository { + t.Fatalf("expected respository unknown error: got %v", respErrs) + } + // -------------------------------- // Attempt to push unsigned manifest with missing layers unsignedManifest := &storage.Manifest{ @@ -300,6 +326,35 @@ func TestManifestAPI(t *testing.T) { if !bytes.Equal(fetchedManifest.Raw, signedManifest.Raw) { t.Fatalf("manifests do not match") } + + // Ensure that the tag is listed. + resp, err = http.Get(tagsURL) + if err != nil { + t.Fatalf("unexpected error getting unknown tags: %v", err) + } + defer resp.Body.Close() + + // Check that we get an unknown repository error when asking for tags + checkResponse(t, "getting unknown manifest tags", resp, http.StatusOK) + dec = json.NewDecoder(resp.Body) + + var tagsResponse tagsAPIResponse + + if err := dec.Decode(&tagsResponse); err != nil { + t.Fatalf("unexpected error decoding error response: %v", err) + } + + if tagsResponse.Name != imageName { + t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName) + } + + if len(tagsResponse.Tags) != 1 { + t.Fatalf("expected some tags in response: %v", tagsResponse.Tags) + } + + if tagsResponse.Tags[0] != tag { + t.Fatalf("tag not as expected: %q != %q", tagsResponse.Tags[0], tag) + } } func putManifest(t *testing.T, msg, url string, v interface{}) *http.Response { diff --git a/errors.go b/errors.go index 9593741db..17758f44c 100644 --- a/errors.go +++ b/errors.go @@ -34,6 +34,9 @@ const ( // match the provided tag. ErrorCodeInvalidTag + // ErrorCodeUnknownRepository when the repository name is not known. + ErrorCodeUnknownRepository + // ErrorCodeUnknownManifest returned when image manifest name and tag is // unknown, accompanied by a 404 status. ErrorCodeUnknownManifest @@ -64,6 +67,7 @@ var errorCodeStrings = map[ErrorCode]string{ ErrorCodeInvalidLength: "INVALID_LENGTH", ErrorCodeInvalidName: "INVALID_NAME", ErrorCodeInvalidTag: "INVALID_TAG", + ErrorCodeUnknownRepository: "UNKNOWN_REPOSITORY", ErrorCodeUnknownManifest: "UNKNOWN_MANIFEST", ErrorCodeInvalidManifest: "INVALID_MANIFEST", ErrorCodeUnverifiedManifest: "UNVERIFIED_MANIFEST", @@ -78,6 +82,7 @@ var errorCodesMessages = map[ErrorCode]string{ ErrorCodeInvalidLength: "provided length did not match content length", ErrorCodeInvalidName: "manifest name did not match URI", ErrorCodeInvalidTag: "manifest tag did not match URI", + ErrorCodeUnknownRepository: "repository not known to registry", ErrorCodeUnknownManifest: "manifest not known", ErrorCodeInvalidManifest: "manifest is invalid", ErrorCodeUnverifiedManifest: "manifest failed signature validation", diff --git a/storage/manifest.go b/storage/manifest.go index daeaa39b3..88782c533 100644 --- a/storage/manifest.go +++ b/storage/manifest.go @@ -3,49 +3,12 @@ package storage import ( "crypto/x509" "encoding/json" - "fmt" - "strings" "github.com/Sirupsen/logrus" - - "github.com/docker/libtrust" - "github.com/docker/docker-registry/digest" + "github.com/docker/libtrust" ) -// ErrUnknownManifest is returned if the manifest is not known by the -// registry. -type ErrUnknownManifest struct { - Name string - Tag string -} - -func (err ErrUnknownManifest) Error() string { - return fmt.Sprintf("unknown manifest name=%s tag=%s", err.Name, err.Tag) -} - -// ErrManifestUnverified is returned when the registry is unable to verify -// the manifest. -type ErrManifestUnverified struct{} - -func (ErrManifestUnverified) Error() string { - return fmt.Sprintf("unverified manifest") -} - -// ErrManifestVerification provides a type to collect errors encountered -// during manifest verification. Currently, it accepts errors of all types, -// but it may be narrowed to those involving manifest verification. -type ErrManifestVerification []error - -func (errs ErrManifestVerification) Error() string { - var parts []string - for _, err := range errs { - parts = append(parts, err.Error()) - } - - return fmt.Sprintf("errors verifying manifest: %v", strings.Join(parts, ",")) -} - // Versioned provides a struct with just the manifest schemaVersion. Incoming // content with unknown schema version can be decoded against this struct to // check the version. diff --git a/storage/manifest_test.go b/storage/manifest_test.go index e45179437..ea634df8d 100644 --- a/storage/manifest_test.go +++ b/storage/manifest_test.go @@ -99,6 +99,20 @@ func TestManifestStorage(t *testing.T) { if !reflect.DeepEqual(fetchedManifest, sm) { t.Fatalf("fetched manifest not equal: %#v != %#v", fetchedManifest, sm) } + + // Grabs the tags and check that this tagged manifest is present + tags, err := ms.Tags(name) + if err != nil { + t.Fatalf("unexpected error fetching tags: %v", err) + } + + if len(tags) != 1 { + t.Fatalf("unexpected tags returned: %v", tags) + } + + if tags[0] != tag { + t.Fatalf("unexpected tag found in tags: %v != %v", tags, []string{tag}) + } } type layerKey struct { diff --git a/storage/manifeststore.go b/storage/manifeststore.go index ebbc6b3c7..69a48d5f7 100644 --- a/storage/manifeststore.go +++ b/storage/manifeststore.go @@ -3,11 +3,56 @@ package storage import ( "encoding/json" "fmt" + "path" + "strings" "github.com/docker/docker-registry/storagedriver" "github.com/docker/libtrust" ) +// ErrUnknownRepository is returned if the named repository is not known by +// the registry. +type ErrUnknownRepository struct { + Name string +} + +func (err ErrUnknownRepository) Error() string { + return fmt.Sprintf("unknown respository name=%s", err.Name) +} + +// ErrUnknownManifest is returned if the manifest is not known by the +// registry. +type ErrUnknownManifest struct { + Name string + Tag string +} + +func (err ErrUnknownManifest) Error() string { + return fmt.Sprintf("unknown manifest name=%s tag=%s", err.Name, err.Tag) +} + +// ErrManifestUnverified is returned when the registry is unable to verify +// the manifest. +type ErrManifestUnverified struct{} + +func (ErrManifestUnverified) Error() string { + return fmt.Sprintf("unverified manifest") +} + +// ErrManifestVerification provides a type to collect errors encountered +// during manifest verification. Currently, it accepts errors of all types, +// but it may be narrowed to those involving manifest verification. +type ErrManifestVerification []error + +func (errs ErrManifestVerification) Error() string { + var parts []string + for _, err := range errs { + parts = append(parts, err.Error()) + } + + return fmt.Sprintf("errors verifying manifest: %v", strings.Join(parts, ",")) +} + type manifestStore struct { driver storagedriver.StorageDriver pathMapper *pathMapper @@ -16,6 +61,34 @@ type manifestStore struct { var _ ManifestService = &manifestStore{} +func (ms *manifestStore) Tags(name string) ([]string, error) { + p, err := ms.pathMapper.path(manifestTagsPath{ + name: name, + }) + if err != nil { + return nil, err + } + + var tags []string + entries, err := ms.driver.List(p) + if err != nil { + switch err := err.(type) { + case storagedriver.PathNotFoundError: + return nil, ErrUnknownRepository{Name: name} + default: + return nil, err + } + } + + for _, entry := range entries { + _, filename := path.Split(entry) + + tags = append(tags, filename) + } + + return tags, nil +} + func (ms *manifestStore) Exists(name, tag string) (bool, error) { p, err := ms.path(name, tag) if err != nil { diff --git a/storage/paths.go b/storage/paths.go index ecc3dd32a..a3538b855 100644 --- a/storage/paths.go +++ b/storage/paths.go @@ -64,6 +64,8 @@ func (pm *pathMapper) path(spec pathSpec) (string, error) { repoPrefix := append(rootPrefix, "repositories") switch v := spec.(type) { + case manifestTagsPath: + return path.Join(append(repoPrefix, v.name, "manifests")...), nil case manifestPathSpec: // TODO(sday): May need to store manifest by architecture. return path.Join(append(repoPrefix, v.name, "manifests", v.tag)...), nil @@ -109,6 +111,14 @@ type pathSpec interface { pathSpec() } +// manifestTagsPath describes the path elements required to point to the +// directory with all manifest tags under the repository. +type manifestTagsPath struct { + name string +} + +func (manifestTagsPath) pathSpec() {} + // manifestPathSpec describes the path elements used to build a manifest path. // The contents should be a signed manifest json file. type manifestPathSpec struct { diff --git a/storage/services.go b/storage/services.go index 1f6d5e51a..da6d88c5d 100644 --- a/storage/services.go +++ b/storage/services.go @@ -52,6 +52,9 @@ func (ss *Services) Manifests() ManifestService { // ManifestService provides operations on image manifests. type ManifestService interface { + // Tags lists the tags under the named repository. + Tags(name string) ([]string, error) + // Exists returns true if the layer exists. Exists(name, tag string) (bool, error) diff --git a/tags.go b/tags.go index d8cea3d3e..4916c1513 100644 --- a/tags.go +++ b/tags.go @@ -1,8 +1,10 @@ package registry import ( + "encoding/json" "net/http" + "github.com/docker/docker-registry/storage" "github.com/gorilla/handlers" ) @@ -22,7 +24,34 @@ type tagsHandler struct { *Context } +type tagsAPIResponse struct { + Name string `json:"name"` + Tags []string `json:"tags"` +} + // GetTags returns a json list of tags for a specific image name. func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) { - // TODO(stevvooe): Implement this method. + defer r.Body.Close() + manifests := th.services.Manifests() + + tags, err := manifests.Tags(th.Name) + if err != nil { + switch err := err.(type) { + case storage.ErrUnknownRepository: + w.WriteHeader(404) + th.Errors.Push(ErrorCodeUnknownRepository, map[string]string{"name": th.Name}) + default: + th.Errors.PushErr(err) + } + return + } + + enc := json.NewEncoder(w) + if err := enc.Encode(tagsAPIResponse{ + Name: th.Name, + Tags: tags, + }); err != nil { + th.Errors.PushErr(err) + return + } } diff --git a/urls.go b/urls.go index d9e77f5e8..8f34a5b1e 100644 --- a/urls.go +++ b/urls.go @@ -39,6 +39,20 @@ func newURLBuilderFromString(root string) (*urlBuilder, error) { return newURLBuilder(u), nil } +func (ub *urlBuilder) buildTagsURL(name string) (string, error) { + route := clonedRoute(ub.router, routeNameTags) + + tagsURL, err := route. + Schemes(ub.url.Scheme). + Host(ub.url.Host). + URL("name", name) + if err != nil { + return "", err + } + + return tagsURL.String(), nil +} + func (ub *urlBuilder) forManifest(m *storage.Manifest) (string, error) { return ub.buildManifestURL(m.Name, m.Tag) }