Skip to content

Conversation

@eyJhb
Copy link
Contributor

@eyJhb eyJhb commented Apr 10, 2020

Adding pagination to the registry, which is required to live up to the OCI Distribution Specification.
This will also fix a bug with tags being returned in random orders.

Now supports pagination if the user requests it, otherwise return all tags.

fixes #3232 OCI Conformance: GET number of tags should be limitable by n query parameter
closes #1936
closes #2452.

@eyJhb eyJhb closed this Apr 11, 2020
@eyJhb eyJhb reopened this Apr 11, 2020
@eyJhb eyJhb force-pushed the pagination branch 2 times, most recently from cebe938 to 42f8922 Compare April 14, 2020 13:48
@eyJhb eyJhb changed the title WIP: Add pagination on /v2/<name>/tags/list Add pagination on /v2/<name>/tags/list Apr 14, 2020
@eyJhb
Copy link
Contributor Author

eyJhb commented Apr 14, 2020

@dmcgowan should be good to go! Have a look at it when you get the time.
Should be the last blocker before a release could be made (as far as I understood from comments).

}

// if no error, means that the user requested `n` entries
if maxEntries, err := strconv.Atoi(q.Get("n")); err == nil {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it makes more sense here to return 400 if n is a garbage value rather than just ignore it. Just go into this block if n is set.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I can do that!
Keep in mind that atm. catalog has the same behavior, where it will not throw any error on a invalid n value (https://github.com/docker/distribution/blob/master/registry/handlers/catalog.go#L41-L44).

Should I make a PR to get them to match up?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that is the right behavior, but considering that API is not used by end clients, it's not super important.

We can possibly discuss the error handling of these in the distribution spec as well if it is too ambiguous. Handling bad input (n is not a number, last does not exist) could be clearly defined.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Lets get this merged first then I can get the other branch up-to-date and use the distribution spec in that as well.
Somewhat easy to do for n, but when last is wrong it is a different case (either the tag does not exists or the repository doesn't).

I currently have this wording for the spec, but feel unsure about the wording.

	// ErrorPaginationNumberInvalid is returned when the requested number
	// of results n is not a integer.
	ErrorPaginationNumberInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{
		Value:   "PAGINATION_NUMBER_INVALID",
		Message: "invalid number of results requested",
		Description: `This error may be returned when the requested
		"n" number of results is not an integer type`,
		HTTPStatusCode: http.StatusBadRequest,
	})

Would love some input, and maybe a suggestion for the last param. Else I will try to word it myself tomorrow :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@dmcgowan fixed in latest commits, please check to ensure it is correct :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hoping a ping is OK,new Github notifications isn't nice @dmcgowan :|

Copy link
Collaborator

Choose a reason for hiding this comment

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

I saw this, but wanted to cross reference these changes with the spec first before merging. Just went through now, the section is https://github.com/opencontainers/distribution-spec/blob/master/spec.md#tags-paginated

if lastEntryIndex != len(tags) && tags[lastEntryIndex] == lastEntry {
tags = tags[lastEntryIndex+1:]
} else {
th.Errors = append(th.Errors, v2.ErrorCodePaginationLastUnknown.WithDetail(map[string]string{"last": lastEntry}))
Copy link
Collaborator

Choose a reason for hiding this comment

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

This error is the one that I wanted to double check. The spec says for last that Result set will include values lexically after last. In this case, the search should be done, but it can still continue even if last doesn't exist but entries exist lexically after it.

@jdolitsky do you mind offering an opinion here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes perfect sense tbh. most have missed that it specified that. I sadly read it as after the index of the last string and values should be sorted lexically.
Seems like that should be the behavior :)

Just say the word and I will update it in this (and update the catalog PR after this is merged to do the same).

Copy link

@pmengelbert pmengelbert Apr 24, 2020

Choose a reason for hiding this comment

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

@dmcgowan @eyJhb I'm here on behalf of @jdolitsky
You're correct, the spec says that the /v2/<name>/tags/list endpoint should return a lexically-ordered list when the n query parameter is present, regardless of whether last is specified.

Paginated tag results can be retrieved by adding an n parameter to the request URL, declaring that the response SHOULD be limited to n results. Starting a paginated flow MAY begin as follows:

GET /v2/<name>/tags/list?n=<integer>
The above specifies that a tags response SHOULD be returned, from the start of the result set, ordered lexically, limiting the number of results to n. The response to such a request would look as follows:

https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pagination

Usually the value of the last key would be acquired by by a Link header from an earlier response. Failing that, the registry should probably behave intelligently when the actual last value doesn't exist.

The current language in the spec is at the SHOULD level, but this will probably become a MUST by the time distribution-spec 1.0 is released.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What I gather from this is the following. last does not have to match a value in the tags, but should specify where in the tags we want to get our tags from legically (so this should be changed in this PR).

I am wondering if last should be ignored, if no n is specified? Below is from the doc.

The client can then issue the request with the above value from the Link header, receiving the values v3 and v4. Note that n may change on the second to last response or be fully omitted, depending on the server implementation.

Keep in mind, this will also change the catalog listing to work the same way as tags.

Copy link
Contributor

Choose a reason for hiding this comment

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

This seems related to this discussion: opencontainers/distribution-spec#132

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, so what I think should happen is the following.

  • n=0 will return a empty list, and will not include a Link, as there are no more entries (depends on how you look at it. But if I want 0 and I want more, I will still get 0).
  • last will be used to find the entries lexically after it, and the index does not need to exists.
    • this could bring some weird bugs for some people regarding encoding. but it does make sense to be able to search like this.
  • last can stand on its own without n, as to use the default n value from the server (which it may impose).

How does this sound @jdolitsky and @dmcgowan ?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Makes sense to me, but we should probably clarify in the spec first. I'm ok making that update in this project with expectation that we may need to make slight changes if we come to a different agreement for the spec

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@dmcgowan so this is good to go, if I make the changes and then we can update the spec in accordance?

Copy link
Collaborator

Choose a reason for hiding this comment

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

yeah, let's go ahead and make those changes and get this merged in. We can tweak the conformance tests and spec after, if we need to come back and sync here we can before the release

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@dmcgowan thougt I had pinged you here, I guess not. It should be good to go! Travis is giving some weird errors, that doesn't seem to be a problem with the code. Should be ready to merge!

@eyJhb
Copy link
Contributor Author

eyJhb commented Jun 25, 2020

Anyone that can merge this? Seems to have been forgotten :)

@thaJeztah
Copy link
Member

Looks like the build was failing; I restarted CI to see if it was a one-off failure

@manishtomar @dmcgowan PTAL

@eyJhb
Copy link
Contributor Author

eyJhb commented Jun 25, 2020

@thaJeztah the errors does not seem related to my work, or am I reading it wrong?

+ check

/bin/sh: 1: golangci-lint: not found

make: *** [check] Error 127

The command "make check" exited with 2.

@jdolitsky
Copy link
Contributor

@eyJhb @thaJeztah - the CI failure is unrelated to the change, I have the same Travis error in an unrelated PR: #3161

@thaJeztah
Copy link
Member

Ah, thanks I see @jdolitsky opened #3187 to fix CI

@eyJhb
Copy link
Contributor Author

eyJhb commented Jun 26, 2020

So, waiting for #3187 so I can cherry pick it. Assume it won't take long

@thaJeztah
Copy link
Member

@eyJhb #3187 was merged; can you rebase your PR to get that fix in? Let me know if you need a hand / instructions for the rebase

@eyJhb
Copy link
Contributor Author

eyJhb commented Jun 29, 2020

@eyJhb #3187 was merged; can you rebase your PR to get that fix in? Let me know if you need a hand / instructions for the rebase

Rebased and passed! Ready to be merged

// get entries after latest, if any specified
if lastEntry := q.Get("last"); lastEntry != "" {
lastEntryIndex := sort.SearchStrings(tags, lastEntry)
tags = tags[lastEntryIndex+1:]
Copy link
Collaborator

Choose a reason for hiding this comment

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

According to the documentation for sort.SearchStrings, x is not present (it could be len(a)). This could lead to a panic here. This should probably be covered in the unit tests and boundary tested.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just seem to remember having fixed that error before.
To do unittests, that would require I know a lot more regarding how unit tests are done in the handlers.

I propose making this change (fixing this), so it will not panic and then we can either merge and someone can make a PR for unit tests, or there can be made a PR based on this.

But considering this has been in the making since 2016, I am not having high hopes for anyone picking up the unittests. And not sure when I will get the time for it, sadly

Copy link
Contributor

Choose a reason for hiding this comment

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

@eyJhb maybe at least to ensure no panic we can add something like

if lastEntry := q.Get("last"); lastEntry != "" {
	lastEntryIndex := sort.SearchStrings(tags, lastEntry) + 1
	if lastEntryIndex < len(tags) {
		tags = tags[lastEntryIndex:]
	}
}

?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jdolitsky ahh damn it, that is a lot prettier than what I just pushed... Lets totally go with that! Let me just rebase again :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

EDIT: we can't do that actually, as we need to set it to a empty list if it has no matches. That is at least what I would think if a user specifies something that is not in the tags (return nothing). But we can do this

What about the newest change?

	if lastEntry := q.Get("last"); lastEntry != "" {
		lastEntryIndex := sort.SearchStrings(tags, lastEntry)

		// as`sort.SearchStrings` can return len(tags), if the
		// specified `lastEntry` is not found, we need to
		// ensure it does not panic when slicing.
		if lastEntryIndex < len(tags) {
			tags = []string{}
		} else {
			tags = tags[lastEntryIndex+1:]
		}
	}

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, noticed that too. I agree. This lgtm

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did however like yours a lot, but we end up having somewhat the same :) Perfect! Lets see if the others agree.
@dmcgowan will you resolve if you think this solution is acceptable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ping @dmcgowan :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ping again, hope it is okay @dmcgowan :)

