From 98a8b96fc8f687d8525b3c2667a4c76d9dbe6f29 Mon Sep 17 00:00:00 2001 From: "sebastian.poxhofer" Date: Mon, 29 Jun 2020 00:48:07 +0200 Subject: [PATCH] feat(terraform-provider): implement updating of helm releases --- lib/manager/terraform/__fixtures__/helm.tf | 40 +++++++++++ .../__snapshots__/extract.spec.ts.snap | 68 +++++++++++++++++++ lib/manager/terraform/extract.spec.ts | 29 ++++++++ lib/manager/terraform/extract.ts | 60 ++++++++++++++-- 4 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 lib/manager/terraform/__fixtures__/helm.tf diff --git a/lib/manager/terraform/__fixtures__/helm.tf b/lib/manager/terraform/__fixtures__/helm.tf new file mode 100644 index 00000000000000..01d0dd84f7c28e --- /dev/null +++ b/lib/manager/terraform/__fixtures__/helm.tf @@ -0,0 +1,40 @@ +# legit use cases +## complete example +resource "helm_release" "example" { + name = "my-redis-release" + repository = "https://kubernetes-charts.storage.googleapis.com" + chart = "redis" + version = "1.0.1" +} + +## example without version, this will default to latest in Terraform +resource "helm_release" "example" { + name = "my-redis-release" + repository = "https://kubernetes-charts.storage.googleapis.com" + chart = "redis" +} + +## local chart +resource "helm_release" "local" { + name = "my-local-chart" + chart = "./charts/example" +} + +## malformed examples +resource "helm_release" "example" { + name = "my-redis-release" + repository = "https://kubernetes-charts.storage.googleapis.com" + version = "4.0.1" +} + +resource "helm_release" "example" { + repository = "https://kubernetes-charts.storage.googleapis.com" + chart = "redis" + version = "5.0.1" +} + +resource "helm_release" "example" { + name = "my-redis-release" + chart = "redis" + version = "6.0.1" +} diff --git a/lib/manager/terraform/__snapshots__/extract.spec.ts.snap b/lib/manager/terraform/__snapshots__/extract.spec.ts.snap index e61646a6faa8bd..2d3476bb075324 100644 --- a/lib/manager/terraform/__snapshots__/extract.spec.ts.snap +++ b/lib/manager/terraform/__snapshots__/extract.spec.ts.snap @@ -1,5 +1,73 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`lib/manager/terraform/extract extractPackageFile() extract helm releases 1`] = ` +Object { + "deps": Array [ + Object { + "currentValue": "1.0.1", + "datasource": "helm", + "depName": "redis", + "depNameShort": "redis", + "depType": "helm", + "registryUrls": Array [ + "https://kubernetes-charts.storage.googleapis.com", + ], + }, + Object { + "datasource": "helm", + "depName": "redis", + "depNameShort": "redis", + "depType": "helm", + "registryUrls": Array [ + "https://kubernetes-charts.storage.googleapis.com", + ], + "skipReason": "unsupported-version", + }, + Object { + "datasource": "helm", + "depName": "./charts/example", + "depNameShort": "./charts/example", + "depType": "helm", + "registryUrls": Array [ + undefined, + ], + "skipReason": "local-chart", + }, + Object { + "currentValue": "4.0.1", + "datasource": "helm", + "depName": undefined, + "depNameShort": undefined, + "depType": "helm", + "registryUrls": Array [ + "https://kubernetes-charts.storage.googleapis.com", + ], + "skipReason": "invalid-name", + }, + Object { + "currentValue": "5.0.1", + "datasource": "helm", + "depName": "redis", + "depNameShort": "redis", + "depType": "helm", + "registryUrls": Array [ + "https://kubernetes-charts.storage.googleapis.com", + ], + }, + Object { + "currentValue": "6.0.1", + "datasource": "helm", + "depName": "redis", + "depNameShort": "redis", + "depType": "helm", + "registryUrls": Array [ + undefined, + ], + }, + ], +} +`; + exports[`lib/manager/terraform/extract extractPackageFile() extracts 1`] = ` Object { "deps": Array [ diff --git a/lib/manager/terraform/extract.spec.ts b/lib/manager/terraform/extract.spec.ts index 9b3e94c93504f8..1b342e39e04c3b 100644 --- a/lib/manager/terraform/extract.spec.ts +++ b/lib/manager/terraform/extract.spec.ts @@ -1,6 +1,7 @@ import { readFileSync } from 'fs'; import { TerraformDependencyTypes, + checkIfStringIsPath, extractPackageFile, getTerraformDependencyType, } from './extract'; @@ -10,6 +11,7 @@ const tf2 = `module "relative" { source = "../../modules/fe" } `; +const helm = readFileSync('lib/manager/terraform/__fixtures__/helm.tf', 'utf8'); describe('lib/manager/terraform/extract', () => { describe('extractPackageFile()', () => { @@ -25,6 +27,12 @@ describe('lib/manager/terraform/extract', () => { it('returns null if only local deps', () => { expect(extractPackageFile(tf2)).toBeNull(); }); + it('extract helm releases', () => { + const res = extractPackageFile(helm); + expect(res).toMatchSnapshot(); + expect(res.deps).toHaveLength(6); + expect(res.deps.filter((dep) => dep.skipReason)).toHaveLength(3); + }); }); describe('getTerraformDependencyType()', () => { it('returns TerraformDependencyTypes.module', () => { @@ -58,4 +66,25 @@ describe('lib/manager/terraform/extract', () => { ); }); }); + describe('checkIfStringIsPath()', () => { + it('check simple string', () => { + expect(checkIfStringIsPath('sdfsgdsfadfhfghfhgdfsdf')).toBe(false); + }); + it('check local path', () => { + expect(checkIfStringIsPath('./sdfsgdsfadfhfghfhgdfsdf')).toBe(true); + }); + it('check up path', () => { + expect(checkIfStringIsPath('../sdfsgdsfadfhfghfhgdfsdf')).toBe(true); + }); + it('check up deep path', () => { + expect( + checkIfStringIsPath('../sdfsgdsfadfhfghfhgdfsdf/sklskdjfksd') + ).toBe(true); + }); + it('check up another deep path', () => { + expect(checkIfStringIsPath('sdfsgdsfadfhfghfhgdfsdf/sklskdjfksd')).toBe( + true + ); + }); + }); }); diff --git a/lib/manager/terraform/extract.ts b/lib/manager/terraform/extract.ts index 3518ebc2f95bc3..c42e4dd44fd5e3 100644 --- a/lib/manager/terraform/extract.ts +++ b/lib/manager/terraform/extract.ts @@ -1,5 +1,6 @@ import * as datasourceGitTags from '../../datasource/git-tags'; import * as datasourceGithubTags from '../../datasource/github-tags'; +import * as datasourceHelm from '../../datasource/helm'; import * as datasourceTerraformModule from '../../datasource/terraform-module'; import * as datasourceTerraformProvider from '../../datasource/terraform-provider'; import { logger } from '../../logger'; @@ -12,6 +13,7 @@ export enum TerraformDependencyTypes { module = 'module', provider = 'provider', required_providers = 'required_providers', + resource = 'resource', } export function getTerraformDependencyType( @@ -27,6 +29,9 @@ export function getTerraformDependencyType( case 'required_providers': { return TerraformDependencyTypes.required_providers; } + case 'resource': { + return TerraformDependencyTypes.resource; + } default: { return TerraformDependencyTypes.unknown; } @@ -42,8 +47,19 @@ function checkFileContainsDependency( }); } -const dependencyBlockExtractionRegex = /^\s*(?module|provider|required_providers)\s+("(?[^"]+)"\s+)?{\s*$/; -const contentCheckList = ['module "', 'provider "', 'required_providers ']; +export function checkIfStringIsPath(path: string): boolean { + const regex = /(.|..)?(\/[^/])+/; + const match = regex.exec(path); + return !!match; +} + +const dependencyBlockExtractionRegex = /^\s*(?[a-z_]+)\s+("(?[^"]+)"\s+)?("(?[^"]+)"\s+)?{\s*$/; +const contentCheckList = [ + 'module "', + 'provider "', + 'required_providers ', + ' "helm_release" ', +]; const keyValueExtractionRegex = /^\s*(?[^\s]+)\s+=\s+"(?[^"]+)"\s*$/; // extracts `exampleKey = exampleValue` const githubRefMatchRegex = /github.com(\/|:)([^/]+\/[a-z0-9-.]+).*\?ref=(.*)$/; const gitTagsRefMatchRegex = /(?:git::)?((?:http|https|ssh):\/\/(?:.*@)?(.*.*\/(.*\/.*)))\?ref=(.*)$/; @@ -68,7 +84,7 @@ export function extractPackageFile(content: string): PackageFile | null { ); if (tfDepType === TerraformDependencyTypes.unknown) { - /* istanbul ignore next */ logger.warn( + /* istanbul ignore next */ logger.debug( `Could not identify TerraformDependencyType ${terraformDependency.groups.type} on line ${lineNumber}.` ); } else if (tfDepType === TerraformDependencyTypes.required_providers) { @@ -85,10 +101,30 @@ export function extractPackageFile(content: string): PackageFile | null { if (kvMatch) { dep.currentValue = kvMatch.groups.value; dep.managerData.moduleName = kvMatch.groups.key; - dep.managerData.versionLine = lineNumber; deps.push(dep); } } while (line.trim() !== '}'); + } else if (tfDepType === TerraformDependencyTypes.resource) { + const dep: PackageDependency = { + managerData: { + terraformDependencyType: tfDepType, + }, + }; + 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; + } + } + } while (line.trim() !== '}'); + deps.push(dep); } else { const dep: PackageDependency = { managerData: { @@ -181,6 +217,22 @@ export function extractPackageFile(content: string): PackageFile | null { if (!isValid(dep.currentValue)) { dep.skipReason = SkipReason.UnsupportedVersion; } + } else if ( + dep.managerData.terraformDependencyType === + TerraformDependencyTypes.resource + ) { + 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; } delete dep.managerData; /* eslint-enable no-param-reassign */