diff --git a/lib/modules/datasource/docker/index.spec.ts b/lib/modules/datasource/docker/index.spec.ts index 5b4fa32cd7..4089c38cc6 100644 --- a/lib/modules/datasource/docker/index.spec.ts +++ b/lib/modules/datasource/docker/index.spec.ts @@ -21,6 +21,7 @@ const ecrMock = mockClient(ECRClient); const baseUrl = 'https://index.docker.io/v2'; const authUrl = 'https://auth.docker.io'; const amazonUrl = 'https://123456789.dkr.ecr.us-east-1.amazonaws.com/v2'; +const dockerHubUrl = 'https://hub.docker.com/v2/repositories'; function mockEcrAuthResolve( res: Partial = {} @@ -41,6 +42,7 @@ describe('modules/datasource/docker/index', () => { }); hostRules.hosts.mockReturnValue([]); delete process.env.RENOVATE_X_DOCKER_MAX_PAGES; + delete process.env.RENOVATE_X_DOCKER_HUB_TAGS; }); describe('getDigest', () => { @@ -1520,7 +1522,12 @@ describe('modules/datasource/docker/index', () => { }); it('adds library/ prefix for Docker Hub (implicit)', async () => { + process.env.RENOVATE_X_DOCKER_HUB_TAGS = 'true'; const tags = ['1.0.0']; + httpMock + .scope(dockerHubUrl) + .get('/library/node/tags?page_size=100') + .reply(404); httpMock .scope(baseUrl) .get('/library/node/tags/list?n=10000') @@ -1548,31 +1555,29 @@ describe('modules/datasource/docker/index', () => { }); it('adds library/ prefix for Docker Hub (explicit)', async () => { - const tags = ['1.0.0']; + process.env.RENOVATE_X_DOCKER_HUB_TAGS = 'true'; + httpMock + .scope(dockerHubUrl) + .get('/library/node/tags?page_size=100') + .reply(200, { + next: `${dockerHubUrl}/library/node/tags?page=2&page_size=100`, + results: [{ name: '1.0.0' }], + }) + .get('/library/node/tags?page=2&page_size=100') + .reply(200, { + results: [{ name: '0.9.0' }], + }); httpMock .scope(baseUrl) - .get('/library/node/tags/list?n=10000') - .reply(401, '', { - 'www-authenticate': - 'Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:library/node:pull"', - }) - .get('/library/node/tags/list?n=10000') - .reply(200, { tags }, {}) .get('/') .reply(200) .get('/library/node/manifests/1.0.0') .reply(200); - httpMock - .scope(authUrl) - .get( - '/token?service=registry.docker.io&scope=repository:library/node:pull' - ) - .reply(200, { token: 'test' }); const res = await getPkgReleases({ datasource: DockerDatasource.id, packageName: 'docker.io/node', }); - expect(res?.releases).toHaveLength(1); + expect(res?.releases).toHaveLength(2); }); it('adds no library/ prefix for other registries', async () => { diff --git a/lib/modules/datasource/docker/index.ts b/lib/modules/datasource/docker/index.ts index f71c82bbb9..01d3b5e8eb 100644 --- a/lib/modules/datasource/docker/index.ts +++ b/lib/modules/datasource/docker/index.ts @@ -30,6 +30,7 @@ import { } from './common'; import { ecrPublicRegex, ecrRegex, isECRMaxResultsError } from './ecr'; import type { Manifest, OciImageConfig } from './schema'; +import type { DockerHubTags } from './types'; const defaultConfig = { commitMessageTopic: '{{{depName}}} Docker tag', @@ -819,6 +820,36 @@ export class DockerDatasource extends Datasource { return digest; } + async getDockerHubTags(dockerRepository: string): Promise { + if (!process.env.RENOVATE_X_DOCKER_HUB_TAGS) { + return null; + } + try { + let index = 0; + let tags: string[] = []; + let url: + | string + | undefined = `https://hub.docker.com/v2/repositories/${dockerRepository}/tags?page_size=100`; + do { + const res: DockerHubTags = (await this.http.getJson(url)) + .body; + tags = tags.concat(res.results.map((tag) => tag.name)); + url = res.next; + index += 1; + } while (url && index < 100); + logger.debug( + `getDockerHubTags(${dockerRepository}): found ${tags.length} tags` + ); + return tags; + } catch (err) { + logger.debug( + { dockerRepository, errMessage: err.message }, + `No Docker Hub tags result - falling back to docker.io api` + ); + } + return null; + } + /** * docker.getReleases * @@ -838,7 +869,11 @@ export class DockerDatasource extends Datasource { packageName, registryUrl! ); - const tags = await this.getTags(registryHost, dockerRepository); + let tags: string[] | null = null; + if (registryHost === 'https://index.docker.io') { + tags = await this.getDockerHubTags(dockerRepository); + } + tags ??= await this.getTags(registryHost, dockerRepository); if (!tags) { return null; } diff --git a/lib/modules/datasource/docker/types.ts b/lib/modules/datasource/docker/types.ts index 9f72be577a..0026ca7d39 100644 --- a/lib/modules/datasource/docker/types.ts +++ b/lib/modules/datasource/docker/types.ts @@ -2,3 +2,8 @@ export interface RegistryRepository { registryHost: string; dockerRepository: string; } + +export interface DockerHubTags { + next?: string; + results: { name: string }[]; +}