Skip to content

New Tags API#2169

Closed
sergeyfd wants to merge 12 commits intodistribution:masterfrom
sergeyfd:taglist
Closed

New Tags API#2169
sergeyfd wants to merge 12 commits intodistribution:masterfrom
sergeyfd:taglist

Conversation

@sergeyfd
Copy link
Copy Markdown
Contributor

This is a PR to cover first two first steps of the plan discussed at #1956 It introduces new TagAPI which allows deleting tags for a given image and also supports legacy Tags List URL /v2/<name>/tags/list by redirecting it to the new one /v2/<name>/tags/_list.

@stevvooe please take a look when you have a chance. It seems rather big but most of the changes are caused by renaming some functions to better reflect their new purpose.

Signed-off-by: Serge Dubrouski sergeyfd@gmail.com

for an image repository can be retrieved with the following request:

GET /v2/<name>/tags/list
GET /v2/<name>/tags/_list
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't match was was added above. Please make sure to regenerate from the template.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not realize that one was generated from another so both were updated manually, What is the process to generate .md from .md.tpl?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make docs/spec/api.md

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

issued:

202 Accepted
Content-Length: None
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really do None for Content-Length? How about 0?

If the tag had already been deleted or did not exist, a `404 Not Found`
response will be issued instead.

> **Note** Manifests can never be deleted using this call. If you delete all
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While writing for the API user helps, please target the implementer of the specification.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean no need for that note?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, just explain that the endpoint should not delete the manifest, but the reference from tag to the manifest, so something like that. This doesn't even need to be a note.

@@ -0,0 +1,62 @@
package handlers
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not make a separate file for this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure? These are two different Dispatchers, but I am fine either way.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is okay to have two separate dispatchers in the same file. Its nice to have everything related to tags in the same file.

I'd also have a not about what is going on here (redirect) + some historical context.

}

// GetTags returns a json list of tags for a specific image name.
func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetTagCompat.

@stevvooe
Copy link
Copy Markdown
Collaborator

@sergeyfd Could you open a road map issue with the steps in the description to tie this all together?

@dmcgowan
Copy link
Copy Markdown
Collaborator

I am 👎 to migrating docker to /v2/<name>/tags/_list. This would have to be done on the clients by relying on a 404 fallback to /v2/<name>/tags/list, which has been a pain in the past. I think it would be best to define an Accept header and explicitly call out the behavior that without the Accept header, the list tag gets interpreted as the _list endpoint. Until we have some sort of api version negotiation with the client, we cannot easily just change the behavior. The other benefit is, DELETE never needs an Accept header and if we create a tag type in the future it makes sense to specify that type on GET.

@sergeyfd
Copy link
Copy Markdown
Contributor Author

sergeyfd commented Jan 23, 2017

So what is the process of generating .md from that template? And what is a road map issue? Is it just a regular issue with the [ROADMAP] in the name or is it something special? Sorry for the novice questions.

docs/spec/api.md Outdated

#### DELETE Tags

Delete the that identified by `name` and `tag`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete the tag?

@sergeyfd sergeyfd mentioned this pull request Jan 24, 2017
3 tasks
@stevvooe
Copy link
Copy Markdown
Collaborator

@sergeyfd Just make an issue describing the steps with some checkboxes. Next to each step, include the related PRs. As far as generating the api.md goes, just use make docs/spec/api.md.

@dmcgowan Clients should be able to handle a simple redirect. Does that not work? As far as API versioning goes, this should be fully compatible. Old clients will redirect and new clients can try one then the other. This is better than version negotiation; it is active discovery.

@sergeyfd
Copy link
Copy Markdown
Contributor Author

Does it look better now?

