Skip to content

Commit

Permalink
registry: Implement Range requests for blobs (#1917)
Browse files Browse the repository at this point in the history
This makes crane registry usable with registry explorer.

Signed-off-by: Jon Johnson <jon.johnson@chainguard.dev>
  • Loading branch information
jonjohnsonjr committed Apr 9, 2024
1 parent 0309184 commit 051d642
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 4 deletions.
61 changes: 57 additions & 4 deletions pkg/registry/blobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ type redirectError struct {
Code int
}

type bytesCloser struct {
*bytes.Reader
}

func (r *bytesCloser) Close() error {
return nil
}

func (e redirectError) Error() string { return fmt.Sprintf("redirecting (%d): %s", e.Code, e.Location) }

// errNotFound represents an error locating the blob.
Expand All @@ -115,6 +123,7 @@ func (m *memHandler) Stat(_ context.Context, _ string, h v1.Hash) (int64, error)
}
return int64(len(b)), nil
}

func (m *memHandler) Get(_ context.Context, _ string, h v1.Hash) (io.ReadCloser, error) {
m.lock.Lock()
defer m.lock.Unlock()
Expand All @@ -123,8 +132,9 @@ func (m *memHandler) Get(_ context.Context, _ string, h v1.Hash) (io.ReadCloser,
if !found {
return nil, errNotFound
}
return io.NopCloser(bytes.NewReader(b)), nil
return &bytesCloser{bytes.NewReader(b)}, nil
}

func (m *memHandler) Put(_ context.Context, _ string, h v1.Hash, rc io.ReadCloser) error {
m.lock.Lock()
defer m.lock.Unlock()
Expand All @@ -137,6 +147,7 @@ func (m *memHandler) Put(_ context.Context, _ string, h v1.Hash, rc io.ReadClose
m.m[h.String()] = all
return nil
}

func (m *memHandler) Delete(_ context.Context, _ string, h v1.Hash) error {
m.lock.Lock()
defer m.lock.Unlock()
Expand Down Expand Up @@ -177,6 +188,7 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError {
service := elem[len(elem)-2]
digest := req.URL.Query().Get("digest")
contentRange := req.Header.Get("Content-Range")
rangeHeader := req.Header.Get("Range")

repo := req.URL.Host + path.Join(elem[1:len(elem)-2]...)

Expand Down Expand Up @@ -265,8 +277,10 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError {

return regErrInternal(err)
}

defer rc.Close()
r = rc

} else {
tmp, err := b.blobHandler.Get(req.Context(), repo, h)
if errors.Is(err, errNotFound) {
Expand All @@ -287,9 +301,48 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError {
r = &buf
}

resp.Header().Set("Content-Length", fmt.Sprint(size))
resp.Header().Set("Docker-Content-Digest", h.String())
resp.WriteHeader(http.StatusOK)
if rangeHeader != "" {
start, end := int64(0), int64(0)
if _, err := fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end); err != nil {
return &regError{
Status: http.StatusRequestedRangeNotSatisfiable,
Code: "BLOB_UNKNOWN",
Message: "We don't understand your Range",
}
}

n := (end + 1) - start
if ra, ok := r.(io.ReaderAt); ok {
if end+1 > size {
return &regError{
Status: http.StatusRequestedRangeNotSatisfiable,
Code: "BLOB_UNKNOWN",
Message: fmt.Sprintf("range end %d > %d size", end+1, size),
}
}
r = io.NewSectionReader(ra, start, n)
} else {
if _, err := io.CopyN(io.Discard, r, start); err != nil {
return &regError{
Status: http.StatusRequestedRangeNotSatisfiable,
Code: "BLOB_UNKNOWN",
Message: fmt.Sprintf("Failed to discard %d bytes", start),
}
}

r = io.LimitReader(r, n)
}

resp.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, size))
resp.Header().Set("Content-Length", fmt.Sprint(n))
resp.Header().Set("Docker-Content-Digest", h.String())
resp.WriteHeader(http.StatusPartialContent)
} else {
resp.Header().Set("Content-Length", fmt.Sprint(size))
resp.Header().Set("Docker-Content-Digest", h.String())
resp.WriteHeader(http.StatusOK)
}

io.Copy(resp, r)
return nil

Expand Down
36 changes: 36 additions & 0 deletions pkg/registry/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,42 @@ func TestCalls(t *testing.T) {
Header: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"},
Want: "foo",
},
{
Description: "GET blob range",
Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"},
Method: "GET",
URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
Code: http.StatusPartialContent,
RequestHeader: map[string]string{
"Range": "bytes=1-2",
},
Header: map[string]string{
"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
"Content-Length": "2",
"Content-Range": "bytes 1-2/3",
},
Want: "oo",
},
{
Description: "GET invalid range header",
Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"},
Method: "GET",
URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
RequestHeader: map[string]string{
"Range": "nibbles=123-456",
},
Code: http.StatusRequestedRangeNotSatisfiable,
},
{
Description: "GET bad blob range",
Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"},
Method: "GET",
URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
RequestHeader: map[string]string{
"Range": "bytes=1-3",
},
Code: http.StatusRequestedRangeNotSatisfiable,
},
{
Description: "HEAD blob",
Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"},
Expand Down

0 comments on commit 051d642

Please sign in to comment.