diff --git a/registry/api/v2/descriptors.go b/registry/api/v2/descriptors.go index 9979abae659..5fc70db6a5c 100644 --- a/registry/api/v2/descriptors.go +++ b/registry/api/v2/descriptors.go @@ -18,6 +18,14 @@ var ( Description: `Name of the target repository.`, } + tagParameterDescriptor = ParameterDescriptor{ + Name: "tag", + Type: "string", + Format: reference.TagRegexp.String(), + Required: true, + Description: `Tag of the target manifest.`, + } + referenceParameterDescriptor = ParameterDescriptor{ Name: "reference", Type: "string", @@ -500,6 +508,64 @@ var routeDescriptors = []RouteDescriptor{ }, }, }, + { + Name: RouteNameTag, + Path: "/v2/{name:" + reference.NameRegexp.String() + "}/tags/{tag:" + reference.TagRegexp.String() + "}", + Entity: "Tags", + Description: "Delete tag", + Methods: []MethodDescriptor{ + { + Method: "DELETE", + Description: "Delete a tag identified by `name` and `tag`.", + Requests: []RequestDescriptor{ + { + Name: "Tags", + Description: "Delete a tag identified by `name` and `tag`.", + Headers: []ParameterDescriptor{ + hostHeader, + authHeader, + }, + PathParameters: []ParameterDescriptor{ + nameParameterDescriptor, + tagParameterDescriptor, + }, + Successes: []ResponseDescriptor{ + { + StatusCode: http.StatusAccepted, + }, + }, + Failures: []ResponseDescriptor{ + unauthorizedResponseDescriptor, + repositoryNotFoundResponseDescriptor, + deniedResponseDescriptor, + tooManyRequestsDescriptor, + { + Name: "Unknown Tag", + Description: "The specified `name` or `Tag` are unknown to the registry and the delete was unable to proceed. Clients can assume the tag was already deleted if this response is returned.", + StatusCode: http.StatusNotFound, + ErrorCodes: []errcode.ErrorCode{ + ErrorCodeNameUnknown, + ErrorCodeTagUnknown, + }, + Body: BodyDescriptor{ + ContentType: "application/json; charset=utf-8", + Format: errorsBody, + }, + }, + { + Name: "Not allowed", + Description: "Tag delete is not allowed because the registry is configured as a pull-through cache or `delete` has been disabled.", + StatusCode: http.StatusMethodNotAllowed, + ErrorCodes: []errcode.ErrorCode{ + errcode.ErrorCodeUnsupported, + }, + }, + }, + }, + }, + }, + }, + }, { Name: RouteNameManifest, Path: "/v2/{name:" + reference.NameRegexp.String() + "}/manifests/{reference:" + reference.TagRegexp.String() + "|" + digest.DigestRegexp.String() + "}", diff --git a/registry/api/v2/errors.go b/registry/api/v2/errors.go index 97d6923aa03..f944262156d 100644 --- a/registry/api/v2/errors.go +++ b/registry/api/v2/errors.go @@ -61,6 +61,15 @@ var ( HTTPStatusCode: http.StatusNotFound, }) + // ErrorCodeTagUnknown when the tag is not known. + ErrorCodeTagUnknown = errcode.Register(errGroup, errcode.ErrorDescriptor{ + Value: "TAG_UNKNOWN", + Message: "tag not known to registry", + Description: `This is returned if the tag used during an operation is + unknown to the registry.`, + HTTPStatusCode: http.StatusNotFound, + }) + // ErrorCodeManifestUnknown returned when image manifest is unknown. ErrorCodeManifestUnknown = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "MANIFEST_UNKNOWN", diff --git a/registry/api/v2/routes.go b/registry/api/v2/routes.go index 5b80d5be76a..90f4fc82092 100644 --- a/registry/api/v2/routes.go +++ b/registry/api/v2/routes.go @@ -8,6 +8,7 @@ const ( RouteNameBase = "base" RouteNameManifest = "manifest" RouteNameTags = "tags" + RouteNameTag = "tag" RouteNameBlob = "blob" RouteNameBlobUpload = "blob-upload" RouteNameBlobUploadChunk = "blob-upload-chunk" @@ -18,6 +19,7 @@ var allEndpoints = []string{ RouteNameManifest, RouteNameCatalog, RouteNameTags, + RouteNameTag, RouteNameBlob, RouteNameBlobUpload, RouteNameBlobUploadChunk, diff --git a/registry/api/v2/routes_test.go b/registry/api/v2/routes_test.go index f632d981c0f..0390edf6965 100644 --- a/registry/api/v2/routes_test.go +++ b/registry/api/v2/routes_test.go @@ -87,6 +87,22 @@ func TestRouter(t *testing.T) { "name": "docker.com/foo/bar/baz", }, }, + { + RouteName: RouteNameTag, + RequestURI: "/v2/docker.com/foo/bar/baz/tags/latest", + Vars: map[string]string{ + "name": "docker.com/foo/bar/baz", + "tag": "latest", + }, + }, + { + RouteName: RouteNameTag, + RequestURI: "/v2/docker.com/foo/bar/baz/tags/v1", + Vars: map[string]string{ + "name": "docker.com/foo/bar/baz", + "tag": "v1", + }, + }, { RouteName: RouteNameBlob, RequestURI: "/v2/foo/bar/blobs/sha256:abcdef0919234", diff --git a/registry/handlers/app.go b/registry/handlers/app.go index 0f30603f0bf..3c1957f06e0 100644 --- a/registry/handlers/app.go +++ b/registry/handlers/app.go @@ -103,6 +103,7 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App { app.register(v2.RouteNameManifest, imageManifestDispatcher) app.register(v2.RouteNameCatalog, catalogDispatcher) app.register(v2.RouteNameTags, tagsDispatcher) + app.register(v2.RouteNameTag, tagDispatcher) app.register(v2.RouteNameBlob, blobDispatcher) app.register(v2.RouteNameBlobUpload, blobUploadDispatcher) app.register(v2.RouteNameBlobUploadChunk, blobUploadDispatcher) diff --git a/registry/handlers/app_test.go b/registry/handlers/app_test.go index 385fa4c6b0b..fe20381426a 100644 --- a/registry/handlers/app_test.go +++ b/registry/handlers/app_test.go @@ -103,6 +103,13 @@ func TestAppDispatcher(t *testing.T) { "name", "foo/bar", }, }, + { + endpoint: v2.RouteNameTag, + vars: []string{ + "name", "foo/bar", + "tag", "sometag", + }, + }, { endpoint: v2.RouteNameBlobUpload, vars: []string{ diff --git a/registry/handlers/context.go b/registry/handlers/context.go index 552db2df627..b4a0ad387ee 100644 --- a/registry/handlers/context.go +++ b/registry/handlers/context.go @@ -52,6 +52,10 @@ func getReference(ctx context.Context) (reference string) { return ctxu.GetStringValue(ctx, "vars.reference") } +func getTag(ctx context.Context) (tag string) { + return ctxu.GetStringValue(ctx, "vars.tag") +} + var errDigestNotAvailable = fmt.Errorf("digest not available in context") func getDigest(ctx context.Context) (dgst digest.Digest, err error) { diff --git a/registry/handlers/tags.go b/registry/handlers/tags.go index 91f1031e32d..a6327ad825f 100644 --- a/registry/handlers/tags.go +++ b/registry/handlers/tags.go @@ -21,11 +21,33 @@ func tagsDispatcher(ctx *Context, r *http.Request) http.Handler { } } +// tagDispatcher constructs the tag delete handler +func tagDispatcher(ctx *Context, r *http.Request) http.Handler { + tagHandler := &tagHandler{ + Context: ctx, + Tag: getTag(ctx), + } + + thandler := handlers.MethodHandler{ + } + + if !ctx.readOnly { + thandler["DELETE"] = http.HandlerFunc(tagHandler.DeleteTag) + } + + return thandler +} + // tagsHandler handles requests for lists of tags under a repository name. type tagsHandler struct { *Context } +type tagHandler struct { + *Context + Tag string +} + type tagsAPIResponse struct { Name string `json:"name"` Tags []string `json:"tags"` @@ -60,3 +82,14 @@ func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) { return } } + +// DeleteTag only do the untag and leave the manifest untouched +func (th *tagHandler) DeleteTag(w http.ResponseWriter, r *http.Request) { + tagService := th.Repository.Tags(th) + if err := tagService.Untag(th.Context, th.Tag); err != nil { + th.Errors = append(th.Errors, err) + return + } + + w.WriteHeader(http.StatusAccepted) +}