Merge pull request #3227 from wy65701436/fix-conformance-416
OCI: add content range handling in patch blob
This commit is contained in:
commit
f65a33d3c8
4 changed files with 96 additions and 8 deletions
|
@ -32,6 +32,17 @@ var (
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ErrorCodeRangeInvalid is returned when uploading a blob if the provided
|
||||||
|
// content range is invalid.
|
||||||
|
ErrorCodeRangeInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{
|
||||||
|
Value: "RANGE_INVALID",
|
||||||
|
Message: "invalid content range",
|
||||||
|
Description: `When a layer is uploaded, the provided range is checked
|
||||||
|
against the uploaded chunk. This error is returned if the range is
|
||||||
|
out of order.`,
|
||||||
|
HTTPStatusCode: http.StatusRequestedRangeNotSatisfiable,
|
||||||
|
})
|
||||||
|
|
||||||
// ErrorCodeNameInvalid is returned when the name in the manifest does not
|
// ErrorCodeNameInvalid is returned when the name in the manifest does not
|
||||||
// match the provided name.
|
// match the provided name.
|
||||||
ErrorCodeNameInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{
|
ErrorCodeNameInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{
|
||||||
|
|
|
@ -533,6 +533,32 @@ func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv {
|
||||||
uploadURLBase, dgst := pushChunk(t, env.builder, imageName, uploadURLBase, layerFile, layerLength)
|
uploadURLBase, dgst := pushChunk(t, env.builder, imageName, uploadURLBase, layerFile, layerLength)
|
||||||
finishUpload(t, env.builder, imageName, uploadURLBase, dgst)
|
finishUpload(t, env.builder, imageName, uploadURLBase, dgst)
|
||||||
|
|
||||||
|
// -----------------------------------------
|
||||||
|
// Do layer push with invalid content range
|
||||||
|
layerFile.Seek(0, io.SeekStart)
|
||||||
|
uploadURLBase, _ = startPushLayer(t, env, imageName)
|
||||||
|
sizeInvalid := chunkOptions{
|
||||||
|
contentRange: "0-20",
|
||||||
|
}
|
||||||
|
resp, err = doPushChunk(t, uploadURLBase, layerFile, sizeInvalid)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error doing push layer request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
checkResponse(t, "putting size invalid chunk", resp, http.StatusBadRequest)
|
||||||
|
|
||||||
|
layerFile.Seek(0, io.SeekStart)
|
||||||
|
uploadURLBase, _ = startPushLayer(t, env, imageName)
|
||||||
|
outOfOrder := chunkOptions{
|
||||||
|
contentRange: "3-22",
|
||||||
|
}
|
||||||
|
resp, err = doPushChunk(t, uploadURLBase, layerFile, outOfOrder)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error doing push layer request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
checkResponse(t, "putting range out of order chunk", resp, http.StatusRequestedRangeNotSatisfiable)
|
||||||
|
|
||||||
// ------------------------
|
// ------------------------
|
||||||
// Use a head request to see if the layer exists.
|
// Use a head request to see if the layer exists.
|
||||||
resp, err = http.Head(layerURL)
|
resp, err = http.Head(layerURL)
|
||||||
|
@ -2242,7 +2268,12 @@ func finishUpload(t *testing.T, ub *v2.URLBuilder, name reference.Named, uploadU
|
||||||
return resp.Header.Get("Location")
|
return resp.Header.Get("Location")
|
||||||
}
|
}
|
||||||
|
|
||||||
func doPushChunk(t *testing.T, uploadURLBase string, body io.Reader) (*http.Response, digest.Digest, error) {
|
type chunkOptions struct {
|
||||||
|
// Content-Range header to set when pushing chunks
|
||||||
|
contentRange string
|
||||||
|
}
|
||||||
|
|
||||||
|
func doPushChunk(t *testing.T, uploadURLBase string, body io.Reader, options chunkOptions) (*http.Response, error) {
|
||||||
u, err := url.Parse(uploadURLBase)
|
u, err := url.Parse(uploadURLBase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error parsing pushLayer url: %v", err)
|
t.Fatalf("unexpected error parsing pushLayer url: %v", err)
|
||||||
|
@ -2254,21 +2285,24 @@ func doPushChunk(t *testing.T, uploadURLBase string, body io.Reader) (*http.Resp
|
||||||
|
|
||||||
uploadURL := u.String()
|
uploadURL := u.String()
|
||||||
|
|
||||||
digester := digest.Canonical.Digester()
|
req, err := http.NewRequest("PATCH", uploadURL, body)
|
||||||
|
|
||||||
req, err := http.NewRequest("PATCH", uploadURL, io.TeeReader(body, digester.Hash()))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error creating new request: %v", err)
|
t.Fatalf("unexpected error creating new request: %v", err)
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
|
if options.contentRange != "" {
|
||||||
|
req.Header.Set("Content-Range", options.contentRange)
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
|
||||||
return resp, digester.Digest(), err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func pushChunk(t *testing.T, ub *v2.URLBuilder, name reference.Named, uploadURLBase string, body io.Reader, length int64) (string, digest.Digest) {
|
func pushChunk(t *testing.T, ub *v2.URLBuilder, name reference.Named, uploadURLBase string, body io.Reader, length int64) (string, digest.Digest) {
|
||||||
resp, dgst, err := doPushChunk(t, uploadURLBase, body)
|
digester := digest.Canonical.Digester()
|
||||||
|
|
||||||
|
resp, err := doPushChunk(t, uploadURLBase, io.TeeReader(body, digester.Hash()), chunkOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error doing push layer request: %v", err)
|
t.Fatalf("unexpected error doing push layer request: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -2285,7 +2319,7 @@ func pushChunk(t *testing.T, ub *v2.URLBuilder, name reference.Named, uploadURLB
|
||||||
"Content-Length": []string{"0"},
|
"Content-Length": []string{"0"},
|
||||||
})
|
})
|
||||||
|
|
||||||
return resp.Header.Get("Location"), dgst
|
return resp.Header.Get("Location"), digester.Digest()
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkResponse(t *testing.T, msg string, resp *http.Response, expectedStatus int) {
|
func checkResponse(t *testing.T, msg string, resp *http.Response, expectedStatus int) {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/distribution/distribution/v3"
|
"github.com/distribution/distribution/v3"
|
||||||
dcontext "github.com/distribution/distribution/v3/context"
|
dcontext "github.com/distribution/distribution/v3/context"
|
||||||
|
@ -133,7 +134,29 @@ func (buh *blobUploadHandler) PatchBlobData(w http.ResponseWriter, r *http.Reque
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(dmcgowan): support Content-Range header to seek and write range
|
cr := r.Header.Get("Content-Range")
|
||||||
|
cl := r.Header.Get("Content-Length")
|
||||||
|
if cr != "" && cl != "" {
|
||||||
|
start, end, err := parseContentRange(cr)
|
||||||
|
if err != nil {
|
||||||
|
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if start > end || start != buh.Upload.Size() {
|
||||||
|
buh.Errors = append(buh.Errors, v2.ErrorCodeRangeInvalid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clInt, err := strconv.ParseInt(cl, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if clInt != (end-start)+1 {
|
||||||
|
buh.Errors = append(buh.Errors, v2.ErrorCodeSizeInvalid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := copyFullPayload(buh, w, r, buh.Upload, -1, "blob PATCH"); err != nil {
|
if err := copyFullPayload(buh, w, r, buh.Upload, -1, "blob PATCH"); err != nil {
|
||||||
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err.Error()))
|
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err.Error()))
|
||||||
|
|
|
@ -3,8 +3,11 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
dcontext "github.com/distribution/distribution/v3/context"
|
dcontext "github.com/distribution/distribution/v3/context"
|
||||||
)
|
)
|
||||||
|
@ -64,3 +67,20 @@ func copyFullPayload(ctx context.Context, responseWriter http.ResponseWriter, r
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseContentRange(cr string) (int64, int64, error) {
|
||||||
|
ranges := strings.Split(cr, "-")
|
||||||
|
if len(ranges) != 2 {
|
||||||
|
return -1, -1, fmt.Errorf("invalid content range format, %s", cr)
|
||||||
|
}
|
||||||
|
start, err := strconv.ParseInt(ranges[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return -1, -1, err
|
||||||
|
}
|
||||||
|
end, err := strconv.ParseInt(ranges[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return -1, -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return start, end, nil
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue