From 938d896d31c553085baf4ad4a91a260db83fd8c3 Mon Sep 17 00:00:00 2001 From: Michael Kriese <michael.kriese@visualon.de> Date: Tue, 28 Jul 2020 12:27:17 +0200 Subject: [PATCH] feat(terraform): support docker provider (#6866) --- lib/manager/terraform/__fixtures__/1.tf | 58 +++++++++- .../__snapshots__/extract.spec.ts.snap | 42 ++++++++ lib/manager/terraform/extract.spec.ts | 4 +- lib/manager/terraform/extract.ts | 4 +- lib/manager/terraform/resources.ts | 100 ++++++++++++++---- lib/manager/terraform/util.ts | 33 ++++++ 6 files changed, 217 insertions(+), 24 deletions(-) diff --git a/lib/manager/terraform/__fixtures__/1.tf b/lib/manager/terraform/__fixtures__/1.tf index ccea08fff7..db6f8e68aa 100644 --- a/lib/manager/terraform/__fixtures__/1.tf +++ b/lib/manager/terraform/__fixtures__/1.tf @@ -168,7 +168,63 @@ terraform { terraform { required_providers { - aws = ">= 2.5.0" + aws = ">= 2.5.0" azurerm = ">= 2.0.0" } } + + +# docker_image resources +# https://www.terraform.io/docs/providers/docker/r/image.html +resource "docker_image" "nginx" { + name = "nginx:1.7.8" +} + +resource "docker_image" "invalid" { +} + +resource "docker_image" "ignore_variable" { + name = "${data.docker_registry_image.ubuntu.name}" + pull_triggers = ["${data.docker_registry_image.ubuntu.sha256_digest}"] +} + + +# docker_container resources +# https://www.terraform.io/docs/providers/docker/r/container.html +resource "docker_container" "foo" { + name = "foo" + image = "nginx:1.7.8" +} + +resource "docker_container" "invalid" { + name = "foo" +} + + +# docker_service resources +# https://www.terraform.io/docs/providers/docker/r/service.html +resource "docker_service" "foo" { + name = "foo-service" + + task_spec { + container_spec { + image = "repo.mycompany.com:8080/foo-service:v1" + } + } + + endpoint_spec { + ports { + target_port = "8080" + } + } +} + +resource "docker_service" "invalid" { +} + +# unsupported resources +resource "not_supported_resource" "foo" { + name = "foo" + image = "nginx:1.7.8" + dummy = "true" +} diff --git a/lib/manager/terraform/__snapshots__/extract.spec.ts.snap b/lib/manager/terraform/__snapshots__/extract.spec.ts.snap index 6f4f5802a0..b4aac1bbb7 100644 --- a/lib/manager/terraform/__snapshots__/extract.spec.ts.snap +++ b/lib/manager/terraform/__snapshots__/extract.spec.ts.snap @@ -297,6 +297,48 @@ Object { "depNameShort": "azurerm", "depType": "terraform", }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": undefined, + "currentValue": "1.7.8", + "datasource": "docker", + "depName": "nginx", + "replaceString": "nginx:1.7.8", + }, + Object { + "skipReason": "invalid-dependency-specification", + }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "datasource": "docker", + "replaceString": "\${data.docker_registry_image.ubuntu.name}", + "skipReason": "contains-variable", + }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": undefined, + "currentValue": "1.7.8", + "datasource": "docker", + "depName": "nginx", + "replaceString": "nginx:1.7.8", + }, + Object { + "skipReason": "invalid-dependency-specification", + }, + Object { + "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}", + "currentDigest": undefined, + "currentValue": "v1", + "datasource": "docker", + "depName": "repo.mycompany.com:8080/foo-service", + "replaceString": "repo.mycompany.com:8080/foo-service:v1", + }, + Object { + "skipReason": "invalid-dependency-specification", + }, + Object { + "skipReason": "unsupported-value", + }, ], } `; diff --git a/lib/manager/terraform/extract.spec.ts b/lib/manager/terraform/extract.spec.ts index 23019135fb..6cd7d5285d 100644 --- a/lib/manager/terraform/extract.spec.ts +++ b/lib/manager/terraform/extract.spec.ts @@ -16,8 +16,8 @@ describe('lib/manager/terraform/extract', () => { it('extracts', () => { const res = extractPackageFile(tf1); expect(res).toMatchSnapshot(); - expect(res.deps).toHaveLength(30); - expect(res.deps.filter((dep) => dep.skipReason)).toHaveLength(6); + expect(res.deps).toHaveLength(38); + expect(res.deps.filter((dep) => dep.skipReason)).toHaveLength(11); }); it('returns null if only local deps', () => { expect(extractPackageFile(tf2)).toBeNull(); diff --git a/lib/manager/terraform/extract.ts b/lib/manager/terraform/extract.ts index ec979a9277..5c55438fd6 100644 --- a/lib/manager/terraform/extract.ts +++ b/lib/manager/terraform/extract.ts @@ -12,6 +12,7 @@ import { } from './resources'; import { TerraformDependencyTypes, + TerraformManagerData, checkFileContainsDependency, getTerraformDependencyType, } from './util'; @@ -22,6 +23,7 @@ const contentCheckList = [ 'provider "', 'required_providers ', ' "helm_release" ', + ' "docker_image" ', ]; export function extractPackageFile(content: string): PackageFile | null { @@ -29,7 +31,7 @@ export function extractPackageFile(content: string): PackageFile | null { if (!checkFileContainsDependency(content, contentCheckList)) { return null; } - let deps: PackageDependency[] = []; + let deps: PackageDependency<TerraformManagerData>[] = []; try { const lines = content.split('\n'); for (let lineNumber = 0; lineNumber < lines.length; lineNumber += 1) { diff --git a/lib/manager/terraform/resources.ts b/lib/manager/terraform/resources.ts index 793b4ced50..d5ac8ba51c 100644 --- a/lib/manager/terraform/resources.ts +++ b/lib/manager/terraform/resources.ts @@ -2,37 +2,62 @@ import * as datasourceHelm from '../../datasource/helm'; import { SkipReason } from '../../types'; import { isValid } from '../../versioning/hashicorp'; import { PackageDependency } from '../common'; +import { getDep } from '../dockerfile/extract'; import { ExtractionResult, + ResourceManagerData, TerraformDependencyTypes, + TerraformResourceTypes, checkIfStringIsPath, keyValueExtractionRegex, + resourceTypeExtractionRegex, } from './util'; +function applyDockerDependency( + dep: PackageDependency<ResourceManagerData>, + value: string +): void { + const dockerDep = getDep(value); + Object.assign(dep, dockerDep); +} + export function extractTerraformResource( startingLine: number, lines: string[] ): ExtractionResult { let lineNumber = startingLine; - let line; + let line = lines[lineNumber]; const deps: PackageDependency[] = []; - const dep: PackageDependency = { + const dep: PackageDependency<ResourceManagerData> = { managerData: { terraformDependencyType: TerraformDependencyTypes.resource, }, }; + const typeMatch = resourceTypeExtractionRegex.exec(line); + + dep.managerData.resourceType = + TerraformResourceTypes[typeMatch?.groups?.type] ?? + TerraformResourceTypes.unknown; + do { lineNumber += 1; line = lines[lineNumber]; const kvMatch = keyValueExtractionRegex.exec(line); if (kvMatch) { - if (kvMatch.groups.key === 'version') { - dep.currentValue = kvMatch.groups.value; - } else if (kvMatch.groups.key === 'chart') { - dep.managerData.chart = kvMatch.groups.value; - } else if (kvMatch.groups.key === 'repository') { - dep.managerData.repository = kvMatch.groups.value; + switch (kvMatch.groups.key) { + case 'chart': + case 'image': + case 'name': + case 'repository': + dep.managerData[kvMatch.groups.key] = kvMatch.groups.value; + break; + case 'version': + dep.currentValue = kvMatch.groups.value; + break; + default: + /* istanbul ignore next */ + break; } } } while (line.trim() !== '}'); @@ -40,19 +65,54 @@ export function extractTerraformResource( return { lineNumber, dependencies: deps }; } -export function analyseTerraformResource(dep: PackageDependency): void { +export function analyseTerraformResource( + dep: PackageDependency<ResourceManagerData> +): void { /* eslint-disable no-param-reassign */ - if (dep.managerData.chart == null) { - dep.skipReason = SkipReason.InvalidName; - } else if (checkIfStringIsPath(dep.managerData.chart)) { - dep.skipReason = SkipReason.LocalChart; - } else if (!isValid(dep.currentValue)) { - dep.skipReason = SkipReason.UnsupportedVersion; + + switch (dep.managerData.resourceType) { + case TerraformResourceTypes.docker_container: + if (!dep.managerData.image) { + dep.skipReason = SkipReason.InvalidDependencySpecification; + } else { + applyDockerDependency(dep, dep.managerData.image); + } + break; + + case TerraformResourceTypes.docker_image: + if (!dep.managerData.name) { + dep.skipReason = SkipReason.InvalidDependencySpecification; + } else { + applyDockerDependency(dep, dep.managerData.name); + } + break; + + case TerraformResourceTypes.docker_service: + if (!dep.managerData.image) { + dep.skipReason = SkipReason.InvalidDependencySpecification; + } else { + applyDockerDependency(dep, dep.managerData.image); + } + break; + + case TerraformResourceTypes.helm_release: + if (dep.managerData.chart == null) { + dep.skipReason = SkipReason.InvalidName; + } else if (checkIfStringIsPath(dep.managerData.chart)) { + dep.skipReason = SkipReason.LocalChart; + } else if (!isValid(dep.currentValue)) { + dep.skipReason = SkipReason.UnsupportedVersion; + } + dep.depType = 'helm'; + dep.registryUrls = [dep.managerData.repository]; + dep.depName = dep.managerData.chart; + dep.depNameShort = dep.managerData.chart; + dep.datasource = datasourceHelm.id; + break; + + default: + dep.skipReason = SkipReason.UnsupportedValue; + break; } - dep.depType = 'helm'; - dep.registryUrls = [dep.managerData.repository]; - dep.depName = dep.managerData.chart; - dep.depNameShort = dep.managerData.chart; - dep.datasource = datasourceHelm.id; /* eslint-enable no-param-reassign */ } diff --git a/lib/manager/terraform/util.ts b/lib/manager/terraform/util.ts index c2fd55855b..80dfada2cf 100644 --- a/lib/manager/terraform/util.ts +++ b/lib/manager/terraform/util.ts @@ -1,6 +1,7 @@ import { PackageDependency } from '../common'; export const keyValueExtractionRegex = /^\s*(?<key>[^\s]+)\s+=\s+"(?<value>[^"]+)"\s*$/; +export const resourceTypeExtractionRegex = /^\s*resource\s+"(?<type>[^\s]+)"\s+"(?<name>[^"]+)"\s*{/; export interface ExtractionResult { lineNumber: number; @@ -15,6 +16,38 @@ export enum TerraformDependencyTypes { resource = 'resource', } +export interface TerraformManagerData { + terraformDependencyType: TerraformDependencyTypes; +} + +export enum TerraformResourceTypes { + unknown = 'unknown', + /** + * https://www.terraform.io/docs/providers/docker/r/container.html + */ + docker_container = 'docker_container', + /** + * https://www.terraform.io/docs/providers/docker/r/image.html + */ + docker_image = 'docker_image', + /** + * https://www.terraform.io/docs/providers/docker/r/service.html + */ + docker_service = 'docker_service', + /** + * https://www.terraform.io/docs/providers/helm/r/release.html + */ + helm_release = 'helm_release', +} + +export interface ResourceManagerData extends TerraformManagerData { + resourceType?: TerraformResourceTypes; + chart?: string; + image?: string; + name?: string; + repository?: string; +} + export function getTerraformDependencyType( value: string ): TerraformDependencyTypes { -- GitLab