@thaJeztah anything we can do to get this merged?

@waynr
Copy link
Contributor

waynr commented Aug 4, 2020

Anything I can do to help get this PR merged? From the comments it looks like everything has been worked out and it is just waiting on a tardy reviewer to give thumbs up.

@eyJhb
Copy link
Contributor Author

eyJhb commented Aug 4, 2020

Anything I can do to help get this PR merged? From the comments it looks like everything has been worked out and it is just waiting on a tardy reviewer to give thumbs up.

I think we are just waiting for someone to merge this. At least as far as I can tell.

@eyJhb
Copy link
Contributor Author

eyJhb commented Aug 20, 2020

Bumping this :)

@thaJeztah
Copy link
Member

@dmcgowan @manishtomar PTAL 🙏

eyJhb added 2 commits April 23, 2021 18:06
Signed-off-by: eyjhb <eyjhbb@gmail.com>
Signed-off-by: eyjhb <eyjhbb@gmail.com>
@eyJhb
Copy link
Contributor Author

eyJhb commented Apr 23, 2021

The code looks good. Thanks @eyJhb for putting this together. There is one change I'd like to make as per @deleteriousEffect comment. Can you please add the newly introduced error code into docs/spec/api.md. Then we are good to go!

Should be good to go now I suppose? :)

