Skip to content

Commit

Permalink
feat(terraform): use HCL parser and introduce class based extractors (#…
Browse files Browse the repository at this point in the history
…19269)

Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com>
  • Loading branch information
secustor and HonkingGoose committed Jan 9, 2023
1 parent 817d2d8 commit 1ab049f
Show file tree
Hide file tree
Showing 36 changed files with 948 additions and 851 deletions.
65 changes: 65 additions & 0 deletions lib/modules/manager/terraform/base.ts
@@ -0,0 +1,65 @@
import is from '@sindresorhus/is';
import { regEx } from '../../../util/regex';
import { TerraformProviderDatasource } from '../../datasource/terraform-provider';
import type { PackageDependency } from '../types';
import type { ProviderLock } from './lockfile/types';
import { getLockedVersion, massageProviderLookupName } from './util';

export abstract class DependencyExtractor {
/**
* Get a list of signals which can be used to scan for potential processable content
* @return a list of content signals
*/
abstract getCheckList(): string[];

/**
* Extract dependencies from a HCL object
* @param hclRoot HCL parsing artifact.
* @param locks currently existing locks
*/
abstract extract(hclRoot: any, locks: ProviderLock[]): PackageDependency[];
}

export abstract class TerraformProviderExtractor extends DependencyExtractor {
sourceExtractionRegex = regEx(
/^(?:(?<hostname>(?:[a-zA-Z0-9-_]+\.+)+[a-zA-Z0-9-_]+)\/)?(?:(?<namespace>[^/]+)\/)?(?<type>[^/]+)/
);

protected analyzeTerraformProvider(
dep: PackageDependency,
locks: ProviderLock[],
depType: string
): PackageDependency {
dep.depType = depType;
dep.depName = dep.managerData?.moduleName;
dep.datasource = TerraformProviderDatasource.id;

if (is.nonEmptyString(dep.managerData?.source)) {
// TODO #7154
const source = this.sourceExtractionRegex.exec(dep.managerData!.source);
if (!source?.groups) {
dep.skipReason = 'unsupported-url';
return dep;
}

// buildin providers https://github.com/terraform-providers
if (source.groups.namespace === 'terraform-providers') {
dep.registryUrls = [`https://releases.hashicorp.com`];
} else if (source.groups.hostname) {
dep.registryUrls = [`https://${source.groups.hostname}`];
dep.packageName = `${source.groups.namespace}/${source.groups.type}`;
} else {
dep.packageName = dep.managerData?.source;
}
}
massageProviderLookupName(dep);

dep.lockedVersion = getLockedVersion(dep, locks);

if (!dep.currentValue) {
dep.skipReason = 'no-version';
}

return dep;
}
}
38 changes: 0 additions & 38 deletions lib/modules/manager/terraform/common.ts

This file was deleted.

