diff --git a/lib/modules/datasource/docker/index.spec.ts b/lib/modules/datasource/docker/index.spec.ts index bec463d42a64907bf3df8cbc1c8266cb96f78c7b..4c2f84af977031c9feaa8bb3763b4c91d0840208 100644 --- a/lib/modules/datasource/docker/index.spec.ts +++ b/lib/modules/datasource/docker/index.spec.ts @@ -101,12 +101,15 @@ describe('modules/datasource/docker/index', () => { }); }); describe('getAuthHeaders', () => { - it('returns "authType token" if both provided', async () => { + beforeEach(() => { httpMock .scope('https://my.local.registry') .get('/v2/', undefined, { badheaders: ['authorization'] }) .reply(401, '', { 'www-authenticate': 'Authenticate you must' }); hostRules.hosts.mockReturnValue([]); + }); + + it('returns "authType token" if both provided', async () => { hostRules.find.mockReturnValue({ authType: 'some-authType', token: 'some-token', @@ -127,11 +130,6 @@ describe('modules/datasource/docker/index', () => { }); it('returns "Bearer token" if only token provided', async () => { - httpMock - .scope('https://my.local.registry') - .get('/v2/', undefined, { badheaders: ['authorization'] }) - .reply(401, '', { 'www-authenticate': 'Authenticate you must' }); - hostRules.hosts.mockReturnValue([]); hostRules.find.mockReturnValue({ token: 'some-token', }); @@ -151,11 +149,6 @@ describe('modules/datasource/docker/index', () => { }); it('fails', async () => { - httpMock - .scope('https://my.local.registry') - .get('/v2/', undefined, { badheaders: ['authorization'] }) - .reply(401, '', { 'www-authenticate': 'Authenticate you must' }); - hostRules.hosts.mockReturnValue([]); httpMock.clear(false); httpMock @@ -171,35 +164,6 @@ describe('modules/datasource/docker/index', () => { expect(headers).toBeNull(); }); - - it('use a HEAD request to the real resource and use the auth header values to get a token', async () => { - httpMock - .scope('https://my.local.registry') - .head('/v2/my/node/resource') - .reply(401, '', { - 'www-authenticate': - 'Bearer realm="https://my.local.registry/oauth2/token",service="my.local.registry",scope="repository:my/node:whatever"', - }) - .get( - '/oauth2/token?service=my.local.registry&scope=repository:my/node:whatever' - ) - .reply(200, { token: 'some-token' }); - - const headers = await getAuthHeaders( - http, - 'https://my.local.registry', - 'my/node/prefix', - 'head', - 'https://my.local.registry/v2/my/node/resource' - ); - - // do not inline, otherwise we get false positive from codeql - expect(headers).toMatchInlineSnapshot(` - Object { - "authorization": "Bearer some-token", - } - `); - }); }); describe('getDigest', () => { @@ -255,7 +219,7 @@ describe('modules/datasource/docker/index', () => { .get('/') .reply(401, '', { 'www-authenticate': - 'Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:library/some-dep:pull "', + 'Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:samalba/my-app:pull "', }) .head('/library/some-dep/manifests/latest') .reply(200, {}, { 'docker-content-digest': 'some-digest' }); @@ -281,7 +245,7 @@ describe('modules/datasource/docker/index', () => { .twice() .reply(401, '', { 'www-authenticate': - 'Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:library/some-dep:pull "', + 'Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:samalba/my-app:pull "', }) .head('/library/some-dep/manifests/some-new-value') .reply(200, undefined, {}) @@ -529,7 +493,7 @@ describe('modules/datasource/docker/index', () => { .get('/') .reply(401, '', { 'www-authenticate': - 'Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:library/some-other-dep:pull "', + 'Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:samalba/my-app:pull "', }) .head('/library/some-other-dep/manifests/8.0.0-alpine') .reply(200, {}, { 'docker-content-digest': 'some-digest' }); @@ -565,7 +529,7 @@ describe('modules/datasource/docker/index', () => { it('returns null if no token', async () => { httpMock .scope(baseUrl) - .head('/library/node/tags/list?n=10000') + .get('/') .reply(200, '', {}) .get('/library/node/tags/list?n=10000') .reply(403); @@ -581,7 +545,7 @@ describe('modules/datasource/docker/index', () => { const tags = ['1.0.0']; httpMock .scope('https://registry.company.com/v2') - .head('/node/tags/list?n=10000') + .get('/') .reply(200, '', {}) .get('/node/tags/list?n=10000') .reply( @@ -612,7 +576,7 @@ describe('modules/datasource/docker/index', () => { const tags = ['1.0.0']; httpMock .scope('https://registry.company.com/v2') - .head('/node/tags/list?n=10000') + .get('/') .reply(200, '', {}) .get('/node/tags/list?n=10000') .reply(200, { tags }, {}) @@ -670,7 +634,7 @@ describe('modules/datasource/docker/index', () => { it('uses lower tag limit for ECR deps', async () => { httpMock .scope(amazonUrl) - .head('/node/tags/list?n=1000') + .get('/') .reply(200, '', {}) // The tag limit parameter `n` needs to be limited to 1000 for ECR // See https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_DescribeRepositories.html#ECR-DescribeRepositories-request-maxResults @@ -695,7 +659,7 @@ describe('modules/datasource/docker/index', () => { it('resolves requests to ECR proxy', async () => { httpMock .scope('https://ecr-proxy.company.com/v2') - .head('/node/tags/list?n=10000') + .get('/') .reply(200, '', {}) .get('/node/tags/list?n=10000') .reply( @@ -759,7 +723,7 @@ describe('modules/datasource/docker/index', () => { httpMock .scope('https://ecr-proxy.company.com/v2') - .head('/node/tags/list?n=10000') + .get('/') .reply(200, '', {}) .get('/node/tags/list?n=10000') .reply(405, maxResultsErrorBody, { @@ -780,7 +744,7 @@ describe('modules/datasource/docker/index', () => { it('returns null when the response code is not 405', async () => { httpMock .scope('https://ecr-proxy.company.com/v2') - .head('/node/tags/list?n=10000') + .get('/') .reply(200, '', {}) .get('/node/tags/list?n=10000') .reply( @@ -811,7 +775,7 @@ describe('modules/datasource/docker/index', () => { it('returns null when no response headers are present', async () => { httpMock .scope('https://ecr-proxy.company.com/v2') - .head('/node/tags/list?n=10000') + .get('/') .reply(200, '', {}) .get('/node/tags/list?n=10000') .reply(405, { @@ -834,7 +798,7 @@ describe('modules/datasource/docker/index', () => { it('returns null when the expected docker header is missing', async () => { httpMock .scope('https://ecr-proxy.company.com/v2') - .head('/node/tags/list?n=10000') + .get('/') .reply(200, '', {}) .get('/node/tags/list?n=10000') .reply( @@ -863,7 +827,7 @@ describe('modules/datasource/docker/index', () => { it('returns null when the response body does not contain an errors object', async () => { httpMock .scope('https://ecr-proxy.company.com/v2') - .head('/node/tags/list?n=10000') + .get('/') .reply(200, '', {}) .get('/node/tags/list?n=10000') .reply( @@ -884,7 +848,7 @@ describe('modules/datasource/docker/index', () => { it('returns null when the response body does not contain errors', async () => { httpMock .scope('https://ecr-proxy.company.com/v2') - .head('/node/tags/list?n=10000') + .get('/') .reply(200, '', {}) .get('/node/tags/list?n=10000') .reply( @@ -907,7 +871,7 @@ describe('modules/datasource/docker/index', () => { it('returns null when the the response errors does not have a message property', async () => { httpMock .scope('https://ecr-proxy.company.com/v2') - .head('/node/tags/list?n=10000') + .get('/') .reply(200, '', {}) .get('/node/tags/list?n=10000') .reply( @@ -934,7 +898,7 @@ describe('modules/datasource/docker/index', () => { it('returns null when the the error message does not have the expected max results error', async () => { httpMock .scope('https://ecr-proxy.company.com/v2') - .head('/node/tags/list?n=10000') + .get('/') .reply(200, '', {}) .get('/node/tags/list?n=10000') .reply( @@ -964,7 +928,7 @@ describe('modules/datasource/docker/index', () => { const tags = ['1.0.0']; httpMock .scope(baseUrl) - .head('/library/node/tags/list?n=10000') + .get('/') .reply(401, '', { 'www-authenticate': 'Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:library/node:pull "', @@ -992,7 +956,7 @@ describe('modules/datasource/docker/index', () => { const tags = ['1.0.0']; httpMock .scope(baseUrl) - .head('/library/node/tags/list?n=10000') + .get('/') .reply(401, '', { 'www-authenticate': 'Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:library/node:pull "', @@ -1020,7 +984,7 @@ describe('modules/datasource/docker/index', () => { const tags = ['1.0.0']; httpMock .scope('https://k8s.gcr.io/v2/') - .head('/kubernetes-dashboard-amd64/tags/list?n=10000') + .get('/') .reply(401, '', { 'www-authenticate': 'Bearer realm="https://k8s.gcr.io/v2/token",service="k8s.gcr.io"', @@ -1045,7 +1009,7 @@ describe('modules/datasource/docker/index', () => { it('returns null on error', async () => { httpMock .scope(baseUrl) - .head('/my/node/tags/list?n=10000') + .get('/') .reply(200, null) .get('/my/node/tags/list?n=10000') .replyWithError('error'); @@ -1059,7 +1023,7 @@ describe('modules/datasource/docker/index', () => { it('strips trailing slash from registry', async () => { httpMock .scope(baseUrl) - .head('/my/node/tags/list?n=10000') + .get('/') .reply(401, '', { 'www-authenticate': 'Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:my/node:pull "', @@ -1084,12 +1048,9 @@ describe('modules/datasource/docker/index', () => { it('returns null if no auth', async () => { hostRules.find.mockReturnValue({}); - httpMock - .scope(baseUrl) - .head('/library/node/tags/list?n=10000') - .reply(401, undefined, { - 'www-authenticate': 'Basic realm="My Private Docker Registry Server"', - }); + httpMock.scope(baseUrl).get('/').reply(401, undefined, { + 'www-authenticate': 'Basic realm="My Private Docker Registry Server"', + }); const res = await getPkgReleases({ datasource: DockerDatasource.id, depName: 'node', @@ -1101,9 +1062,7 @@ describe('modules/datasource/docker/index', () => { httpMock .scope('https://registry.company.com/v2') .get('/') - .times(2) - .reply(200) - .head('/node/tags/list?n=10000') + .times(3) .reply(200) .get('/node/tags/list?n=10000') .reply(200, { @@ -1166,9 +1125,7 @@ describe('modules/datasource/docker/index', () => { httpMock .scope('https://registry.company.com/v2') .get('/') - .times(3) - .reply(200) - .head('/node/tags/list?n=10000') + .times(4) .reply(200) .get('/node/tags/list?n=10000') .reply(200, { tags: ['abc'] }) @@ -1208,8 +1165,7 @@ describe('modules/datasource/docker/index', () => { httpMock .scope('https://registry.company.com/v2') .get('/') - .reply(200) - .head('/node/tags/list?n=10000') + .times(2) .reply(200) .get('/node/tags/list?n=10000') .reply(200, { tags: ['latest'] }) @@ -1233,8 +1189,7 @@ describe('modules/datasource/docker/index', () => { httpMock .scope('https://registry.company.com/v2') .get('/') - .reply(200) - .head('/node/tags/list?n=10000') + .times(2) .reply(200) .get('/node/tags/list?n=10000') .reply(200, { tags: ['latest'] }) @@ -1257,8 +1212,7 @@ describe('modules/datasource/docker/index', () => { httpMock .scope('https://registry.company.com/v2') .get('/') - .reply(200) - .head('/node/tags/list?n=10000') + .times(2) .reply(200) .get('/node/tags/list?n=10000') .reply(200, { tags: ['latest'] }) @@ -1278,9 +1232,7 @@ describe('modules/datasource/docker/index', () => { httpMock .scope('https://registry.company.com/v2') .get('/') - .times(3) - .reply(200) - .head('/node/tags/list?n=10000') + .times(4) .reply(200) .get('/node/tags/list?n=10000') .reply(200, { tags: ['1'] }) @@ -1324,9 +1276,7 @@ describe('modules/datasource/docker/index', () => { httpMock .scope('https://registry.company.com/v2') .get('/') - .times(3) - .reply(200) - .head('/node/tags/list?n=10000') + .times(4) .reply(200) .get('/node/tags/list?n=10000') .reply(200, { tags: ['1'] }) @@ -1369,8 +1319,7 @@ describe('modules/datasource/docker/index', () => { httpMock .scope('https://registry.company.com/v2') .get('/') - .reply(200) - .head('/node/tags/list?n=10000') + .times(2) .reply(200) .get('/node/tags/list?n=10000') .reply(200, { tags: ['latest'] }) @@ -1396,11 +1345,7 @@ describe('modules/datasource/docker/index', () => { badheaders: ['authorization'], }) .get('/') - .times(2) - .reply(401, '', { - 'www-authenticate': 'Basic realm="My Private Docker Registry Server"', - }) - .head('/node/tags/list?n=10000') + .times(3) .reply(401, '', { 'www-authenticate': 'Basic realm="My Private Docker Registry Server"', }); diff --git a/lib/modules/datasource/docker/index.ts b/lib/modules/datasource/docker/index.ts index f2c1243a040db46945337d33f17d7e89e8897232..b1fc62122a362718de4acb7a197e86a27e6e1473 100644 --- a/lib/modules/datasource/docker/index.ts +++ b/lib/modules/datasource/docker/index.ts @@ -53,18 +53,17 @@ function isDockerHost(host: string): boolean { export async function getAuthHeaders( http: Http, registryHost: string, - dockerRepository: string, - apiCheckMode: 'head' | 'get' = 'get', - apiCheckUrl = `${registryHost}/v2/` + dockerRepository: string ): Promise<OutgoingHttpHeaders | null> { try { - const apiCheckResponse = await http[apiCheckMode](apiCheckUrl, { + const apiCheckUrl = `${registryHost}/v2/`; + const apiCheckResponse = await http.get(apiCheckUrl, { throwHttpErrors: false, noAuth: true, }); if (apiCheckResponse.statusCode === 200) { - logger.debug({ apiCheckUrl }, 'No registry auth required'); + logger.debug({ registryHost }, 'No registry auth required'); return {}; } if ( @@ -72,7 +71,7 @@ export async function getAuthHeaders( !is.nonEmptyString(apiCheckResponse.headers['www-authenticate']) ) { logger.warn( - { apiCheckUrl, res: apiCheckResponse }, + { registryHost, res: apiCheckResponse }, 'Invalid registry response' ); return null; @@ -136,12 +135,7 @@ export async function getAuthHeaders( return opts.headers; } - let scope = `repository:${dockerRepository}:pull`; - if (is.string(authenticateHeader.params.scope)) { - scope = authenticateHeader.params.scope; - } - - const authUrl = `${authenticateHeader.params.realm}?service=${authenticateHeader.params.service}&scope=${scope}`; + const authUrl = `${authenticateHeader.params.realm}?service=${authenticateHeader.params.service}&scope=repository:${dockerRepository}:pull`; logger.trace( { registryHost, dockerRepository, authUrl }, `Obtaining docker registry token` @@ -686,9 +680,7 @@ export class DockerDatasource extends Datasource { const headers = await getAuthHeaders( this.http, registryHost, - dockerRepository, - 'head', - url + dockerRepository ); if (!headers) { logger.debug('Failed to get authHeaders for getTags lookup');