diff --git a/lib/datasource/docker.js b/lib/datasource/docker.js index 35f1b3a1be18cf41c052dea31702580f5747421f..23fb2efb17b3911f9ca926694df76982291098aa 100644 --- a/lib/datasource/docker.js +++ b/lib/datasource/docker.js @@ -1,17 +1,31 @@ const got = require('got'); +const parseLinkHeader = require('parse-link-header'); module.exports = { getDigest, getTags, }; -const registry = 'https://index.docker.io/v2'; +function massageRegistry(input) { + let registry = input; + if (!registry || registry === 'docker.io') { + registry = 'index.docker.io'; // eslint-disable-line no-param-reassign + } + if (!registry.match('$https?://')) { + registry = `https://${registry}`; // eslint-disable-line no-param-reassign + } + return registry; +} function getRepository(pkgName) { return pkgName.includes('/') ? pkgName : `library/${pkgName}`; } -async function getHeaders(repository) { +async function getAuthHeaders(registry, repository) { + // istanbul ignore if + if (registry !== 'https://index.docker.io') { + return {}; + } try { const authUrl = `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repository}:pull`; logger.debug(`Obtaining docker registry token for ${repository}`); @@ -35,18 +49,19 @@ async function getHeaders(repository) { } } -async function getDigest(name, tag = 'latest') { +async function getDigest(registry, name, tag = 'latest') { + logger.debug(`getDigest(${registry}, ${name}, ${tag})`); + const massagedRegistry = massageRegistry(registry); + const repository = getRepository(name); try { - const repository = getRepository(name); - const headers = await getHeaders(repository); - const url = `${registry}/${repository}/manifests/${tag}`; - const digest = (await got(url, { json: true, headers })).headers[ - 'docker-content-digest' - ]; + const url = `${massagedRegistry}/v2/${repository}/manifests/${tag}`; + const headers = await getAuthHeaders(massagedRegistry, repository); + headers.accept = 'application/vnd.docker.distribution.manifest.v2+json'; + const digest = (await got(url, { json: true, headers, timeout: 10000 })) + .headers['docker-content-digest']; logger.debug({ digest }, 'Got docker digest'); return digest; - } catch (err) { - // istanbul ignore if + } catch (err) /* istanbul ignore next */ { if (err.statusCode === 401) { logger.info( { err, body: err.response ? err.response.body : undefined, name, tag }, @@ -54,7 +69,6 @@ async function getDigest(name, tag = 'latest') { ); return null; } - // istanbul ignore if if (err.statusCode === 404) { logger.info( { err, body: err.response ? err.response.body : undefined, name, tag }, @@ -62,7 +76,6 @@ async function getDigest(name, tag = 'latest') { ); return null; } - // istanbul ignore if if (err.statusCode >= 500 && err.statusCode < 600) { logger.warn( { err, body: err.response ? err.response.body : undefined, name, tag }, @@ -70,6 +83,14 @@ async function getDigest(name, tag = 'latest') { ); throw new Error('registry-failure'); } + if (err.code === 'ETIMEDOUT') { + logger.info( + { massagedRegistry }, + 'Timeout when attempting to connect to docker registry' + ); + logger.debug({ err }); + return null; + } logger.info( { err, body: err.response ? err.response.body : undefined, name, tag }, 'Unknown Error looking up docker image digest' @@ -78,20 +99,38 @@ async function getDigest(name, tag = 'latest') { } } -async function getTags(name) { +async function getTags(registry, name) { + logger.debug(`getTags(${registry}, ${name})`); + const massagedRegistry = massageRegistry(registry); + const repository = getRepository(name); try { - const repository = getRepository(name); - const url = `${registry}/${repository}/tags/list?n=10000`; - const headers = await getHeaders(repository); - const { tags } = (await got(url, { json: true, headers })).body; - logger.debug({ tags }, 'Got docker tags'); + let url = `${massagedRegistry}/v2/${repository}/tags/list?n=10000`; + const headers = await getAuthHeaders(massagedRegistry, repository); + let tags = []; + let page = 1; + do { + const res = await got(url, { json: true, headers, timeout: 10000 }); + tags = tags.concat(res.body.tags); + const linkHeader = parseLinkHeader(res.headers.link); + url = linkHeader && linkHeader.next ? linkHeader.next.url : null; + page += 1; + } while (url && page < 20); + logger.debug({ length: tags.length }, 'Got docker tags'); + logger.trace({ tags }); return tags; - } catch (err) { - // istanbul ignore if + } catch (err) /* istanbul ignore next */ { if (err.statusCode >= 500 && err.statusCode < 600) { logger.warn({ err }, 'docker registry failure: internal error'); throw new Error('registry-failure'); } + if (err.code === 'ETIMEDOUT') { + logger.info( + { massagedRegistry }, + 'Timeout when attempting to connect to docker registry' + ); + logger.debug({ err }); + return null; + } logger.warn({ err, name }, 'Error getting docker image tags'); return null; } diff --git a/lib/manager/docker/package.js b/lib/manager/docker/package.js index b3d04dc7fd5c282e859fd2b6d997badd165f7951..fa70d03b094bb79e390a8d8d1c2e243520fe06b5 100644 --- a/lib/manager/docker/package.js +++ b/lib/manager/docker/package.js @@ -11,7 +11,6 @@ module.exports = { async function getPackageUpdates(config) { const { dockerRegistry, - currentFrom, depName, currentDepTag, currentTag, @@ -19,17 +18,14 @@ async function getPackageUpdates(config) { unstablePattern, ignoreUnstable, } = config; - if (dockerRegistry) { - logger.info( - { currentFrom, dockerRegistry }, - 'Skipping Dockerfile image with custom host' - ); - return []; - } const upgrades = []; if (currentDigest || config.pinDigests) { logger.debug('Checking docker pinDigests'); - const newDigest = await dockerApi.getDigest(depName, currentTag); + const newDigest = await dockerApi.getDigest( + dockerRegistry, + depName, + currentTag + ); if (!newDigest) { logger.debug({ content: config.content }, 'Dockerfile no digest'); } @@ -38,7 +34,13 @@ async function getPackageUpdates(config) { upgrade.newTag = currentTag || 'latest'; upgrade.newDigest = newDigest; upgrade.newDigestShort = newDigest.slice(7, 13); - upgrade.newFrom = `${depName}:${upgrade.newTag}@${newDigest}`; + if (dockerRegistry) { + upgrade.newFrom = `${dockerRegistry}/`; + } else { + upgrade.newFrom = ''; + } + upgrade.newFrom += `${depName}:${upgrade.newTag}@${newDigest}`; + if (currentDigest) { upgrade.type = 'digest'; upgrade.isDigest = true; @@ -62,7 +64,7 @@ async function getPackageUpdates(config) { const currentMajor = semver.major(padRange(tagVersion)); const currentlyStable = isStable(tagVersion, unstablePattern); let versionList = []; - const allTags = await dockerApi.getTags(config.depName); + const allTags = await dockerApi.getTags(dockerRegistry, config.depName); if (allTags) { versionList = allTags .filter(tag => getSuffix(tag) === tagSuffix) @@ -130,15 +132,20 @@ async function getPackageUpdates(config) { }; upgrade.newVersion = newTag; upgrade.newDepTag = `${config.depName}:${upgrade.newTag}`; - let newFrom = upgrade.newDepTag; + if (dockerRegistry) { + upgrade.newFrom = `${dockerRegistry}/`; + } else { + upgrade.newFrom = ''; + } + upgrade.newFrom += `${depName}:${upgrade.newTag}`; if (config.currentDigest || config.pinDigests) { upgrade.newDigest = await dockerApi.getDigest( + dockerRegistry, config.depName, upgrade.newTag ); - newFrom = `${newFrom}@${upgrade.newDigest}`; + upgrade.newFrom += `@${upgrade.newDigest}`; } - upgrade.newFrom = newFrom; if (newVersionMajor > currentMajor) { upgrade.type = 'major'; upgrade.isMajor = true; diff --git a/lib/manager/docker/update.js b/lib/manager/docker/update.js index 0a2c868d7f8a76b0f1be3364a0ce1118008add31..6d39255587014855cbeee7f175190b5c50f5b861 100644 --- a/lib/manager/docker/update.js +++ b/lib/manager/docker/update.js @@ -6,9 +6,9 @@ function setNewValue(currentFileContent, upgrade) { try { logger.debug(`setNewValue: ${upgrade.newFrom}`); const oldLine = new RegExp( - `(^|\\n)${upgrade.fromPrefix}(\\s+)${upgrade.depName}[^\\s]*(\\s?)${ - upgrade.fromSuffix - }\\n` + `(^|\\n)${upgrade.fromPrefix}(\\s+)${ + upgrade.dockerRegistry ? upgrade.dockerRegistry + '/' : '' + }${upgrade.depName}[^\\s]*(\\s?)${upgrade.fromSuffix}\\n` ); const newLine = `$1${upgrade.fromPrefix}$2${upgrade.newFrom}$3${ upgrade.fromSuffix diff --git a/test/datasource/docker.spec.js b/test/datasource/docker.spec.js index cbd7efaa105cb3884c082c8860398979a9c62ad8..d0b1d58714f9804ab1aa0fcc6b117fe10e8bdb32 100644 --- a/test/datasource/docker.spec.js +++ b/test/datasource/docker.spec.js @@ -10,12 +10,12 @@ describe('api/docker', () => { }); it('returns null if no token', async () => { got.mockReturnValueOnce({ body: {} }); - const res = await docker.getDigest('some-name', undefined); + const res = await docker.getDigest(undefined, 'some-name', undefined); expect(res).toBe(null); }); it('returns null if errored', async () => { got.mockReturnValueOnce({ body: { token: 'some-token' } }); - const res = await docker.getDigest('some-name', undefined); + const res = await docker.getDigest(undefined, 'some-name', undefined); expect(res).toBe(null); }); it('returns digest', async () => { @@ -23,7 +23,7 @@ describe('api/docker', () => { got.mockReturnValueOnce({ headers: { 'docker-content-digest': 'some-digest' }, }); - const res = await docker.getDigest('some-name', undefined); + const res = await docker.getDigest(undefined, 'some-name', undefined); expect(res).toBe('some-digest'); }); it('supports scoped names', async () => { @@ -31,26 +31,26 @@ describe('api/docker', () => { got.mockReturnValueOnce({ headers: { 'docker-content-digest': 'some-digest' }, }); - const res = await docker.getDigest('some/name', undefined); + const res = await docker.getDigest(undefined, 'some/name', undefined); expect(res).toBe('some-digest'); }); }); describe('getTags', () => { it('returns null if no token', async () => { got.mockReturnValueOnce({ body: {} }); - const res = await docker.getTags('node'); + const res = await docker.getTags(undefined, 'node'); expect(res).toBe(null); }); it('returns tags', async () => { const tags = ['a', 'b']; - got.mockReturnValueOnce({ body: { token: 'some-token ' } }); - got.mockReturnValueOnce({ body: { tags } }); - const res = await docker.getTags('my/node'); - expect(res).toBe(tags); + got.mockReturnValueOnce({ headers: {}, body: { token: 'some-token ' } }); + got.mockReturnValueOnce({ headers: {}, body: { tags } }); + const res = await docker.getTags(undefined, 'my/node'); + expect(res).toEqual(tags); }); it('returns null on error', async () => { got.mockReturnValueOnce({}); - const res = await docker.getTags('node'); + const res = await docker.getTags(undefined, 'node'); expect(res).toBe(null); }); }); diff --git a/test/manager/docker/__snapshots__/package.spec.js.snap b/test/manager/docker/__snapshots__/package.spec.js.snap index 6eec3282adecfeb6d5f0f57b9762585c53fc83e3..f891a37523558ed4787b889c30ef925fa698482b 100644 --- a/test/manager/docker/__snapshots__/package.spec.js.snap +++ b/test/manager/docker/__snapshots__/package.spec.js.snap @@ -37,6 +37,19 @@ Array [ ] `; +exports[`lib/workers/package/docker getPackageUpdates returns a digest when registry is present 1`] = ` +Array [ + Object { + "isDigest": true, + "newDigest": "sha256:1234567890", + "newDigestShort": "123456", + "newFrom": "docker.io/some-dep:1.0.0@sha256:1234567890", + "newTag": "1.0.0", + "type": "digest", + }, +] +`; + exports[`lib/workers/package/docker getPackageUpdates returns major and minor upgrades 1`] = ` Array [ Object { @@ -118,7 +131,7 @@ Array [ "isMajor": true, "newDepTag": "some-dep:3.0.0", "newDigest": "sha256:one", - "newFrom": "some-dep:3.0.0@sha256:one", + "newFrom": "docker.io/some-dep:3.0.0@sha256:one", "newTag": "3.0.0", "newVersion": "3.0.0", "newVersionMajor": "3", diff --git a/test/manager/docker/package.spec.js b/test/manager/docker/package.spec.js index 7ee98d2fa53f2af051d198698ac5c2b66fa151ec..4105764f5e9dd96d55338e074786e27bd594a878 100644 --- a/test/manager/docker/package.spec.js +++ b/test/manager/docker/package.spec.js @@ -47,6 +47,15 @@ describe('lib/workers/package/docker', () => { expect(res).toHaveLength(1); expect(res[0].type).toEqual('digest'); }); + it('returns a digest when registry is present', async () => { + config.dockerRegistry = 'docker.io'; + config.currentFrom = 'docker.io/some-dep:1.0.0@sha256:abcdefghijklmnop'; + dockerApi.getDigest.mockReturnValueOnce('sha256:1234567890'); + const res = await docker.getPackageUpdates(config); + expect(res).toMatchSnapshot(); + expect(res).toHaveLength(1); + expect(res[0].type).toEqual('digest'); + }); it('adds latest tag', async () => { delete config.currentTag; dockerApi.getDigest.mockReturnValueOnce('sha256:1234567890'); @@ -67,6 +76,7 @@ describe('lib/workers/package/docker', () => { expect(await docker.getPackageUpdates(config)).toEqual([]); }); it('returns only one upgrade if automerging major', async () => { + config.dockerRegistry = 'docker.io'; dockerApi.getDigest.mockReturnValueOnce(config.currentDigest); dockerApi.getDigest.mockReturnValueOnce('sha256:one'); dockerApi.getTags.mockReturnValueOnce([