diff --git a/lib/manager/terraform/__fixtures__/1.tf b/lib/manager/terraform/__fixtures__/1.tf index 793474273fa3b1..ccea08fff7ae99 100644 --- a/lib/manager/terraform/__fixtures__/1.tf +++ b/lib/manager/terraform/__fixtures__/1.tf @@ -159,3 +159,16 @@ module "gittags_http" { module "gittags_ssh" { source = "git::ssh://git@bitbucket.com/hashicorp/example?ref=v1.0.3" } + +terraform { + required_providers { + aws = ">= 2.7.0" + } +} + +terraform { + required_providers { + aws = ">= 2.5.0" + azurerm = ">= 2.0.0" + } +} diff --git a/lib/manager/terraform/__snapshots__/extract.spec.ts.snap b/lib/manager/terraform/__snapshots__/extract.spec.ts.snap index 7fd0c2231ce5d8..195e5888bf9b42 100644 --- a/lib/manager/terraform/__snapshots__/extract.spec.ts.snap +++ b/lib/manager/terraform/__snapshots__/extract.spec.ts.snap @@ -140,7 +140,7 @@ Object { "depName": "helm", "depNameShort": "helm", "depType": "terraform", - "skipReason": "no-version", + "skipReason": "unsupported-version", }, Object { "currentValue": "V1.9", @@ -192,6 +192,27 @@ Object { "depType": "gitTags", "lookupName": "ssh://git@bitbucket.com/hashicorp/example", }, + Object { + "currentValue": ">= 2.7.0", + "datasource": "terraform-provider", + "depName": "aws", + "depNameShort": "aws", + "depType": "terraform", + }, + Object { + "currentValue": ">= 2.5.0", + "datasource": "terraform-provider", + "depName": "aws", + "depNameShort": "aws", + "depType": "terraform", + }, + Object { + "currentValue": ">= 2.0.0", + "datasource": "terraform-provider", + "depName": "azurerm", + "depNameShort": "azurerm", + "depType": "terraform", + }, ], } `; diff --git a/lib/manager/terraform/extract.spec.ts b/lib/manager/terraform/extract.spec.ts index 5e833e462e27cd..9b3e94c93504f8 100644 --- a/lib/manager/terraform/extract.spec.ts +++ b/lib/manager/terraform/extract.spec.ts @@ -19,7 +19,7 @@ describe('lib/manager/terraform/extract', () => { it('extracts', () => { const res = extractPackageFile(tf1); expect(res).toMatchSnapshot(); - expect(res.deps).toHaveLength(27); + expect(res.deps).toHaveLength(30); expect(res.deps.filter((dep) => dep.skipReason)).toHaveLength(6); }); it('returns null if only local deps', () => { @@ -42,6 +42,11 @@ describe('lib/manager/terraform/extract', () => { TerraformDependencyTypes.unknown ); }); + it('returns TerraformDependencyTypes.required_providers', () => { + expect(getTerraformDependencyType('required_providers')).toBe( + TerraformDependencyTypes.required_providers + ); + }); it('returns TerraformDependencyTypes.unknown on empty string', () => { expect(getTerraformDependencyType('')).toBe( TerraformDependencyTypes.unknown diff --git a/lib/manager/terraform/extract.ts b/lib/manager/terraform/extract.ts index 1419501ef2957e..dad95431b6ee61 100644 --- a/lib/manager/terraform/extract.ts +++ b/lib/manager/terraform/extract.ts @@ -11,6 +11,7 @@ export enum TerraformDependencyTypes { unknown = 'unknown', module = 'module', provider = 'provider', + required_providers = 'required_providers', } export function getTerraformDependencyType( @@ -23,15 +24,25 @@ export function getTerraformDependencyType( case 'provider': { return TerraformDependencyTypes.provider; } + case 'required_providers': { + return TerraformDependencyTypes.required_providers; + } default: { return TerraformDependencyTypes.unknown; } } } +const dependencyBlockExtractionRegex = /^\s*(?module|provider|required_providers)\s+("(?[^"]+)"\s+)?{\s*$/; +const keyValueExtractionRegex = /^\s*(?[^\s]+)\s+=\s+"(?[^"]+)"\s*$/; // extracts `exampleKey = exampleValue` + export function extractPackageFile(content: string): PackageFile | null { logger.trace({ content }, 'terraform.extractPackageFile()'); - if (!content.includes('module "') && !content.includes('provider "')) { + if ( + !content.includes('module "') && + !content.includes('provider "') && + !content.includes('required_providers ') + ) { return null; } const deps: PackageDependency[] = []; @@ -39,36 +50,54 @@ export function extractPackageFile(content: string): PackageFile | null { const lines = content.split('\n'); for (let lineNumber = 0; lineNumber < lines.length; lineNumber += 1) { let line = lines[lineNumber]; - const terraformDependency = /^(module|provider)\s+"([^"]+)"\s+{\s*$/.exec( - line - ); + const terraformDependency = dependencyBlockExtractionRegex.exec(line); if (terraformDependency) { - logger.trace(`Matched ${terraformDependency[1]} on line ${lineNumber}`); - const tfDepType: TerraformDependencyTypes = getTerraformDependencyType( - terraformDependency[1] + logger.trace( + `Matched ${terraformDependency.groups.type} on line ${lineNumber}` + ); + const tfDepType = getTerraformDependencyType( + terraformDependency.groups.type ); - const dep: PackageDependency = { - managerData: { - moduleName: terraformDependency[2], - terraformDependencyType: tfDepType, - }, - }; + if (tfDepType === TerraformDependencyTypes.unknown) { - /* istanbul ignore next */ logger.trace( - `Could not identify TerraformDependencyType ${terraformDependency[1]} on line ${lineNumber}.` + /* istanbul ignore next */ logger.warn( + `Could not identify TerraformDependencyType ${terraformDependency.groups.type} on line ${lineNumber}.` ); + } else if (tfDepType === TerraformDependencyTypes.required_providers) { + do { + const dep: PackageDependency = { + managerData: { + terraformDependencyType: tfDepType, + }, + }; + + lineNumber += 1; + line = lines[lineNumber]; + const kvMatch = keyValueExtractionRegex.exec(line); + if (kvMatch) { + dep.currentValue = kvMatch.groups.value; + dep.managerData.moduleName = kvMatch.groups.key; + dep.managerData.versionLine = lineNumber; + deps.push(dep); + } + } while (line.trim() !== '}'); } else { + const dep: PackageDependency = { + managerData: { + moduleName: terraformDependency.groups.lookupName, + terraformDependencyType: tfDepType, + }, + }; do { lineNumber += 1; line = lines[lineNumber]; - const kvMatch = /^\s*([^\s]+)\s+=\s+"([^"]+)"\s*$/.exec(line); + const kvMatch = keyValueExtractionRegex.exec(line); if (kvMatch) { - const [, key, value] = kvMatch; - if (key === 'version') { - dep.currentValue = value; + if (kvMatch.groups.key === 'version') { + dep.currentValue = kvMatch.groups.value; dep.managerData.versionLine = lineNumber; - } else if (key === 'source') { - dep.managerData.source = value; + } else if (kvMatch.groups.key === 'source') { + dep.managerData.source = kvMatch.groups.value; dep.managerData.sourceLine = lineNumber; } } @@ -100,7 +129,6 @@ export function extractPackageFile(content: string): PackageFile | null { dep.currentValue = githubRefMatch[3]; dep.datasource = datasourceGithubTags.id; dep.lookupName = depNameShort; - dep.managerData.lineNumber = dep.managerData.sourceLine; if (!isVersion(dep.currentValue)) { dep.skipReason = SkipReason.UnsupportedVersion; } @@ -119,7 +147,6 @@ export function extractPackageFile(content: string): PackageFile | null { } dep.currentValue = gitTagsRefMatch[4]; dep.datasource = datasourceGitTags.id; - dep.managerData.lineNumber = dep.managerData.sourceLine; if (!isVersion(dep.currentValue)) { dep.skipReason = SkipReason.UnsupportedVersion; } @@ -131,7 +158,6 @@ export function extractPackageFile(content: string): PackageFile | null { dep.depType = 'terraform'; dep.depName = moduleParts.join('/'); dep.depNameShort = dep.depName; - dep.managerData.lineNumber = dep.managerData.versionLine; dep.datasource = datasourceTerraformModule.id; } } else { @@ -140,19 +166,16 @@ export function extractPackageFile(content: string): PackageFile | null { } } else if ( dep.managerData.terraformDependencyType === - TerraformDependencyTypes.provider + TerraformDependencyTypes.provider || + dep.managerData.terraformDependencyType === + TerraformDependencyTypes.required_providers ) { dep.depType = 'terraform'; dep.depName = dep.managerData.moduleName; dep.depNameShort = dep.managerData.moduleName; - dep.managerData.lineNumber = dep.managerData.versionLine; dep.datasource = datasourceTerraformProvider.id; - if (dep.managerData.lineNumber) { - if (!isValid(dep.currentValue)) { - dep.skipReason = SkipReason.UnsupportedVersion; - } - } else if (!dep.skipReason) { - dep.skipReason = SkipReason.NoVersion; + if (!isValid(dep.currentValue)) { + dep.skipReason = SkipReason.UnsupportedVersion; } } delete dep.managerData; diff --git a/package.json b/package.json index c0e886d0586691..4fe28e8aaee292 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,8 @@ "Tanuel ", "Viral Ruparel ", "Vladimir Starkov ", - "Mikhail Yakushin " + "Mikhail Yakushin ", + "Sebastian Poxhofer " ], "license": "AGPL-3.0", "bugs": {