summaryrefslogtreecommitdiffstats
path: root/pkg/registry/registry_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/registry/registry_test.go')
-rw-r--r--pkg/registry/registry_test.go609
1 files changed, 609 insertions, 0 deletions
diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go
new file mode 100644
index 0000000..0ee492e
--- /dev/null
+++ b/pkg/registry/registry_test.go
@@ -0,0 +1,609 @@
+// Copyright 2018 Google LLC All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package registry_test
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/registry"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+)
+
+const (
+ weirdIndex = `{
+ "manifests": [
+ {
+ "digest":"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ "mediaType":"application/vnd.oci.image.layer.nondistributable.v1.tar+gzip"
+ },{
+ "digest":"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ "mediaType":"application/xml"
+ },{
+ "digest":"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ "mediaType":"application/vnd.oci.image.manifest.v1+json"
+ }
+ ]
+}`
+)
+
+func sha256String(s string) string {
+ h, _, _ := v1.SHA256(strings.NewReader(s))
+ return h.Hex
+}
+
+func TestCalls(t *testing.T) {
+ tcs := []struct {
+ Description string
+
+ // Request / setup
+ URL string
+ Digests map[string]string
+ Manifests map[string]string
+ BlobStream map[string]string
+ RequestHeader map[string]string
+
+ // Response
+ Code int
+ Header map[string]string
+ Method string
+ Body string // request body to send
+ Want string // response body to expect
+ }{
+ {
+ Description: "/v2 returns 200",
+ Method: "GET",
+ URL: "/v2",
+ Code: http.StatusOK,
+ Header: map[string]string{"Docker-Distribution-API-Version": "registry/2.0"},
+ },
+ {
+ Description: "/v2/ returns 200",
+ Method: "GET",
+ URL: "/v2/",
+ Code: http.StatusOK,
+ Header: map[string]string{"Docker-Distribution-API-Version": "registry/2.0"},
+ },
+ {
+ Description: "/v2/bad returns 404",
+ Method: "GET",
+ URL: "/v2/bad",
+ Code: http.StatusNotFound,
+ Header: map[string]string{"Docker-Distribution-API-Version": "registry/2.0"},
+ },
+ {
+ Description: "GET non existent blob",
+ Method: "GET",
+ URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "HEAD non existent blob",
+ Method: "HEAD",
+ URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "GET bad digest",
+ Method: "GET",
+ URL: "/v2/foo/blobs/sha256:asd",
+ Code: http.StatusBadRequest,
+ },
+ {
+ Description: "HEAD bad digest",
+ Method: "HEAD",
+ URL: "/v2/foo/blobs/sha256:asd",
+ Code: http.StatusBadRequest,
+ },
+ {
+ Description: "bad blob verb",
+ Method: "FOO",
+ URL: "/v2/foo/blobs/sha256:asd",
+ Code: http.StatusBadRequest,
+ },
+ {
+ Description: "GET containerless blob",
+ Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"},
+ Method: "GET",
+ URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ Code: http.StatusOK,
+ Header: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"},
+ Want: "foo",
+ },
+ {
+ Description: "GET blob",
+ Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"},
+ Method: "GET",
+ URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ Code: http.StatusOK,
+ Header: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"},
+ Want: "foo",
+ },
+ {
+ Description: "HEAD blob",
+ Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"},
+ Method: "HEAD",
+ URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ Code: http.StatusOK,
+ Header: map[string]string{
+ "Content-Length": "3",
+ "Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ },
+ },
+ {
+ Description: "DELETE blob",
+ Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"},
+ Method: "DELETE",
+ URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ Code: http.StatusAccepted,
+ },
+ {
+ Description: "blob url with no container",
+ Method: "GET",
+ URL: "/v2/blobs/sha256:asd",
+ Code: http.StatusBadRequest,
+ },
+ {
+ Description: "uploadurl",
+ Method: "POST",
+ URL: "/v2/foo/blobs/uploads",
+ Code: http.StatusAccepted,
+ Header: map[string]string{"Range": "0-0"},
+ },
+ {
+ Description: "uploadurl",
+ Method: "POST",
+ URL: "/v2/foo/blobs/uploads/",
+ Code: http.StatusAccepted,
+ Header: map[string]string{"Range": "0-0"},
+ },
+ {
+ Description: "upload put missing digest",
+ Method: "PUT",
+ URL: "/v2/foo/blobs/uploads/1",
+ Code: http.StatusBadRequest,
+ },
+ {
+ Description: "monolithic upload good digest",
+ Method: "POST",
+ URL: "/v2/foo/blobs/uploads?digest=sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ Code: http.StatusCreated,
+ Body: "foo",
+ Header: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"},
+ },
+ {
+ Description: "monolithic upload bad digest",
+ Method: "POST",
+ URL: "/v2/foo/blobs/uploads?digest=sha256:fake",
+ Code: http.StatusBadRequest,
+ Body: "foo",
+ },
+ {
+ Description: "upload good digest",
+ Method: "PUT",
+ URL: "/v2/foo/blobs/uploads/1?digest=sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ Code: http.StatusCreated,
+ Body: "foo",
+ Header: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"},
+ },
+ {
+ Description: "upload bad digest",
+ Method: "PUT",
+ URL: "/v2/foo/blobs/uploads/1?digest=sha256:baddigest",
+ Code: http.StatusBadRequest,
+ Body: "foo",
+ },
+ {
+ Description: "stream upload",
+ Method: "PATCH",
+ URL: "/v2/foo/blobs/uploads/1",
+ Code: http.StatusNoContent,
+ Body: "foo",
+ Header: map[string]string{
+ "Range": "0-2",
+ "Location": "/v2/foo/blobs/uploads/1",
+ },
+ },
+ {
+ Description: "stream duplicate upload",
+ Method: "PATCH",
+ URL: "/v2/foo/blobs/uploads/1",
+ Code: http.StatusBadRequest,
+ Body: "foo",
+ BlobStream: map[string]string{"1": "foo"},
+ },
+ {
+ Description: "stream finish upload",
+ Method: "PUT",
+ URL: "/v2/foo/blobs/uploads/1?digest=sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ BlobStream: map[string]string{"1": "foo"},
+ Code: http.StatusCreated,
+ Header: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"},
+ },
+ {
+ Description: "get missing manifest",
+ Method: "GET",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "head missing manifest",
+ Method: "HEAD",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "get missing manifest good container",
+ Manifests: map[string]string{"foo/manifests/latest": "foo"},
+ Method: "GET",
+ URL: "/v2/foo/manifests/bar",
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "head missing manifest good container",
+ Manifests: map[string]string{"foo/manifests/latest": "foo"},
+ Method: "HEAD",
+ URL: "/v2/foo/manifests/bar",
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "get manifest by tag",
+ Manifests: map[string]string{"foo/manifests/latest": "foo"},
+ Method: "GET",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusOK,
+ Want: "foo",
+ },
+ {
+ Description: "get manifest by digest",
+ Manifests: map[string]string{"foo/manifests/latest": "foo"},
+ Method: "GET",
+ URL: "/v2/foo/manifests/sha256:" + sha256String("foo"),
+ Code: http.StatusOK,
+ Want: "foo",
+ },
+ {
+ Description: "head manifest",
+ Manifests: map[string]string{"foo/manifests/latest": "foo"},
+ Method: "HEAD",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusOK,
+ },
+ {
+ Description: "create manifest",
+ Method: "PUT",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusCreated,
+ Body: "foo",
+ },
+ {
+ Description: "create index",
+ Method: "PUT",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusCreated,
+ Body: weirdIndex,
+ RequestHeader: map[string]string{
+ "Content-Type": "application/vnd.oci.image.index.v1+json",
+ },
+ Manifests: map[string]string{"foo/manifests/image": "foo"},
+ },
+ {
+ Description: "create index missing child",
+ Method: "PUT",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusNotFound,
+ Body: weirdIndex,
+ RequestHeader: map[string]string{
+ "Content-Type": "application/vnd.oci.image.index.v1+json",
+ },
+ },
+ {
+ Description: "bad index body",
+ Method: "PUT",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusBadRequest,
+ Body: "foo",
+ RequestHeader: map[string]string{
+ "Content-Type": "application/vnd.oci.image.index.v1+json",
+ },
+ },
+ {
+ Description: "bad manifest method",
+ Method: "BAR",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusBadRequest,
+ },
+ {
+ Description: "Chunk upload start",
+ Method: "PATCH",
+ URL: "/v2/foo/blobs/uploads/1",
+ RequestHeader: map[string]string{"Content-Range": "0-3"},
+ Code: http.StatusNoContent,
+ Body: "foo",
+ Header: map[string]string{
+ "Range": "0-2",
+ "Location": "/v2/foo/blobs/uploads/1",
+ },
+ },
+ {
+ Description: "Chunk upload bad content range",
+ Method: "PATCH",
+ URL: "/v2/foo/blobs/uploads/1",
+ RequestHeader: map[string]string{"Content-Range": "0-bar"},
+ Code: http.StatusRequestedRangeNotSatisfiable,
+ Body: "foo",
+ },
+ {
+ Description: "Chunk upload overlaps previous data",
+ Method: "PATCH",
+ URL: "/v2/foo/blobs/uploads/1",
+ BlobStream: map[string]string{"1": "foo"},
+ RequestHeader: map[string]string{"Content-Range": "2-5"},
+ Code: http.StatusRequestedRangeNotSatisfiable,
+ Body: "bar",
+ },
+ {
+ Description: "Chunk upload after previous data",
+ Method: "PATCH",
+ URL: "/v2/foo/blobs/uploads/1",
+ BlobStream: map[string]string{"1": "foo"},
+ RequestHeader: map[string]string{"Content-Range": "3-6"},
+ Code: http.StatusNoContent,
+ Body: "bar",
+ Header: map[string]string{
+ "Range": "0-5",
+ "Location": "/v2/foo/blobs/uploads/1",
+ },
+ },
+ {
+ Description: "DELETE Unknown name",
+ Method: "DELETE",
+ URL: "/v2/test/honk/manifests/latest",
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "DELETE Unknown manifest",
+ Manifests: map[string]string{"honk/manifests/latest": "honk"},
+ Method: "DELETE",
+ URL: "/v2/honk/manifests/tag-honk",
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "DELETE existing manifest",
+ Manifests: map[string]string{"foo/manifests/latest": "foo"},
+ Method: "DELETE",
+ URL: "/v2/foo/manifests/latest",
+ Code: http.StatusAccepted,
+ },
+ {
+ Description: "DELETE existing manifest by digest",
+ Manifests: map[string]string{"foo/manifests/latest": "foo"},
+ Method: "DELETE",
+ URL: "/v2/foo/manifests/sha256:" + sha256String("foo"),
+ Code: http.StatusAccepted,
+ },
+ {
+ Description: "list tags",
+ Manifests: map[string]string{"foo/manifests/latest": "foo", "foo/manifests/tag1": "foo"},
+ Method: "GET",
+ URL: "/v2/foo/tags/list?n=1000",
+ Code: http.StatusOK,
+ Want: `{"name":"foo","tags":["latest","tag1"]}`,
+ },
+ {
+ Description: "limit tags",
+ Manifests: map[string]string{"foo/manifests/latest": "foo", "foo/manifests/tag1": "foo"},
+ Method: "GET",
+ URL: "/v2/foo/tags/list?n=1",
+ Code: http.StatusOK,
+ Want: `{"name":"foo","tags":["latest"]}`,
+ },
+ {
+ Description: "offset tags",
+ Manifests: map[string]string{"foo/manifests/latest": "foo", "foo/manifests/tag1": "foo"},
+ Method: "GET",
+ URL: "/v2/foo/tags/list?last=latest",
+ Code: http.StatusOK,
+ Want: `{"name":"foo","tags":["tag1"]}`,
+ },
+ {
+ Description: "list non existing tags",
+ Method: "GET",
+ URL: "/v2/foo/tags/list?n=1000",
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "list repos",
+ Manifests: map[string]string{"foo/manifests/latest": "foo", "bar/manifests/latest": "bar"},
+ Method: "GET",
+ URL: "/v2/_catalog?n=1000",
+ Code: http.StatusOK,
+ },
+ {
+ Description: "fetch references",
+ Method: "GET",
+ URL: "/v2/foo/referrers/sha256:" + sha256String("foo"),
+ Code: http.StatusOK,
+ Manifests: map[string]string{
+ "foo/manifests/image": "foo",
+ "foo/manifests/points-to-image": "{\"subject\": {\"digest\": \"sha256:" + sha256String("foo") + "\"}}",
+ },
+ },
+ {
+ Description: "fetch references, subject pointing elsewhere",
+ Method: "GET",
+ URL: "/v2/foo/referrers/sha256:" + sha256String("foo"),
+ Code: http.StatusOK,
+ Manifests: map[string]string{
+ "foo/manifests/image": "foo",
+ "foo/manifests/points-to-image": "{\"subject\": {\"digest\": \"sha256:" + sha256String("nonexistant") + "\"}}",
+ },
+ },
+ {
+ Description: "fetch references, no results",
+ Method: "GET",
+ URL: "/v2/foo/referrers/sha256:" + sha256String("foo"),
+ Code: http.StatusOK,
+ Manifests: map[string]string{
+ "foo/manifests/image": "foo",
+ },
+ },
+ {
+ Description: "fetch references, missing repo",
+ Method: "GET",
+ URL: "/v2/does-not-exist/referrers/sha256:" + sha256String("foo"),
+ Code: http.StatusNotFound,
+ },
+ {
+ Description: "fetch references, bad target (tag vs. digest)",
+ Method: "GET",
+ URL: "/v2/foo/referrers/latest",
+ Code: http.StatusBadRequest,
+ },
+ {
+ Description: "fetch references, bad method",
+ Method: "POST",
+ URL: "/v2/foo/referrers/sha256:" + sha256String("foo"),
+ Code: http.StatusBadRequest,
+ },
+ }
+
+ for _, tc := range tcs {
+
+ var logger *log.Logger
+ testf := func(t *testing.T) {
+
+ opts := []registry.Option{registry.WithReferrersSupport(true)}
+ if logger != nil {
+ opts = append(opts, registry.Logger(logger))
+ }
+ r := registry.New(opts...)
+ s := httptest.NewServer(r)
+ defer s.Close()
+
+ for manifest, contents := range tc.Manifests {
+ u, err := url.Parse(s.URL + "/v2/" + manifest)
+ if err != nil {
+ t.Fatalf("Error parsing %q: %v", s.URL+"/v2", err)
+ }
+ req := &http.Request{
+ Method: "PUT",
+ URL: u,
+ Body: io.NopCloser(strings.NewReader(contents)),
+ }
+ t.Log(req.Method, req.URL)
+ resp, err := s.Client().Do(req)
+ if err != nil {
+ t.Fatalf("Error uploading manifest: %v", err)
+ }
+ if resp.StatusCode != http.StatusCreated {
+ body, _ := io.ReadAll(resp.Body)
+ t.Fatalf("Error uploading manifest got status: %d %s", resp.StatusCode, body)
+ }
+ t.Logf("created manifest with digest %v", resp.Header.Get("Docker-Content-Digest"))
+ }
+
+ for digest, contents := range tc.Digests {
+ u, err := url.Parse(fmt.Sprintf("%s/v2/foo/blobs/uploads/1?digest=%s", s.URL, digest))
+ if err != nil {
+ t.Fatalf("Error parsing %q: %v", s.URL+tc.URL, err)
+ }
+ req := &http.Request{
+ Method: "PUT",
+ URL: u,
+ Body: io.NopCloser(strings.NewReader(contents)),
+ }
+ t.Log(req.Method, req.URL)
+ resp, err := s.Client().Do(req)
+ if err != nil {
+ t.Fatalf("Error uploading digest: %v", err)
+ }
+ if resp.StatusCode != http.StatusCreated {
+ body, _ := io.ReadAll(resp.Body)
+ t.Fatalf("Error uploading digest got status: %d %s", resp.StatusCode, body)
+ }
+ }
+
+ for upload, contents := range tc.BlobStream {
+ u, err := url.Parse(fmt.Sprintf("%s/v2/foo/blobs/uploads/%s", s.URL, upload))
+ if err != nil {
+ t.Fatalf("Error parsing %q: %v", s.URL+tc.URL, err)
+ }
+ req := &http.Request{
+ Method: "PATCH",
+ URL: u,
+ Body: io.NopCloser(strings.NewReader(contents)),
+ }
+ t.Log(req.Method, req.URL)
+ resp, err := s.Client().Do(req)
+ if err != nil {
+ t.Fatalf("Error streaming blob: %v", err)
+ }
+ if resp.StatusCode != http.StatusNoContent {
+ body, _ := io.ReadAll(resp.Body)
+ t.Fatalf("Error streaming blob: %d %s", resp.StatusCode, body)
+ }
+
+ }
+
+ u, err := url.Parse(s.URL + tc.URL)
+ if err != nil {
+ t.Fatalf("Error parsing %q: %v", s.URL+tc.URL, err)
+ }
+ req := &http.Request{
+ Method: tc.Method,
+ URL: u,
+ Body: io.NopCloser(strings.NewReader(tc.Body)),
+ Header: map[string][]string{},
+ }
+ for k, v := range tc.RequestHeader {
+ req.Header.Set(k, v)
+ }
+ t.Log(req.Method, req.URL)
+ resp, err := s.Client().Do(req)
+ if err != nil {
+ t.Fatalf("Error getting %q: %v", tc.URL, err)
+ }
+ defer resp.Body.Close()
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ t.Errorf("Reading response body: %v", err)
+ }
+ if resp.StatusCode != tc.Code {
+ t.Errorf("Incorrect status code, got %d, want %d; body: %s", resp.StatusCode, tc.Code, body)
+ }
+
+ for k, v := range tc.Header {
+ r := resp.Header.Get(k)
+ if r != v {
+ t.Errorf("Incorrect header %q received, got %q, want %q", k, r, v)
+ }
+ }
+
+ if tc.Want != "" && string(body) != tc.Want {
+ t.Errorf("Incorrect response body, got %q, want %q", body, tc.Want)
+ }
+ }
+ t.Run(tc.Description, testf)
+ logger = log.New(io.Discard, "", log.Ldate)
+ t.Run(tc.Description+" - custom log", testf)
+ }
+}