From 37b1c8f0de59a781a499fa249351e075e3231db8 Mon Sep 17 00:00:00 2001 From: Rhys Arkins <rhys@arkins.net> Date: Sun, 22 Jul 2018 06:33:11 +0200 Subject: [PATCH] feat: gitlabci.yml support (#1744) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds support for .gitlabci.yml files. Part of the logic is same as Docker Compose files, however the “services†list is new/different so requires additional logic. Closes #1598 --- lib/config/definitions.js | 12 +++ lib/manager/gitlabci/extract.js | 56 +++++++++++ lib/manager/gitlabci/index.js | 10 ++ lib/manager/gitlabci/update.js | 42 ++++++++ lib/manager/index.js | 1 + test/_fixtures/gitlabci/gitlab-ci.yaml | 97 +++++++++++++++++++ .../__snapshots__/extract.spec.js.snap | 81 ++++++++++++++++ test/manager/gitlabci/extract.spec.js | 26 +++++ test/manager/gitlabci/update.spec.js | 82 ++++++++++++++++ .../extract/__snapshots__/index.spec.js.snap | 3 + website/docs/configuration-options.md | 4 + 11 files changed, 414 insertions(+) create mode 100644 lib/manager/gitlabci/extract.js create mode 100644 lib/manager/gitlabci/index.js create mode 100644 lib/manager/gitlabci/update.js create mode 100644 test/_fixtures/gitlabci/gitlab-ci.yaml create mode 100644 test/manager/gitlabci/__snapshots__/extract.spec.js.snap create mode 100644 test/manager/gitlabci/extract.spec.js create mode 100644 test/manager/gitlabci/update.spec.js diff --git a/lib/config/definitions.js b/lib/config/definitions.js index e0ab85a21d..b0ad426459 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 0000000000..f8017e699e --- /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 0000000000..5549e42b60 --- /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 0000000000..462dd4e078 --- /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 4d3c3f686c..d648305761 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 0000000000..37736f369d --- /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 0000000000..70e590613e --- /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 0000000000..78741eefc0 --- /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 0000000000..ac6f77e84a --- /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 707924ce03..c281ecd58c 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 12a309dfcf..e0b6f3b62d 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. -- GitLab