diff --git a/lib/config/definitions.js b/lib/config/definitions.js index e0ab85a21d5b2dcc0f92300b7b65f9c1d1253032..b0ad42645908af32d92ec9df6c77ec72d175c196 100644 --- a/lib/config/definitions.js +++ b/lib/config/definitions.js @@ -1066,6 +1066,18 @@ const options = [ mergeable: true, cli: false, }, + { + name: 'gitlabci', + description: + 'Configuration object for GitLab CI yml renovation. Also inherits settings from `docker` object.', + stage: 'repository', + type: 'json', + default: { + fileMatch: ['^.gitlab-ci.yml$'], + }, + mergeable: true, + cli: false, + }, { name: 'nuget', description: 'Configuration object for C#/Nuget', diff --git a/lib/manager/gitlabci/extract.js b/lib/manager/gitlabci/extract.js new file mode 100644 index 0000000000000000000000000000000000000000..f8017e699e3bbced8e9ad2f8a236007e4419795c --- /dev/null +++ b/lib/manager/gitlabci/extract.js @@ -0,0 +1,56 @@ +const { getDep } = require('../dockerfile/extract'); + +module.exports = { + extractDependencies, +}; + +function extractDependencies(content) { + const deps = []; + try { + const lines = content.split('\n'); + for (let lineNumber = 0; lineNumber < lines.length; lineNumber += 1) { + const line = lines[lineNumber]; + const imageMatch = line.match(/^\s*image:\s*'?"?([^\s]+)'?"?\s*$/); + if (imageMatch) { + logger.trace(`Matched image on line ${lineNumber}`); + const currentFrom = imageMatch[1]; + const dep = getDep(currentFrom); + dep.lineNumber = lineNumber; + dep.depType = 'image'; + deps.push(dep); + } + const services = line.match(/^\s*services:\s*$/); + if (services) { + logger.trace(`Matched services on line ${lineNumber}`); + let foundImage; + do { + foundImage = false; + const serviceImageLine = lines[lineNumber + 1]; + logger.trace(`serviceImageLine: "${serviceImageLine}"`); + const serviceImageMatch = serviceImageLine.match( + /^\s*-\s*'?"?([^\s'"]+)'?"?\s*$/ + ); + if (serviceImageMatch) { + logger.trace('serviceImageMatch'); + foundImage = true; + const currentFrom = serviceImageMatch[1]; + lineNumber += 1; + const dep = getDep(currentFrom); + dep.lineNumber = lineNumber; + dep.depType = 'service-image'; + deps.push(dep); + } + } while (foundImage); + } + } + } catch (err) /* istanbul ignore next */ { + logger.error( + { err, message: err.message }, + 'Error extracting GitLab CI dependencies' + ); + } + if (!deps.length) { + return null; + } + return { deps }; +} diff --git a/lib/manager/gitlabci/index.js b/lib/manager/gitlabci/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5549e42b60034aa403b42b687243eac9ce402789 --- /dev/null +++ b/lib/manager/gitlabci/index.js @@ -0,0 +1,10 @@ +const { extractDependencies } = require('./extract'); +const { updateDependency } = require('./update'); + +const language = 'docker'; + +module.exports = { + extractDependencies, + language, + updateDependency, +}; diff --git a/lib/manager/gitlabci/update.js b/lib/manager/gitlabci/update.js new file mode 100644 index 0000000000000000000000000000000000000000..462dd4e07801cd60c74bb81a6ae9c2eb44284c1a --- /dev/null +++ b/lib/manager/gitlabci/update.js @@ -0,0 +1,42 @@ +const { getNewFrom } = require('../dockerfile/update'); + +module.exports = { + updateDependency, +}; + +function updateDependency(currentFileContent, upgrade) { + try { + const newFrom = getNewFrom(upgrade); + const lines = currentFileContent.split('\n'); + const lineToChange = lines[upgrade.lineNumber]; + if (upgrade.depType === 'image') { + const imageLine = new RegExp(/^(\s*image:\s*'?"?)[^\s'"]+('?"?\s*)$/); + if (!lineToChange.match(imageLine)) { + logger.debug('No image line found'); + return null; + } + const newLine = lineToChange.replace(imageLine, `$1${newFrom}$2`); + if (newLine === lineToChange) { + logger.debug('No changes necessary'); + return currentFileContent; + } + lines[upgrade.lineNumber] = newLine; + return lines.join('\n'); + } + const serviceLine = new RegExp(/^(\s*-\s*'?"?)[^\s'"]+('?"?\s*)$/); + if (!lineToChange.match(serviceLine)) { + logger.debug('No image line found'); + return null; + } + const newLine = lineToChange.replace(serviceLine, `$1${newFrom}$2`); + if (newLine === lineToChange) { + logger.debug('No changes necessary'); + return currentFileContent; + } + lines[upgrade.lineNumber] = newLine; + return lines.join('\n'); + } catch (err) { + logger.info({ err }, 'Error setting new Dockerfile value'); + return null; + } +} diff --git a/lib/manager/index.js b/lib/manager/index.js index 4d3c3f686c334fbc88593bec7f10afdfed2e5b56..d64830576171b8888a3ba692cd64e055145a4266 100644 --- a/lib/manager/index.js +++ b/lib/manager/index.js @@ -5,6 +5,7 @@ const managerList = [ 'composer', 'docker-compose', 'dockerfile', + 'gitlabci', 'meteor', 'npm', 'nvm', diff --git a/test/_fixtures/gitlabci/gitlab-ci.yaml b/test/_fixtures/gitlabci/gitlab-ci.yaml new file mode 100644 index 0000000000000000000000000000000000000000..37736f369defc2e937bd6208956888dbdfb55026 --- /dev/null +++ b/test/_fixtures/gitlabci/gitlab-ci.yaml @@ -0,0 +1,97 @@ +.executor-docker: &executor-docker + tags: + - docker + +.executor-docker-in-docker: &executor-docker-privileged + tags: + - docker-privileged + +.executor-docker-in-docker: &executor-docker-in-docker + tags: + - docker-in-docker + +.docker-login: &docker-login + before_script: + - echo $CI_JOB_TOKEN | docker login -u gitlab-ci-token --password-stdin $CI_REGISTRY + +.docker-logout: &docker-logout + after_script: + - docker logout $CI_REGISTRY + +.build-image: &build-image + export BUILD_IMAGE=$CI_REGISTRY_IMAGE:${CI_COMMIT_TAG:-$CI_COMMIT_REF_SLUG} + +stages: + - compliance-tests + - build + - sast + - unit-tests + - release + +variables: + LATEST_IMAGE: $CI_REGISTRY_IMAGE:latest + +hadolint: + stage: compliance-tests + <<: *executor-docker + image: hadolint/hadolint:latest + script: + - hadolint Dockerfile + +build: + stage: build + <<: *executor-docker-privileged + <<: *docker-login + script: + - *build-image + - docker build --label "org.label-schema.build-date=$(date +%Y-%m-%dT%T%z)" --label "org.label-schema.version=$CI_COMMIT_REF_NAME" --tag $BUILD_IMAGE . + - docker push $BUILD_IMAGE + <<: *docker-logout + +sast:container: + stage: sast + <<: *executor-docker-in-docker + image: docker:latest + services: + - docker:dind + <<: *docker-login + script: + - *build-image + - docker run -d --name db arminc/clair-db:latest + - docker run -p 6060:6060 --link db:postgres -d --name clair arminc/clair-local-scan:v2.0.1 + - apk add -U wget ca-certificates + - docker pull $BUILD_IMAGE + - wget https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64 + - mv clair-scanner_linux_amd64 clair-scanner + - chmod +x clair-scanner + - touch clair-whitelist.yml + - ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-sast-container-report.json -l clair.log -w clair-whitelist.yml $BUILD_IMAGE || true + <<: *docker-logout + artifacts: + paths: [gl-sast-container-report.json] + +constainer-structure-test: + stage: unit-tests + <<: *executor-docker-in-docker + image: docker:latest + services: + - docker:dind + <<: *docker-login + script: + - *build-image + - docker pull $BUILD_IMAGE + - echo "container-structure-test test --verbose --image $BUILD_IMAGE --config /tests/container/*.json" | docker run --rm -i --volume /var/run/docker.sock:/var/run/docker.sock --volume $CI_PROJECT_DIR/tests/container:/tests/container:ro $LATEST_IMAGE /bin/sh; exit $? + <<: *docker-logout + +release: + stage: release + <<: *executor-docker-privileged + <<: *docker-login + script: + - *build-image + - docker pull $BUILD_IMAGE + - docker tag $BUILD_IMAGE $LATEST_IMAGE + - docker push $LATEST_IMAGE + <<: *docker-logout + only: + - tags diff --git a/test/manager/gitlabci/__snapshots__/extract.spec.js.snap b/test/manager/gitlabci/__snapshots__/extract.spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..70e590613e149a64433da7f8c3d9df6d58a1e8e6 --- /dev/null +++ b/test/manager/gitlabci/__snapshots__/extract.spec.js.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`lib/manager/gitlabci/extract extractDependencies() extracts multiple image lines 1`] = ` +Array [ + Object { + "currentDepTag": "hadolint/hadolint:latest", + "currentDepTagDigest": "hadolint/hadolint:latest", + "currentDigest": undefined, + "currentFrom": "hadolint/hadolint:latest", + "currentTag": "latest", + "currentValue": "latest", + "depName": "hadolint/hadolint", + "depType": "image", + "dockerRegistry": undefined, + "lineNumber": 36, + "purl": "pkg:docker/hadolint/hadolint", + "tagSuffix": undefined, + "versionScheme": "docker", + }, + Object { + "currentDepTag": "docker:latest", + "currentDepTagDigest": "docker:latest", + "currentDigest": undefined, + "currentFrom": "docker:latest", + "currentTag": "latest", + "currentValue": "latest", + "depName": "docker", + "depType": "image", + "dockerRegistry": undefined, + "lineNumber": 53, + "purl": "pkg:docker/docker", + "tagSuffix": undefined, + "versionScheme": "docker", + }, + Object { + "currentDepTag": "docker:dind", + "currentDepTagDigest": "docker:dind", + "currentDigest": undefined, + "currentFrom": "docker:dind", + "currentTag": "dind", + "currentValue": "dind", + "depName": "docker", + "depType": "service-image", + "dockerRegistry": undefined, + "lineNumber": 55, + "purl": "pkg:docker/docker", + "tagSuffix": undefined, + "versionScheme": "docker", + }, + Object { + "currentDepTag": "docker:latest", + "currentDepTagDigest": "docker:latest", + "currentDigest": undefined, + "currentFrom": "docker:latest", + "currentTag": "latest", + "currentValue": "latest", + "depName": "docker", + "depType": "image", + "dockerRegistry": undefined, + "lineNumber": 75, + "purl": "pkg:docker/docker", + "tagSuffix": undefined, + "versionScheme": "docker", + }, + Object { + "currentDepTag": "docker:dind", + "currentDepTagDigest": "docker:dind", + "currentDigest": undefined, + "currentFrom": "docker:dind", + "currentTag": "dind", + "currentValue": "dind", + "depName": "docker", + "depType": "service-image", + "dockerRegistry": undefined, + "lineNumber": 77, + "purl": "pkg:docker/docker", + "tagSuffix": undefined, + "versionScheme": "docker", + }, +] +`; diff --git a/test/manager/gitlabci/extract.spec.js b/test/manager/gitlabci/extract.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..78741eefc07df67409844e8f3689852ed7196b64 --- /dev/null +++ b/test/manager/gitlabci/extract.spec.js @@ -0,0 +1,26 @@ +const fs = require('fs'); +const { + extractDependencies, +} = require('../../../lib/manager/gitlabci/extract'); + +const yamlFile = fs.readFileSync( + 'test/_fixtures/gitlabci/gitlab-ci.yaml', + 'utf8' +); + +describe('lib/manager/gitlabci/extract', () => { + describe('extractDependencies()', () => { + let config; + beforeEach(() => { + config = {}; + }); + it('returns null for empty', () => { + expect(extractDependencies('nothing here', config)).toBe(null); + }); + it('extracts multiple image lines', () => { + const res = extractDependencies(yamlFile, config); + expect(res.deps).toMatchSnapshot(); + expect(res.deps).toHaveLength(5); + }); + }); +}); diff --git a/test/manager/gitlabci/update.spec.js b/test/manager/gitlabci/update.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..ac6f77e84a48b287805646743ac3e696776c715f --- /dev/null +++ b/test/manager/gitlabci/update.spec.js @@ -0,0 +1,82 @@ +const fs = require('fs'); +const dcUpdate = require('../../../lib/manager/gitlabci/update'); + +const yamlFile = fs.readFileSync( + 'test/_fixtures/gitlabci/gitlab-ci.yaml', + 'utf8' +); + +describe('manager/gitlabci/update', () => { + describe('updateDependency', () => { + it('replaces existing value', () => { + const upgrade = { + lineNumber: 36, + depType: 'image', + depName: 'hadolint/hadolint', + newValue: '7.0.0', + newDigest: 'sha256:abcdefghijklmnop', + }; + const res = dcUpdate.updateDependency(yamlFile, upgrade); + expect(res).not.toEqual(yamlFile); + expect(res.includes(upgrade.newDigest)).toBe(true); + }); + it('returns same', () => { + const upgrade = { + depType: 'image', + lineNumber: 36, + depName: 'hadolint/hadolint', + newValue: 'latest', + }; + const res = dcUpdate.updateDependency(yamlFile, upgrade); + expect(res).toEqual(yamlFile); + }); + it('returns null if mismatch', () => { + const upgrade = { + lineNumber: 17, + depType: 'image', + depName: 'postgres', + newValue: '9.6.8', + newDigest: 'sha256:abcdefghijklmnop', + }; + const res = dcUpdate.updateDependency(yamlFile, upgrade); + expect(res).toBe(null); + }); + it('replaces service-image update', () => { + const upgrade = { + lineNumber: 55, + depType: 'service-image', + depName: 'hadolint/hadolint', + newValue: '7.0.0', + newDigest: 'sha256:abcdefghijklmnop', + }; + const res = dcUpdate.updateDependency(yamlFile, upgrade); + expect(res).not.toEqual(yamlFile); + expect(res.includes(upgrade.newDigest)).toBe(true); + }); + it('returns null if service-image mismatch', () => { + const upgrade = { + lineNumber: 17, + depType: 'service-image', + depName: 'postgres', + newValue: '9.6.8', + newDigest: 'sha256:abcdefghijklmnop', + }; + const res = dcUpdate.updateDependency(yamlFile, upgrade); + expect(res).toBe(null); + }); + it('returns service-image same', () => { + const upgrade = { + depType: 'serviceimage', + lineNumber: 55, + depName: 'docker', + newValue: 'dind', + }; + const res = dcUpdate.updateDependency(yamlFile, upgrade); + expect(res).toEqual(yamlFile); + }); + it('returns null if error', () => { + const res = dcUpdate.updateDependency(null, null); + expect(res).toBe(null); + }); + }); +}); diff --git a/test/workers/repository/extract/__snapshots__/index.spec.js.snap b/test/workers/repository/extract/__snapshots__/index.spec.js.snap index 707924ce03ba008092db0d7e662eb40cef79d11f..c281ecd58c327b8ebff75366cf373ddab8422ae3 100644 --- a/test/workers/repository/extract/__snapshots__/index.spec.js.snap +++ b/test/workers/repository/extract/__snapshots__/index.spec.js.snap @@ -20,6 +20,9 @@ Object { "dockerfile": Array [ Object {}, ], + "gitlabci": Array [ + Object {}, + ], "meteor": Array [ Object {}, ], diff --git a/website/docs/configuration-options.md b/website/docs/configuration-options.md index 12a309dfcf4b5ecefd7bd3b334f3d8e1ba0903da..e0b6f3b62d803a2f2c2dc0f00918c488ec67ac48 100644 --- a/website/docs/configuration-options.md +++ b/website/docs/configuration-options.md @@ -199,6 +199,10 @@ See https://renovatebot.com/docs/configuration-reference/config-presets for deta ## fileMatch +## gitlabci + +Add to this configuration setting if you need to override any of the GitLab CI default settings. Use the `docker` config object instead if you wish for configuration to apply across all Docker-related package managers. + ## group The default configuration for groups are essentially internal to Renovate and you normally shouldn't need to modify them. However, you may choose to _add_ settings to any group by defining your own `group` configuration object.