func (th *tagsHandler) GetTagsCompat(w http.ResponseWriter, r *http.Request) {
ctxu.GetLogger(th).Debug("GetTags")

if th.Tag == "list" {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Provide a little comment about why.

Tags []string `json:"tags"`
}

func (th *tagsHandler) GetTagsCompat(w http.ResponseWriter, r *http.Request) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure to document the handler.

@stevvooe
Copy link
Copy Markdown
Collaborator

@sergeyfd This is looking better!

I think there needs to be a little work in the client. I am not quite sure how we want to handle fallback, quite yet but making the client handle it will help explore possibilities.

@sergeyfd
Copy link
Copy Markdown
Contributor Author

@stevvooe I added a couple of comments to document new handler. I am not sure what changes are needed on the client side. With this change client will start using BuildTagsListURL, which returns new _list URL. By default http.client in Go follows all redirection so old list URL should work as well if somebody uses old version of the Distribution client. I had to explicitly disable default behavior in the test case. But if you have something in mind please do let me know.

@stevvooe
Copy link
Copy Markdown
Collaborator

With this change client will start using BuildTagsListURL, which returns new _list URL. By default http.client in Go follows all redirection so old list URL should work as well if somebody uses old version of the Distribution client. I had to explicitly disable default behavior in the test case. But if you have something in mind please do let me know

This needs to handle both cases. If _list does not exist, it needs to fallback to list.

@sergeyfd
Copy link
Copy Markdown
Contributor Author

sergeyfd commented Feb 1, 2017

Makes sense. Please take a look at the latest commit.

var compatUrl bool

// This is needed to be compatible with old registries that use "list" instead of "_list"
_, compatUrl = ctx.Value("tags.url.compat").(bool)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I'm on board with this use of context. Just make a helper that takes the URL.

})
if i < 2 {
headers.Set("Link", "<"+s.URL+"/v2/"+repo.Name()+"/tags/list?n=1&last="+tagsList[i]+`>; rel="next"`)
headers.Set("Link", "<"+s.URL+"/v2/"+repo.Name()+"/tags/"+ref+"?n=1&last="+tagsList[i]+`>; rel="next"`)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's try and setup tests that have the following:

  1. Older client hits tags/list and is redirected.
  2. Newer client hits tags/_list against old registry and falls back.
  3. Newer client hits tags/_list against new registry.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is split between different test cases. 1) is covered in handlers/api_test.go and 2) and 3) are covered in client/repository_test.go

@@ -1000,8 +1000,12 @@ to last response or be fully omitted, depending on the server implementation.
It may be necessary to list all of the tags under a given repository. The tags
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed this earlier: make sure to create a change entry at the top of the specification.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@stevvooe
Copy link
Copy Markdown
Collaborator

stevvooe commented Feb 1, 2017

@sergeyfd Thanks for sticking with this one! It is a tricky change and we'll keep filling in everything to ensure that it will be a smooth transition.

}
defer resp.Body.Close()

