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 <wxiaoguang@gmail.com>
This commit is contained in:
parent
d94f15c2fd
commit
69fc510d6d
3 changed files with 92 additions and 5 deletions
|
@ -316,8 +316,10 @@ func ContainerRoutes(ctx gocontext.Context) *web.Route {
|
||||||
r.Group("/blobs/uploads", func() {
|
r.Group("/blobs/uploads", func() {
|
||||||
r.Post("", container.InitiateUploadBlob)
|
r.Post("", container.InitiateUploadBlob)
|
||||||
r.Group("/{uuid}", func() {
|
r.Group("/{uuid}", func() {
|
||||||
|
r.Get("", container.GetUploadBlob)
|
||||||
r.Patch("", container.UploadBlob)
|
r.Patch("", container.UploadBlob)
|
||||||
r.Put("", container.EndUploadBlob)
|
r.Put("", container.EndUploadBlob)
|
||||||
|
r.Delete("", container.CancelUploadBlob)
|
||||||
})
|
})
|
||||||
}, reqPackageAccess(perm.AccessModeWrite))
|
}, reqPackageAccess(perm.AccessModeWrite))
|
||||||
r.Group("/blobs/{digest}", func() {
|
r.Group("/blobs/{digest}", func() {
|
||||||
|
@ -377,7 +379,7 @@ func ContainerRoutes(ctx gocontext.Context) *web.Route {
|
||||||
}
|
}
|
||||||
|
|
||||||
m := blobsUploadsPattern.FindStringSubmatch(path)
|
m := blobsUploadsPattern.FindStringSubmatch(path)
|
||||||
if len(m) == 3 && (isPut || isPatch) {
|
if len(m) == 3 && (isGet || isPut || isPatch || isDelete) {
|
||||||
reqPackageAccess(perm.AccessModeWrite)(ctx)
|
reqPackageAccess(perm.AccessModeWrite)(ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
|
@ -391,10 +393,14 @@ func ContainerRoutes(ctx gocontext.Context) *web.Route {
|
||||||
|
|
||||||
ctx.SetParams("uuid", m[2])
|
ctx.SetParams("uuid", m[2])
|
||||||
|
|
||||||
if isPatch {
|
if isGet {
|
||||||
|
container.GetUploadBlob(ctx)
|
||||||
|
} else if isPatch {
|
||||||
container.UploadBlob(ctx)
|
container.UploadBlob(ctx)
|
||||||
} else {
|
} else if isPut {
|
||||||
container.EndUploadBlob(ctx)
|
container.EndUploadBlob(ctx)
|
||||||
|
} else {
|
||||||
|
container.CancelUploadBlob(ctx)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
|
||||||
func UploadBlob(ctx *context.Context) {
|
func UploadBlob(ctx *context.Context) {
|
||||||
image := ctx.Params("image")
|
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) {
|
func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) {
|
||||||
digest := ctx.Params("digest")
|
digest := ctx.Params("digest")
|
||||||
|
|
||||||
|
|
|
@ -205,18 +205,54 @@ func TestPackageContainer(t *testing.T) {
|
||||||
assert.Equal(t, uuid, resp.Header().Get("Docker-Upload-Uuid"))
|
assert.Equal(t, uuid, resp.Header().Get("Docker-Upload-Uuid"))
|
||||||
assert.Equal(t, contentRange, resp.Header().Get("Range"))
|
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)
|
pbu, err = packages_model.GetBlobUploadByID(db.DefaultContext, uuid)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.EqualValues(t, len(blobContent), pbu.BytesReceived)
|
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))
|
req = NewRequest(t, "PUT", fmt.Sprintf("%s?digest=%s", setting.AppURL+uploadURL[1:], blobDigest))
|
||||||
addTokenAuthHeader(req, userToken)
|
addTokenAuthHeader(req, userToken)
|
||||||
resp = MakeRequest(t, req, http.StatusCreated)
|
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, 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"))
|
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 {
|
for _, tag := range tags {
|
||||||
|
|
Loading…
Reference in a new issue