From c71089c653eb50a878bdcc7fd5ec7dd8f339a836 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Tue, 9 Dec 2014 11:06:51 -0800 Subject: [PATCH 1/6] Implement Tags method on ManifestService --- storage/manifest.go | 10 ++++++++++ storage/manifest_test.go | 14 ++++++++++++++ storage/manifeststore.go | 30 ++++++++++++++++++++++++++++++ storage/paths.go | 10 ++++++++++ storage/services.go | 3 +++ 5 files changed, 67 insertions(+) diff --git a/storage/manifest.go b/storage/manifest.go index daeaa39b..6c506244 100644 --- a/storage/manifest.go +++ b/storage/manifest.go @@ -13,6 +13,16 @@ import ( "github.com/docker/docker-registry/digest" ) +// 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 { diff --git a/storage/manifest_test.go b/storage/manifest_test.go index e4517943..ea634df8 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 ebbc6b3c..33b16390 100644 --- a/storage/manifeststore.go +++ b/storage/manifeststore.go @@ -3,6 +3,7 @@ package storage import ( "encoding/json" "fmt" + "path" "github.com/docker/docker-registry/storagedriver" "github.com/docker/libtrust" @@ -16,6 +17,35 @@ 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 { + logrus.Infof("%#v", err) + 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 ecc3dd32..a3538b85 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 1f6d5e51..da6d88c5 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) From 6cbd22c5f0844f8d35789f2d98b3815b0481081d Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Tue, 9 Dec 2014 13:38:07 -0800 Subject: [PATCH 2/6] Implement tags HTTP API handler --- tags.go | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/tags.go b/tags.go index d8cea3d3..4916c151 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 + } } From 49d13f9a083e01838242476853ec4c752bd658db Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Tue, 9 Dec 2014 13:40:44 -0800 Subject: [PATCH 3/6] Move manifest store errors to where they happen --- storage/manifest.go | 49 +--------------------------------------- storage/manifeststore.go | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 48 deletions(-) diff --git a/storage/manifest.go b/storage/manifest.go index 6c506244..88782c53 100644 --- a/storage/manifest.go +++ b/storage/manifest.go @@ -3,59 +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" ) -// 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, ",")) -} - // 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/manifeststore.go b/storage/manifeststore.go index 33b16390..c7833a7c 100644 --- a/storage/manifeststore.go +++ b/storage/manifeststore.go @@ -4,11 +4,55 @@ 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 From 33b2b80a8c01c7c6c066ad5a182fd58519ae42e7 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Tue, 9 Dec 2014 14:19:07 -0800 Subject: [PATCH 4/6] Remove errant log message --- storage/manifeststore.go | 1 - 1 file changed, 1 deletion(-) diff --git a/storage/manifeststore.go b/storage/manifeststore.go index c7833a7c..69a48d5f 100644 --- a/storage/manifeststore.go +++ b/storage/manifeststore.go @@ -72,7 +72,6 @@ func (ms *manifestStore) Tags(name string) ([]string, error) { var tags []string entries, err := ms.driver.List(p) if err != nil { - logrus.Infof("%#v", err) switch err := err.(type) { case storagedriver.PathNotFoundError: return nil, ErrUnknownRepository{Name: name} From 10e5276c0ec89f2cb349bb8fe8421ae03f0af72a Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Tue, 9 Dec 2014 14:19:38 -0800 Subject: [PATCH 5/6] Add error code for unknown repository --- errors.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/errors.go b/errors.go index 9593741d..17758f44 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", From 722ca3584118a18786898bde42f9368b3749efaf Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Tue, 9 Dec 2014 15:36:26 -0800 Subject: [PATCH 6/6] Test Tags HTTP API methods during manifest upload --- api_test.go | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++ urls.go | 14 ++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/api_test.go b/api_test.go index 41f3de69..15ba0ca6 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/urls.go b/urls.go index d9e77f5e..8f34a5b1 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) }