if resp.StatusCode == http.StatusNotFound && !compatURL {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs to cover a broader range of possible status codes. Possibly any 4xx except 401. We haven't defined what the behavior must be for undefined routes which in some cases could leave proxies to set the rules.

var compatURL bool

// This is needed to be compatible with old registries that use "list" instead of "_list"
_, compatURL = ctx.Value(compatTagsURLKey{}).(bool)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using context, you can just define a func (t *tags) all(ctx context.Context, fallback bool) ([]string, error) method which All calls and will call itself recursively. Passing arguments through context should be avoided whenever possible.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dmcgowan I suggested he has an unexported helper that takes the url.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way @dmcgowan suggested seems as less intrusive change to the original code so I changed my code that way. I am not sure how to go with "helper that takes url" idea for this case.

@dmcgowan
Copy link
Copy Markdown
Collaborator

dmcgowan commented Feb 1, 2017

Design sounds good, marking as code review

docs/spec/api.md Outdated
| GET | `/v2/` | Base | Check that the endpoint implements Docker Registry API V2. |
| 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>/tags/<reference>` | Tags | Redirect to tags list. |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think <reference> is a typo, shouldn't it be list?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not. The route is used for DELETE operations and GET is just a special case where only 'list' reference supported for backward compatibility.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For DELETE sure, this is GET. Your change description specifically mentions redirecting /tags/list to /tags/_list:

It introduces new TagAPI which allows deleting tags for a given image and also supports legacy Tags List URL /v2//tags/list by redirecting it to the new one /v2//tags/_list.

So I am a bit confused by this line.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, just reread your comment. If only list is supported as <reference> why bother to call it reference?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That part of template is generated from /registry/api/v2/descriptors.go Over there it's just one route for GET and DELETE with uses <reference> to get correct tag. It works same way for DELETE and GET and if we were developing from scratches we would not implement GET at all. But we have to be backward compatible, hence the only <reference> supported in GET is list and that's just a special case. I see no reason to define a special route and handler for it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, it wasn't clear that was coming from the auto-generation.

sergeyfd added 10 commits July 1, 2018 16:48
Signed-off-by: Serge Dubrouski <sergeyfd@gmail.com>
Signed-off-by: Serge Dubrouski <sergeyfd@gmail.com>
Signed-off-by: Serge Dubrouski <sergeyfd@gmail.com>
Signed-off-by: Serge Dubrouski <sergeyfd@gmail.com>
Signed-off-by: Serge Dubrouski <sergeyfd@gmail.com>
Signed-off-by: Serge Dubrouski <sergeyfd@gmail.com>
Signed-off-by: Serge Dubrouski <sergeyfd@gmail.com>
Signed-off-by: Serge Dubrouski <sergeyfd@gmail.com>
Signed-off-by: Serge Dubrouski <sergeyfd@gmail.com>
Signed-off-by: Serge Dubrouski <sergeyfd@gmail.com>
@prune998
Copy link
Copy Markdown

prune998 commented Oct 1, 2018

still not merged. Will it be at some point ?

@ayufan
Copy link
Copy Markdown

ayufan commented Oct 1, 2018 via email

@timrizzi
Copy link
Copy Markdown

timrizzi commented May 1, 2019

Any update on this request?

@shauneq2
Copy link
Copy Markdown

Any update on this? It is fairly critical for us because we have a registry cleanup script which purges old images. But if tag "old" points to the same image (manifest) as tag "new" then the new tag is removed as well as the old one. This will leave customers who reference the image by the new tag without an image to pull!!!

@esantoro
Copy link
Copy Markdown

Any update on this ?

@esantoro
Copy link
Copy Markdown

A little more than a month and then this issue will be three years old! 🎉
And still no api to delete a single tag! 🎉

@thaJeztah
Copy link
Copy Markdown
Member

With the distribution spec now living elsewhere, this should likely now be opened as a proposal in https://github.com/opencontainers/distribution-spec

@bmerry
Copy link
Copy Markdown

bmerry commented Dec 19, 2019

For those looking for a workaround: you can push a new temporary manifest to the tag and then delete it again, which will ensure you don't delete anything else. I've implemented the following in my cleanup script:

  • Fetch the manifest.
  • Fetch the image config from it (the script only handles v2-2 manifests).
  • Append an extra, fake entry to the history section of the image config.
  • Push the new image config.
  • Push a new manifest, referencing this new image config, to the tag, and get its digest.
  • Delete this digest.

This will leave some extra detritus in the registry, but it should get cleaned up by garbage collection. Note that this process never removes any manifests that were originally in the registry, so to actually get space savings you need to use the garbage collector with --delete-untagged.

I've only just implemented this and haven't yet used it on a production registry, so caveat emptor.

@dmcgowan
Copy link
Copy Markdown
Collaborator

Additions to the specification are no longer being considered here. The specification has moved to https://github.com/opencontainers/distribution-spec

@joaodrp
Copy link
Copy Markdown
Collaborator

joaodrp commented May 27, 2021

For anyone waiting for this, the tag delete API has been added to the OCI Distribution spec in v1.0.0. I've created a PR to implement it here: #3427

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.