From a0bc0e9488055df49d516318b0678a7ffa25d2cb Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 8 Feb 2017 11:59:22 -0800 Subject: [PATCH] Implement the full spec for the old Docker V1 registry search API This API is still (apparently) being used by the Docker CLI for `docker search` (why?!) and we therefore have customers expecting this to work the same way as the DockerHub. --- endpoints/v1/index.py | 48 ++++++++++++++++++++++++++++++------------ test/registry_tests.py | 25 ++++++++++++++++++++++ 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/endpoints/v1/index.py b/endpoints/v1/index.py index c4f815f98..b15af1b84 100644 --- a/endpoints/v1/index.py +++ b/endpoints/v1/index.py @@ -289,27 +289,28 @@ def put_repository_auth(namespace_name, repo_name): def get_search(): query = request.args.get('q') + try: + limit = min(100, max(1, int(request.args.get('n', 25)))) + except ValueError: + limit = 25 + + try: + page = max(0, int(request.args.get('page', 1))) + except ValueError: + page = 1 + username = None user = get_authenticated_user() if user is not None: username = user.username - results = [] - if query: - _conduct_repo_search(username, query, results) - - data = { - "query": query, - "num_results": len(results), - "results" : results - } - + data = _conduct_repo_search(username, query, limit, page) resp = make_response(json.dumps(data), 200) resp.mimetype = 'application/json' return resp -def _conduct_repo_search(username, query, results): +def _conduct_repo_search(username, query, limit=25, page=1): """ Finds matching repositories. """ def can_read(repo): if repo.is_public: @@ -317,13 +318,32 @@ def _conduct_repo_search(username, query, results): return ReadRepositoryPermission(repo.namespace_user.username, repo.name).can() - only_public = username is None - matching_repos = model.get_sorted_matching_repositories(query, only_public, can_read, limit=5) + # Note: We put a max 5 page limit here. The Docker CLI doesn't seem to use the + # pagination and most customers hitting the API should be using V2 catalog, so this + # is a safety net for our slow search below, since we have to use the slow approach + # of finding *all* the results, and then slicing in-memory, because this old API requires + # the *full* page count in the returned results. + _MAX_PAGE_COUNT = 5 + page = min(page, _MAX_PAGE_COUNT) - for repo in matching_repos: + only_public = username is None + matching_repos = model.get_sorted_matching_repositories(query, only_public, can_read, + limit=limit*_MAX_PAGE_COUNT) + results = [] + for repo in matching_repos[(page - 1) * _MAX_PAGE_COUNT:limit]: results.append({ 'name': repo.namespace_name + '/' + repo.name, 'description': repo.description, 'is_public': repo.is_public, 'href': '/repository/' + repo.namespace_name + '/' + repo.name }) + + # Defined: https://docs.docker.com/v1.6/reference/api/registry_api/ + return { + 'query': query, + 'num_results': len(results), + 'num_pages': (len(matching_repos) / limit) + 1, + 'page': page, + 'page_size': limit, + 'results': results, + } diff --git a/test/registry_tests.py b/test/registry_tests.py index da2228552..21cc7ad8e 100644 --- a/test/registry_tests.py +++ b/test/registry_tests.py @@ -1289,6 +1289,31 @@ class V1RegistryTests(V1RegistryPullMixin, V1RegistryPushMixin, RegistryTestsMix self.assertEquals(1, len(data['results'])) + def test_search_pagination(self): + # Check for the first page. + resp = self.conduct('GET', '/v1/search', params=dict(q='s', n='1')) + data = resp.json() + self.assertEquals('s', data['query']) + + self.assertEquals(1, data['num_results']) + self.assertEquals(1, len(data['results'])) + + self.assertEquals(1, data['page']) + self.assertTrue(data['num_pages'] > 1) + + # Check for the followup pages. + for page_index in range(1, data['num_pages']): + resp = self.conduct('GET', '/v1/search', params=dict(q='s', n='1', page=page_index)) + data = resp.json() + self.assertEquals('s', data['query']) + + self.assertEquals(1, data['num_results']) + self.assertEquals(1, len(data['results'])) + + self.assertEquals(1, data['page']) + self.assertTrue(data['num_pages'] > 1) + + def test_users(self): # Not logged in, should 404. self.conduct('GET', '/v1/users', expected_code=404)