Skip to content

Commit

Permalink
Add GET and DELETE endpoints for Docker blob uploads (#21367)
Browse files Browse the repository at this point in the history
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 <wxiaoguang@gmail.com>
  • Loading branch information
KN4CK3R and wxiaoguang authored Oct 7, 2022
1 parent d94f15c commit 69fc510
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 5 deletions.
12 changes: 9 additions & 3 deletions routers/api/packages/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down
45 changes: 45 additions & 0 deletions routers/api/packages/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")

Expand Down
40 changes: 38 additions & 2 deletions tests/integration/api_packages_container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 69fc510

Please sign in to comment.