Merge pull request #3227 from wy65701436/fix-conformance-416
OCI: add content range handling in patch blobmaster
						commit
						f65a33d3c8
					
				|  | @ -32,6 +32,17 @@ var ( | |||
| 		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
 | ||||
| 	// match the provided name.
 | ||||
| 	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) | ||||
| 	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.
 | ||||
| 	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") | ||||
| } | ||||
| 
 | ||||
| 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) | ||||
| 	if err != nil { | ||||
| 		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() | ||||
| 
 | ||||
| 	digester := digest.Canonical.Digester() | ||||
| 
 | ||||
| 	req, err := http.NewRequest("PATCH", uploadURL, io.TeeReader(body, digester.Hash())) | ||||
| 	req, err := http.NewRequest("PATCH", uploadURL, body) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("unexpected error creating new request: %v", err) | ||||
| 	} | ||||
| 	req.Header.Set("Content-Type", "application/octet-stream") | ||||
| 	if options.contentRange != "" { | ||||
| 		req.Header.Set("Content-Range", options.contentRange) | ||||
| 	} | ||||
| 
 | ||||
| 	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) { | ||||
| 	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 { | ||||
| 		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"}, | ||||
| 	}) | ||||
| 
 | ||||
| 	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) { | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import ( | |||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strconv" | ||||
| 
 | ||||
| 	"github.com/distribution/distribution/v3" | ||||
| 	dcontext "github.com/distribution/distribution/v3/context" | ||||
|  | @ -133,7 +134,29 @@ func (buh *blobUploadHandler) PatchBlobData(w http.ResponseWriter, r *http.Reque | |||
| 		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 { | ||||
| 		buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err.Error())) | ||||
|  |  | |||
|  | @ -3,8 +3,11 @@ package handlers | |||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	dcontext "github.com/distribution/distribution/v3/context" | ||||
| ) | ||||
|  | @ -64,3 +67,20 @@ func copyFullPayload(ctx context.Context, responseWriter http.ResponseWriter, r | |||
| 
 | ||||
| 	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 New Issue