diff --git a/lib/datasource/docker/index.js b/lib/datasource/docker/index.js index 0cff2b6d9940fcbd767200f084bab3c71d858cac..7d29ddbedb69f11641375ebfacf2e8a101c63e8a 100644 --- a/lib/datasource/docker/index.js +++ b/lib/datasource/docker/index.js @@ -327,6 +327,120 @@ async function getTags(registry, repository) { } } +/* + * docker.getLabels + * + * This function will: + * - Return the labels for the requested image + */ + +// istanbul ignore next +async function getLabels(registry, repository, tag) { + logger.debug(`getLabels(${registry}, ${repository}, ${tag})`); + const cacheNamespace = 'datasource-docker-labels'; + const cacheKey = `${registry}:${repository}:${tag}`; + const cachedResult = await renovateCache.get(cacheNamespace, cacheKey); + // istanbul ignore if + if (cachedResult) { + return cachedResult; + } + try { + const manifestResponse = await getManifestResponse( + registry, + repository, + tag + ); + // If getting the manifest fails here, then abort + // This means that the latest tag doesn't have a manifest, which shouldn't + // be possible + if (!manifestResponse) { + logger.warn( + { + registry, + repository, + tag, + }, + 'docker registry failure: failed to get manifest for tag' + ); + return {}; + } + const manifest = JSON.parse(manifestResponse.body); + let labels = {}; + if (manifest.schemaVersion === 1) { + labels = JSON.parse(manifest.history[0].v1Compatibility).container_config + .Labels; + if (!labels) { + labels = {}; + } + } + if (manifest.schemaVersion === 2) { + const configDigest = manifest.config.digest; + const headers = await getAuthHeaders(registry, repository); + if (!headers) { + logger.info('No docker auth found - returning'); + return {}; + } + const url = `${registry}/v2/${repository}/blobs/${configDigest}`; + const configResponse = await got(url, { + headers, + timeout: 10000, + }); + labels = JSON.parse(configResponse.body).config.Labels; + } + + if (labels) { + logger.debug( + { + labels, + }, + 'found labels in manifest' + ); + } + const cacheMinutes = 60; + await renovateCache.set(cacheNamespace, cacheKey, labels, cacheMinutes); + return labels; + } catch (err) { + if (err.statusCode === 401) { + logger.info( + { registry, dockerRepository: repository }, + 'Unauthorized docker lookup' + ); + logger.debug({ err }); + } else if (err.statusCode === 404) { + logger.warn( + { + err, + registry, + repository, + tag, + }, + 'Config Manifest is unknown' + ); + } else if (err.statusCode === 429 && registry.endsWith('docker.io')) { + logger.warn({ err }, 'docker registry failure: too many requests'); + } else if (err.statusCode >= 500 && err.statusCode < 600) { + logger.warn( + { + err, + registry, + repository, + tag, + }, + 'docker registry failure: internal error' + ); + } else if (err.code === 'ETIMEDOUT') { + logger.info( + { registry }, + 'Timeout when attempting to connect to docker registry' + ); + logger.debug({ err }); + } else { + logger.warn({ err }, 'Unknown error getting Docker labels'); + } + return {}; + } +} + /* * docker.getPkgReleases * @@ -354,5 +468,12 @@ async function getPkgReleases({ lookupName, registryUrls }) { dockerRepository: repository, releases, }; + + const latestTag = tags.includes('latest') ? 'latest' : tags[tags.length - 1]; + const labels = await getLabels(registry, repository, latestTag); + // istanbul ignore if + if ('org.opencontainers.image.source' in labels) { + ret.sourceUrl = labels['org.opencontainers.image.source']; + } return ret; } diff --git a/test/datasource/__snapshots__/docker.spec.js.snap b/test/datasource/__snapshots__/docker.spec.js.snap index f4fec2c48a3271073329ee3afa0cfba240c1c80b..8df4e8b071dd4b133f4682d22e0a56404c4aa84a 100644 --- a/test/datasource/__snapshots__/docker.spec.js.snap +++ b/test/datasource/__snapshots__/docker.spec.js.snap @@ -29,6 +29,21 @@ exports[`api/docker getPkgReleases adds library/ prefix for Docker Hub (explicit "timeout": 10000, }, ], + Array [ + "https://index.docker.io/v2/", + Object { + "throwHttpErrors": false, + }, + ], + Array [ + "https://index.docker.io/v2/library/node/manifests/1.0.0", + Object { + "headers": Object { + "accept": "application/vnd.docker.distribution.manifest.v2+json", + }, + "timeout": 10000, + }, + ], ], "results": Array [ Object { @@ -59,6 +74,19 @@ exports[`api/docker getPkgReleases adds library/ prefix for Docker Hub (explicit "headers": Object {}, }, }, + Object { + "isThrow": false, + "value": Object { + "headers": Object {}, + }, + }, + Object { + "isThrow": false, + "value": Object { + "body": Object {}, + "headers": Object {}, + }, + }, ], } `; @@ -92,6 +120,21 @@ exports[`api/docker getPkgReleases adds library/ prefix for Docker Hub (implicit "timeout": 10000, }, ], + Array [ + "https://index.docker.io/v2/", + Object { + "throwHttpErrors": false, + }, + ], + Array [ + "https://index.docker.io/v2/library/node/manifests/1.0.0", + Object { + "headers": Object { + "accept": "application/vnd.docker.distribution.manifest.v2+json", + }, + "timeout": 10000, + }, + ], ], "results": Array [ Object { @@ -122,6 +165,19 @@ exports[`api/docker getPkgReleases adds library/ prefix for Docker Hub (implicit "headers": Object {}, }, }, + Object { + "isThrow": false, + "value": Object { + "headers": Object {}, + }, + }, + Object { + "isThrow": false, + "value": Object { + "body": Object {}, + "headers": Object {}, + }, + }, ], } `; @@ -155,6 +211,21 @@ exports[`api/docker getPkgReleases adds no library/ prefix for other registries "timeout": 10000, }, ], + Array [ + "https://k8s.gcr.io/v2/", + Object { + "throwHttpErrors": false, + }, + ], + Array [ + "https://k8s.gcr.io/v2/kubernetes-dashboard-amd64/manifests/1.0.0", + Object { + "headers": Object { + "accept": "application/vnd.docker.distribution.manifest.v2+json", + }, + "timeout": 10000, + }, + ], ], "results": Array [ Object { @@ -185,6 +256,19 @@ exports[`api/docker getPkgReleases adds no library/ prefix for other registries "headers": Object {}, }, }, + Object { + "isThrow": false, + "value": Object { + "headers": Object {}, + }, + }, + Object { + "isThrow": false, + "value": Object { + "body": Object {}, + "headers": Object {}, + }, + }, ], } `; @@ -206,6 +290,12 @@ exports[`api/docker getPkgReleases uses custom registry in depName 1`] = ` "timeout": 10000, }, ], + Array [ + "https://registry.company.com/v2/", + Object { + "throwHttpErrors": false, + }, + ], ], "results": Array [ Object { @@ -225,6 +315,10 @@ exports[`api/docker getPkgReleases uses custom registry in depName 1`] = ` "headers": Object {}, }, }, + Object { + "isThrow": false, + "value": undefined, + }, ], } `; @@ -245,5 +339,20 @@ Array [ "timeout": 10000, }, ], + Array [ + "https://registry.company.com/v2/", + Object { + "throwHttpErrors": false, + }, + ], + Array [ + "https://registry.company.com/v2/node/manifests/1.0.0", + Object { + "headers": Object { + "accept": "application/vnd.docker.distribution.manifest.v2+json", + }, + "timeout": 10000, + }, + ], ] `; diff --git a/test/datasource/docker.spec.js b/test/datasource/docker.spec.js index 812b5aa43982c05a91070149e0b3d9e886189d74..a81470ee2f171f10ff1ccadc27eb5842d2b4db49 100644 --- a/test/datasource/docker.spec.js +++ b/test/datasource/docker.spec.js @@ -198,6 +198,10 @@ describe('api/docker', () => { headers: {}, }); got.mockReturnValueOnce({ headers: {}, body: { tags } }); + got.mockReturnValueOnce({ + headers: {}, + }); + got.mockReturnValueOnce({ headers: {}, body: {} }); const config = { datasource: 'docker', depName: 'node', @@ -233,6 +237,10 @@ describe('api/docker', () => { }); got.mockReturnValueOnce({ headers: {}, body: { token: 'some-token ' } }); got.mockReturnValueOnce({ headers: {}, body: { tags } }); + got.mockReturnValueOnce({ + headers: {}, + }); + got.mockReturnValueOnce({ headers: {}, body: {} }); const res = await getPkgReleases({ datasource: 'docker', depName: 'node', @@ -250,6 +258,10 @@ describe('api/docker', () => { }); got.mockReturnValueOnce({ headers: {}, body: { token: 'some-token ' } }); got.mockReturnValueOnce({ headers: {}, body: { tags } }); + got.mockReturnValueOnce({ + headers: {}, + }); + got.mockReturnValueOnce({ headers: {}, body: {} }); const res = await getPkgReleases({ datasource: 'docker', depName: 'docker.io/node', @@ -267,6 +279,10 @@ describe('api/docker', () => { }); got.mockReturnValueOnce({ headers: {}, body: { token: 'some-token ' } }); got.mockReturnValueOnce({ headers: {}, body: { tags } }); + got.mockReturnValueOnce({ + headers: {}, + }); + got.mockReturnValueOnce({ headers: {}, body: {} }); const res = await getPkgReleases({ datasource: 'docker', depName: 'k8s.gcr.io/kubernetes-dashboard-amd64',