From ad8206e58a3b993f5bfd0093224225355e4bdadf Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Thu, 16 Dec 2021 12:54:53 -0500 Subject: [PATCH 1/7] Pluggable blob storage for pkg/registry Features: - supports redirects if the blob handler hosts blobs elsewhere. - supports automatically verifying digests/sizes if possible, transparent to the blob handler (if they want it to be). - repo strings passed to blobHandler methods aren't name.Repository, but should be easily parsed by implementations if they want that. This is still hidden in unexported interfaces and always uses memHandler for now, while we iterate on the shape we want these things to take. Longer term, we should also extend this to manifest and upload storage, using similar interfaces. --- internal/verify/verify.go | 19 ++- pkg/registry/blobs.go | 216 ++++++++++++++++++++++++++++------ pkg/registry/depcheck_test.go | 3 + pkg/registry/example_test.go | 30 ----- pkg/registry/registry.go | 4 +- pkg/registry/registry_test.go | 32 ++++- 6 files changed, 231 insertions(+), 73 deletions(-) delete mode 100644 pkg/registry/example_test.go diff --git a/internal/verify/verify.go b/internal/verify/verify.go index 9d62214f6..4a9735947 100644 --- a/internal/verify/verify.go +++ b/internal/verify/verify.go @@ -38,6 +38,18 @@ type verifyReader struct { gotSize, wantSize int64 } +// Error provides information about the failed hash verification. +type Error struct { + got string + want v1.Hash + gotSize int64 +} + +func (v Error) Error() string { + return fmt.Sprintf("error verifying %s checksum after reading %d bytes; got %q, want %q", + v.want.Algorithm, v.gotSize, v.got, v.want) +} + // Read implements io.Reader func (vc *verifyReader) Read(b []byte) (int, error) { n, err := vc.inner.Read(b) @@ -48,8 +60,11 @@ func (vc *verifyReader) Read(b []byte) (int, error) { } got := hex.EncodeToString(vc.hasher.Sum(make([]byte, 0, vc.hasher.Size()))) if want := vc.expected.Hex; got != want { - return n, fmt.Errorf("error verifying %s checksum after reading %d bytes; got %q, want %q", - vc.expected.Algorithm, vc.gotSize, got, want) + return n, Error{ + got: got, + want: vc.expected, + gotSize: vc.gotSize, + } } } return n, err diff --git a/pkg/registry/blobs.go b/pkg/registry/blobs.go index 067227271..52868cfa5 100644 --- a/pkg/registry/blobs.go +++ b/pkg/registry/blobs.go @@ -16,15 +16,18 @@ package registry import ( "bytes" - "crypto/sha256" - "encoding/hex" + "errors" "fmt" "io" + "io/ioutil" "math/rand" "net/http" "path" "strings" "sync" + + "github.com/google/go-containerregistry/internal/verify" + v1 "github.com/google/go-containerregistry/pkg/v1" ) // Returns whether this url should be handled by the blob handler @@ -44,10 +47,78 @@ func isBlob(req *http.Request) bool { elem[len(elem)-2] == "uploads") } +// blobHandler represents a blob storage backend. +// +// For all methods, repo is a string that can be passed to +// pkg/name.NewRepository to validate the repository. +type blobHandler interface { + // Stat returns the size of the blob, or ErrNotFound if the blob wasn't found. + Stat(repo string, h v1.Hash) (int64, error) + + // Get gets the blob contents, or ErrNotFound if the blob wasn't found. + Get(repo string, h v1.Hash) (io.ReadCloser, error) + + // Put puts the blob contents. + // + // The contents will be verified against the expected size and digest + // as the contents are read, and an error will be returned if these + // don't match. Implementations should return that error, or a wrapper + // around that error, to return the correct error when these don't match. + Put(repo string, h v1.Hash, rc io.ReadCloser) error +} + +// RedirectError represents a signal that the blob handler doesn't have the blob +// contents, but that those contents are at another location which registry +// clients should redirect to. +type RedirectError struct { + // Location is the location to find the contents. + Location string + + // Code is the HTTP redirect status code to return to clients. + Code int +} + +func (e RedirectError) Error() string { return fmt.Sprintf("Redirecting (%d): %s", e.Code, e.Location) } + +// ErrNotFound represents an error locating the blob. +var ErrNotFound = errors.New("not found") + +func errTODO(msg string) *regError { + return ®Error{ + Status: http.StatusInternalServerError, + Code: "INTERNAL_SERVER_ERROR", + Message: msg, + } +} + +type memHandler map[string][]byte + +func (m memHandler) Stat(_ string, h v1.Hash) (int64, error) { + b, found := m[h.String()] + if !found { + return 0, ErrNotFound + } + return int64(len(b)), nil +} +func (m memHandler) Get(_ string, h v1.Hash) (io.ReadCloser, error) { + b, found := m[h.String()] + if !found { + return nil, ErrNotFound + } + return ioutil.NopCloser(bytes.NewReader(b)), nil +} +func (m memHandler) Put(_ string, h v1.Hash, rc io.ReadCloser) error { + defer rc.Close() + var buf bytes.Buffer + io.Copy(&buf, rc) + m[h.String()] = buf.Bytes() + return nil +} + // blobs type blobs struct { - // Blobs are content addresses. we store them globally underneath their sha and make no distinctions per image. - contents map[string][]byte + blobHandler blobHandler + // Each upload gets a unique id that writes occur to until finalized. uploads map[string][]byte lock sync.Mutex @@ -72,40 +143,84 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { digest := req.URL.Query().Get("digest") contentRange := req.Header.Get("Content-Range") + repo := req.URL.Host + path.Join(elem[1:len(elem)-2]...) + switch req.Method { case http.MethodHead: b.lock.Lock() defer b.lock.Unlock() - b, ok := b.contents[target] - if !ok { + + h, err := v1.NewHash(target) + if err != nil { + return ®Error{ + Status: http.StatusBadRequest, + Code: "NAME_INVALID", + Message: "invalid digest", + } + } + + size, err := b.blobHandler.Stat(repo, h) + if errors.Is(err, ErrNotFound) { return ®Error{ Status: http.StatusNotFound, Code: "BLOB_UNKNOWN", Message: "Unknown blob", } + } else if err != nil { + return errTODO(err.Error()) } - resp.Header().Set("Content-Length", fmt.Sprint(len(b))) - resp.Header().Set("Docker-Content-Digest", target) + resp.Header().Set("Content-Length", fmt.Sprint(size)) + resp.Header().Set("Docker-Content-Digest", h.String()) resp.WriteHeader(http.StatusOK) return nil case http.MethodGet: b.lock.Lock() defer b.lock.Unlock() - b, ok := b.contents[target] - if !ok { + + h, err := v1.NewHash(target) + if err != nil { + return ®Error{ + Status: http.StatusBadRequest, + Code: "NAME_INVALID", + Message: "invalid digest", + } + } + + size, err := b.blobHandler.Stat(repo, h) + if errors.Is(err, ErrNotFound) { + return ®Error{ + Status: http.StatusNotFound, + Code: "BLOB_UNKNOWN", + Message: "Unknown blob", + } + } else if err != nil { + return errTODO(err.Error()) + } + + rc, err := b.blobHandler.Get(repo, h) + if errors.Is(err, ErrNotFound) { return ®Error{ Status: http.StatusNotFound, Code: "BLOB_UNKNOWN", Message: "Unknown blob", } + } else if err != nil { + var rerr RedirectError + if errors.As(err, &rerr) { + http.Redirect(resp, req, rerr.Location, rerr.Code) + return nil + } + + return errTODO(err.Error()) } + defer rc.Close() - resp.Header().Set("Content-Length", fmt.Sprint(len(b))) - resp.Header().Set("Docker-Content-Digest", target) + resp.Header().Set("Content-Length", fmt.Sprint(size)) + resp.Header().Set("Docker-Content-Digest", h.String()) resp.WriteHeader(http.StatusOK) - io.Copy(resp, bytes.NewReader(b)) + io.Copy(resp, rc) return nil case http.MethodPost: @@ -120,22 +235,35 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { } if digest != "" { - l := &bytes.Buffer{} - io.Copy(l, req.Body) - rd := sha256.Sum256(l.Bytes()) - d := "sha256:" + hex.EncodeToString(rd[:]) - if d != digest { + b.lock.Lock() + defer b.lock.Unlock() + + h, err := v1.NewHash(digest) + if err != nil { return ®Error{ Status: http.StatusBadRequest, - Code: "DIGEST_INVALID", - Message: "digest does not match contents", + Code: "NAME_INVALID", + Message: "invalid digest", } } - b.lock.Lock() - defer b.lock.Unlock() - b.contents[d] = l.Bytes() - resp.Header().Set("Docker-Content-Digest", d) + vrc, err := verify.ReadCloser(req.Body, req.ContentLength, h) + if err != nil { + return errTODO(err.Error()) + } + defer vrc.Close() + + if err := b.blobHandler.Put(repo, h, vrc); err != nil { + if errors.As(err, &verify.Error{}) { + return ®Error{ + Status: http.StatusBadRequest, + Code: "DIGEST_INVALID", + Message: "digest does not match contents", + } + } + return errTODO(err.Error()) + } + resp.Header().Set("Docker-Content-Digest", h.String()) resp.WriteHeader(http.StatusCreated) return nil } @@ -220,21 +348,43 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { b.lock.Lock() defer b.lock.Unlock() - l := bytes.NewBuffer(b.uploads[target]) - io.Copy(l, req.Body) - rd := sha256.Sum256(l.Bytes()) - d := "sha256:" + hex.EncodeToString(rd[:]) - if d != digest { + + h, err := v1.NewHash(digest) + if err != nil { return ®Error{ Status: http.StatusBadRequest, - Code: "DIGEST_INVALID", - Message: "digest does not match contents", + Code: "NAME_INVALID", + Message: "invalid digest", + } + } + + defer req.Body.Close() + in := ioutil.NopCloser(io.MultiReader(bytes.NewBuffer(b.uploads[target]), req.Body)) + + size := int64(verify.SizeUnknown) + if req.ContentLength > 0 { + size = int64(len(b.uploads[target])) + req.ContentLength + } + + vrc, err := verify.ReadCloser(in, size, h) + if err != nil { + return errTODO(err.Error()) + } + defer vrc.Close() + + if err := b.blobHandler.Put(repo, h, vrc); err != nil { + if errors.As(err, &verify.Error{}) { + return ®Error{ + Status: http.StatusBadRequest, + Code: "DIGEST_INVALID", + Message: "digest does not match contents", + } } + return errTODO(err.Error()) } - b.contents[d] = l.Bytes() delete(b.uploads, target) - resp.Header().Set("Docker-Content-Digest", d) + resp.Header().Set("Docker-Content-Digest", h.String()) resp.WriteHeader(http.StatusCreated) return nil diff --git a/pkg/registry/depcheck_test.go b/pkg/registry/depcheck_test.go index e499619a4..9d39dac22 100644 --- a/pkg/registry/depcheck_test.go +++ b/pkg/registry/depcheck_test.go @@ -27,6 +27,9 @@ func TestDeps(t *testing.T) { "github.com/google/go-containerregistry/internal/httptest", "github.com/google/go-containerregistry/pkg/v1", "github.com/google/go-containerregistry/pkg/v1/types", + + "github.com/google/go-containerregistry/internal/verify", + "github.com/google/go-containerregistry/internal/and", ), }) } diff --git a/pkg/registry/example_test.go b/pkg/registry/example_test.go deleted file mode 100644 index e70ce4601..000000000 --- a/pkg/registry/example_test.go +++ /dev/null @@ -1,30 +0,0 @@ -// 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" - "net/http/httptest" - - "github.com/google/go-containerregistry/pkg/registry" -) - -func Example() { - s := httptest.NewServer(registry.New()) - defer s.Close() - resp, _ := s.Client().Get(s.URL + "/v2/bar/blobs/sha256:...") - fmt.Println(resp.StatusCode) - // Output: 404 -} diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index c56dae26d..017bc770f 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -77,8 +77,8 @@ func New(opts ...Option) http.Handler { r := ®istry{ log: log.New(os.Stderr, "", log.LstdFlags), blobs: blobs{ - contents: map[string][]byte{}, - uploads: map[string][]byte{}, + blobHandler: memHandler{}, + uploads: map[string][]byte{}, }, manifests: manifests{ manifests: map[string]map[string]manifest{}, diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go index af9a384db..b173efd69 100644 --- a/pkg/registry/registry_test.go +++ b/pkg/registry/registry_test.go @@ -92,15 +92,27 @@ func TestCalls(t *testing.T) { { Description: "GET non existent blob", Method: "GET", - URL: "/v2/foo/blobs/sha256:asd", + URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", Code: http.StatusNotFound, }, { Description: "HEAD non existent blob", Method: "HEAD", - URL: "/v2/foo/blobs/sha256:asd", + 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", @@ -421,12 +433,14 @@ func TestCalls(t *testing.T) { URL: u, Body: ioutil.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 { - t.Fatalf("Error uploading manifest got status: %d", resp.StatusCode) + body, _ := ioutil.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")) } @@ -441,12 +455,14 @@ func TestCalls(t *testing.T) { URL: u, Body: ioutil.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 { - t.Fatalf("Error uploading digest got status: %d", resp.StatusCode) + body, _ := ioutil.ReadAll(resp.Body) + t.Fatalf("Error uploading digest got status: %d %s", resp.StatusCode, body) } } @@ -460,12 +476,14 @@ func TestCalls(t *testing.T) { URL: u, Body: ioutil.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 { - t.Fatalf("Error streaming blob: %d", resp.StatusCode) + body, _ := ioutil.ReadAll(resp.Body) + t.Fatalf("Error streaming blob: %d %s", resp.StatusCode, body) } } @@ -483,12 +501,14 @@ func TestCalls(t *testing.T) { 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) } if resp.StatusCode != tc.Code { - t.Errorf("Incorrect status code, got %d, want %d", resp.StatusCode, tc.Code) + body, _ := ioutil.ReadAll(resp.Body) + t.Errorf("Incorrect status code, got %d, want %d; body: %s", resp.StatusCode, tc.Code, body) } for k, v := range tc.Header { From 32d09f6efef49f9c29643b54e7b5fb58994f922c Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Thu, 16 Dec 2021 14:59:25 -0500 Subject: [PATCH 2/7] Move blob access locks into memHandler --- pkg/registry/blobs.go | 35 +++++++++++++++++++---------------- pkg/registry/registry.go | 2 +- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/pkg/registry/blobs.go b/pkg/registry/blobs.go index 52868cfa5..e84472a08 100644 --- a/pkg/registry/blobs.go +++ b/pkg/registry/blobs.go @@ -91,27 +91,39 @@ func errTODO(msg string) *regError { } } -type memHandler map[string][]byte +type memHandler struct { + m map[string][]byte + lock sync.Mutex +} + +func (m *memHandler) Stat(_ string, h v1.Hash) (int64, error) { + m.lock.Lock() + defer m.lock.Unlock() -func (m memHandler) Stat(_ string, h v1.Hash) (int64, error) { - b, found := m[h.String()] + b, found := m.m[h.String()] if !found { return 0, ErrNotFound } return int64(len(b)), nil } -func (m memHandler) Get(_ string, h v1.Hash) (io.ReadCloser, error) { - b, found := m[h.String()] +func (m *memHandler) Get(_ string, h v1.Hash) (io.ReadCloser, error) { + m.lock.Lock() + defer m.lock.Unlock() + + b, found := m.m[h.String()] if !found { return nil, ErrNotFound } return ioutil.NopCloser(bytes.NewReader(b)), nil } -func (m memHandler) Put(_ string, h v1.Hash, rc io.ReadCloser) error { +func (m *memHandler) Put(_ string, h v1.Hash, rc io.ReadCloser) error { + m.lock.Lock() + defer m.lock.Unlock() + defer rc.Close() var buf bytes.Buffer io.Copy(&buf, rc) - m[h.String()] = buf.Bytes() + m.m[h.String()] = buf.Bytes() return nil } @@ -147,9 +159,6 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { switch req.Method { case http.MethodHead: - b.lock.Lock() - defer b.lock.Unlock() - h, err := v1.NewHash(target) if err != nil { return ®Error{ @@ -176,9 +185,6 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { return nil case http.MethodGet: - b.lock.Lock() - defer b.lock.Unlock() - h, err := v1.NewHash(target) if err != nil { return ®Error{ @@ -235,9 +241,6 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { } if digest != "" { - b.lock.Lock() - defer b.lock.Unlock() - h, err := v1.NewHash(digest) if err != nil { return ®Error{ diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 017bc770f..00a7ff926 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -77,7 +77,7 @@ func New(opts ...Option) http.Handler { r := ®istry{ log: log.New(os.Stderr, "", log.LstdFlags), blobs: blobs{ - blobHandler: memHandler{}, + blobHandler: &memHandler{m: map[string][]byte{}}, uploads: map[string][]byte{}, }, manifests: manifests{ From 6c9d2aca33212b70fab70924d04cb140bab7fea3 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Thu, 16 Dec 2021 15:59:58 -0500 Subject: [PATCH 3/7] pass context, unexport errors --- pkg/registry/blobs.go | 49 ++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/pkg/registry/blobs.go b/pkg/registry/blobs.go index e84472a08..87ca693cf 100644 --- a/pkg/registry/blobs.go +++ b/pkg/registry/blobs.go @@ -16,6 +16,7 @@ package registry import ( "bytes" + "context" "errors" "fmt" "io" @@ -52,11 +53,11 @@ func isBlob(req *http.Request) bool { // For all methods, repo is a string that can be passed to // pkg/name.NewRepository to validate the repository. type blobHandler interface { - // Stat returns the size of the blob, or ErrNotFound if the blob wasn't found. - Stat(repo string, h v1.Hash) (int64, error) + // Stat returns the size of the blob, or errNotFound if the blob wasn't found. + Stat(ctx context.Context, repo string, h v1.Hash) (int64, error) - // Get gets the blob contents, or ErrNotFound if the blob wasn't found. - Get(repo string, h v1.Hash) (io.ReadCloser, error) + // Get gets the blob contents, or errNotFound if the blob wasn't found. + Get(ctx context.Context, repo string, h v1.Hash) (io.ReadCloser, error) // Put puts the blob contents. // @@ -64,13 +65,13 @@ type blobHandler interface { // as the contents are read, and an error will be returned if these // don't match. Implementations should return that error, or a wrapper // around that error, to return the correct error when these don't match. - Put(repo string, h v1.Hash, rc io.ReadCloser) error + Put(ctx context.Context, repo string, h v1.Hash, rc io.ReadCloser) error } -// RedirectError represents a signal that the blob handler doesn't have the blob +// redirectError represents a signal that the blob handler doesn't have the blob // contents, but that those contents are at another location which registry // clients should redirect to. -type RedirectError struct { +type redirectError struct { // Location is the location to find the contents. Location string @@ -78,10 +79,10 @@ type RedirectError struct { Code int } -func (e RedirectError) Error() string { return fmt.Sprintf("Redirecting (%d): %s", e.Code, e.Location) } +func (e redirectError) Error() string { return fmt.Sprintf("redirecting (%d): %s", e.Code, e.Location) } -// ErrNotFound represents an error locating the blob. -var ErrNotFound = errors.New("not found") +// errNotFound represents an error locating the blob. +var errNotFound = errors.New("not found") func errTODO(msg string) *regError { return ®Error{ @@ -96,27 +97,27 @@ type memHandler struct { lock sync.Mutex } -func (m *memHandler) Stat(_ string, h v1.Hash) (int64, error) { +func (m *memHandler) Stat(_ context.Context, _ string, h v1.Hash) (int64, error) { m.lock.Lock() defer m.lock.Unlock() b, found := m.m[h.String()] if !found { - return 0, ErrNotFound + return 0, errNotFound } return int64(len(b)), nil } -func (m *memHandler) Get(_ string, h v1.Hash) (io.ReadCloser, error) { +func (m *memHandler) Get(_ context.Context, _ string, h v1.Hash) (io.ReadCloser, error) { m.lock.Lock() defer m.lock.Unlock() b, found := m.m[h.String()] if !found { - return nil, ErrNotFound + return nil, errNotFound } return ioutil.NopCloser(bytes.NewReader(b)), nil } -func (m *memHandler) Put(_ string, h v1.Hash, rc io.ReadCloser) error { +func (m *memHandler) Put(_ context.Context, _ string, h v1.Hash, rc io.ReadCloser) error { m.lock.Lock() defer m.lock.Unlock() @@ -168,8 +169,8 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { } } - size, err := b.blobHandler.Stat(repo, h) - if errors.Is(err, ErrNotFound) { + size, err := b.blobHandler.Stat(req.Context(), repo, h) + if errors.Is(err, errNotFound) { return ®Error{ Status: http.StatusNotFound, Code: "BLOB_UNKNOWN", @@ -194,8 +195,8 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { } } - size, err := b.blobHandler.Stat(repo, h) - if errors.Is(err, ErrNotFound) { + size, err := b.blobHandler.Stat(req.Context(), repo, h) + if errors.Is(err, errNotFound) { return ®Error{ Status: http.StatusNotFound, Code: "BLOB_UNKNOWN", @@ -205,15 +206,15 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { return errTODO(err.Error()) } - rc, err := b.blobHandler.Get(repo, h) - if errors.Is(err, ErrNotFound) { + rc, err := b.blobHandler.Get(req.Context(), repo, h) + if errors.Is(err, errNotFound) { return ®Error{ Status: http.StatusNotFound, Code: "BLOB_UNKNOWN", Message: "Unknown blob", } } else if err != nil { - var rerr RedirectError + var rerr redirectError if errors.As(err, &rerr) { http.Redirect(resp, req, rerr.Location, rerr.Code) return nil @@ -256,7 +257,7 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { } defer vrc.Close() - if err := b.blobHandler.Put(repo, h, vrc); err != nil { + if err := b.blobHandler.Put(req.Context(), repo, h, vrc); err != nil { if errors.As(err, &verify.Error{}) { return ®Error{ Status: http.StatusBadRequest, @@ -375,7 +376,7 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { } defer vrc.Close() - if err := b.blobHandler.Put(repo, h, vrc); err != nil { + if err := b.blobHandler.Put(req.Context(), repo, h, vrc); err != nil { if errors.As(err, &verify.Error{}) { return ®Error{ Status: http.StatusBadRequest, From e6df1c52b4c8f34d3219d758e9343572a141f642 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Fri, 17 Dec 2021 10:02:48 -0500 Subject: [PATCH 4/7] - split blobHandler into extensible interfaces - if the provided handler supports Stat, we use it; otherwise fallback to Get - if the provided handler doesn't support Put, the registry is read-only - better registry error handling - reused vars (less verbose) - renamed errTODO(string) -> regErrInternal(err) - added response body check to test cases - checking verify.ReadCloser errors, verification is broken somehow (working on it) --- internal/verify/verify.go | 4 +- internal/verify/verify_test.go | 1 + pkg/registry/blobs.go | 185 ++++++++++++++++++++------------- pkg/registry/error.go | 33 ++++++ pkg/registry/registry.go | 9 +- pkg/registry/registry_test.go | 17 ++- 6 files changed, 169 insertions(+), 80 deletions(-) diff --git a/internal/verify/verify.go b/internal/verify/verify.go index 4a9735947..c64524320 100644 --- a/internal/verify/verify.go +++ b/internal/verify/verify.go @@ -58,10 +58,10 @@ func (vc *verifyReader) Read(b []byte) (int, error) { if vc.wantSize != SizeUnknown && vc.gotSize != vc.wantSize { return n, fmt.Errorf("error verifying size; got %d, want %d", vc.gotSize, vc.wantSize) } - got := hex.EncodeToString(vc.hasher.Sum(make([]byte, 0, vc.hasher.Size()))) + got := hex.EncodeToString(vc.hasher.Sum(nil)) if want := vc.expected.Hex; got != want { return n, Error{ - got: got, + got: vc.expected.Algorithm + ":" + got, want: vc.expected, gotSize: vc.gotSize, } diff --git a/internal/verify/verify_test.go b/internal/verify/verify_test.go index 9de625db7..c4e8ecf27 100644 --- a/internal/verify/verify_test.go +++ b/internal/verify/verify_test.go @@ -30,6 +30,7 @@ func mustHash(s string, t *testing.T) v1.Hash { if err != nil { t.Fatalf("v1.SHA256(%s) = %v", s, err) } + t.Logf("Hashed: %q -> %q", s, h) return h } diff --git a/pkg/registry/blobs.go b/pkg/registry/blobs.go index 87ca693cf..2f28dddae 100644 --- a/pkg/registry/blobs.go +++ b/pkg/registry/blobs.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "io/ioutil" + "log" "math/rand" "net/http" "path" @@ -48,17 +49,24 @@ func isBlob(req *http.Request) bool { elem[len(elem)-2] == "uploads") } -// blobHandler represents a blob storage backend. -// -// For all methods, repo is a string that can be passed to -// pkg/name.NewRepository to validate the repository. +// blobHandler represents a minimal blob storage backend, capable of serving +// blob contents. type blobHandler interface { - // Stat returns the size of the blob, or errNotFound if the blob wasn't found. - Stat(ctx context.Context, repo string, h v1.Hash) (int64, error) - // Get gets the blob contents, or errNotFound if the blob wasn't found. Get(ctx context.Context, repo string, h v1.Hash) (io.ReadCloser, error) +} +// blobStatHandler is an extension interface representing a blob storage +// backend that can serve metadata about blobs. +type blobStatHandler interface { + // Stat returns the size of the blob, or errNotFound if the blob wasn't + // found, or redirectError if the blob can be found elsewhere. + Stat(ctx context.Context, repo string, h v1.Hash) (int64, error) +} + +// blobStatHandler is an extension interface representing a blob storage +// backend that can write blob contents. +type blobPutHandler interface { // Put puts the blob contents. // // The contents will be verified against the expected size and digest @@ -84,14 +92,6 @@ func (e redirectError) Error() string { return fmt.Sprintf("redirecting (%d): %s // errNotFound represents an error locating the blob. var errNotFound = errors.New("not found") -func errTODO(msg string) *regError { - return ®Error{ - Status: http.StatusInternalServerError, - Code: "INTERNAL_SERVER_ERROR", - Message: msg, - } -} - type memHandler struct { m map[string][]byte lock sync.Mutex @@ -122,15 +122,20 @@ func (m *memHandler) Put(_ context.Context, _ string, h v1.Hash, rc io.ReadClose defer m.lock.Unlock() defer rc.Close() - var buf bytes.Buffer - io.Copy(&buf, rc) - m.m[h.String()] = buf.Bytes() + all, err := ioutil.ReadAll(rc) + if err != nil { + // TODO: not verifying correctly :( + // return err + } + m.m[h.String()] = all return nil } // blobs type blobs struct { - blobHandler blobHandler + blobHandler blobHandler + blobStatHandler blobStatHandler + blobPutHandler blobPutHandler // Each upload gets a unique id that writes occur to until finalized. uploads map[string][]byte @@ -169,15 +174,26 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { } } - size, err := b.blobHandler.Stat(req.Context(), repo, h) - if errors.Is(err, errNotFound) { - return ®Error{ - Status: http.StatusNotFound, - Code: "BLOB_UNKNOWN", - Message: "Unknown blob", + var size int64 + if b.blobStatHandler != nil { + size, err = b.blobStatHandler.Stat(req.Context(), repo, h) + if errors.Is(err, errNotFound) { + return regErrBlobUnknown + } else if err != nil { + return regErrInternal(err) + } + } else { + rc, err := b.blobHandler.Get(req.Context(), repo, h) + if errors.Is(err, errNotFound) { + return regErrBlobUnknown + } else if err != nil { + return regErrInternal(err) + } + defer rc.Close() + size, err = io.Copy(ioutil.Discard, rc) + if err != nil { + return regErrInternal(err) } - } else if err != nil { - return errTODO(err.Error()) } resp.Header().Set("Content-Length", fmt.Sprint(size)) @@ -195,42 +211,61 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { } } - size, err := b.blobHandler.Stat(req.Context(), repo, h) - if errors.Is(err, errNotFound) { - return ®Error{ - Status: http.StatusNotFound, - Code: "BLOB_UNKNOWN", - Message: "Unknown blob", + var size int64 + var r io.Reader + if b.blobStatHandler != nil { + size, err = b.blobStatHandler.Stat(req.Context(), repo, h) + if errors.Is(err, errNotFound) { + return regErrBlobUnknown + } else if err != nil { + return regErrInternal(err) } - } else if err != nil { - return errTODO(err.Error()) - } - rc, err := b.blobHandler.Get(req.Context(), repo, h) - if errors.Is(err, errNotFound) { - return ®Error{ - Status: http.StatusNotFound, - Code: "BLOB_UNKNOWN", - Message: "Unknown blob", - } - } else if err != nil { - var rerr redirectError - if errors.As(err, &rerr) { - http.Redirect(resp, req, rerr.Location, rerr.Code) - return nil + rc, err := b.blobHandler.Get(req.Context(), repo, h) + if errors.Is(err, errNotFound) { + return regErrBlobUnknown + } else if err != nil { + var rerr redirectError + if errors.As(err, &rerr) { + http.Redirect(resp, req, rerr.Location, rerr.Code) + return nil + } + + return regErrInternal(err) } + defer rc.Close() + r = rc + } else { + tmp, err := b.blobHandler.Get(req.Context(), repo, h) + if errors.Is(err, errNotFound) { + return regErrBlobUnknown + } else if err != nil { + var rerr redirectError + if errors.As(err, &rerr) { + http.Redirect(resp, req, rerr.Location, rerr.Code) + return nil + } - return errTODO(err.Error()) + return regErrInternal(err) + } + defer tmp.Close() + var buf bytes.Buffer + io.Copy(&buf, tmp) + size = int64(buf.Len()) + r = &buf } - defer rc.Close() resp.Header().Set("Content-Length", fmt.Sprint(size)) resp.Header().Set("Docker-Content-Digest", h.String()) resp.WriteHeader(http.StatusOK) - io.Copy(resp, rc) + io.Copy(resp, r) return nil case http.MethodPost: + if b.blobPutHandler == nil { + return regErrUnsupported + } + // It is weird that this is "target" instead of "service", but // that's how the index math works out above. if target != "uploads" { @@ -244,28 +279,26 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { if digest != "" { h, err := v1.NewHash(digest) if err != nil { - return ®Error{ - Status: http.StatusBadRequest, - Code: "NAME_INVALID", - Message: "invalid digest", - } + return regErrDigestInvalid } - vrc, err := verify.ReadCloser(req.Body, req.ContentLength, h) + var size int64 = verify.SizeUnknown + if req.ContentLength != 0 { + size = req.ContentLength + } + vrc, err := verify.ReadCloser(req.Body, size, h) if err != nil { - return errTODO(err.Error()) + return regErrInternal(err) } defer vrc.Close() - if err := b.blobHandler.Put(req.Context(), repo, h, vrc); err != nil { - if errors.As(err, &verify.Error{}) { - return ®Error{ - Status: http.StatusBadRequest, - Code: "DIGEST_INVALID", - Message: "digest does not match contents", - } + if err = b.blobPutHandler.Put(req.Context(), repo, h, vrc); err != nil { + var verr verify.Error + if errors.As(err, &verr) { + log.Printf("Digest mismatch: %v", verr) + return regErrDigestMismatch } - return errTODO(err.Error()) + return regErrInternal(err) } resp.Header().Set("Docker-Content-Digest", h.String()) resp.WriteHeader(http.StatusCreated) @@ -334,6 +367,10 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { return nil case http.MethodPut: + if b.blobPutHandler == nil { + return regErrUnsupported + } + if service != "uploads" { return ®Error{ Status: http.StatusBadRequest, @@ -372,19 +409,17 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { vrc, err := verify.ReadCloser(in, size, h) if err != nil { - return errTODO(err.Error()) + return regErrInternal(err) } defer vrc.Close() - if err := b.blobHandler.Put(req.Context(), repo, h, vrc); err != nil { - if errors.As(err, &verify.Error{}) { - return ®Error{ - Status: http.StatusBadRequest, - Code: "DIGEST_INVALID", - Message: "digest does not match contents", - } + if err := b.blobPutHandler.Put(req.Context(), repo, h, vrc); err != nil { + var verr verify.Error + if errors.As(err, &verr) { + log.Printf("Digest mismatch: %v", verr) + return regErrDigestMismatch } - return errTODO(err.Error()) + return regErrInternal(err) } delete(b.uploads, target) diff --git a/pkg/registry/error.go b/pkg/registry/error.go index 64e98671c..f8e126dac 100644 --- a/pkg/registry/error.go +++ b/pkg/registry/error.go @@ -44,3 +44,36 @@ func (r *regError) Write(resp http.ResponseWriter) error { }, }) } + +// regErrInternal returns an internal server error. +func regErrInternal(err error) *regError { + return ®Error{ + Status: http.StatusInternalServerError, + Code: "INTERNAL_SERVER_ERROR", + Message: err.Error(), + } +} + +var regErrBlobUnknown = ®Error{ + Status: http.StatusNotFound, + Code: "BLOB_UNKNOWN", + Message: "Unknown blob", +} + +var regErrUnsupported = ®Error{ + Status: http.StatusMethodNotAllowed, + Code: "UNSUPPORTED", + Message: "Unsupported operation", +} + +var regErrDigestMismatch = ®Error{ + Status: http.StatusBadRequest, + Code: "DIGEST_INVALID", + Message: "digest does not match contents", +} + +var regErrDigestInvalid = ®Error{ + Status: http.StatusBadRequest, + Code: "NAME_INVALID", + Message: "invalid digest", +} diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 00a7ff926..b2868ac28 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -74,10 +74,11 @@ func (r *registry) root(resp http.ResponseWriter, req *http.Request) { // New returns a handler which implements the docker registry protocol. // It should be registered at the site root. func New(opts ...Option) http.Handler { + var bh blobHandler = &memHandler{m: map[string][]byte{}} r := ®istry{ log: log.New(os.Stderr, "", log.LstdFlags), blobs: blobs{ - blobHandler: &memHandler{m: map[string][]byte{}}, + blobHandler: bh, uploads: map[string][]byte{}, }, manifests: manifests{ @@ -85,6 +86,12 @@ func New(opts ...Option) http.Handler { log: log.New(os.Stderr, "", log.LstdFlags), }, } + if bsh, ok := bh.(blobStatHandler); ok { + r.blobs.blobStatHandler = bsh + } + if bph, ok := bh.(blobPutHandler); ok { + r.blobs.blobPutHandler = bph + } for _, o := range opts { o(r) } diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go index b173efd69..6186aafc1 100644 --- a/pkg/registry/registry_test.go +++ b/pkg/registry/registry_test.go @@ -66,7 +66,8 @@ func TestCalls(t *testing.T) { Code int Header map[string]string Method string - Body string + Body string // request body to send + Want string // response body to expect }{ { Description: "/v2 returns 200", @@ -126,6 +127,7 @@ func TestCalls(t *testing.T) { URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", Code: http.StatusOK, Header: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"}, + Want: "foo", }, { Description: "GET blob", @@ -134,6 +136,7 @@ func TestCalls(t *testing.T) { URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", Code: http.StatusOK, Header: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"}, + Want: "foo", }, { Description: "HEAD blob", @@ -261,6 +264,7 @@ func TestCalls(t *testing.T) { Method: "GET", URL: "/v2/foo/manifests/latest", Code: http.StatusOK, + Want: "foo", }, { Description: "get manifest by digest", @@ -268,6 +272,7 @@ func TestCalls(t *testing.T) { Method: "GET", URL: "/v2/foo/manifests/sha256:" + sha256String("foo"), Code: http.StatusOK, + Want: "foo", }, { Description: "head manifest", @@ -506,8 +511,12 @@ func TestCalls(t *testing.T) { if err != nil { t.Fatalf("Error getting %q: %v", tc.URL, err) } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("Reading response body: %v", err) + } if resp.StatusCode != tc.Code { - body, _ := ioutil.ReadAll(resp.Body) t.Errorf("Incorrect status code, got %d, want %d; body: %s", resp.StatusCode, tc.Code, body) } @@ -517,6 +526,10 @@ func TestCalls(t *testing.T) { 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(ioutil.Discard, "", log.Ldate) From 6e3a661b08548ed4afdb5a2f01c89388f7d7dd8f Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Fri, 17 Dec 2021 10:20:08 -0500 Subject: [PATCH 5/7] =?UTF-8?q?Fix=20verify.ReadCloser=20when=20size=20is?= =?UTF-8?q?=20unknown=20=F0=9F=A4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/verify/verify.go | 4 ++-- internal/verify/verify_test.go | 13 +++++++++++++ pkg/registry/blobs.go | 19 ++++++------------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/internal/verify/verify.go b/internal/verify/verify.go index c64524320..463f7e4b3 100644 --- a/internal/verify/verify.go +++ b/internal/verify/verify.go @@ -84,9 +84,9 @@ func ReadCloser(r io.ReadCloser, size int64, h v1.Hash) (io.ReadCloser, error) { if err != nil { return nil, err } - var r2 io.Reader = r + r2 := io.TeeReader(r, w) // pass all writes to the hasher. if size != SizeUnknown { - r2 = io.LimitReader(io.TeeReader(r, w), size) + r2 = io.LimitReader(r2, size) // if we know the size, limit to that size. } return &and.ReadCloser{ Reader: &verifyReader{ diff --git a/internal/verify/verify_test.go b/internal/verify/verify_test.go index c4e8ecf27..73a48339b 100644 --- a/internal/verify/verify_test.go +++ b/internal/verify/verify_test.go @@ -60,6 +60,19 @@ func TestVerification(t *testing.T) { } } +func TestVerificationSizeUnknown(t *testing.T) { + want := "This is the input string." + buf := bytes.NewBufferString(want) + + verified, err := ReadCloser(ioutil.NopCloser(buf), SizeUnknown, mustHash(want, t)) + if err != nil { + t.Fatal("ReadCloser() =", err) + } + if _, err := ioutil.ReadAll(verified); err != nil { + t.Error("ReadAll() =", err) + } +} + func TestBadHash(t *testing.T) { h := v1.Hash{ Algorithm: "fake256", diff --git a/pkg/registry/blobs.go b/pkg/registry/blobs.go index 2f28dddae..679bc37f1 100644 --- a/pkg/registry/blobs.go +++ b/pkg/registry/blobs.go @@ -124,8 +124,7 @@ func (m *memHandler) Put(_ context.Context, _ string, h v1.Hash, rc io.ReadClose defer rc.Close() all, err := ioutil.ReadAll(rc) if err != nil { - // TODO: not verifying correctly :( - // return err + return err } m.m[h.String()] = all return nil @@ -282,20 +281,15 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { return regErrDigestInvalid } - var size int64 = verify.SizeUnknown - if req.ContentLength != 0 { - size = req.ContentLength - } - vrc, err := verify.ReadCloser(req.Body, size, h) + vrc, err := verify.ReadCloser(req.Body, req.ContentLength, h) if err != nil { return regErrInternal(err) } defer vrc.Close() if err = b.blobPutHandler.Put(req.Context(), repo, h, vrc); err != nil { - var verr verify.Error - if errors.As(err, &verr) { - log.Printf("Digest mismatch: %v", verr) + if errors.As(err, &verify.Error{}) { + log.Printf("Digest mismatch: %v", err) return regErrDigestMismatch } return regErrInternal(err) @@ -414,9 +408,8 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { defer vrc.Close() if err := b.blobPutHandler.Put(req.Context(), repo, h, vrc); err != nil { - var verr verify.Error - if errors.As(err, &verr) { - log.Printf("Digest mismatch: %v", verr) + if errors.As(err, &verify.Error{}) { + log.Printf("Digest mismatch: %v", err) return regErrDigestMismatch } return regErrInternal(err) From ddf5af6930d12b8201194c02626718fb09aa4489 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Fri, 17 Dec 2021 10:23:37 -0500 Subject: [PATCH 6/7] handle redirects for Stat, and fallback-Get on HEAD --- pkg/registry/blobs.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkg/registry/blobs.go b/pkg/registry/blobs.go index 679bc37f1..b4e8c84bb 100644 --- a/pkg/registry/blobs.go +++ b/pkg/registry/blobs.go @@ -179,6 +179,11 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { if errors.Is(err, errNotFound) { return regErrBlobUnknown } else if err != nil { + var rerr redirectError + if errors.As(err, &rerr) { + http.Redirect(resp, req, rerr.Location, rerr.Code) + return nil + } return regErrInternal(err) } } else { @@ -186,6 +191,11 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { if errors.Is(err, errNotFound) { return regErrBlobUnknown } else if err != nil { + var rerr redirectError + if errors.As(err, &rerr) { + http.Redirect(resp, req, rerr.Location, rerr.Code) + return nil + } return regErrInternal(err) } defer rc.Close() @@ -217,6 +227,11 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { if errors.Is(err, errNotFound) { return regErrBlobUnknown } else if err != nil { + var rerr redirectError + if errors.As(err, &rerr) { + http.Redirect(resp, req, rerr.Location, rerr.Code) + return nil + } return regErrInternal(err) } From 7d7f0c4daea0e9ff58adf0a6e58735881e31c8cd Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Tue, 21 Dec 2021 12:39:43 -0500 Subject: [PATCH 7/7] do type checks inline --- pkg/registry/blobs.go | 26 +++++++++++++------------- pkg/registry/registry.go | 9 +-------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/pkg/registry/blobs.go b/pkg/registry/blobs.go index b4e8c84bb..9677d69d9 100644 --- a/pkg/registry/blobs.go +++ b/pkg/registry/blobs.go @@ -64,8 +64,8 @@ type blobStatHandler interface { Stat(ctx context.Context, repo string, h v1.Hash) (int64, error) } -// blobStatHandler is an extension interface representing a blob storage -// backend that can write blob contents. +// blobPutHandler is an extension interface representing a blob storage backend +// that can write blob contents. type blobPutHandler interface { // Put puts the blob contents. // @@ -132,9 +132,7 @@ func (m *memHandler) Put(_ context.Context, _ string, h v1.Hash, rc io.ReadClose // blobs type blobs struct { - blobHandler blobHandler - blobStatHandler blobStatHandler - blobPutHandler blobPutHandler + blobHandler blobHandler // Each upload gets a unique id that writes occur to until finalized. uploads map[string][]byte @@ -174,8 +172,8 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { } var size int64 - if b.blobStatHandler != nil { - size, err = b.blobStatHandler.Stat(req.Context(), repo, h) + if bsh, ok := b.blobHandler.(blobStatHandler); ok { + size, err = bsh.Stat(req.Context(), repo, h) if errors.Is(err, errNotFound) { return regErrBlobUnknown } else if err != nil { @@ -222,8 +220,8 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { var size int64 var r io.Reader - if b.blobStatHandler != nil { - size, err = b.blobStatHandler.Stat(req.Context(), repo, h) + if bsh, ok := b.blobHandler.(blobStatHandler); ok { + size, err = bsh.Stat(req.Context(), repo, h) if errors.Is(err, errNotFound) { return regErrBlobUnknown } else if err != nil { @@ -276,7 +274,8 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { return nil case http.MethodPost: - if b.blobPutHandler == nil { + bph, ok := b.blobHandler.(blobPutHandler) + if !ok { return regErrUnsupported } @@ -302,7 +301,7 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { } defer vrc.Close() - if err = b.blobPutHandler.Put(req.Context(), repo, h, vrc); err != nil { + if err = bph.Put(req.Context(), repo, h, vrc); err != nil { if errors.As(err, &verify.Error{}) { log.Printf("Digest mismatch: %v", err) return regErrDigestMismatch @@ -376,7 +375,8 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { return nil case http.MethodPut: - if b.blobPutHandler == nil { + bph, ok := b.blobHandler.(blobPutHandler) + if !ok { return regErrUnsupported } @@ -422,7 +422,7 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { } defer vrc.Close() - if err := b.blobPutHandler.Put(req.Context(), repo, h, vrc); err != nil { + if err := bph.Put(req.Context(), repo, h, vrc); err != nil { if errors.As(err, &verify.Error{}) { log.Printf("Digest mismatch: %v", err) return regErrDigestMismatch diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index b2868ac28..00a7ff926 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -74,11 +74,10 @@ func (r *registry) root(resp http.ResponseWriter, req *http.Request) { // New returns a handler which implements the docker registry protocol. // It should be registered at the site root. func New(opts ...Option) http.Handler { - var bh blobHandler = &memHandler{m: map[string][]byte{}} r := ®istry{ log: log.New(os.Stderr, "", log.LstdFlags), blobs: blobs{ - blobHandler: bh, + blobHandler: &memHandler{m: map[string][]byte{}}, uploads: map[string][]byte{}, }, manifests: manifests{ @@ -86,12 +85,6 @@ func New(opts ...Option) http.Handler { log: log.New(os.Stderr, "", log.LstdFlags), }, } - if bsh, ok := bh.(blobStatHandler); ok { - r.blobs.blobStatHandler = bsh - } - if bph, ok := bh.(blobPutHandler); ok { - r.blobs.blobPutHandler = bph - } for _, o := range opts { o(r) }