From f72cb1d2bac075e4d26792f290cad4ff95de3911 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 3 Oct 2016 21:10:39 +0300 Subject: [PATCH] Fix tags API pagination and add a test --- endpoints/v2/__init__.py | 3 +- test/registry_tests.py | 104 +++++++++++++++++++++++++++------------ 2 files changed, 74 insertions(+), 33 deletions(-) diff --git a/endpoints/v2/__init__.py b/endpoints/v2/__init__.py index 07e325df9..8d611de0a 100644 --- a/endpoints/v2/__init__.py +++ b/endpoints/v2/__init__.py @@ -55,8 +55,9 @@ def paginate(limit_kwarg_name='limit', offset_kwarg_name='offset', offset = page_info.get('offset', 0) def callback(num_results, response): - if num_results <= limit: + if num_results < limit: return + next_page_token = encrypt_page_token({'offset': limit + offset}) link = get_app_url() + url_for(request.endpoint, **request.view_args) link += '?%s; rel="next"' % urlencode({'n': limit, 'next_page': next_page_token}) diff --git a/test/registry_tests.py b/test/registry_tests.py index 218f5576b..680d5133a 100644 --- a/test/registry_tests.py +++ b/test/registry_tests.py @@ -526,9 +526,8 @@ class V2RegistryMixin(BaseRegistryMixin): class V2RegistryPushMixin(V2RegistryMixin): push_version = 'v2' - def do_push(self, namespace, repository, username, password, images=None, tag_name=None, - cancel=False, invalid=False, expect_failure=None, scopes=None, - munge_shas=False): + def do_push(self, namespace, repository, username, password, images=None, tag_names=None, + cancel=False, invalid=False, expect_failure=None, scopes=None, munge_shas=False): images = images or self._get_default_images() repo_name = _get_repo_name(namespace, repository) @@ -546,23 +545,28 @@ class V2RegistryPushMixin(V2RegistryMixin): if expected_auth_code != 200: return - # Build a fake manifest. - tag_name = tag_name or 'latest' - builder = DockerSchema1ManifestBuilder(namespace, repository, tag_name) - full_contents = {} - - for image_data in reversed(images): - full_contents[image_data['id']] = _get_full_contents(image_data, additional_fields=munge_shas) - checksum = 'sha256:' + hashlib.sha256(full_contents[image_data['id']]).hexdigest() - if invalid: - checksum = 'sha256:' + hashlib.sha256('foobarbaz').hexdigest() - - builder.add_layer(checksum, json.dumps(image_data)) - expected_code = _get_expected_code(expect_failure, 2, 404) + tag_names = tag_names or ['latest'] - # Build the manifest. - manifest = builder.build(_JWK) + manifests = {} + full_contents = {} + for image_data in reversed(images): + full_contents[image_data['id']] = _get_full_contents(image_data, + additional_fields=munge_shas) + + # Build a fake manifest. + for tag_name in tag_names: + builder = DockerSchema1ManifestBuilder(namespace, repository, tag_name) + + for image_data in reversed(images): + checksum = 'sha256:' + hashlib.sha256(full_contents[image_data['id']]).hexdigest() + if invalid: + checksum = 'sha256:' + hashlib.sha256('foobarbaz').hexdigest() + + builder.add_layer(checksum, json.dumps(image_data)) + + # Build the manifest. + manifests[tag_name] = builder.build(_JWK) # Push the image's layers. checksums = {} @@ -636,14 +640,17 @@ class V2RegistryPushMixin(V2RegistryMixin): self.assertEquals(response.headers['Docker-Content-Digest'], checksum) self.assertEquals(response.headers['Content-Length'], str(len(layer_bytes))) - # Write the manifest. If we expect it to be invalid, we expect a 404 code. Otherwise, we expect - # a 202 response for success. - put_code = 404 if invalid else 202 - self.conduct('PUT', '/v2/%s/manifests/%s' % (repo_name, tag_name), - data=manifest.bytes, expected_code=put_code, - headers={'Content-Type': 'application/json'}, auth='jwt') + for tag_name in tag_names: + manifest = manifests[tag_name] - return checksums, manifest.digest + # Write the manifest. If we expect it to be invalid, we expect a 404 code. Otherwise, we expect + # a 202 response for success. + put_code = 404 if invalid else 202 + self.conduct('PUT', '/v2/%s/manifests/%s' % (repo_name, tag_name), + data=manifest.bytes, expected_code=put_code, + headers={'Content-Type': 'application/json'}, auth='jwt') + + return checksums, manifests class V2RegistryPullMixin(V2RegistryMixin): @@ -1147,9 +1154,38 @@ class V1RegistryTests(V1RegistryPullMixin, V1RegistryPushMixin, RegistryTestsMix class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMixin, RegistryTestCaseMixin, LiveServerTestCase): """ Tests for V2 registry. """ + def test_tags_pagination(self): + # Push 10 tags. + tag_names = ['tag-%s' % i for i in range(0, 10)] + self.do_push('public', 'new-repo', 'public', 'password', tag_names=tag_names) + + encountered = set() + + # Ensure tags list is properly paginated. + relative_url = '/v2/public/new-repo/tags/list?n=5' + for i in range(0, 3): + result = self.conduct('GET', relative_url, auth='jwt') + result_json = result.json() + encountered.update(set(result_json['tags'])) + + if 'Link' not in result.headers: + break + + # Check the next page of results. + link = result.headers['Link'] + self.assertTrue(link.endswith('; rel="next"')) + + url, _ = link.split(';') + relative_url = url[len(self.get_server_url()):] + + encountered.update(set(result_json['tags'])) + + # Ensure we found all the results. + self.assertEquals(encountered, set(tag_names)) + def test_numeric_tag(self): # Push a new repository. - self.do_push('public', 'new-repo', 'public', 'password', tag_name='1234') + self.do_push('public', 'new-repo', 'public', 'password', tag_names=['1234']) # Pull the repository. self.do_pull('public', 'new-repo', 'public', 'password', manifest_id='1234') @@ -1172,7 +1208,8 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix 'contents': 'somecontent' }] - (_, digest) = self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images) + (_, manifests) = self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images) + digest = manifests['latest'].digest self.conduct_api_login('devtable', 'password') labels = self.conduct('GET', '/api/v1/repository/devtable/newrepo/manifest/' + digest + '/labels').json() @@ -1194,7 +1231,8 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix 'contents': 'somecontent' }] - (_, digest) = self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images) + (_, manifests) = self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images) + digest = manifests['latest'].digest self.conduct_api_login('devtable', 'password') labels = self.conduct('GET', '/api/v1/repository/devtable/newrepo/manifest/' + digest + '/labels').json() @@ -1246,7 +1284,8 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix def test_delete_manifest(self): # Push a new repo with the latest tag. - (_, digest) = self.do_push('devtable', 'newrepo', 'devtable', 'password') + (_, manifests) = self.do_push('devtable', 'newrepo', 'devtable', 'password') + digest = manifests['latest'].digest # Ensure the pull works. self.do_pull('devtable', 'newrepo', 'devtable', 'password') @@ -1289,7 +1328,8 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix def test_pull_by_checksum(self): # Add a new repository under the user, so we have a real repository to pull. - _, digest = self.do_push('devtable', 'newrepo', 'devtable', 'password') + _, manifests = self.do_push('devtable', 'newrepo', 'devtable', 'password') + digest = manifests['latest'].digest # Attempt to pull by digest. self.do_pull('devtable', 'newrepo', 'devtable', 'password', manifest_id=digest) @@ -1437,10 +1477,10 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix # Create the repo. self.do_push('devtable', 'newrepo', 'devtable', 'password', images=latest_images, - tag_name='latest') + tag_names=['latest']) self.do_push('devtable', 'newrepo', 'devtable', 'password', images=foobar_images, - tag_name='foobar') + tag_names=['foobar']) # Retrieve the tags. response = self.conduct('GET', '/v2/devtable/newrepo/tags/list', auth='jwt', expected_code=200)