From 69fc510d6dcf9cda7993eae8cd5c7725b345a9a1 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Fri, 7 Oct 2022 17:30:59 +0200 Subject: [PATCH] Add GET and DELETE endpoints for Docker blob uploads (#21367) This PR adds support for https://docs.docker.com/registry/spec/api/#get-blob-upload https://docs.docker.com/registry/spec/api/#delete-blob-upload Both are not required by the OCI spec but some clients call these endpoints. Co-authored-by: wxiaoguang --- routers/api/packages/api.go | 12 +++-- routers/api/packages/container/container.go | 45 +++++++++++++++++++ .../api_packages_container_test.go | 40 ++++++++++++++++- 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 3354fe12d..0889006dd 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -316,8 +316,10 @@ func ContainerRoutes(ctx gocontext.Context) *web.Route { r.Group("/blobs/uploads", func() { r.Post("", container.InitiateUploadBlob) r.Group("/{uuid}", func() { + r.Get("", container.GetUploadBlob) r.Patch("", container.UploadBlob) r.Put("", container.EndUploadBlob) + r.Delete("", container.CancelUploadBlob) }) }, reqPackageAccess(perm.AccessModeWrite)) r.Group("/blobs/{digest}", func() { @@ -377,7 +379,7 @@ func ContainerRoutes(ctx gocontext.Context) *web.Route { } m := blobsUploadsPattern.FindStringSubmatch(path) - if len(m) == 3 && (isPut || isPatch) { + if len(m) == 3 && (isGet || isPut || isPatch || isDelete) { reqPackageAccess(perm.AccessModeWrite)(ctx) if ctx.Written() { return @@ -391,10 +393,14 @@ func ContainerRoutes(ctx gocontext.Context) *web.Route { ctx.SetParams("uuid", m[2]) - if isPatch { + if isGet { + container.GetUploadBlob(ctx) + } else if isPatch { container.UploadBlob(ctx) - } else { + } else if isPut { container.EndUploadBlob(ctx) + } else { + container.CancelUploadBlob(ctx) } return } diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index b961cd4af..5bc64e1b2 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -248,6 +248,27 @@ func InitiateUploadBlob(ctx *context.Context) { }) } +// https://docs.docker.com/registry/spec/api/#get-blob-upload +func GetUploadBlob(ctx *context.Context) { + uuid := ctx.Params("uuid") + + upload, err := packages_model.GetBlobUploadByID(ctx, uuid) + if err != nil { + if err == packages_model.ErrPackageBlobUploadNotExist { + apiErrorDefined(ctx, errBlobUploadUnknown) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + setResponseHeaders(ctx.Resp, &containerHeaders{ + Range: fmt.Sprintf("0-%d", upload.BytesReceived), + UploadUUID: upload.ID, + Status: http.StatusNoContent, + }) +} + // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks func UploadBlob(ctx *context.Context) { image := ctx.Params("image") @@ -354,6 +375,30 @@ func EndUploadBlob(ctx *context.Context) { }) } +// https://docs.docker.com/registry/spec/api/#delete-blob-upload +func CancelUploadBlob(ctx *context.Context) { + uuid := ctx.Params("uuid") + + _, err := packages_model.GetBlobUploadByID(ctx, uuid) + if err != nil { + if err == packages_model.ErrPackageBlobUploadNotExist { + apiErrorDefined(ctx, errBlobUploadUnknown) + } else { + apiError(ctx, http.StatusInternalServerError, err) + } + return + } + + if err := container_service.RemoveBlobUploadByID(ctx, uuid); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + setResponseHeaders(ctx.Resp, &containerHeaders{ + Status: http.StatusNoContent, + }) +} + func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) { digest := ctx.Params("digest") diff --git a/tests/integration/api_packages_container_test.go b/tests/integration/api_packages_container_test.go index adced5d66..b8a388437 100644 --- a/tests/integration/api_packages_container_test.go +++ b/tests/integration/api_packages_container_test.go @@ -205,18 +205,54 @@ func TestPackageContainer(t *testing.T) { assert.Equal(t, uuid, resp.Header().Get("Docker-Upload-Uuid")) assert.Equal(t, contentRange, resp.Header().Get("Range")) + uploadURL = resp.Header().Get("Location") + + req = NewRequest(t, "GET", setting.AppURL+uploadURL[1:]) + addTokenAuthHeader(req, userToken) + resp = MakeRequest(t, req, http.StatusNoContent) + + assert.Equal(t, uuid, resp.Header().Get("Docker-Upload-Uuid")) + assert.Equal(t, fmt.Sprintf("0-%d", len(blobContent)), resp.Header().Get("Range")) + pbu, err = packages_model.GetBlobUploadByID(db.DefaultContext, uuid) assert.NoError(t, err) assert.EqualValues(t, len(blobContent), pbu.BytesReceived) - uploadURL = resp.Header().Get("Location") - req = NewRequest(t, "PUT", fmt.Sprintf("%s?digest=%s", setting.AppURL+uploadURL[1:], blobDigest)) addTokenAuthHeader(req, userToken) resp = MakeRequest(t, req, http.StatusCreated) assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, blobDigest), resp.Header().Get("Location")) assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest")) + + t.Run("Cancel", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url)) + addTokenAuthHeader(req, userToken) + resp := MakeRequest(t, req, http.StatusAccepted) + + uuid := resp.Header().Get("Docker-Upload-Uuid") + assert.NotEmpty(t, uuid) + + uploadURL := resp.Header().Get("Location") + assert.NotEmpty(t, uploadURL) + + req = NewRequest(t, "GET", setting.AppURL+uploadURL[1:]) + addTokenAuthHeader(req, userToken) + resp = MakeRequest(t, req, http.StatusNoContent) + + assert.Equal(t, uuid, resp.Header().Get("Docker-Upload-Uuid")) + assert.Equal(t, "0-0", resp.Header().Get("Range")) + + req = NewRequest(t, "DELETE", setting.AppURL+uploadURL[1:]) + addTokenAuthHeader(req, userToken) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "GET", setting.AppURL+uploadURL[1:]) + addTokenAuthHeader(req, userToken) + MakeRequest(t, req, http.StatusNotFound) + }) }) for _, tag := range tags {