@milosgajdos milosgajdos requested a review from waynr May 7, 2021 09:34
@codecov-commenter
Copy link

Codecov Report

Merging #3143 (9cf3999) into main (6891d94) will decrease coverage by 0.15%.
The diff coverage is n/a.

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3143      +/-   ##
==========================================
- Coverage   56.31%   56.16%   -0.16%     
==========================================
  Files         102      102              
  Lines        7243     7266      +23     
==========================================
+ Hits         4079     4081       +2     
- Misses       2523     2542      +19     
- Partials      641      643       +2     
Impacted Files Coverage Δ
...istribution/distribution/registry/handlers/tags.go 40.00% <0.00%> (-30.84%) ⬇️
...distribution/distribution/registry/handlers/app.go 46.16% <0.00%> (-0.09%) ⬇️
...bution/distribution/configuration/configuration.go 62.03% <0.00%> (ø)
...bution/distribution/registry/api/v2/descriptors.go 100.00% <0.00%> (ø)
...ribution/distribution/registry/storage/tagstore.go 75.00% <0.00%> (+0.22%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6891d94...9cf3999. Read the comment docs.

@milosgajdos milosgajdos requested a review from wy65701436 May 7, 2021 09:43
@milosgajdos milosgajdos changed the title Add pagination on /v2/<name>/tags/list OCI: Add pagination on /v2/<name>/tags/list May 7, 2021

// there is no guarantee for the order,
// therefore sort before return.
sort.Strings(tags)
Copy link
Collaborator

@wy65701436 wy65701436 May 11, 2021

Choose a reason for hiding this comment

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

IMO, it's better to put the sorting in the tagHandler, just let's the tags cosumer to judge.

@wy65701436
Copy link
Collaborator

can we add UT for this? To test different situation, both positive and negative.

  • httptest.NewRequest(http.MethodGet, "/v2/library/hello-world/tags/list", nil)
  • httptest.NewRequest(http.MethodGet, "/v2/hello-world/tags/list?n=1", nil)
  • httptest.NewRequest(http.MethodGet, "/v2/hello-world/tags/list?n=3", nil)
  • httptest.NewRequest(http.MethodGet, "/v2/hello-world/tags/list?last=v1&n=1", nil)

// ensure it does not panic when slicing.
if lastEntryIndex == len(tags) {
tags = []string{}
} else {
Copy link
Collaborator

@wy65701436 wy65701436 May 11, 2021

Choose a reason for hiding this comment

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

if the last doesn't exist, should not be a bad request?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can do partial searches, using the lastEntryIndex, so it doesn't need to be a exact match, and therefore is not really a error as far as I understand :)

Copy link
Collaborator

@wy65701436 wy65701436 left a comment

Choose a reason for hiding this comment

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

lgtm

@joaodrp
Copy link
Collaborator

joaodrp commented May 13, 2021

can we add UT for this? To test different situation, both positive and negative.

  • httptest.NewRequest(http.MethodGet, "/v2/library/hello-world/tags/list", nil)
  • httptest.NewRequest(http.MethodGet, "/v2/hello-world/tags/list?n=1", nil)
  • httptest.NewRequest(http.MethodGet, "/v2/hello-world/tags/list?n=3", nil)
  • httptest.NewRequest(http.MethodGet, "/v2/hello-world/tags/list?last=v1&n=1", nil)

@eyJhb, could you please add some tests to registry/handlers/api_test.go?

@eyJhb
Copy link
Contributor Author

eyJhb commented May 13, 2021

@eyJhb, could you please add some tests to registry/handlers/api_test.go?
I am on the last weeks of my semester project, I will try to see once I have time. But if anyone wants to throw some in that have time, feel free :)

