diff --git a/docs/configuration.md b/docs/configuration.md index a06553f473f684674ebc9322a348a47548de2232..fd9b7a91d613890d145fa320f92523410bad3f09 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -725,3 +725,30 @@ Obviously, you can't set repository or package file location with this method. <td>`RENOVATE_METEOR`</td> <td>`--meteor`<td> </tr> +<tr> + <td>`docker`</td> + <td>Configuration object for Dockerfile renovation</td> + <td>json</td> + <td><pre>{ + "enabled": false, + "branchName": "{{branchPrefix}}docker-{{depName}}-{{currentTag}}", + "commitMessage": "Update {{depName}}:{{currentTag}} digest", + "prTitle": "Update Dockerfile image {{depName}}@{{currentTag}} digest", + "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image `{{depName}}@{{currentTag}}` to the latest digest (`{{newDigest}}`).\n\n{{#if schedule}}\n**Note**: This PR was created on a configured schedule (\"{{schedule}}\"{{#if timezone}} in timezone `{{timezone}}`{{/if}}) and will not receive updates outside those times.\n{{/if}}\n\n{{#if hasErrors}}\n\n---\n\n### Errors\n\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\n\n{{#each errors as |error|}}\n- `{{error.depName}}`: {{error.message}}\n{{/each}}\n{{/if}}\n\n{{#if hasWarnings}}\n\n---\n\n### Warnings\n\nPlease make sure the following warnings are safe to ignore:\n\n{{#each warnings as |warning|}}\n- `{{warning.depName}}`: {{warning.message}}\n{{/each}}\n{{/if}}\n\n---\n\nThis {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).", + "pin": { + "prTitle": "Pin Dockerfile image {{depName}}@{{currentTag}} digest", + "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Docker base image `{{depName}}@{{currentTag}}` to use a digest (`{{newDigest}}`).\nThis digest will then be kept updated via Pull Requests whenever the image is updated on the Docker registry.\n\n{{#if schedule}}\n**Note**: This PR was created on a configured schedule (\"{{schedule}}\"{{#if timezone}} in timezone `{{timezone}}`{{/if}}) and will not receive updates outside those times.\n{{/if}}\n\n{{#if hasErrors}}\n\n---\n\n### Errors\n\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\n\n{{#each errors as |error|}}\n- `{{error.depName}}`: {{error.message}}\n{{/each}}\n{{/if}}\n\n{{#if hasWarnings}}\n\n---\n\n### Warnings\n\nPlease make sure the following warnings are safe to ignore:\n\n{{#each warnings as |warning|}}\n- `{{warning.depName}}`: {{warning.message}}\n{{/each}}\n{{/if}}\n\n---\n\nThis {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com).", + "groupName": "Pin Docker Digests", + "group": { + "prTitle": "Pin Docker digests", + "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Dockerfiles to use image digests.\n\n{{#if schedule}}\n**Note**: This PR was created on a configured schedule (\"{{schedule}}\"{{#if timezone}} in timezone `{{timezone}}`{{/if}}) and will not receive updates outside those times.\n{{/if}}\n\n{{#each upgrades as |upgrade|}}\n- {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}`{{depName}}`{{/if}}: `{{upgrade.newDigest}}`\n{{/each}}\n\n{{#if hasErrors}}\n\n---\n\n### Errors\n\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\n\n{{#each errors as |error|}}\n- `{{error.depName}}`: {{error.message}}\n{{/each}}\n{{/if}}\n\n{{#if hasWarnings}}\n\n---\n\n### Warnings\n\nPlease make sure the following warnings are safe to ignore:\n\n{{#each warnings as |warning|}}\n- `{{warning.depName}}`: {{warning.message}}\n{{/each}}\n{{/if}}\n\n---\n\nThis {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com)." + } + }, + "group": { + "prTitle": "Update Docker {{groupName}} digests", + "prBody": "This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Dockerfiles to use image digests.\n\n{{#if schedule}}\n**Note**: This PR was created on a configured schedule (\"{{schedule}}\"{{#if timezone}} in timezone `{{timezone}}`{{/if}}) and will not receive updates outside those times.\n{{/if}}\n\n{{#each upgrades as |upgrade|}}\n- {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}`{{depName}}`{{/if}}: `{{upgrade.newDigest}}`\n{{/each}}\n\n{{#if hasErrors}}\n\n---\n\n### Errors\n\nRenovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR.\n\n{{#each errors as |error|}}\n- `{{error.depName}}`: {{error.message}}\n{{/each}}\n{{/if}}\n\n{{#if hasWarnings}}\n\n---\n\n### Warnings\n\nPlease make sure the following warnings are safe to ignore:\n\n{{#each warnings as |warning|}}\n- `{{warning.depName}}`: {{warning.message}}\n{{/each}}\n{{/if}}\n\n---\n\nThis {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com)." + } +}</pre></td> + <td>`RENOVATE_DOCKER`</td> + <td><td> +</tr> diff --git a/lib/api/docker.js b/lib/api/docker.js new file mode 100644 index 0000000000000000000000000000000000000000..159b058d1ed21cffec43d0ae1920d0f799338578 --- /dev/null +++ b/lib/api/docker.js @@ -0,0 +1,33 @@ +const got = require('got'); + +module.exports = { + getDigest, +}; + +async function getDigest(name, tag, logger) { + const repository = name.includes('/') ? name : `library/${name}`; + try { + const authUrl = `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repository}:pull`; + logger.debug(`Obtaining docker registry token for ${repository}`); + const token = (await got(authUrl, { json: true })).body.token; + if (!token) { + logger.warn('Failed to obtain docker registry token'); + return null; + } + logger.debug('Got docker registry token'); + const url = `https://index.docker.io/v2/${repository}/manifests/${tag || + 'latest'}`; + const headers = { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.docker.distribution.manifest.v2+json', + }; + const digest = (await got(url, { json: true, headers })).headers[ + 'docker-content-digest' + ]; + logger.debug({ digest }, 'Got docker digest'); + return digest; + } catch (err) { + logger.warn({ err, name, tag }, 'Error getting docker image digest'); + return null; + } +} diff --git a/lib/config/definitions.js b/lib/config/definitions.js index 887ea335467907f6d323b76b8567709011fea23b..3558d63993d0b0f3fde016b6286a4289e215a110 100644 --- a/lib/config/definitions.js +++ b/lib/config/definitions.js @@ -557,7 +557,6 @@ const options = [ description: 'Requested reviewers for Pull Requests (GitHub only)', type: 'list', }, - // meteor { name: 'meteor', description: 'Configuration object for meteor package.js renovation', @@ -566,6 +565,34 @@ const options = [ default: { enabled: false }, mergeable: true, }, + { + name: 'docker', + description: 'Configuration object for Dockerfile renovation', + stage: 'repository', + type: 'json', + default: { + enabled: false, + branchName: template('branchName', 'docker'), + commitMessage: template('commitMessage', 'docker'), + prTitle: template('prTitle', 'docker'), + prBody: template('prBody', 'docker'), + pin: { + prTitle: template('prTitle', 'docker-pin'), + prBody: template('prBody', 'docker-pin'), + groupName: 'Pin Docker Digests', + group: { + prTitle: template('prTitle', 'docker-pin-group'), + prBody: template('prBody', 'docker-pin-group'), + }, + }, + group: { + prTitle: template('prTitle', 'docker-group'), + prBody: template('prBody', 'docker-group'), + }, + }, + mergeable: true, + cli: false, + }, ]; function getOptions() { diff --git a/lib/config/templates/docker-group/pr-body.hbs b/lib/config/templates/docker-group/pr-body.hbs new file mode 100644 index 0000000000000000000000000000000000000000..dd3c54f5f2ae234aabc54edd684640a73d77209e --- /dev/null +++ b/lib/config/templates/docker-group/pr-body.hbs @@ -0,0 +1,39 @@ +This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Dockerfiles to use image digests. + +{{#if schedule}} +**Note**: This PR was created on a configured schedule ("{{schedule}}"{{#if timezone}} in timezone `{{timezone}}`{{/if}}) and will not receive updates outside those times. +{{/if}} + +{{#each upgrades as |upgrade|}} +- {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}`{{depName}}`{{/if}}: `{{upgrade.newDigest}}` +{{/each}} + +{{#if hasErrors}} + +--- + +### Errors + +Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR. + +{{#each errors as |error|}} +- `{{error.depName}}`: {{error.message}} +{{/each}} +{{/if}} + +{{#if hasWarnings}} + +--- + +### Warnings + +Please make sure the following warnings are safe to ignore: + +{{#each warnings as |warning|}} +- `{{warning.depName}}`: {{warning.message}} +{{/each}} +{{/if}} + +--- + +This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com). diff --git a/lib/config/templates/docker-group/pr-title.hbs b/lib/config/templates/docker-group/pr-title.hbs new file mode 100644 index 0000000000000000000000000000000000000000..b4feb91a674539c8db31b7748ea490f07af17c7e --- /dev/null +++ b/lib/config/templates/docker-group/pr-title.hbs @@ -0,0 +1 @@ +Update Docker {{groupName}} digests diff --git a/lib/config/templates/docker-pin-group/pr-body.hbs b/lib/config/templates/docker-pin-group/pr-body.hbs new file mode 100644 index 0000000000000000000000000000000000000000..8d8572f8f74291d16f14f57a456a496477f5d39c --- /dev/null +++ b/lib/config/templates/docker-pin-group/pr-body.hbs @@ -0,0 +1,39 @@ +This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Dockerfiles to use image digests. + +{{#if schedule}} +**Note**: This PR was created on a configured schedule ("{{schedule}}"{{#if timezone}} in timezone `{{timezone}}`{{/if}}) and will not receive updates outside those times. +{{/if}} + +{{#each upgrades as |upgrade|}} +- {{#if repositoryUrl}}[{{upgrade.depName}}]({{upgrade.repositoryUrl}}){{else}}`{{depName}}`{{/if}}: `{{upgrade.newDigest}}` +{{/each}} + +{{#if hasErrors}} + +--- + +### Errors + +Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR. + +{{#each errors as |error|}} +- `{{error.depName}}`: {{error.message}} +{{/each}} +{{/if}} + +{{#if hasWarnings}} + +--- + +### Warnings + +Please make sure the following warnings are safe to ignore: + +{{#each warnings as |warning|}} +- `{{warning.depName}}`: {{warning.message}} +{{/each}} +{{/if}} + +--- + +This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com). diff --git a/lib/config/templates/docker-pin-group/pr-title.hbs b/lib/config/templates/docker-pin-group/pr-title.hbs new file mode 100644 index 0000000000000000000000000000000000000000..719292a0f94f5ec7b406ad8335aa2f6d1e3324bd --- /dev/null +++ b/lib/config/templates/docker-pin-group/pr-title.hbs @@ -0,0 +1 @@ +Pin Docker digests diff --git a/lib/config/templates/docker-pin/pr-body.hbs b/lib/config/templates/docker-pin/pr-body.hbs new file mode 100644 index 0000000000000000000000000000000000000000..5d22c990801af3786f255172ca5439bc89c43148 --- /dev/null +++ b/lib/config/templates/docker-pin/pr-body.hbs @@ -0,0 +1,36 @@ +This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request pins Docker base image `{{depName}}@{{currentTag}}` to use a digest (`{{newDigest}}`). +This digest will then be kept updated via Pull Requests whenever the image is updated on the Docker registry. + +{{#if schedule}} +**Note**: This PR was created on a configured schedule ("{{schedule}}"{{#if timezone}} in timezone `{{timezone}}`{{/if}}) and will not receive updates outside those times. +{{/if}} + +{{#if hasErrors}} + +--- + +### Errors + +Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR. + +{{#each errors as |error|}} +- `{{error.depName}}`: {{error.message}} +{{/each}} +{{/if}} + +{{#if hasWarnings}} + +--- + +### Warnings + +Please make sure the following warnings are safe to ignore: + +{{#each warnings as |warning|}} +- `{{warning.depName}}`: {{warning.message}} +{{/each}} +{{/if}} + +--- + +This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com). diff --git a/lib/config/templates/docker-pin/pr-title.hbs b/lib/config/templates/docker-pin/pr-title.hbs new file mode 100644 index 0000000000000000000000000000000000000000..ba04e763863ff48ec5d93a9bad6d8fd92a657719 --- /dev/null +++ b/lib/config/templates/docker-pin/pr-title.hbs @@ -0,0 +1 @@ +Pin Dockerfile image {{depName}}@{{currentTag}} digest diff --git a/lib/config/templates/docker/branch-name.hbs b/lib/config/templates/docker/branch-name.hbs new file mode 100644 index 0000000000000000000000000000000000000000..29bde578be3fddb582f0952378462db978094c4b --- /dev/null +++ b/lib/config/templates/docker/branch-name.hbs @@ -0,0 +1 @@ +{{branchPrefix}}docker-{{depName}}-{{currentTag}} diff --git a/lib/config/templates/docker/commit-message.hbs b/lib/config/templates/docker/commit-message.hbs new file mode 100644 index 0000000000000000000000000000000000000000..0f3d052939d594e72a93170cf42fdb1bb5adf112 --- /dev/null +++ b/lib/config/templates/docker/commit-message.hbs @@ -0,0 +1 @@ +Update {{depName}}:{{currentTag}} digest diff --git a/lib/config/templates/docker/pr-body.hbs b/lib/config/templates/docker/pr-body.hbs new file mode 100644 index 0000000000000000000000000000000000000000..84718b30b2b7a296f11c24d03940b49e5055f2cd --- /dev/null +++ b/lib/config/templates/docker/pr-body.hbs @@ -0,0 +1,35 @@ +This {{#if isGitHub}}Pull{{else}}Merge{{/if}} Request updates Docker base image `{{depName}}@{{currentTag}}` to the latest digest (`{{newDigest}}`). + +{{#if schedule}} +**Note**: This PR was created on a configured schedule ("{{schedule}}"{{#if timezone}} in timezone `{{timezone}}`{{/if}}) and will not receive updates outside those times. +{{/if}} + +{{#if hasErrors}} + +--- + +### Errors + +Renovate encountered some errors when processing your repository, so you are being notified here even if they do not directly apply to this PR. + +{{#each errors as |error|}} +- `{{error.depName}}`: {{error.message}} +{{/each}} +{{/if}} + +{{#if hasWarnings}} + +--- + +### Warnings + +Please make sure the following warnings are safe to ignore: + +{{#each warnings as |warning|}} +- `{{warning.depName}}`: {{warning.message}} +{{/each}} +{{/if}} + +--- + +This {{#if isGitHub}}PR{{else}}MR{{/if}} has been generated by [Renovate Bot](https://renovateapp.com). diff --git a/lib/config/templates/docker/pr-title.hbs b/lib/config/templates/docker/pr-title.hbs new file mode 100644 index 0000000000000000000000000000000000000000..9924b03b5856a83ea4be6cb342c6c3258cd9e9ec --- /dev/null +++ b/lib/config/templates/docker/pr-title.hbs @@ -0,0 +1 @@ +Update Dockerfile image {{depName}}@{{currentTag}} digest diff --git a/lib/workers/branch/dockerfile.js b/lib/workers/branch/dockerfile.js new file mode 100644 index 0000000000000000000000000000000000000000..52bb16f5106d012eb5e170bc2f8b4fb1716c5b1c --- /dev/null +++ b/lib/workers/branch/dockerfile.js @@ -0,0 +1,19 @@ +module.exports = { + setNewValue, +}; + +function setNewValue( + currentFileContent, + depName, + currentVersion, + newVersion, + logger +) { + logger.debug(`setNewValue: ${depName} = ${newVersion}`); + const regexReplace = new RegExp(`(^|\n)FROM ${currentVersion}\n`); + const newFileContent = currentFileContent.replace( + regexReplace, + `$1FROM ${newVersion}\n` + ); + return newFileContent; +} diff --git a/lib/workers/branch/package-files.js b/lib/workers/branch/package-files.js index 1620614db1fe3c1e3e6b0c28b05916969d3e0e76..523dd239b2afee366e6d1cafa113496d611475d6 100644 --- a/lib/workers/branch/package-files.js +++ b/lib/workers/branch/package-files.js @@ -1,5 +1,6 @@ const packageJsonHelper = require('./package-json'); const packageJsHelper = require('./package-js'); +const dockerfileHelper = require('./dockerfile'); module.exports = { getUpdatedPackageFiles, @@ -17,7 +18,7 @@ async function getUpdatedPackageFiles(config) { upgrade.packageFile, config.parentBranch )); - let newContent; + let newContent = existingContent; if (upgrade.packageFile.endsWith('package.json')) { newContent = packageJsonHelper.setNewValue( existingContent, @@ -26,7 +27,7 @@ async function getUpdatedPackageFiles(config) { upgrade.newVersion, config.logger ); - } else { + } else if (upgrade.packageFile.endsWith('package.js')) { newContent = packageJsHelper.setNewValue( existingContent, upgrade.depName, @@ -34,6 +35,14 @@ async function getUpdatedPackageFiles(config) { upgrade.newVersion, config.logger ); + } else if (upgrade.packageFile.endsWith('Dockerfile')) { + newContent = dockerfileHelper.setNewValue( + existingContent, + upgrade.depName, + upgrade.currentFrom, + upgrade.newFrom, + config.logger + ); } if (newContent !== existingContent) { logger.debug('Updating packageFile content'); diff --git a/lib/workers/dep-type/index.js b/lib/workers/dep-type/index.js index a7ff97aef2eab628b91da0ee9aaba24316358472..b1178a093e0da3e8f8d397b94ed81e3e0dfc6f84 100644 --- a/lib/workers/dep-type/index.js +++ b/lib/workers/dep-type/index.js @@ -15,15 +15,18 @@ async function renovateDepType(packageContent, config) { logger.debug('depType is disabled'); return []; } - let deps; + let deps = []; if (config.packageFile.endsWith('package.json')) { // Extract all dependencies from the package.json deps = await packageJson.extractDependencies( packageContent, config.depType ); - logger.debug(`currentDeps length is ${deps.length}`); - logger.debug({ deps }, `currentDeps`); + deps = deps.filter( + dependency => config.monorepoPackages.indexOf(dependency.depName) === -1 + ); + logger.debug(`deps length is ${deps.length}`); + logger.debug({ deps }, `deps`); } else if (config.packageFile.endsWith('package.js')) { deps = packageContent .match(/Npm\.depends\({([\s\S]*?)}\);/)[1] @@ -35,11 +38,21 @@ async function renovateDepType(packageContent, config) { depName: arr[0], currentVersion: arr[1], })); + } else if (config.packageFile.endsWith('Dockerfile')) { + const [imagetag, currentDigest] = config.currentFrom.split('@'); + const [depName, currentTag] = imagetag.split(':'); + logger.info({ depName, currentTag, currentDigest }, 'Dockerfile'); + deps = [ + { + depType: 'docker', + depName, + currentTag: currentTag || 'latest', + currentDigest, + }, + ]; } deps = deps.filter( - dependency => - config.ignoreDeps.indexOf(dependency.depName) === -1 && - config.monorepoPackages.indexOf(dependency.depName) === -1 + dependency => config.ignoreDeps.indexOf(dependency.depName) === -1 ); logger.debug(`filtered deps length is ${deps.length}`); logger.debug({ deps }, `filtered deps`); diff --git a/lib/workers/package-file/index.js b/lib/workers/package-file/index.js index 33bc37f64ce84a925a41d666bc57fc0d6ea64437..0d4f967230c061c48b8d6d5f80543e3647359df5 100644 --- a/lib/workers/package-file/index.js +++ b/lib/workers/package-file/index.js @@ -7,6 +7,7 @@ let logger = require('../../logger'); module.exports = { renovatePackageFile, renovateMeteorPackageFile, + renovateDockerfile, }; async function renovatePackageFile(packageFileConfig) { @@ -91,3 +92,23 @@ async function renovateMeteorPackageFile(packageFileConfig) { logger.info('Finished processing package file'); return upgrades; } + +async function renovateDockerfile(packageFileConfig) { + let upgrades = []; + logger = packageFileConfig.logger; + logger.info(`Processing Dockerfile`); + + // Check if config is disabled + if (packageFileConfig.enabled === false) { + logger.info('Dockerfile is disabled'); + return upgrades; + } + upgrades = upgrades.concat( + await depTypeWorker.renovateDepType( + packageFileConfig.content, + packageFileConfig + ) + ); + logger.info('Finished processing Dockerfile'); + return upgrades; +} diff --git a/lib/workers/package/docker.js b/lib/workers/package/docker.js new file mode 100644 index 0000000000000000000000000000000000000000..fb1e070a85ec760f3082ebeb79d461f8ed3bb404 --- /dev/null +++ b/lib/workers/package/docker.js @@ -0,0 +1,32 @@ +const dockerApi = require('../../api/docker'); + +module.exports = { + renovateDockerImage, +}; + +async function renovateDockerImage(config) { + const newDigest = await dockerApi.getDigest( + config.depName, + config.currentTag, + config.logger + ); + if (!newDigest || config.currentDigest === newDigest) { + return []; + } + const upgrade = {}; + upgrade.newTag = config.currentTag; + upgrade.newDigest = newDigest; + upgrade.newFrom = config.depName; + if (upgrade.newTag) { + upgrade.newFrom += `:${upgrade.newTag}`; + } + upgrade.newFrom += `@${upgrade.newDigest}`; + if (config.currentDigest) { + upgrade.type = 'digest'; + upgrade.isDigest = true; + } else { + upgrade.type = 'pin'; + upgrade.isPin = true; + } + return [upgrade]; +} diff --git a/lib/workers/package/index.js b/lib/workers/package/index.js index d3f73de10dfeff290534902f5731fbe000762bae..bedf0875806ed311ec71c4671003d066677753a0 100644 --- a/lib/workers/package/index.js +++ b/lib/workers/package/index.js @@ -1,4 +1,5 @@ const configParser = require('../../config'); +const { renovateDockerImage } = require('./docker'); const { renovateNpmPackage } = require('./npm'); module.exports = { @@ -13,8 +14,13 @@ async function renovatePackage(config) { logger.debug('package is disabled'); return []; } - // npm - const results = await renovateNpmPackage(config); + let results; + if (config.depType === 'docker') { + results = await renovateDockerImage(config); + } else { + // npm + results = await renovateNpmPackage(config); + } logger.debug({ results }, `${config.depName} lookup results`); // Flatten the result on top of config, add repositoryUrl return results.map(result => { diff --git a/lib/workers/repository/apis.js b/lib/workers/repository/apis.js index 662df87e40f57b58755c75d531f4104390243fa7..44e57c0e874b07e49c8a09c434eb07076a19578e 100644 --- a/lib/workers/repository/apis.js +++ b/lib/workers/repository/apis.js @@ -278,6 +278,11 @@ async function detectPackageFiles(input) { logger.info(`Found ${meteorPackageFiles.length} meteor package files`); config.packageFiles = config.packageFiles.concat(meteorPackageFiles); } + if (config.docker.enabled) { + const dockerFiles = await config.api.findFilePaths('Dockerfile'); + logger.info(`Found ${dockerFiles.length} Dockerfiles`); + config.packageFiles = config.packageFiles.concat(dockerFiles); + } return config; } @@ -396,6 +401,25 @@ async function resolvePackageFiles(inputConfig) { } else if (packageFile.packageFile.endsWith('package.js')) { // meteor packageFile = configParser.mergeChildConfig(config.meteor, packageFile); + } else if (packageFile.packageFile.endsWith('Dockerfile')) { + // docker + packageFile = configParser.mergeChildConfig(config.docker, packageFile); + logger.debug(`Resolving packageFile ${JSON.stringify(packageFile)}`); + packageFile.content = await config.api.getFileContent( + packageFile.packageFile, + config.baseBranch + ); + const strippedComment = packageFile.content.replace(/^(#.*?\n)+/, ''); + const fromMatch = strippedComment.match(/^FROM (.*)\n/); + if (!fromMatch) { + logger.debug( + { content: packageFile.content, strippedComment }, + 'No FROM found' + ); + continue; // eslint-disable-line + } + packageFile.currentFrom = fromMatch[1]; + logger.debug('Adding Dockerfile'); } packageFiles.push(packageFile); diff --git a/lib/workers/repository/index.js b/lib/workers/repository/index.js index 053f6588e09f2f39b211af41e05d67090292a4de..4d3466d218d9f2ece414ad48f1ba67762f291186 100644 --- a/lib/workers/repository/index.js +++ b/lib/workers/repository/index.js @@ -69,7 +69,7 @@ async function renovateRepository(repoConfig, token) { config = await apis.detectPackageFiles(config); // If we can't detect any package.json then return if (config.packageFiles.length === 0) { - logger.info('Cannot detect package.json'); + logger.info('Cannot detect package files'); return; } logger.debug( diff --git a/lib/workers/repository/upgrades.js b/lib/workers/repository/upgrades.js index 428402c14adad5680eaa47e3887b188aab11a3bd..946b35176341b3a868ab0a23c859dc62ef8dc300 100644 --- a/lib/workers/repository/upgrades.js +++ b/lib/workers/repository/upgrades.js @@ -34,6 +34,11 @@ async function determineRepoUpgrades(config) { upgrades = upgrades.concat( await packageFileWorker.renovateMeteorPackageFile(packageFileConfig) ); + } else if (packageFileConfig.packageFile.endsWith('Dockerfile')) { + logger.info('Renovating Dockerfile FROM'); + upgrades = upgrades.concat( + await packageFileWorker.renovateDockerfile(packageFileConfig) + ); } } return upgrades; diff --git a/test/api/docker.spec.js b/test/api/docker.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..2ee71546629ddb44e08a205e4e9cc9cae1151e06 --- /dev/null +++ b/test/api/docker.spec.js @@ -0,0 +1,39 @@ +const docker = require('../../lib/api/docker'); +const got = require('got'); +const logger = require('../_fixtures/logger'); + +jest.mock('got'); + +describe('api/docker', () => { + describe('getDigest', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + it('returns null if no token', async () => { + got.mockReturnValueOnce({ body: {} }); + const res = await docker.getDigest('some-name', undefined, logger); + expect(res).toBe(null); + }); + it('returns null if errored', async () => { + got.mockReturnValueOnce({ body: { token: 'some-token' } }); + const res = await docker.getDigest('some-name', undefined, logger); + expect(res).toBe(null); + }); + it('returns digest', async () => { + got.mockReturnValueOnce({ body: { token: 'some-token' } }); + got.mockReturnValueOnce({ + headers: { 'docker-content-digest': 'some-digest' }, + }); + const res = await docker.getDigest('some-name', undefined, logger); + expect(res).toBe('some-digest'); + }); + it('supports scoped names', async () => { + got.mockReturnValueOnce({ body: { token: 'some-token' } }); + got.mockReturnValueOnce({ + headers: { 'docker-content-digest': 'some-digest' }, + }); + const res = await docker.getDigest('some/name', undefined, logger); + expect(res).toBe('some-digest'); + }); + }); +}); diff --git a/test/workers/branch/__snapshots__/dockerfile.spec.js.snap b/test/workers/branch/__snapshots__/dockerfile.spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..f69760bb1b822e3d705af42b0bf678f8235ad95c --- /dev/null +++ b/test/workers/branch/__snapshots__/dockerfile.spec.js.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`workers/branch/dockerfile setNewValue replaces existing value 1`] = ` +"# comment FROM node:8 +FROM node:8@sha256:abcdefghijklmnop +RUN something +" +`; diff --git a/test/workers/branch/dockerfile.spec.js b/test/workers/branch/dockerfile.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..424a6d8547a1b653f7854217b44ed70dfe9dfffe --- /dev/null +++ b/test/workers/branch/dockerfile.spec.js @@ -0,0 +1,22 @@ +const dockerfile = require('../../../lib/workers/branch/dockerfile'); +const logger = require('../../_fixtures/logger'); + +describe('workers/branch/dockerfile', () => { + describe('setNewValue', () => { + it('replaces existing value', () => { + const currentFileContent = + '# comment FROM node:8\nFROM node:8\nRUN something\n'; + const depName = 'node'; + const currentVersion = 'node:8'; + const newVersion = 'node:8@sha256:abcdefghijklmnop'; + const res = dockerfile.setNewValue( + currentFileContent, + depName, + currentVersion, + newVersion, + logger + ); + expect(res).toMatchSnapshot(); + }); + }); +}); diff --git a/test/workers/branch/package-files.spec.js b/test/workers/branch/package-files.spec.js index 60488b8aeb23653302a3da98a0fd08a0dacdd74c..72db0c10f069f2dd81b73cff61cb0014085ec9b7 100644 --- a/test/workers/branch/package-files.spec.js +++ b/test/workers/branch/package-files.spec.js @@ -1,5 +1,6 @@ const packageJsonHelper = require('../../../lib/workers/branch/package-json'); const packageJsHelper = require('../../../lib/workers/branch/package-js'); +const dockerHelper = require('../../../lib/workers/branch/dockerfile'); const { getUpdatedPackageFiles, } = require('../../../lib/workers/branch/package-files'); @@ -16,6 +17,7 @@ describe('workers/branch/package-files', () => { logger, }; packageJsonHelper.setNewValue = jest.fn(); + dockerHelper.setNewValue = jest.fn(); packageJsHelper.setNewValue = jest.fn(); }); it('returns empty if lock file maintenance', async () => { @@ -26,14 +28,14 @@ describe('workers/branch/package-files', () => { it('returns updated files', async () => { config.upgrades = [ { packageFile: 'package.json' }, - { packageFile: 'backend/package.json' }, + { packageFile: 'Dockerfile' }, { packageFile: 'packages/foo/package.js' }, ]; config.api.getFileContent.mockReturnValueOnce('old content 1'); config.api.getFileContent.mockReturnValueOnce('old content 2'); config.api.getFileContent.mockReturnValueOnce('old content 3'); packageJsonHelper.setNewValue.mockReturnValueOnce('old content 1'); - packageJsonHelper.setNewValue.mockReturnValueOnce('new content 2'); + dockerHelper.setNewValue.mockReturnValueOnce('new content 2'); packageJsHelper.setNewValue.mockReturnValueOnce('old content 3'); const res = await getUpdatedPackageFiles(config); expect(res).toHaveLength(1); diff --git a/test/workers/dep-type/index.spec.js b/test/workers/dep-type/index.spec.js index 025e19c862abacaf6e483250dadc1a46c36eb7ff..964fa64424b51cf5b3846d5992c2da93cd73463a 100644 --- a/test/workers/dep-type/index.spec.js +++ b/test/workers/dep-type/index.spec.js @@ -58,6 +58,12 @@ describe('lib/workers/dep-type/index', () => { const res = await depTypeWorker.renovateDepType(content, config); expect(res).toHaveLength(6); }); + it('returns upgrades for docker', async () => { + config.packageFile = 'Dockerfile'; + config.currentFrom = 'node'; + const res = await depTypeWorker.renovateDepType('some-content', config); + expect(res).toHaveLength(1); + }); }); describe('getDepConfig(depTypeConfig, dep)', () => { const depTypeConfig = { diff --git a/test/workers/package-file/index.spec.js b/test/workers/package-file/index.spec.js index 23de2716c0374a498219e458c26df6323a4245b3..1cc19d4669adbb228fb8f3bc5aa96520a6c4ad0e 100644 --- a/test/workers/package-file/index.spec.js +++ b/test/workers/package-file/index.spec.js @@ -65,4 +65,26 @@ describe('packageFileWorker', () => { expect(res).toHaveLength(2); }); }); + describe('renovateDockerfile', () => { + let config; + beforeEach(() => { + config = { + ...defaultConfig, + packageFile: 'Dockerfile', + repoIsOnboarded: true, + logger, + }; + depTypeWorker.renovateDepType.mockReturnValue([]); + }); + it('returns empty if disabled', async () => { + config.enabled = false; + const res = await packageFileWorker.renovateDockerfile(config); + expect(res).toEqual([]); + }); + it('returns upgrades', async () => { + depTypeWorker.renovateDepType.mockReturnValueOnce([{}, {}]); + const res = await packageFileWorker.renovateDockerfile(config); + expect(res).toHaveLength(2); + }); + }); }); diff --git a/test/workers/package/docker.spec.js b/test/workers/package/docker.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..0db380b7ed59f1a09ea5e7b7e71e88759d8d1b46 --- /dev/null +++ b/test/workers/package/docker.spec.js @@ -0,0 +1,42 @@ +const dockerApi = require('../../../lib/api/docker'); +const docker = require('../../../lib/workers/package/docker'); +const defaultConfig = require('../../../lib/config/defaults').getConfig(); +const logger = require('../../_fixtures/logger'); + +// jest.mock('../../../lib/api/docker'); +dockerApi.getDigest = jest.fn(); + +describe('lib/workers/package/docker', () => { + describe('renovateDockerImage', () => { + let config; + beforeEach(() => { + config = { + ...defaultConfig, + logger, + depName: 'some-dep', + currentTag: '1.0.0', + currentDigest: 'sha256:abcdefghijklmnop', + }; + }); + it('returns empty if no digest', async () => { + expect(await docker.renovateDockerImage(config)).toEqual([]); + }); + it('returns empty if digest is same', async () => { + dockerApi.getDigest.mockReturnValueOnce(config.currentDigest); + expect(await docker.renovateDockerImage(config)).toEqual([]); + }); + it('returns a digest', async () => { + dockerApi.getDigest.mockReturnValueOnce('sha256:1234567890'); + const res = await docker.renovateDockerImage(config); + expect(res).toHaveLength(1); + expect(res[0].type).toEqual('digest'); + }); + it('returns a pin', async () => { + delete config.currentDigest; + dockerApi.getDigest.mockReturnValueOnce('sha256:1234567890'); + const res = await docker.renovateDockerImage(config); + expect(res).toHaveLength(1); + expect(res[0].type).toEqual('pin'); + }); + }); +}); diff --git a/test/workers/package/index.spec.js b/test/workers/package/index.spec.js index 7f4b25492c0329d6c7b45a374121c4b7a3a37048..6dd95485dd7d046d1e488efecaf203a702a7332d 100644 --- a/test/workers/package/index.spec.js +++ b/test/workers/package/index.spec.js @@ -2,8 +2,10 @@ const pkgWorker = require('../../../lib/workers/package/index'); const defaultConfig = require('../../../lib/config/defaults').getConfig(); const configParser = require('../../../lib/config'); const logger = require('../../_fixtures/logger'); +const docker = require('../../../lib/workers/package/docker'); const npm = require('../../../lib/workers/package/npm'); +jest.mock('../../../lib/workers/package/docker'); jest.mock('../../../lib/workers/package/npm'); describe('lib/workers/package/index', () => { @@ -20,6 +22,12 @@ describe('lib/workers/package/index', () => { const res = await pkgWorker.renovatePackage(config); expect(res).toMatchObject([]); }); + it('calls docker', async () => { + docker.renovateDockerImage.mockReturnValueOnce([]); + config.depType = 'docker'; + const res = await pkgWorker.renovatePackage(config); + expect(res).toMatchObject([]); + }); it('calls npm', async () => { npm.renovateNpmPackage.mockReturnValueOnce([]); config.depType = 'npm'; diff --git a/test/workers/repository/__snapshots__/apis.spec.js.snap b/test/workers/repository/__snapshots__/apis.spec.js.snap index d21d5fc421e9928003d7a9f3934a2d3fe5b05943..add163b03e078922f182189f41fa3ba6927d518e 100644 --- a/test/workers/repository/__snapshots__/apis.spec.js.snap +++ b/test/workers/repository/__snapshots__/apis.spec.js.snap @@ -34,6 +34,13 @@ Array [ ] `; +exports[`workers/repository/apis detectPackageFiles(config) finds Dockerfiles 1`] = ` +Array [ + "package.json", + "Dockerfile", +] +`; + exports[`workers/repository/apis detectPackageFiles(config) finds meteor package files 1`] = ` Array [ "package.json", diff --git a/test/workers/repository/apis.spec.js b/test/workers/repository/apis.spec.js index b8a04c5699bc2e1c080c88bb0275bb5c3f0a59be..6b0b2e0b4e2ab54ed8982d1d79d05dddad97f5fc 100644 --- a/test/workers/repository/apis.spec.js +++ b/test/workers/repository/apis.spec.js @@ -216,6 +216,7 @@ describe('workers/repository/apis', () => { describe('detectPackageFiles(config)', () => { it('adds package files to object', async () => { const config = { + ...defaultConfig, api: { findFilePaths: jest.fn(() => [ 'package.json', @@ -229,11 +230,11 @@ describe('workers/repository/apis', () => { warnings: [], }; const res = await apis.detectPackageFiles(config); - expect(res).toMatchObject(config); expect(res.packageFiles).toMatchSnapshot(); }); it('finds meteor package files', async () => { const config = { + ...defaultConfig, api: { findFilePaths: jest.fn(), }, @@ -248,7 +249,23 @@ describe('workers/repository/apis', () => { 'modules/something/package.js', ]); const res = await apis.detectPackageFiles(config); - expect(res).toMatchObject(config); + expect(res.packageFiles).toMatchSnapshot(); + }); + it('finds Dockerfiles', async () => { + const config = { + ...defaultConfig, + api: { + findFilePaths: jest.fn(), + }, + docker: { + enabled: true, + }, + logger, + warnings: [], + }; + config.api.findFilePaths.mockReturnValueOnce(['package.json']); + config.api.findFilePaths.mockReturnValueOnce(['Dockerfile']); + const res = await apis.detectPackageFiles(config); expect(res.packageFiles).toMatchSnapshot(); }); it('ignores node modules', async () => { @@ -328,6 +345,22 @@ describe('workers/repository/apis', () => { expect(res.packageFiles).toHaveLength(3); expect(res.packageFiles).toMatchSnapshot(); }); + it('handles dockerfile', async () => { + config.packageFiles = [{ packageFile: 'Dockerfile' }]; + config.api.getFileContent.mockReturnValueOnce( + '# some content\nFROM node:8\nRUN something' + ); + const res = await apis.resolvePackageFiles(config); + expect(res.packageFiles).toHaveLength(1); + }); + it('handles dockerfile with no FROM', async () => { + config.packageFiles = [{ packageFile: 'Dockerfile' }]; + config.api.getFileContent.mockReturnValueOnce( + '# some content\n# FROM node:8\nRUN something' + ); + const res = await apis.resolvePackageFiles(config); + expect(res.packageFiles).toHaveLength(0); + }); }); }); describe('migrateAndValidate', () => { diff --git a/test/workers/repository/upgrades.spec.js b/test/workers/repository/upgrades.spec.js index 0adbcc7553f0e070225e5096fcf162fdf370bd96..fcd9495e9e80957b79b8d9e9bce91754c958891b 100644 --- a/test/workers/repository/upgrades.spec.js +++ b/test/workers/repository/upgrades.spec.js @@ -30,7 +30,7 @@ describe('workers/repository/upgrades', () => { }); it('returns array if upgrades found', async () => { config.packageFiles = [ - 'package.json', + 'Dockerfile', { packageFile: 'backend/package.json', }, @@ -38,7 +38,7 @@ describe('workers/repository/upgrades', () => { packageFile: 'frontend/package.js', }, ]; - packageFileWorker.renovatePackageFile.mockReturnValueOnce(['a']); + packageFileWorker.renovateDockerfile.mockReturnValueOnce(['a']); packageFileWorker.renovatePackageFile.mockReturnValueOnce(['b', 'c']); packageFileWorker.renovateMeteorPackageFile.mockReturnValueOnce(['d']); const res = await upgrades.determineRepoUpgrades(config);