30 changes: 20 additions & 10 deletions lib/modules/manager/terraform/extract.spec.ts
Expand Up @@ -401,8 +401,8 @@ describe('modules/manager/terraform/extract', () => {

it('extracts docker resources', async () => {
const res = await extractPackageFile(docker, 'docker.tf', {});
expect(res?.deps).toHaveLength(8);
expect(res?.deps.filter((dep) => dep.skipReason)).toHaveLength(5);
expect(res?.deps).toHaveLength(6);
expect(res?.deps.filter((dep) => dep.skipReason)).toHaveLength(3);
expect(res?.deps).toIncludeAllPartialMembers([
{
autoReplaceStringTemplate:
Expand All @@ -414,6 +414,7 @@ describe('modules/manager/terraform/extract', () => {
replaceString: 'nginx:1.7.8',
},
{
depType: 'docker_image',
skipReason: 'invalid-dependency-specification',
},
{
Expand All @@ -434,6 +435,7 @@ describe('modules/manager/terraform/extract', () => {
replaceString: 'nginx:1.7.8',
},
{
depType: 'docker_container',
skipReason: 'invalid-dependency-specification',
},
{
Expand All @@ -446,12 +448,6 @@ describe('modules/manager/terraform/extract', () => {
depType: 'docker_service',
replaceString: 'repo.mycompany.com:8080/foo-service:v1',
},
{
skipReason: 'invalid-dependency-specification',
},
{
skipReason: 'invalid-value',
},
]);
});

Expand Down Expand Up @@ -504,7 +500,10 @@ describe('modules/manager/terraform/extract', () => {
currentValue: '1.21.5',
depType: 'kubernetes_job',
},
{ skipReason: 'invalid-value' },
{
depType: 'kubernetes_job',
skipReason: 'invalid-dependency-specification',
},
{
depName: 'nginx',
currentValue: '1.21.6',
Expand Down Expand Up @@ -553,12 +552,23 @@ describe('modules/manager/terraform/extract', () => {
]);
});

it('returns null if only local deps', async () => {
it('returns dep with skipReason local', async () => {
const src = codeBlock`
module "relative" {
source = "../fe"
}
`;
expect(await extractPackageFile(src, '2.tf', {})).toMatchObject({
deps: [{ skipReason: 'local' }],
});
});

it('returns null with only not added resources', async () => {
const src = codeBlock`
resource "test_resource" "relative" {
source = "../fe"
}
`;
expect(await extractPackageFile(src, '2.tf', {})).toBeNull();
});

Expand Down
164 changes: 28 additions & 136 deletions lib/modules/manager/terraform/extract.ts
@@ -1,158 +1,50 @@
import is from '@sindresorhus/is';
import { logger } from '../../../logger';
import { newlineRegex, regEx } from '../../../util/regex';
import type { ExtractConfig, PackageDependency, PackageFile } from '../types';
import type { ProviderLock } from './lockfile/types';
import { extractLocks, findLockFile, readLockFile } from './lockfile/util';
import { analyseTerraformModule, extractTerraformModule } from './modules';
import {
analyzeTerraformProvider,
extractTerraformProvider,
} from './providers';
import {
analyzeTerraformRequiredProvider,
extractTerraformRequiredProviders,
} from './required-providers';
import {
analyseTerraformVersion,
extractTerraformRequiredVersion,
} from './required-version';
import {
analyseTerraformResource,
extractTerraformResource,
} from './resources';
import type { ExtractionResult, TerraformManagerData } from './types';
import type { ExtractConfig, PackageFile } from '../types';
import { resourceExtractors } from './extractors';
import * as hcl from './hcl';
import {
checkFileContainsDependency,
getTerraformDependencyType,
extractLocksForPackageFile,
} from './util';

const dependencyBlockExtractionRegex = regEx(
/^\s*(?<type>[a-z_]+)\s+("(?<packageName>[^"]+)"\s+)?("(?<terraformName>[^"]+)"\s+)?{\s*$/
);
const contentCheckList = [
'module "',
'provider "',
'"docker_',
'"kubernetes_',
'required_providers ',
' "helm_release" ',
' "docker_image" ',
'required_version',
'terraform_version', // part of tfe_workspace
];

export async function extractPackageFile(
content: string,
fileName: string,
config: ExtractConfig
): Promise<PackageFile | null> {
logger.trace({ content }, 'terraform.extractPackageFile()');
if (!checkFileContainsDependency(content, contentCheckList)) {

const passedExtractors = [];
for (const extractor of resourceExtractors) {
if (checkFileContainsDependency(content, extractor.getCheckList())) {
passedExtractors.push(extractor);
}
}

if (!passedExtractors.length) {
logger.trace(
{ fileName },
'preflight content check has not found any relevant content'
);
return null;
}
let deps: PackageDependency<TerraformManagerData>[] = [];
try {
const lines = content.split(newlineRegex);
for (let lineNumber = 0; lineNumber < lines.length; lineNumber += 1) {
const line = lines[lineNumber];
const terraformDependency = dependencyBlockExtractionRegex.exec(line);
if (terraformDependency?.groups) {
logger.trace(
`Matched ${terraformDependency.groups.type} on line ${lineNumber}`
);
const tfDepType = getTerraformDependencyType(
terraformDependency.groups.type
);
let result: ExtractionResult | null = null;
switch (tfDepType) {
case 'required_providers': {
result = extractTerraformRequiredProviders(lineNumber, lines);
break;
}
case 'provider': {
result = extractTerraformProvider(
lineNumber,
lines,
terraformDependency.groups.packageName
);
break;
}
case 'module': {
result = extractTerraformModule(
lineNumber,
lines,
terraformDependency.groups.packageName
);
break;
}
case 'resource': {
result = extractTerraformResource(lineNumber, lines);
break;
}
case 'terraform_version': {
result = extractTerraformRequiredVersion(lineNumber, lines);
break;
}
/* istanbul ignore next */
default:
logger.trace(
`Could not identify TerraformDependencyType ${terraformDependency.groups.type} on line ${lineNumber}.`
);
break;
}
if (result) {
lineNumber = result.lineNumber;
deps = deps.concat(result.dependencies);
result = null;
}
}
}
} catch (err) /* istanbul ignore next */ {
logger.warn({ err }, 'Error extracting terraform plugins');
}
logger.trace(
{ fileName },
`preflight content check passed for extractors: [${passedExtractors
.map((value) => value.constructor.name)
.toString()}]`
);

const locks: ProviderLock[] = [];
const lockFilePath = findLockFile(fileName);
if (lockFilePath) {
const lockFileContent = await readLockFile(lockFilePath);
if (lockFileContent) {
const extractedLocks = extractLocks(lockFileContent);
if (is.nonEmptyArray(extractedLocks)) {
locks.push(...extractedLocks);
}
}
}
const dependencies = [];
const hclMap = hcl.parseHCL(content);

deps.forEach((dep) => {
switch (dep.managerData?.terraformDependencyType) {
case 'required_providers':
analyzeTerraformRequiredProvider(dep, locks);
break;
case 'provider':
analyzeTerraformProvider(dep, locks);
break;
case 'module':
analyseTerraformModule(dep);
break;
case 'resource':
analyseTerraformResource(dep);
break;
case 'terraform_version':
analyseTerraformVersion(dep);
break;
/* istanbul ignore next */
default:
}
const locks = await extractLocksForPackageFile(fileName);

delete dep.managerData;
});
if (deps.some((dep) => dep.skipReason !== 'local')) {
return { deps };
for (const extractor of passedExtractors) {
const deps = extractor.extract(hclMap, locks);
dependencies.push(...deps);
}
return null;

dependencies.forEach((value) => delete value.managerData);
return { deps: dependencies };
}

0 comments on commit 1ab049f

Please sign in to comment.