@joaodrp
Copy link
Collaborator

joaodrp commented May 22, 2021

@eyJhb, could you please add some tests to registry/handlers/api_test.go?
I am on the last weeks of my semester project, I will try to see once I have time. But if anyone wants to throw some in that have time, feel free :)

Alright, I'll push a separate PR with test coverage. Just tested this locally with no problems.

@rbbl-dev
Copy link

i just pulled a fresh image of this registry and i'm questioning my sanity.

for me the tags are not sorted and n and last are ignored but this MR claims to fix those things.
grafik

@sudo-bmitch
Copy link
Contributor

@rbbl-dev this is in the 3.0.0 milestone. What version of the registry are you running?

@rbbl-dev
Copy link

2.8.2 which is the newest on https://hub.docker.com/_/registry/tags @sudo-bmitch

flavio added a commit to rbbl-dev/oci-distribution that referenced this pull request Sep 26, 2023
Consume `distribution/distribution:edge` container image when testing
the list tags functionality.

Currently this is the only container image that includes this fix:
distribution/distribution#3143

This can be removed once distribution v3 is released

Signed-off-by: Flavio Castelli <fcastelli@suse.com>
rbbl-dev added a commit to rbbl-dev/oci-distribution that referenced this pull request Oct 7, 2023
the list tags functionality.

Currently this is the only container image that includes this fix:
distribution/distribution#3143

This can be removed once distribution v3 is released
rbbl-dev added a commit to rbbl-dev/oci-distribution that referenced this pull request Oct 7, 2023
the list tags functionality.

Currently this is the only container image that includes this fix:
distribution/distribution#3143

This can be removed once distribution v3 is released

Signed-off-by: rbbl-dev <dev@rbbl.cc>
rbbl-dev added a commit to rbbl-dev/oci-distribution that referenced this pull request Oct 7, 2023
the list tags functionality.

Currently this is the only container image that includes this fix:
distribution/distribution#3143

This can be removed once distribution v3 is released

Signed-off-by: rbbl-dev <dev@rbbl.cc>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conformance Related to conformance to distribution specification

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OCI Conformance: GET number of tags should be limitable by n query parameter API different responses HTTP API Tags Paginated not work