forked from TrueCloudLab/distribution
Add tag delete API
Signed-off-by: João Pereira <484633+joaodrp@users.noreply.github.com>
This commit is contained in:
parent
d80a63f1ea
commit
6ae6df7d75
8 changed files with 179 additions and 23 deletions
|
@ -1113,7 +1113,7 @@ A list of methods and URIs are covered in the table below:
|
||||||
| GET | `/v2/<name>/tags/list` | Tags | Fetch the tags under the repository identified by `name`. |
|
| GET | `/v2/<name>/tags/list` | Tags | Fetch the tags under the repository identified by `name`. |
|
||||||
| GET | `/v2/<name>/manifests/<reference>` | Manifest | Fetch the manifest identified by `name` and `reference` where `reference` can be a tag or digest. A `HEAD` request can also be issued to this endpoint to obtain resource information without receiving all data. |
|
| GET | `/v2/<name>/manifests/<reference>` | Manifest | Fetch the manifest identified by `name` and `reference` where `reference` can be a tag or digest. A `HEAD` request can also be issued to this endpoint to obtain resource information without receiving all data. |
|
||||||
| PUT | `/v2/<name>/manifests/<reference>` | Manifest | Put the manifest identified by `name` and `reference` where `reference` can be a tag or digest. |
|
| PUT | `/v2/<name>/manifests/<reference>` | Manifest | Put the manifest identified by `name` and `reference` where `reference` can be a tag or digest. |
|
||||||
| DELETE | `/v2/<name>/manifests/<reference>` | Manifest | Delete the manifest identified by `name` and `reference`. Note that a manifest can _only_ be deleted by `digest`. |
|
| DELETE | `/v2/<name>/manifests/<reference>` | Manifest | Delete the manifest or tag identified by `name` and `reference` where `reference` can be a tag or digest. Note that a manifest can _only_ be deleted by digest. |
|
||||||
| GET | `/v2/<name>/blobs/<digest>` | Blob | Retrieve the blob from the registry identified by `digest`. A `HEAD` request can also be issued to this endpoint to obtain resource information without receiving all data. |
|
| GET | `/v2/<name>/blobs/<digest>` | Blob | Retrieve the blob from the registry identified by `digest`. A `HEAD` request can also be issued to this endpoint to obtain resource information without receiving all data. |
|
||||||
| DELETE | `/v2/<name>/blobs/<digest>` | Blob | Delete the blob identified by `name` and `digest` |
|
| DELETE | `/v2/<name>/blobs/<digest>` | Blob | Delete the blob identified by `name` and `digest` |
|
||||||
| POST | `/v2/<name>/blobs/uploads/` | Initiate Blob Upload | Initiate a resumable blob upload. If successful, an upload location will be provided to complete the upload. Optionally, if the `digest` parameter is present, the request body will be used to complete the upload in a single request. |
|
| POST | `/v2/<name>/blobs/uploads/` | Initiate Blob Upload | Initiate a resumable blob upload. If successful, an upload location will be provided to complete the upload. Optionally, if the `digest` parameter is present, the request body will be used to complete the upload in a single request. |
|
||||||
|
@ -1142,7 +1142,8 @@ The error codes encountered via the API are enumerated in the following table:
|
||||||
`MANIFEST_UNVERIFIED` | manifest failed signature verification | During manifest upload, if the manifest fails signature verification, this error will be returned.
|
`MANIFEST_UNVERIFIED` | manifest failed signature verification | During manifest upload, if the manifest fails signature verification, this error will be returned.
|
||||||
`NAME_INVALID` | invalid repository name | Invalid repository name encountered either during manifest validation or any API operation.
|
`NAME_INVALID` | invalid repository name | Invalid repository name encountered either during manifest validation or any API operation.
|
||||||
`NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry.
|
`NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry.
|
||||||
`PAGINATION_NUMBER_INVALID` | invalid number of results requested | Returned when the `n` parameter (number of results to return) is not an integer, or `n` is negative.
|
`PAGINATION_NUMBER_INVALID` | invalid number of results requested | Returned when the "n" parameter (number of results to return) is not an integer, or "n" is negative.
|
||||||
|
`RANGE_INVALID` | invalid content range | 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.
|
||||||
`SIZE_INVALID` | provided length did not match content length | When a layer is uploaded, the provided size will be checked against the uploaded content. If they do not match, this error will be returned.
|
`SIZE_INVALID` | provided length did not match content length | When a layer is uploaded, the provided size will be checked against the uploaded content. If they do not match, this error will be returned.
|
||||||
`TAG_INVALID` | manifest tag did not match URI | During a manifest upload, if the tag in the manifest does not match the uri tag, this error will be returned.
|
`TAG_INVALID` | manifest tag did not match URI | During a manifest upload, if the tag in the manifest does not match the uri tag, this error will be returned.
|
||||||
`UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate.
|
`UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate.
|
||||||
|
@ -2240,7 +2241,7 @@ The error codes that may be included in the response body are enumerated below:
|
||||||
|
|
||||||
#### DELETE Manifest
|
#### DELETE Manifest
|
||||||
|
|
||||||
Delete the manifest identified by `name` and `reference`. Note that a manifest can _only_ be deleted by `digest`.
|
Delete the manifest or tag identified by `name` and `reference` where `reference` can be a tag or digest. Note that a manifest can _only_ be deleted by digest.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -2475,7 +2476,7 @@ Content-Type: application/json
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The specified `name` or `reference` are unknown to the registry and the delete was unable to proceed. Clients can assume the manifest was already deleted if this response is returned.
|
The specified `name` or `reference` are unknown to the registry and the delete was unable to proceed. Clients can assume the manifest or tag was already deleted if this response is returned.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -2494,7 +2495,7 @@ The error codes that may be included in the response body are enumerated below:
|
||||||
405 Method Not Allowed
|
405 Method Not Allowed
|
||||||
```
|
```
|
||||||
|
|
||||||
Manifest delete is not allowed because the registry is configured as a pull-through cache or `delete` has been disabled.
|
Manifest or tag delete is not allowed because the registry is configured as a pull-through cache or `delete` has been disabled.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -655,7 +655,7 @@ var routeDescriptors = []RouteDescriptor{
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Method: "DELETE",
|
Method: "DELETE",
|
||||||
Description: "Delete the manifest identified by `name` and `reference`. Note that a manifest can _only_ be deleted by `digest`.",
|
Description: "Delete the manifest or tag identified by `name` and `reference` where `reference` can be a tag or digest. Note that a manifest can _only_ be deleted by digest.",
|
||||||
Requests: []RequestDescriptor{
|
Requests: []RequestDescriptor{
|
||||||
{
|
{
|
||||||
Headers: []ParameterDescriptor{
|
Headers: []ParameterDescriptor{
|
||||||
|
@ -691,7 +691,7 @@ var routeDescriptors = []RouteDescriptor{
|
||||||
tooManyRequestsDescriptor,
|
tooManyRequestsDescriptor,
|
||||||
{
|
{
|
||||||
Name: "Unknown Manifest",
|
Name: "Unknown Manifest",
|
||||||
Description: "The specified `name` or `reference` are unknown to the registry and the delete was unable to proceed. Clients can assume the manifest was already deleted if this response is returned.",
|
Description: "The specified `name` or `reference` are unknown to the registry and the delete was unable to proceed. Clients can assume the manifest or tag was already deleted if this response is returned.",
|
||||||
StatusCode: http.StatusNotFound,
|
StatusCode: http.StatusNotFound,
|
||||||
ErrorCodes: []errcode.ErrorCode{
|
ErrorCodes: []errcode.ErrorCode{
|
||||||
ErrorCodeNameUnknown,
|
ErrorCodeNameUnknown,
|
||||||
|
@ -704,7 +704,7 @@ var routeDescriptors = []RouteDescriptor{
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Not allowed",
|
Name: "Not allowed",
|
||||||
Description: "Manifest delete is not allowed because the registry is configured as a pull-through cache or `delete` has been disabled.",
|
Description: "Manifest or tag delete is not allowed because the registry is configured as a pull-through cache or `delete` has been disabled.",
|
||||||
StatusCode: http.StatusMethodNotAllowed,
|
StatusCode: http.StatusMethodNotAllowed,
|
||||||
ErrorCodes: []errcode.ErrorCode{
|
ErrorCodes: []errcode.ErrorCode{
|
||||||
errcode.ErrorCodeUnsupported,
|
errcode.ErrorCodeUnsupported,
|
||||||
|
|
|
@ -364,7 +364,30 @@ func (t *tags) Tag(ctx context.Context, tag string, desc distribution.Descriptor
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tags) Untag(ctx context.Context, tag string) error {
|
func (t *tags) Untag(ctx context.Context, tag string) error {
|
||||||
panic("not implemented")
|
ref, err := reference.WithTag(t.name, tag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u, err := t.ub.BuildManifestURL(ref)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("DELETE", u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := t.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if SuccessStatus(resp.StatusCode) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return HandleErrorResponse(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
type manifests struct {
|
type manifests struct {
|
||||||
|
|
|
@ -1425,6 +1425,43 @@ func TestManifestTags(t *testing.T) {
|
||||||
// TODO(dmcgowan): Check for error cases
|
// TODO(dmcgowan): Check for error cases
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTagDelete(t *testing.T) {
|
||||||
|
tag := "latest"
|
||||||
|
repo, _ := reference.WithName("test.example.com/repo/delete")
|
||||||
|
newRandomSchemaV1Manifest(repo, tag, 1)
|
||||||
|
|
||||||
|
var m testutil.RequestResponseMap
|
||||||
|
m = append(m, testutil.RequestResponseMapping{
|
||||||
|
Request: testutil.Request{
|
||||||
|
Method: "DELETE",
|
||||||
|
Route: "/v2/" + repo.Name() + "/manifests/" + tag,
|
||||||
|
},
|
||||||
|
Response: testutil.Response{
|
||||||
|
StatusCode: http.StatusAccepted,
|
||||||
|
Headers: map[string][]string{
|
||||||
|
"Content-Length": {"0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
e, c := testServer(m)
|
||||||
|
defer c()
|
||||||
|
|
||||||
|
r, err := NewRepository(repo, e, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
ts := r.Tags(ctx)
|
||||||
|
|
||||||
|
if err := ts.Untag(ctx, tag); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := ts.Untag(ctx, tag); err == nil {
|
||||||
|
t.Fatal("expected error deleting unknown tag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestObtainsErrorForMissingTag(t *testing.T) {
|
func TestObtainsErrorForMissingTag(t *testing.T) {
|
||||||
repo, _ := reference.WithName("test.example.com/repo")
|
repo, _ := reference.WithName("test.example.com/repo")
|
||||||
|
|
||||||
|
|
|
@ -844,6 +844,93 @@ func TestManifestAPI(t *testing.T) {
|
||||||
testManifestAPIManifestList(t, env2, schema2Args)
|
testManifestAPIManifestList(t, env2, schema2Args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestManifestAPI_DeleteTag(t *testing.T) {
|
||||||
|
env := newTestEnv(t, false)
|
||||||
|
defer env.Shutdown()
|
||||||
|
|
||||||
|
imageName, err := reference.WithName("foo/bar")
|
||||||
|
checkErr(t, err, "building image name")
|
||||||
|
|
||||||
|
tag := "latest"
|
||||||
|
dgst := createRepository(env, t, imageName.Name(), tag)
|
||||||
|
|
||||||
|
ref, err := reference.WithTag(imageName, tag)
|
||||||
|
checkErr(t, err, "building tag reference")
|
||||||
|
|
||||||
|
u, err := env.builder.BuildManifestURL(ref)
|
||||||
|
checkErr(t, err, "building tag URL")
|
||||||
|
|
||||||
|
resp, err := httpDelete(u)
|
||||||
|
m := "deleting tag"
|
||||||
|
checkErr(t, err, m)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
checkResponse(t, m, resp, http.StatusAccepted)
|
||||||
|
if resp.Body != http.NoBody {
|
||||||
|
t.Fatal("unexpected response body")
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := "checking tag no longer exists"
|
||||||
|
resp, err = http.Get(u)
|
||||||
|
checkErr(t, err, msg)
|
||||||
|
checkResponse(t, msg, resp, http.StatusNotFound)
|
||||||
|
|
||||||
|
digestRef, err := reference.WithDigest(imageName, dgst)
|
||||||
|
checkErr(t, err, "building manifest digest reference")
|
||||||
|
|
||||||
|
u, err = env.builder.BuildManifestURL(digestRef)
|
||||||
|
checkErr(t, err, "building manifest URL")
|
||||||
|
|
||||||
|
msg = "checking manifest still exists"
|
||||||
|
resp, err = http.Head(u)
|
||||||
|
checkErr(t, err, msg)
|
||||||
|
checkResponse(t, msg, resp, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManifestAPI_DeleteTag_Unknown(t *testing.T) {
|
||||||
|
env := newTestEnv(t, false)
|
||||||
|
defer env.Shutdown()
|
||||||
|
|
||||||
|
imageName, err := reference.WithName("foo/bar")
|
||||||
|
checkErr(t, err, "building named object")
|
||||||
|
|
||||||
|
ref, err := reference.WithTag(imageName, "latest")
|
||||||
|
checkErr(t, err, "building tag reference")
|
||||||
|
|
||||||
|
u, err := env.builder.BuildManifestURL(ref)
|
||||||
|
checkErr(t, err, "building tag URL")
|
||||||
|
|
||||||
|
resp, err := httpDelete(u)
|
||||||
|
msg := "deleting unknown tag"
|
||||||
|
checkErr(t, err, msg)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
checkResponse(t, msg, resp, http.StatusNotFound)
|
||||||
|
checkBodyHasErrorCodes(t, msg, resp, v2.ErrorCodeManifestUnknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManifestAPI_DeleteTag_ReadOnly(t *testing.T) {
|
||||||
|
env := newTestEnv(t, false)
|
||||||
|
defer env.Shutdown()
|
||||||
|
env.app.readOnly = true
|
||||||
|
|
||||||
|
imageName, err := reference.WithName("foo/bar")
|
||||||
|
checkErr(t, err, "building named object")
|
||||||
|
|
||||||
|
ref, err := reference.WithTag(imageName, "latest")
|
||||||
|
checkErr(t, err, "building tag reference")
|
||||||
|
|
||||||
|
u, err := env.builder.BuildManifestURL(ref)
|
||||||
|
checkErr(t, err, "building URL")
|
||||||
|
|
||||||
|
resp, err := httpDelete(u)
|
||||||
|
msg := "deleting tag"
|
||||||
|
checkErr(t, err, msg)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
checkResponse(t, msg, resp, http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
|
||||||
// storageManifestErrDriverFactory implements the factory.StorageDriverFactory interface.
|
// storageManifestErrDriverFactory implements the factory.StorageDriverFactory interface.
|
||||||
type storageManifestErrDriverFactory struct{}
|
type storageManifestErrDriverFactory struct{}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"github.com/distribution/distribution/v3/registry/api/errcode"
|
"github.com/distribution/distribution/v3/registry/api/errcode"
|
||||||
v2 "github.com/distribution/distribution/v3/registry/api/v2"
|
v2 "github.com/distribution/distribution/v3/registry/api/v2"
|
||||||
"github.com/distribution/distribution/v3/registry/auth"
|
"github.com/distribution/distribution/v3/registry/auth"
|
||||||
|
"github.com/distribution/distribution/v3/registry/storage/driver"
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
@ -481,15 +482,31 @@ func (imh *manifestHandler) applyResourcePolicy(manifest distribution.Manifest)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteManifest removes the manifest with the given digest from the registry.
|
// DeleteManifest removes the manifest with the given digest or the tag with the given name from the registry.
|
||||||
func (imh *manifestHandler) DeleteManifest(w http.ResponseWriter, r *http.Request) {
|
func (imh *manifestHandler) DeleteManifest(w http.ResponseWriter, r *http.Request) {
|
||||||
dcontext.GetLogger(imh).Debug("DeleteImageManifest")
|
dcontext.GetLogger(imh).Debug("DeleteImageManifest")
|
||||||
|
|
||||||
if imh.Tag != "" {
|
if imh.App.isCache {
|
||||||
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnsupported)
|
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnsupported)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if imh.Tag != "" {
|
||||||
|
tagService := imh.Repository.Tags(imh.Context)
|
||||||
|
if err := tagService.Untag(imh.Context, imh.Tag); err != nil {
|
||||||
|
switch err.(type) {
|
||||||
|
case distribution.ErrTagUnknown:
|
||||||
|
case driver.PathNotFoundError:
|
||||||
|
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown)
|
||||||
|
default:
|
||||||
|
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
manifests, err := imh.Repository.Manifests(imh)
|
manifests, err := imh.Repository.Manifests(imh)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
imh.Errors = append(imh.Errors, err)
|
imh.Errors = append(imh.Errors, err)
|
||||||
|
|
|
@ -112,16 +112,7 @@ func (ts *tagStore) Untag(ctx context.Context, tag string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ts.blobStore.driver.Delete(ctx, tagPath); err != nil {
|
return ts.blobStore.driver.Delete(ctx, tagPath)
|
||||||
switch err.(type) {
|
|
||||||
case storagedriver.PathNotFoundError:
|
|
||||||
return nil // Untag is idempotent, we don't care if it didn't exist
|
|
||||||
default:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// linkedBlobStore returns the linkedBlobStore for the named tag, allowing one
|
// linkedBlobStore returns the linkedBlobStore for the named tag, allowing one
|
||||||
|
|
|
@ -98,8 +98,8 @@ func TestTagStoreUnTag(t *testing.T) {
|
||||||
desc := distribution.Descriptor{Digest: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}
|
desc := distribution.Descriptor{Digest: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}
|
||||||
|
|
||||||
err := tags.Untag(ctx, "latest")
|
err := tags.Untag(ctx, "latest")
|
||||||
if err != nil {
|
if err == nil {
|
||||||
t.Error(err)
|
t.Error("expected error removing unknown tag")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tags.Tag(ctx, "latest", desc)
|
err = tags.Tag(ctx, "latest", desc)
|
||||||
|
|
Loading…
Reference in a new issue