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