From c22b7f838ff2b7e5fadd527fd5ce98920f4e5bdb Mon Sep 17 00:00:00 2001 From: Giorgos Karagounis <40686064+giokara-oqton@users.noreply.github.com> Date: Tue, 25 May 2021 13:00:36 +0200 Subject: [PATCH] feat(docker): quay api v1 for tags (#10093) Co-authored-by: Michael Kriese <michael.kriese@visualon.de> Co-authored-by: Jamie Magee <jamie.magee@gmail.com> --- .../docker/__snapshots__/index.spec.ts.snap | 37 ++++++++++++++ lib/datasource/docker/index.spec.ts | 37 ++++++++++++++ lib/datasource/docker/index.ts | 50 ++++++++++++------- lib/datasource/docker/quay.ts | 26 ++++++++++ 4 files changed, 133 insertions(+), 17 deletions(-) create mode 100644 lib/datasource/docker/quay.ts diff --git a/lib/datasource/docker/__snapshots__/index.spec.ts.snap b/lib/datasource/docker/__snapshots__/index.spec.ts.snap index d0768acadc..be528e2811 100644 --- a/lib/datasource/docker/__snapshots__/index.spec.ts.snap +++ b/lib/datasource/docker/__snapshots__/index.spec.ts.snap @@ -1175,3 +1175,40 @@ Array [ }, ] `; + +exports[`datasource/docker/index getReleases uses quay api 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic c29tZS11c2VybmFtZTpzb21lLXBhc3N3b3Jk", + "host": "quay.io", + "user-agent": "https://github.com/renovatebot/renovate", + }, + "method": "GET", + "url": "https://quay.io/api/v1/repository/bitnami/redis/tag/?limit=100&page=1&onlyActiveTags=true", + }, + Object { + "headers": Object { + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic c29tZS11c2VybmFtZTpzb21lLXBhc3N3b3Jk", + "host": "quay.io", + "user-agent": "https://github.com/renovatebot/renovate", + }, + "method": "GET", + "url": "https://quay.io/v2/", + }, + Object { + "headers": Object { + "accept": "application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Basic c29tZS11c2VybmFtZTpzb21lLXBhc3N3b3Jk", + "host": "quay.io", + "user-agent": "https://github.com/renovatebot/renovate", + }, + "method": "GET", + "url": "https://quay.io/v2/bitnami/redis/manifests/5.0.12", + }, +] +`; diff --git a/lib/datasource/docker/index.spec.ts b/lib/datasource/docker/index.spec.ts index 39918e8257..2b82f07638 100644 --- a/lib/datasource/docker/index.spec.ts +++ b/lib/datasource/docker/index.spec.ts @@ -447,6 +447,43 @@ describe(getName(), () => { expect(res.releases).toHaveLength(1); expect(httpMock.getTrace()).toMatchSnapshot(); }); + it('uses quay api', async () => { + const tags = [{ name: '5.0.12' }]; + httpMock + .scope('https://quay.io') + .get( + '/api/v1/repository/bitnami/redis/tag/?limit=100&page=1&onlyActiveTags=true' + ) + .reply(200, { tags, has_additional: false }) + .get('/v2/') + .reply(200, '', {}) + .get('/v2/bitnami/redis/manifests/5.0.12') + .reply(200, '', {}); + const config = { + datasource: id, + depName: 'bitnami/redis', + registryUrls: ['https://quay.io'], + }; + const res = await getPkgReleases(config); + expect(res.releases).toHaveLength(1); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + it('uses quay api and test error', async () => { + httpMock + .scope('https://quay.io') + .get( + '/api/v1/repository/bitnami/redis/tag/?limit=100&page=1&onlyActiveTags=true' + ) + .reply(500); + const config = { + datasource: id, + depName: 'bitnami/redis', + registryUrls: ['https://quay.io'], + }; + await expect(getPkgReleases(config)).rejects.toThrow( + 'external-host-error' + ); + }); it('uses lower tag limit for ECR deps', async () => { httpMock .scope(amazonUrl) diff --git a/lib/datasource/docker/index.ts b/lib/datasource/docker/index.ts index cd34dbc231..bce3b074a0 100644 --- a/lib/datasource/docker/index.ts +++ b/lib/datasource/docker/index.ts @@ -16,6 +16,7 @@ import { http, id, } from './common'; +import { getTagsQuayRegistry } from './quay'; // TODO: add got typings when available (#9646) // TODO: replace www-authenticate with https://www.npmjs.com/package/auth-header (#9645) @@ -53,11 +54,35 @@ export const defaultConfig = { }, }; -async function getTags( +async function getDockerApiTags( registry: string, repository: string ): Promise<string[] | null> { let tags: string[] = []; + // AWS ECR limits the maximum number of results to 1000 + // See https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_DescribeRepositories.html#ECR-DescribeRepositories-request-maxResults + const limit = ecrRegex.test(registry) ? 1000 : 10000; + let url = `${registry}/v2/${repository}/tags/list?n=${limit}`; + const headers = await getAuthHeaders(registry, repository); + if (!headers) { + logger.debug('Failed to get authHeaders for getTags lookup'); + return null; + } + let page = 1; + do { + const res = await http.getJson<{ tags: string[] }>(url, { headers }); + tags = tags.concat(res.body.tags); + const linkHeader = parseLinkHeader(res.headers.link as string); + url = linkHeader?.next ? URL.resolve(url, linkHeader.next.url) : null; + page += 1; + } while (url && page < 20); + return tags; +} + +async function getTags( + registry: string, + repository: string +): Promise<string[] | null> { try { const cacheNamespace = 'datasource-docker-tags'; const cacheKey = `${registry}:${repository}`; @@ -69,23 +94,14 @@ async function getTags( if (cachedResult !== undefined) { return cachedResult; } - // AWS ECR limits the maximum number of results to 1000 - // See https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_DescribeRepositories.html#ECR-DescribeRepositories-request-maxResults - const limit = ecrRegex.test(registry) ? 1000 : 10000; - let url = `${registry}/v2/${repository}/tags/list?n=${limit}`; - const headers = await getAuthHeaders(registry, repository); - if (!headers) { - logger.debug('Failed to get authHeaders for getTags lookup'); - return null; + + const isQuay = registry === 'https://quay.io'; + let tags: string[] | null; + if (isQuay) { + tags = await getTagsQuayRegistry(repository); + } else { + tags = await getDockerApiTags(registry, repository); } - let page = 1; - do { - const res = await http.getJson<{ tags: string[] }>(url, { headers }); - tags = tags.concat(res.body.tags); - const linkHeader = parseLinkHeader(res.headers.link as string); - url = linkHeader?.next ? URL.resolve(url, linkHeader.next.url) : null; - page += 1; - } while (url && page < 20); const cacheMinutes = 30; await packageCache.set(cacheNamespace, cacheKey, tags, cacheMinutes); return tags; diff --git a/lib/datasource/docker/quay.ts b/lib/datasource/docker/quay.ts new file mode 100644 index 0000000000..f0cca90bee --- /dev/null +++ b/lib/datasource/docker/quay.ts @@ -0,0 +1,26 @@ +import { http } from './common'; + +export async function getTagsQuayRegistry( + repository: string +): Promise<string[]> { + const registry = 'https://quay.io'; + let tags: string[] = []; + const limit = 100; + + const pageUrl = (page: number): string => + `${registry}/api/v1/repository/${repository}/tag/?limit=${limit}&page=${page}&onlyActiveTags=true`; + + let page = 1; + let url = pageUrl(page); + do { + const res = await http.getJson<{ + tags: { name: string }[]; + has_additional: boolean; + }>(url, {}); + const pageTags = res.body.tags.map((tag) => tag.name); + tags = tags.concat(pageTags); + page += 1; + url = res.body.has_additional ? pageUrl(page) : null; + } while (url && page < 20); + return tags; +} -- GitLab