From 1ab049f7f4fdc24a3aa4aa39c8dcd94b943a4395 Mon Sep 17 00:00:00 2001 From: Sebastian Poxhofer <secustor@users.noreply.github.com> Date: Mon, 9 Jan 2023 14:16:20 +0100 Subject: [PATCH] feat(terraform): use HCL parser and introduce class based extractors (#19269) Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> --- lib/modules/manager/terraform/base.ts | 65 +++++++ lib/modules/manager/terraform/common.ts | 38 ---- lib/modules/manager/terraform/extract.spec.ts | 30 ++-- lib/modules/manager/terraform/extract.ts | 164 +++--------------- .../manager/terraform/extract/kubernetes.ts | 75 -------- lib/modules/manager/terraform/extractors.ts | 18 ++ .../{ => extractors/others}/modules.spec.ts | 10 +- .../terraform/extractors/others/modules.ts | 118 +++++++++++++ .../terraform/extractors/others/providers.ts | 36 ++++ .../resources/generic-docker-image-ref.ts | 99 +++++++++++ .../resources/generic-docker-image.spec.ts | 10 ++ .../extractors/resources/helm-release.spec.ts | 10 ++ .../extractors/resources/helm-release.ts | 39 +++++ .../resources/terraform-workspace.ts | 35 ++++ .../resources/terraform-workspaces.spec.ts | 10 ++ .../terraform/extractors/resources/utils.ts | 85 +++++++++ .../terraform-block/required-provider.spec.ts | 15 ++ .../terraform-block/required-provider.ts | 57 ++++++ .../terraform-block/terraform-version.spec.ts | 10 ++ .../terraform-block/terraform-version.ts | 40 +++++ .../hcl/__fixtures__/lockedVersion.tf | 15 ++ .../terraform/hcl/__fixtures__/modules.tf | 24 +++ .../terraform/hcl/__fixtures__/resources.tf | 29 ++++ .../hcl/__fixtures__/resources.tf.json | 28 +++ .../manager/terraform/hcl/index.spec.ts | 111 ++++++++++++ lib/modules/manager/terraform/hcl/index.ts | 9 + lib/modules/manager/terraform/modules.ts | 100 ----------- lib/modules/manager/terraform/providers.ts | 106 ----------- .../manager/terraform/required-providers.ts | 83 --------- .../manager/terraform/required-version.ts | 52 ------ lib/modules/manager/terraform/resources.ts | 163 ----------------- lib/modules/manager/terraform/types.ts | 24 +-- lib/modules/manager/terraform/util.spec.ts | 33 ---- lib/modules/manager/terraform/util.ts | 52 ++---- package.json | 1 + yarn.lock | 5 + 36 files changed, 948 insertions(+), 851 deletions(-) create mode 100644 lib/modules/manager/terraform/base.ts delete mode 100644 lib/modules/manager/terraform/common.ts delete mode 100644 lib/modules/manager/terraform/extract/kubernetes.ts create mode 100644 lib/modules/manager/terraform/extractors.ts rename lib/modules/manager/terraform/{ => extractors/others}/modules.spec.ts (96%) create mode 100644 lib/modules/manager/terraform/extractors/others/modules.ts create mode 100644 lib/modules/manager/terraform/extractors/others/providers.ts create mode 100644 lib/modules/manager/terraform/extractors/resources/generic-docker-image-ref.ts create mode 100644 lib/modules/manager/terraform/extractors/resources/generic-docker-image.spec.ts create mode 100644 lib/modules/manager/terraform/extractors/resources/helm-release.spec.ts create mode 100644 lib/modules/manager/terraform/extractors/resources/helm-release.ts create mode 100644 lib/modules/manager/terraform/extractors/resources/terraform-workspace.ts create mode 100644 lib/modules/manager/terraform/extractors/resources/terraform-workspaces.spec.ts create mode 100644 lib/modules/manager/terraform/extractors/resources/utils.ts create mode 100644 lib/modules/manager/terraform/extractors/terraform-block/required-provider.spec.ts create mode 100644 lib/modules/manager/terraform/extractors/terraform-block/required-provider.ts create mode 100644 lib/modules/manager/terraform/extractors/terraform-block/terraform-version.spec.ts create mode 100644 lib/modules/manager/terraform/extractors/terraform-block/terraform-version.ts create mode 100644 lib/modules/manager/terraform/hcl/__fixtures__/lockedVersion.tf create mode 100644 lib/modules/manager/terraform/hcl/__fixtures__/modules.tf create mode 100644 lib/modules/manager/terraform/hcl/__fixtures__/resources.tf create mode 100644 lib/modules/manager/terraform/hcl/__fixtures__/resources.tf.json create mode 100644 lib/modules/manager/terraform/hcl/index.spec.ts create mode 100644 lib/modules/manager/terraform/hcl/index.ts delete mode 100644 lib/modules/manager/terraform/modules.ts delete mode 100644 lib/modules/manager/terraform/providers.ts delete mode 100644 lib/modules/manager/terraform/required-providers.ts delete mode 100644 lib/modules/manager/terraform/required-version.ts delete mode 100644 lib/modules/manager/terraform/resources.ts delete mode 100644 lib/modules/manager/terraform/util.spec.ts diff --git a/lib/modules/manager/terraform/base.ts b/lib/modules/manager/terraform/base.ts new file mode 100644 index 0000000000..52a5bea3ba --- /dev/null +++ b/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; + } +} diff --git a/lib/modules/manager/terraform/common.ts b/lib/modules/manager/terraform/common.ts deleted file mode 100644 index 03c3269151..0000000000 --- a/lib/modules/manager/terraform/common.ts +++ /dev/null @@ -1,38 +0,0 @@ -export type TerraformDependencyTypes = - | 'unknown' - | 'module' - | 'provider' - | 'required_providers' - | 'resource' - | 'terraform_version'; - -export const TerraformResourceTypes: Record<string, string[]> = { - unknown: ['unknown'], - generic_image_resource: [ - // Docker provider: https://registry.terraform.io/providers/kreuzwerker/docker - 'docker_container', - 'docker_service', - // Kubernetes provider: https://registry.terraform.io/providers/hashicorp/kubernetes - 'kubernetes_cron_job', - 'kubernetes_cron_job_v1', - 'kubernetes_daemon_set', - 'kubernetes_daemon_set_v1', - 'kubernetes_daemonset', - 'kubernetes_deployment', - 'kubernetes_deployment_v1', - 'kubernetes_job', - 'kubernetes_job_v1', - 'kubernetes_pod', - 'kubernetes_pod_v1', - 'kubernetes_replication_controller', - 'kubernetes_replication_controller_v1', - 'kubernetes_stateful_set', - 'kubernetes_stateful_set_v1', - ], - // https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs/resources/image - docker_image: ['docker_image'], - // https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release - helm_release: ['helm_release'], - // https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/resources/workspace - tfe_workspace: ['tfe_workspace'], -}; diff --git a/lib/modules/manager/terraform/extract.spec.ts b/lib/modules/manager/terraform/extract.spec.ts index db4463d4d5..7f355ffa21 100644 --- a/lib/modules/manager/terraform/extract.spec.ts +++ b/lib/modules/manager/terraform/extract.spec.ts @@ -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: @@ -414,6 +414,7 @@ describe('modules/manager/terraform/extract', () => { replaceString: 'nginx:1.7.8', }, { + depType: 'docker_image', skipReason: 'invalid-dependency-specification', }, { @@ -434,6 +435,7 @@ describe('modules/manager/terraform/extract', () => { replaceString: 'nginx:1.7.8', }, { + depType: 'docker_container', skipReason: 'invalid-dependency-specification', }, { @@ -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', - }, ]); }); @@ -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', @@ -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(); }); diff --git a/lib/modules/manager/terraform/extract.ts b/lib/modules/manager/terraform/extract.ts index 296ca3b296..d0676c8151 100644 --- a/lib/modules/manager/terraform/extract.ts +++ b/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 }; } diff --git a/lib/modules/manager/terraform/extract/kubernetes.ts b/lib/modules/manager/terraform/extract/kubernetes.ts deleted file mode 100644 index 55d09f5c35..0000000000 --- a/lib/modules/manager/terraform/extract/kubernetes.ts +++ /dev/null @@ -1,75 +0,0 @@ -import is from '@sindresorhus/is'; -import { logger } from '../../../../logger'; -import { regEx } from '../../../../util/regex'; -import type { PackageDependency } from '../../types'; -import type { ExtractionResult, ResourceManagerData } from '../types'; -import { keyValueExtractionRegex } from '../util'; - -export function extractTerraformKubernetesResource( - startingLine: number, - lines: string[], - resourceType: string -): ExtractionResult { - let lineNumber = startingLine; - const deps: PackageDependency<ResourceManagerData>[] = []; - - /** - * Iterates over all lines of the resource to extract the relevant key value pairs, - * e.g. the chart name for helm charts or the terraform_version for tfe_workspace - */ - let braceCounter = 0; - let inContainer = -1; - do { - // istanbul ignore if - if (lineNumber > lines.length - 1) { - logger.debug(`Malformed Terraform file detected.`); - } - - const line = lines[lineNumber]; - - // istanbul ignore else - if (is.string(line)) { - // `{` will be counted with +1 and `}` with -1. Therefore if we reach braceCounter == 0. We have found the end of the terraform block - const openBrackets = (line.match(regEx(/\{/g)) ?? []).length; - const closedBrackets = (line.match(regEx(/\}/g)) ?? []).length; - braceCounter = braceCounter + openBrackets - closedBrackets; - - if (line.match(regEx(/^\s*(?:init_)?container(?:\s*\{|$)/s))) { - inContainer = braceCounter; - } else if (braceCounter < inContainer) { - inContainer = -1; - } - - const managerData: ResourceManagerData = { - terraformDependencyType: 'resource', - resourceType, - }; - const dep: PackageDependency<ResourceManagerData> = { - managerData, - }; - - const kvMatch = keyValueExtractionRegex.exec(line); - if (kvMatch?.groups && inContainer > 0) { - switch (kvMatch.groups.key) { - case 'image': - managerData[kvMatch.groups.key] = kvMatch.groups.value; - managerData.sourceLine = lineNumber; - deps.push(dep); - break; - default: - /* istanbul ignore next */ - break; - } - } - } else { - // stop - something went wrong - braceCounter = 0; - inContainer = -1; - } - lineNumber += 1; - } while (braceCounter !== 0); - - // remove last lineNumber addition to not skip a line after the last bracket - lineNumber -= 1; - return { lineNumber, dependencies: deps }; -} diff --git a/lib/modules/manager/terraform/extractors.ts b/lib/modules/manager/terraform/extractors.ts new file mode 100644 index 0000000000..1a16410a47 --- /dev/null +++ b/lib/modules/manager/terraform/extractors.ts @@ -0,0 +1,18 @@ +import type { DependencyExtractor } from './base'; +import { ModuleExtractor } from './extractors/others/modules'; +import { ProvidersExtractor } from './extractors/others/providers'; +import { GenericDockerImageRef } from './extractors/resources/generic-docker-image-ref'; +import { HelmReleaseExtractor } from './extractors/resources/helm-release'; +import { TerraformWorkspaceExtractor } from './extractors/resources/terraform-workspace'; +import { RequiredProviderExtractor } from './extractors/terraform-block/required-provider'; +import { TerraformVersionExtractor } from './extractors/terraform-block/terraform-version'; + +export const resourceExtractors: DependencyExtractor[] = [ + new HelmReleaseExtractor(), + new GenericDockerImageRef(), + new TerraformWorkspaceExtractor(), + new RequiredProviderExtractor(), + new TerraformVersionExtractor(), + new ProvidersExtractor(), + new ModuleExtractor(), +]; diff --git a/lib/modules/manager/terraform/modules.spec.ts b/lib/modules/manager/terraform/extractors/others/modules.spec.ts similarity index 96% rename from lib/modules/manager/terraform/modules.spec.ts rename to lib/modules/manager/terraform/extractors/others/modules.spec.ts index 6e7c6bb265..88c204c1b4 100644 --- a/lib/modules/manager/terraform/modules.spec.ts +++ b/lib/modules/manager/terraform/extractors/others/modules.spec.ts @@ -1,11 +1,19 @@ import { + ModuleExtractor, azureDevOpsSshRefMatchRegex, bitbucketRefMatchRegex, gitTagsRefMatchRegex, githubRefMatchRegex, } from './modules'; -describe('modules/manager/terraform/modules', () => { +describe('modules/manager/terraform/extractors/others/modules', () => { + const extractor = new ModuleExtractor(); + + it('return empty array if no module is found', () => { + const res = extractor.extract({}); + expect(res).toBeArrayOfSize(0); + }); + describe('githubRefMatchRegex', () => { it('should split project and tag from source', () => { const groups = githubRefMatchRegex.exec( diff --git a/lib/modules/manager/terraform/extractors/others/modules.ts b/lib/modules/manager/terraform/extractors/others/modules.ts new file mode 100644 index 0000000000..e12fe3253b --- /dev/null +++ b/lib/modules/manager/terraform/extractors/others/modules.ts @@ -0,0 +1,118 @@ +import is from '@sindresorhus/is'; +import { logger } from '../../../../../logger'; +import { regEx } from '../../../../../util/regex'; +import { BitBucketTagsDatasource } from '../../../../datasource/bitbucket-tags'; +import { GitTagsDatasource } from '../../../../datasource/git-tags'; +import { GithubTagsDatasource } from '../../../../datasource/github-tags'; +import { TerraformModuleDatasource } from '../../../../datasource/terraform-module'; +import type { PackageDependency } from '../../../types'; +import { DependencyExtractor } from '../../base'; + +export const githubRefMatchRegex = regEx( + /github\.com([/:])(?<project>[^/]+\/[a-z0-9-_.]+).*\?ref=(?<tag>.*)$/i +); +export const bitbucketRefMatchRegex = regEx( + /(?:git::)?(?<url>(?:http|https|ssh)?(?::\/\/)?(?:.*@)?(?<path>bitbucket\.org\/(?<workspace>.*)\/(?<project>.*).git\/?(?<subfolder>.*)))\?ref=(?<tag>.*)$/ +); +export const gitTagsRefMatchRegex = regEx( + /(?:git::)?(?<url>(?:(?:http|https|ssh):\/\/)?(?:.*@)?(?<path>.*\/(?<project>.*\/.*)))\?ref=(?<tag>.*)$/ +); +export const azureDevOpsSshRefMatchRegex = regEx( + /(?:git::)?(?<url>git@ssh\.dev\.azure\.com:v3\/(?<organization>[^/]*)\/(?<project>[^/]*)\/(?<repository>[^/]*))(?<modulepath>.*)?\?ref=(?<tag>.*)$/ +); +const hostnameMatchRegex = regEx(/^(?<hostname>([\w|\d]+\.)+[\w|\d]+)/); + +export class ModuleExtractor extends DependencyExtractor { + getCheckList(): string[] { + return ['module']; + } + + extract(hclRoot: any): PackageDependency[] { + const modules = hclRoot.module; + if (is.nullOrUndefined(modules)) { + return []; + } + + const dependencies = []; + for (const moduleKeys of Object.keys(modules)) { + const module = modules[moduleKeys]; + for (const moduleElement of module) { + const dep = { + currentValue: moduleElement.version, + managerData: { + source: moduleElement.source, + }, + }; + const massagedDep = this.analyseTerraformModule(dep); + dependencies.push(massagedDep); + } + } + return dependencies; + } + + private analyseTerraformModule(dep: PackageDependency): PackageDependency { + // TODO #7154 + const source = dep.managerData!.source as string; + const githubRefMatch = githubRefMatchRegex.exec(source); + const bitbucketRefMatch = bitbucketRefMatchRegex.exec(source); + const gitTagsRefMatch = gitTagsRefMatchRegex.exec(source); + const azureDevOpsSshRefMatch = azureDevOpsSshRefMatchRegex.exec(source); + + if (githubRefMatch?.groups) { + dep.packageName = githubRefMatch.groups.project.replace( + regEx(/\.git$/), + '' + ); + dep.depType = 'module'; + dep.depName = 'github.com/' + dep.packageName; + dep.currentValue = githubRefMatch.groups.tag; + dep.datasource = GithubTagsDatasource.id; + } else if (bitbucketRefMatch?.groups) { + dep.depType = 'module'; + dep.depName = + bitbucketRefMatch.groups.workspace + + '/' + + bitbucketRefMatch.groups.project; + dep.packageName = dep.depName; + dep.currentValue = bitbucketRefMatch.groups.tag; + dep.datasource = BitBucketTagsDatasource.id; + } else if (azureDevOpsSshRefMatch?.groups) { + dep.depType = 'module'; + dep.depName = `${azureDevOpsSshRefMatch.groups.organization}/${azureDevOpsSshRefMatch.groups.project}/${azureDevOpsSshRefMatch.groups.repository}${azureDevOpsSshRefMatch.groups.modulepath}`; + dep.packageName = azureDevOpsSshRefMatch.groups.url; + dep.currentValue = azureDevOpsSshRefMatch.groups.tag; + dep.datasource = GitTagsDatasource.id; + } else if (gitTagsRefMatch?.groups) { + dep.depType = 'module'; + if (gitTagsRefMatch.groups.path.includes('//')) { + logger.debug('Terraform module contains subdirectory'); + dep.depName = gitTagsRefMatch.groups.path.split('//')[0]; + const tempLookupName = gitTagsRefMatch.groups.url.split('//'); + dep.packageName = tempLookupName[0] + '//' + tempLookupName[1]; + } else { + dep.depName = gitTagsRefMatch.groups.path.replace('.git', ''); + dep.packageName = gitTagsRefMatch.groups.url; + } + dep.currentValue = gitTagsRefMatch.groups.tag; + dep.datasource = GitTagsDatasource.id; + } else if (source) { + const moduleParts = source.split('//')[0].split('/'); + if (moduleParts[0] === '..') { + dep.skipReason = 'local'; + } else if (moduleParts.length >= 3) { + const hostnameMatch = hostnameMatchRegex.exec(source); + if (hostnameMatch?.groups) { + dep.registryUrls = [`https://${hostnameMatch.groups.hostname}`]; + } + dep.depType = 'module'; + dep.depName = moduleParts.join('/'); + dep.datasource = TerraformModuleDatasource.id; + } + } else { + logger.debug({ dep }, 'terraform dep has no source'); + dep.skipReason = 'no-source'; + } + + return dep; + } +} diff --git a/lib/modules/manager/terraform/extractors/others/providers.ts b/lib/modules/manager/terraform/extractors/others/providers.ts new file mode 100644 index 0000000000..c2cbd5702f --- /dev/null +++ b/lib/modules/manager/terraform/extractors/others/providers.ts @@ -0,0 +1,36 @@ +import is from '@sindresorhus/is'; +import type { PackageDependency } from '../../../types'; +import { TerraformProviderExtractor } from '../../base'; +import type { ProviderLock } from '../../lockfile/types'; + +export class ProvidersExtractor extends TerraformProviderExtractor { + getCheckList(): string[] { + return ['provider']; + } + + extract(hclRoot: any, locks: ProviderLock[]): PackageDependency[] { + const providerTypes = hclRoot.provider; + if (is.nullOrUndefined(providerTypes)) { + return []; + } + + const dependencies = []; + for (const providerTypeName of Object.keys(providerTypes)) { + for (const providerTypeElement of providerTypes[providerTypeName]) { + const dep = this.analyzeTerraformProvider( + { + currentValue: providerTypeElement.version, + managerData: { + moduleName: providerTypeName, + source: providerTypeElement.source, + }, + }, + locks, + 'provider' + ); + dependencies.push(dep); + } + } + return dependencies; + } +} diff --git a/lib/modules/manager/terraform/extractors/resources/generic-docker-image-ref.ts b/lib/modules/manager/terraform/extractors/resources/generic-docker-image-ref.ts new file mode 100644 index 0000000000..43d53b21e3 --- /dev/null +++ b/lib/modules/manager/terraform/extractors/resources/generic-docker-image-ref.ts @@ -0,0 +1,99 @@ +import is from '@sindresorhus/is'; +import { getDep } from '../../../dockerfile/extract'; +import type { PackageDependency } from '../../../types'; +import { DependencyExtractor } from '../../base'; +import { generic_image_resource } from './utils'; + +export class GenericDockerImageRef extends DependencyExtractor { + getCheckList(): string[] { + return generic_image_resource.map((value) => `"${value.type}"`); + } + + extract(hclMap: any): PackageDependency[] { + const resourceTypMap = hclMap.resource; + if (is.nullOrUndefined(resourceTypMap)) { + return []; + } + + const dependencies = []; + + for (const image_resource_def of generic_image_resource) { + const { type, path } = image_resource_def; + const resourceInstancesMap = resourceTypMap[type]; + // is there a resource with current looked at type ( `image_resource_def` ) + if (is.nullOrUndefined(resourceInstancesMap)) { + continue; + } + + // loop over instances of a resource type + for (const instanceName of Object.keys(resourceInstancesMap)) { + const instanceList = resourceInstancesMap[instanceName]; + for (const instance of instanceList) { + dependencies.push( + ...this.walkPath({ depType: type }, instance, path) + ); + } + } + } + return dependencies; + } + + /** + * Recursively follow the path to find elements on the path. + * If a path element is '*' the parentElement will be interpreted as a list + * and each element will be followed + * @param abstractDep dependency which will used as basis for adding attributes + * @param parentElement element from which the next element will be extracted + * @param leftPath path elements left to walk down + */ + private walkPath( + abstractDep: PackageDependency, + parentElement: any, + leftPath: string[] + ): PackageDependency[] { + const dependencies: PackageDependency[] = []; + // if there are no path elements left, we have reached the end of the path + if (leftPath.length === 0) { + // istanbul ignore if + if (is.nullOrUndefined(parentElement)) { + return [ + { + ...abstractDep, + skipReason: 'invalid-dependency-specification', + }, + ]; + } + const test = getDep(parentElement); + const dep: PackageDependency = { + ...abstractDep, + ...test, + }; + return [dep]; + } + + // is this a list iterator + const pathElement = leftPath[0]; + + // get sub element + const element = parentElement[pathElement]; + if (is.nullOrUndefined(element)) { + return leftPath.length === 1 // if this is the last element assume a false defined dependency + ? [ + { + ...abstractDep, + skipReason: 'invalid-dependency-specification', + }, + ] + : []; + } + if (is.array(element)) { + for (const arrayElement of element) { + dependencies.push( + ...this.walkPath(abstractDep, arrayElement, leftPath.slice(1)) + ); + } + return dependencies; + } + return this.walkPath(abstractDep, element, leftPath.slice(1)); + } +} diff --git a/lib/modules/manager/terraform/extractors/resources/generic-docker-image.spec.ts b/lib/modules/manager/terraform/extractors/resources/generic-docker-image.spec.ts new file mode 100644 index 0000000000..b6f4314926 --- /dev/null +++ b/lib/modules/manager/terraform/extractors/resources/generic-docker-image.spec.ts @@ -0,0 +1,10 @@ +import { GenericDockerImageRef } from './generic-docker-image-ref'; + +describe('modules/manager/terraform/extractors/resources/generic-docker-image', () => { + const extractor = new GenericDockerImageRef(); + + it('return empty array if no resource is found', () => { + const res = extractor.extract({}); + expect(res).toBeArrayOfSize(0); + }); +}); diff --git a/lib/modules/manager/terraform/extractors/resources/helm-release.spec.ts b/lib/modules/manager/terraform/extractors/resources/helm-release.spec.ts new file mode 100644 index 0000000000..84f2451c80 --- /dev/null +++ b/lib/modules/manager/terraform/extractors/resources/helm-release.spec.ts @@ -0,0 +1,10 @@ +import { HelmReleaseExtractor } from './helm-release'; + +describe('modules/manager/terraform/extractors/resources/helm-release', () => { + const extractor = new HelmReleaseExtractor(); + + it('return empty array if no resource is found', () => { + const res = extractor.extract({}); + expect(res).toBeArrayOfSize(0); + }); +}); diff --git a/lib/modules/manager/terraform/extractors/resources/helm-release.ts b/lib/modules/manager/terraform/extractors/resources/helm-release.ts new file mode 100644 index 0000000000..8742603b3f --- /dev/null +++ b/lib/modules/manager/terraform/extractors/resources/helm-release.ts @@ -0,0 +1,39 @@ +import is from '@sindresorhus/is'; +import { HelmDatasource } from '../../../../datasource/helm'; +import type { PackageDependency } from '../../../types'; +import { DependencyExtractor } from '../../base'; +import { checkIfStringIsPath } from '../../util'; + +export class HelmReleaseExtractor extends DependencyExtractor { + getCheckList(): string[] { + return [`"helm_release"`]; + } + + override extract(hclMap: any): PackageDependency[] { + const dependencies = []; + + const helmReleases = hclMap?.resource?.helm_release; + if (is.nullOrUndefined(helmReleases)) { + return []; + } + for (const helmReleaseName of Object.keys(helmReleases)) { + for (const helmRelease of helmReleases[helmReleaseName]) { + const dep: PackageDependency = { + currentValue: helmRelease.version, + depType: 'helm_release', + registryUrls: [helmRelease.repository], + depName: helmRelease.chart, + datasource: HelmDatasource.id, + }; + if (!helmRelease.chart) { + dep.skipReason = 'invalid-name'; + } else if (checkIfStringIsPath(helmRelease.chart)) { + dep.skipReason = 'local-chart'; + } + dependencies.push(dep); + } + } + + return dependencies; + } +} diff --git a/lib/modules/manager/terraform/extractors/resources/terraform-workspace.ts b/lib/modules/manager/terraform/extractors/resources/terraform-workspace.ts new file mode 100644 index 0000000000..906678ceb3 --- /dev/null +++ b/lib/modules/manager/terraform/extractors/resources/terraform-workspace.ts @@ -0,0 +1,35 @@ +import is from '@sindresorhus/is'; +import type { PackageDependency } from '../../../types'; +import { TerraformVersionExtractor } from '../terraform-block/terraform-version'; + +export class TerraformWorkspaceExtractor extends TerraformVersionExtractor { + override getCheckList(): string[] { + return [`"tfe_workspace"`]; + } + + override extract(hclMap: any): PackageDependency[] { + const dependencies = []; + + const workspaces = hclMap?.resource?.tfe_workspace; + if (is.nullOrUndefined(workspaces)) { + return []; + } + + for (const workspaceName of Object.keys(workspaces)) { + for (const workspace of workspaces[workspaceName]) { + const dep: PackageDependency = this.analyseTerraformVersion({ + currentValue: workspace.terraform_version, + }); + + if (is.nullOrUndefined(workspace.terraform_version)) { + dep.skipReason = 'no-version'; + } + dependencies.push({ + ...dep, + depType: 'tfe_workspace', + }); + } + } + return dependencies; + } +} diff --git a/lib/modules/manager/terraform/extractors/resources/terraform-workspaces.spec.ts b/lib/modules/manager/terraform/extractors/resources/terraform-workspaces.spec.ts new file mode 100644 index 0000000000..ad1fa22d33 --- /dev/null +++ b/lib/modules/manager/terraform/extractors/resources/terraform-workspaces.spec.ts @@ -0,0 +1,10 @@ +import { TerraformWorkspaceExtractor } from './terraform-workspace'; + +describe('modules/manager/terraform/extractors/resources/terraform-workspaces', () => { + const extractor = new TerraformWorkspaceExtractor(); + + it('return empty array if no resource is found', () => { + const res = extractor.extract({}); + expect(res).toBeArrayOfSize(0); + }); +}); diff --git a/lib/modules/manager/terraform/extractors/resources/utils.ts b/lib/modules/manager/terraform/extractors/resources/utils.ts new file mode 100644 index 0000000000..144125d964 --- /dev/null +++ b/lib/modules/manager/terraform/extractors/resources/utils.ts @@ -0,0 +1,85 @@ +import type { GenericImageResourceDef } from '../../types'; + +const KubernetesSpecContainer = ['spec', 'container', 'image']; +const KubernetesSpecInitContainer = ['spec', 'init_container', 'image']; +const KubernetesSpecTemplate = [ + 'spec', + 'template', + 'spec', + 'container', + 'image', +]; +const KubernetesSpecTemplateInit = [ + 'spec', + 'template', + 'spec', + 'init_container', + 'image', +]; +const KubernetesJobTemplate = [ + 'spec', + 'job_template', + 'spec', + 'template', + 'spec', + 'container', + 'image', +]; +const KubernetesJobTemplateInit = [ + 'spec', + 'job_template', + 'spec', + 'template', + 'spec', + 'init_container', + 'image', +]; + +export const generic_image_resource: GenericImageResourceDef[] = [ + // Docker provider: https://registry.terraform.io/providers/kreuzwerker/docker + { type: 'docker_image', path: ['name'] }, + { type: 'docker_container', path: ['image'] }, + { type: 'docker_service', path: ['task_spec', 'container_spec', 'image'] }, + // Kubernetes provider: https://registry.terraform.io/providers/hashicorp/kubernetes + { type: 'kubernetes_pod', path: KubernetesSpecContainer }, + { type: 'kubernetes_pod', path: KubernetesSpecInitContainer }, + { type: 'kubernetes_pod_v1', path: KubernetesSpecContainer }, + { type: 'kubernetes_pod_v1', path: KubernetesSpecInitContainer }, + { type: 'kubernetes_cron_job', path: KubernetesJobTemplate }, + { type: 'kubernetes_cron_job', path: KubernetesJobTemplateInit }, + { type: 'kubernetes_cron_job_v1', path: KubernetesJobTemplate }, + { type: 'kubernetes_cron_job_v1', path: KubernetesJobTemplateInit }, + { type: 'kubernetes_daemonset', path: KubernetesSpecTemplate }, + { type: 'kubernetes_daemonset', path: KubernetesSpecTemplateInit }, + { type: 'kubernetes_daemon_set_v1', path: KubernetesSpecTemplate }, + { type: 'kubernetes_daemon_set_v1', path: KubernetesSpecTemplateInit }, + { type: 'kubernetes_deployment', path: KubernetesSpecTemplate }, + { type: 'kubernetes_deployment', path: KubernetesSpecTemplateInit }, + { type: 'kubernetes_deployment_v1', path: KubernetesSpecTemplate }, + { type: 'kubernetes_deployment_v1', path: KubernetesSpecTemplateInit }, + { type: 'kubernetes_job', path: KubernetesSpecTemplate }, + { type: 'kubernetes_job', path: KubernetesSpecTemplateInit }, + { type: 'kubernetes_job_v1', path: KubernetesSpecTemplate }, + { type: 'kubernetes_job_v1', path: KubernetesSpecTemplateInit }, + { type: 'kubernetes_cron_job', path: KubernetesSpecInitContainer }, + { type: 'kubernetes_cron_job', path: KubernetesSpecInitContainer }, + { type: 'kubernetes_cron_job_v1', path: KubernetesSpecInitContainer }, + { type: 'kubernetes_cron_job_v1', path: KubernetesSpecInitContainer }, + { type: 'kubernetes_replication_controller', path: KubernetesSpecTemplate }, + { + type: 'kubernetes_replication_controller', + path: KubernetesSpecTemplateInit, + }, + { + type: 'kubernetes_replication_controller_v1', + path: KubernetesSpecTemplate, + }, + { + type: 'kubernetes_replication_controller_v1', + path: KubernetesSpecTemplateInit, + }, + { type: 'kubernetes_stateful_set', path: KubernetesSpecTemplate }, + { type: 'kubernetes_stateful_set', path: KubernetesSpecTemplateInit }, + { type: 'kubernetes_stateful_set_v1', path: KubernetesSpecTemplate }, + { type: 'kubernetes_stateful_set_v1', path: KubernetesSpecTemplateInit }, +]; diff --git a/lib/modules/manager/terraform/extractors/terraform-block/required-provider.spec.ts b/lib/modules/manager/terraform/extractors/terraform-block/required-provider.spec.ts new file mode 100644 index 0000000000..6cb9029858 --- /dev/null +++ b/lib/modules/manager/terraform/extractors/terraform-block/required-provider.spec.ts @@ -0,0 +1,15 @@ +import { RequiredProviderExtractor } from './required-provider'; + +describe('modules/manager/terraform/extractors/terraform-block/required-provider', () => { + const extractor = new RequiredProviderExtractor(); + + it('return empty array if no terraform block is found', () => { + const res = extractor.extract({}, []); + expect(res).toBeArrayOfSize(0); + }); + + it('return empty array if no required_providers block is found', () => { + const res = extractor.extract({ terraform: [{}] }, []); + expect(res).toBeArrayOfSize(0); + }); +}); diff --git a/lib/modules/manager/terraform/extractors/terraform-block/required-provider.ts b/lib/modules/manager/terraform/extractors/terraform-block/required-provider.ts new file mode 100644 index 0000000000..6d198e04b2 --- /dev/null +++ b/lib/modules/manager/terraform/extractors/terraform-block/required-provider.ts @@ -0,0 +1,57 @@ +import is from '@sindresorhus/is'; +import type { PackageDependency } from '../../../types'; +import { TerraformProviderExtractor } from '../../base'; +import type { ProviderLock } from '../../lockfile/types'; + +export class RequiredProviderExtractor extends TerraformProviderExtractor { + getCheckList(): string[] { + return ['required_providers']; + } + + extract(hclRoot: any, locks: ProviderLock[]): PackageDependency[] { + const terraformBlocks = hclRoot?.terraform; + if (is.nullOrUndefined(terraformBlocks)) { + return []; + } + + const dependencies: PackageDependency[] = []; + for (const terraformBlock of terraformBlocks) { + const requiredProviders = terraformBlock.required_providers; + if (is.nullOrUndefined(requiredProviders)) { + continue; + } + + for (const requiredProvidersMap of requiredProviders) { + for (const requiredProviderName of Object.keys(requiredProvidersMap)) { + const value = requiredProvidersMap[requiredProviderName]; + + // name = version declaration method + let dep: PackageDependency; + if (typeof value === 'string') { + dep = { + currentValue: value, + managerData: { + moduleName: requiredProviderName, + }, + }; + } + // block declaration aws = { source = 'aws', version = '2.0.0' } + dep ??= { + currentValue: value['version'], + managerData: { + moduleName: requiredProviderName, + source: value['source'], + }, + }; + const massagedDep = this.analyzeTerraformProvider( + dep, + locks, + 'required_provider' + ); + dependencies.push(massagedDep); + } + } + } + return dependencies; + } +} diff --git a/lib/modules/manager/terraform/extractors/terraform-block/terraform-version.spec.ts b/lib/modules/manager/terraform/extractors/terraform-block/terraform-version.spec.ts new file mode 100644 index 0000000000..7dd86a0f5d --- /dev/null +++ b/lib/modules/manager/terraform/extractors/terraform-block/terraform-version.spec.ts @@ -0,0 +1,10 @@ +import { TerraformVersionExtractor } from './terraform-version'; + +describe('modules/manager/terraform/extractors/terraform-block/terraform-version', () => { + const extractor = new TerraformVersionExtractor(); + + it('return empty array if no terraform block is found', () => { + const res = extractor.extract({}); + expect(res).toBeArrayOfSize(0); + }); +}); diff --git a/lib/modules/manager/terraform/extractors/terraform-block/terraform-version.ts b/lib/modules/manager/terraform/extractors/terraform-block/terraform-version.ts new file mode 100644 index 0000000000..c8a090bc02 --- /dev/null +++ b/lib/modules/manager/terraform/extractors/terraform-block/terraform-version.ts @@ -0,0 +1,40 @@ +import is from '@sindresorhus/is'; +import { GithubReleasesDatasource } from '../../../../datasource/github-releases'; +import type { PackageDependency } from '../../../types'; +import { DependencyExtractor } from '../../base'; + +export class TerraformVersionExtractor extends DependencyExtractor { + getCheckList(): string[] { + return ['required_version']; + } + + extract(hclRoot: any): PackageDependency[] { + const terraformBlocks = hclRoot?.terraform; + if (is.nullOrUndefined(terraformBlocks)) { + return []; + } + + const dependencies = []; + for (const terraformBlock of terraformBlocks) { + const requiredVersion = terraformBlock.required_version; + if (is.nullOrUndefined(requiredVersion)) { + continue; + } + + dependencies.push( + this.analyseTerraformVersion({ + currentValue: requiredVersion, + }) + ); + } + return dependencies; + } + + protected analyseTerraformVersion(dep: PackageDependency): PackageDependency { + dep.depType = 'required_version'; + dep.datasource = GithubReleasesDatasource.id; + dep.depName = 'hashicorp/terraform'; + dep.extractVersion = 'v(?<version>.*)$'; + return dep; + } +} diff --git a/lib/modules/manager/terraform/hcl/__fixtures__/lockedVersion.tf b/lib/modules/manager/terraform/hcl/__fixtures__/lockedVersion.tf new file mode 100644 index 0000000000..fc354ff15d --- /dev/null +++ b/lib/modules/manager/terraform/hcl/__fixtures__/lockedVersion.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + aws = { + source = "aws" + version = "~> 3.0" + } + azurerm = { + version = "~> 2.50.0" + } + kubernetes = { + source = "terraform.example.com/example/kubernetes" + version = ">= 1.0" + } + } +} diff --git a/lib/modules/manager/terraform/hcl/__fixtures__/modules.tf b/lib/modules/manager/terraform/hcl/__fixtures__/modules.tf new file mode 100644 index 0000000000..6c8fd665c3 --- /dev/null +++ b/lib/modules/manager/terraform/hcl/__fixtures__/modules.tf @@ -0,0 +1,24 @@ +module "foo" { + source = "github.com/hashicorp/example?ref=v1.0.0" +} + +module "bar" { + source = "github.com/hashicorp/example?ref=next" +} + +module "repo-with-non-semver-ref" { + source = "github.com/githubuser/myrepo//terraform/modules/moduleone?ref=tfmodule_one-v0.0.9" +} + +module "repo-with-dot" { + source = "github.com/hashicorp/example.2.3?ref=v1.0.0" +} + +module "repo-with-dot-and-git-suffix" { + source = "github.com/hashicorp/example.2.3.git?ref=v1.0.0" +} + +module "consul" { + source = "hashicorp/consul/aws" + version = "0.1.0" +} diff --git a/lib/modules/manager/terraform/hcl/__fixtures__/resources.tf b/lib/modules/manager/terraform/hcl/__fixtures__/resources.tf new file mode 100644 index 0000000000..b0dbb41ae6 --- /dev/null +++ b/lib/modules/manager/terraform/hcl/__fixtures__/resources.tf @@ -0,0 +1,29 @@ +# 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" + } + } +} diff --git a/lib/modules/manager/terraform/hcl/__fixtures__/resources.tf.json b/lib/modules/manager/terraform/hcl/__fixtures__/resources.tf.json new file mode 100644 index 0000000000..1e4549dbd1 --- /dev/null +++ b/lib/modules/manager/terraform/hcl/__fixtures__/resources.tf.json @@ -0,0 +1,28 @@ +{ + "resource": { + "aws_instance": { + "example": { + "provisioner": [ + { + "local-exec": { + "command": "echo 'Hello World' >example.txt" + } + }, + { + "file": { + "source": "example.txt", + "destination": "/tmp/example.txt" + } + }, + { + "remote-exec": { + "inline": [ + "sudo install-something -f /tmp/example.txt" + ] + } + } + ] + } + } + } +} diff --git a/lib/modules/manager/terraform/hcl/index.spec.ts b/lib/modules/manager/terraform/hcl/index.spec.ts new file mode 100644 index 0000000000..aada37398a --- /dev/null +++ b/lib/modules/manager/terraform/hcl/index.spec.ts @@ -0,0 +1,111 @@ +import { Fixtures } from '../../../../../test/fixtures'; +import { parseHCL, parseJSON } from './index'; + +const modulesTF = Fixtures.get('modules.tf'); +const resourcesTF = Fixtures.get('resources.tf'); +const resourcesTFJSON = Fixtures.get('resources.tf.json'); +const lockedVersion = Fixtures.get('lockedVersion.tf'); + +describe('modules/manager/terraform/hcl/index', () => { + describe('parseHCL()', () => { + it('should return flat modules', async () => { + const res = await parseHCL(modulesTF); + expect(Object.keys(res.module)).toBeArrayOfSize(6); + expect(res).toMatchObject({ + module: { + bar: [ + { + source: 'github.com/hashicorp/example?ref=next', + }, + ], + consul: [ + { + source: 'hashicorp/consul/aws', + version: '0.1.0', + }, + ], + foo: [ + { + source: 'github.com/hashicorp/example?ref=v1.0.0', + }, + ], + 'repo-with-dot': [ + { + source: 'github.com/hashicorp/example.2.3?ref=v1.0.0', + }, + ], + 'repo-with-dot-and-git-suffix': [ + { + source: 'github.com/hashicorp/example.2.3.git?ref=v1.0.0', + }, + ], + 'repo-with-non-semver-ref': [ + { + source: + 'github.com/githubuser/myrepo//terraform/modules/moduleone?ref=tfmodule_one-v0.0.9', + }, + ], + }, + }); + }); + + it('should return nested terraform block', async () => { + const res = await parseHCL(lockedVersion); + expect(res).toMatchObject({ + terraform: [ + { + required_providers: [ + { + aws: {}, + azurerm: {}, + kubernetes: {}, + }, + ], + }, + ], + }); + }); + + it('should return resource blocks', async () => { + const res = await parseHCL(resourcesTF); + expect(res).toMatchObject({ + resource: { + docker_container: { + foo: {}, + invalid: {}, + }, + docker_service: { + foo: [ + { + name: 'foo-service', + task_spec: [ + { + container_spec: {}, + }, + ], + endpoint_spec: [ + { + ports: {}, + }, + ], + }, + ], + }, + }, + }); + }); + }); + + describe('parseJSON', () => { + it('should parse json', async () => { + const res = await parseJSON(resourcesTFJSON); + expect(res).toMatchObject({ + resource: { + aws_instance: { + example: {}, + }, + }, + }); + }); + }); +}); diff --git a/lib/modules/manager/terraform/hcl/index.ts b/lib/modules/manager/terraform/hcl/index.ts new file mode 100644 index 0000000000..9598e001c6 --- /dev/null +++ b/lib/modules/manager/terraform/hcl/index.ts @@ -0,0 +1,9 @@ +import * as hcl_parser from 'hcl2-parser'; + +export function parseHCL(content: string): any { + return hcl_parser.parseToObject(content)[0]; +} + +export function parseJSON(content: string): any { + return JSON.parse(content); +} diff --git a/lib/modules/manager/terraform/modules.ts b/lib/modules/manager/terraform/modules.ts deleted file mode 100644 index bb151f687d..0000000000 --- a/lib/modules/manager/terraform/modules.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { logger } from '../../../logger'; -import { regEx } from '../../../util/regex'; -import { BitBucketTagsDatasource } from '../../datasource/bitbucket-tags'; -import { GitTagsDatasource } from '../../datasource/git-tags'; -import { GithubTagsDatasource } from '../../datasource/github-tags'; -import { TerraformModuleDatasource } from '../../datasource/terraform-module'; -import type { PackageDependency } from '../types'; -import { extractTerraformProvider } from './providers'; -import type { ExtractionResult } from './types'; - -export const githubRefMatchRegex = regEx( - /github\.com([/:])(?<project>[^/]+\/[a-z0-9-_.]+).*\?ref=(?<tag>.*)$/i -); -export const bitbucketRefMatchRegex = regEx( - /(?:git::)?(?<url>(?:http|https|ssh)?(?::\/\/)?(?:.*@)?(?<path>bitbucket\.org\/(?<workspace>.*)\/(?<project>.*).git\/?(?<subfolder>.*)))\?ref=(?<tag>.*)$/ -); -export const gitTagsRefMatchRegex = regEx( - /(?:git::)?(?<url>(?:(?:http|https|ssh):\/\/)?(?:.*@)?(?<path>.*\/(?<project>.*\/.*)))\?ref=(?<tag>.*)$/ -); -export const azureDevOpsSshRefMatchRegex = regEx( - /(?:git::)?(?<url>git@ssh\.dev\.azure\.com:v3\/(?<organization>[^/]*)\/(?<project>[^/]*)\/(?<repository>[^/]*))(?<modulepath>.*)?\?ref=(?<tag>.*)$/ -); -const hostnameMatchRegex = regEx(/^(?<hostname>([\w|\d]+\.)+[\w|\d]+)/); - -export function extractTerraformModule( - startingLine: number, - lines: string[], - moduleName: string -): ExtractionResult { - const result = extractTerraformProvider(startingLine, lines, moduleName); - result.dependencies.forEach((dep) => { - // TODO #7154 - dep.managerData!.terraformDependencyType = 'module'; - }); - return result; -} - -export function analyseTerraformModule(dep: PackageDependency): void { - // TODO #7154 - const source = dep.managerData!.source as string; - const githubRefMatch = githubRefMatchRegex.exec(source); - const bitbucketRefMatch = bitbucketRefMatchRegex.exec(source); - const gitTagsRefMatch = gitTagsRefMatchRegex.exec(source); - const azureDevOpsSshRefMatch = azureDevOpsSshRefMatchRegex.exec(source); - - if (githubRefMatch?.groups) { - dep.packageName = githubRefMatch.groups.project.replace( - regEx(/\.git$/), - '' - ); - dep.depType = 'module'; - dep.depName = 'github.com/' + dep.packageName; - dep.currentValue = githubRefMatch.groups.tag; - dep.datasource = GithubTagsDatasource.id; - } else if (bitbucketRefMatch?.groups) { - dep.depType = 'module'; - dep.depName = - bitbucketRefMatch.groups.workspace + - '/' + - bitbucketRefMatch.groups.project; - dep.packageName = dep.depName; - dep.currentValue = bitbucketRefMatch.groups.tag; - dep.datasource = BitBucketTagsDatasource.id; - } else if (azureDevOpsSshRefMatch?.groups) { - dep.depType = 'module'; - dep.depName = `${azureDevOpsSshRefMatch.groups.organization}/${azureDevOpsSshRefMatch.groups.project}/${azureDevOpsSshRefMatch.groups.repository}${azureDevOpsSshRefMatch.groups.modulepath}`; - dep.packageName = azureDevOpsSshRefMatch.groups.url; - dep.currentValue = azureDevOpsSshRefMatch.groups.tag; - dep.datasource = GitTagsDatasource.id; - } else if (gitTagsRefMatch?.groups) { - dep.depType = 'module'; - if (gitTagsRefMatch.groups.path.includes('//')) { - logger.debug('Terraform module contains subdirectory'); - dep.depName = gitTagsRefMatch.groups.path.split('//')[0]; - const tempLookupName = gitTagsRefMatch.groups.url.split('//'); - dep.packageName = tempLookupName[0] + '//' + tempLookupName[1]; - } else { - dep.depName = gitTagsRefMatch.groups.path.replace('.git', ''); - dep.packageName = gitTagsRefMatch.groups.url; - } - dep.currentValue = gitTagsRefMatch.groups.tag; - dep.datasource = GitTagsDatasource.id; - } else if (source) { - const moduleParts = source.split('//')[0].split('/'); - if (moduleParts[0] === '..') { - dep.skipReason = 'local'; - } else if (moduleParts.length >= 3) { - const hostnameMatch = hostnameMatchRegex.exec(source); - if (hostnameMatch?.groups) { - dep.registryUrls = [`https://${hostnameMatch.groups.hostname}`]; - } - dep.depType = 'module'; - dep.depName = moduleParts.join('/'); - dep.datasource = TerraformModuleDatasource.id; - } - } else { - logger.debug({ dep }, 'terraform dep has no source'); - dep.skipReason = 'no-source'; - } -} diff --git a/lib/modules/manager/terraform/providers.ts b/lib/modules/manager/terraform/providers.ts deleted file mode 100644 index 10501357ff..0000000000 --- a/lib/modules/manager/terraform/providers.ts +++ /dev/null @@ -1,106 +0,0 @@ -import is from '@sindresorhus/is'; -import { logger } from '../../../logger'; -import { regEx } from '../../../util/regex'; -import { TerraformProviderDatasource } from '../../datasource/terraform-provider'; -import type { PackageDependency } from '../types'; -import type { ProviderLock } from './lockfile/types'; -import type { ExtractionResult, TerraformManagerData } from './types'; -import { - getLockedVersion, - keyValueExtractionRegex, - massageProviderLookupName, -} from './util'; - -export const sourceExtractionRegex = regEx( - /^(?:(?<hostname>(?:[a-zA-Z0-9-_]+\.+)+[a-zA-Z0-9-_]+)\/)?(?:(?<namespace>[^/]+)\/)?(?<type>[^/]+)/ -); - -export function extractTerraformProvider( - startingLine: number, - lines: string[], - moduleName: string -): ExtractionResult { - let lineNumber = startingLine; - const deps: PackageDependency<TerraformManagerData>[] = []; - const dep: PackageDependency<TerraformManagerData> = { - managerData: { - moduleName, - terraformDependencyType: 'provider', - }, - }; - let braceCounter = 0; - do { - // istanbul ignore if - if (lineNumber > lines.length - 1) { - logger.debug(`Malformed Terraform file detected.`); - } - - const line = lines[lineNumber]; - - // istanbul ignore else - if (is.string(line)) { - // `{` will be counted with +1 and `}` with -1. Therefore if we reach braceCounter == 0. We have found the end of the terraform block - const openBrackets = (line.match(regEx(/\{/g)) ?? []).length; - const closedBrackets = (line.match(regEx(/\}/g)) ?? []).length; - braceCounter = braceCounter + openBrackets - closedBrackets; - - // only update fields inside the root block - if (braceCounter === 1) { - const kvMatch = keyValueExtractionRegex.exec(line); - if (kvMatch?.groups) { - if (kvMatch.groups.key === 'version') { - dep.currentValue = kvMatch.groups.value; - } else if (kvMatch.groups.key === 'source') { - // TODO #7154 - dep.managerData!.source = kvMatch.groups.value; - dep.managerData!.sourceLine = lineNumber; - } - } - } - } else { - // stop - something went wrong - braceCounter = 0; - } - lineNumber += 1; - } while (braceCounter !== 0); - deps.push(dep); - - // remove last lineNumber addition to not skip a line after the last bracket - lineNumber -= 1; - return { lineNumber, dependencies: deps }; -} - -export function analyzeTerraformProvider( - dep: PackageDependency, - locks: ProviderLock[] -): void { - dep.depType = 'provider'; - dep.depName = dep.managerData?.moduleName; - dep.datasource = TerraformProviderDatasource.id; - - if (is.nonEmptyString(dep.managerData?.source)) { - // TODO #7154 - const source = sourceExtractionRegex.exec(dep.managerData!.source); - if (!source?.groups) { - dep.skipReason = 'unsupported-url'; - return; - } - - // 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'; - } -} diff --git a/lib/modules/manager/terraform/required-providers.ts b/lib/modules/manager/terraform/required-providers.ts deleted file mode 100644 index 5e48455bae..0000000000 --- a/lib/modules/manager/terraform/required-providers.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { regEx } from '../../../util/regex'; -import type { PackageDependency } from '../types'; -import type { ProviderLock } from './lockfile/types'; -import { analyzeTerraformProvider } from './providers'; -import type { ExtractionResult, TerraformManagerData } from './types'; -import { keyValueExtractionRegex } from './util'; - -export const providerBlockExtractionRegex = regEx(/^\s*(?<key>[^\s]+)\s+=\s+{/); - -function extractBlock( - lineNum: number, - lines: string[], - dep: PackageDependency -): number { - let lineNumber = lineNum; - let line: string; - do { - lineNumber += 1; - line = lines[lineNumber]; - const kvMatch = keyValueExtractionRegex.exec(line); - if (kvMatch?.groups) { - switch (kvMatch.groups.key) { - case 'source': - // TODO #7154 - dep.managerData!.source = kvMatch.groups.value; - break; - - case 'version': - dep.currentValue = kvMatch.groups.value; - break; - - /* istanbul ignore next */ - default: - break; - } - } - } while (line.trim() !== '}'); - return lineNumber; -} - -export function extractTerraformRequiredProviders( - startingLine: number, - lines: string[] -): ExtractionResult { - let lineNumber = startingLine; - let line: string; - const deps: PackageDependency<TerraformManagerData>[] = []; - do { - const dep: PackageDependency<TerraformManagerData> = { - managerData: { - terraformDependencyType: 'required_providers', - }, - }; - - lineNumber += 1; - line = lines[lineNumber]; - const kvMatch = keyValueExtractionRegex.exec(line); - if (kvMatch?.groups) { - dep.currentValue = kvMatch.groups.value; - // TODO #7154 - dep.managerData!.moduleName = kvMatch.groups.key; - deps.push(dep); - } else { - const nameMatch = providerBlockExtractionRegex.exec(line); - - if (nameMatch?.groups) { - // TODO #7154 - dep.managerData!.moduleName = nameMatch.groups.key; - lineNumber = extractBlock(lineNumber, lines, dep); - deps.push(dep); - } - } - } while (line.trim() !== '}'); - return { lineNumber, dependencies: deps }; -} - -export function analyzeTerraformRequiredProvider( - dep: PackageDependency, - locks: ProviderLock[] -): void { - analyzeTerraformProvider(dep, locks); - dep.depType = `required_provider`; -} diff --git a/lib/modules/manager/terraform/required-version.ts b/lib/modules/manager/terraform/required-version.ts deleted file mode 100644 index 9db85821ed..0000000000 --- a/lib/modules/manager/terraform/required-version.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { logger } from '../../../logger'; -import { regEx } from '../../../util/regex'; -import { GithubReleasesDatasource } from '../../datasource/github-releases'; -import type { PackageDependency } from '../types'; -import type { ExtractionResult, TerraformManagerData } from './types'; -import { keyValueExtractionRegex } from './util'; - -export function extractTerraformRequiredVersion( - startingLine: number, - lines: string[] -): ExtractionResult | null { - const deps: PackageDependency<TerraformManagerData>[] = []; - let lineNumber = startingLine; - let braceCounter = 0; - do { - // istanbul ignore if - if (lineNumber > lines.length - 1) { - logger.debug(`Malformed Terraform file detected.`); - } - - const line = lines[lineNumber]; - // `{` will be counted wit +1 and `}` with -1. Therefore if we reach braceCounter == 0. We have found the end of the terraform block - const openBrackets = (line.match(regEx(/\{/g)) ?? []).length; - const closedBrackets = (line.match(regEx(/\}/g)) ?? []).length; - braceCounter = braceCounter + openBrackets - closedBrackets; - - const kvMatch = keyValueExtractionRegex.exec(line); - if (kvMatch?.groups && kvMatch.groups.key === 'required_version') { - const dep: PackageDependency<TerraformManagerData> = { - currentValue: kvMatch.groups.value, - lineNumber, - managerData: { - terraformDependencyType: 'terraform_version', - }, - }; - deps.push(dep); - // returning starting line as required_providers are also in the terraform block - // if we would return the position of the required_version line we would potentially skip the providers - return { lineNumber: startingLine, dependencies: deps }; - } - - lineNumber += 1; - } while (braceCounter !== 0); - return null; -} - -export function analyseTerraformVersion(dep: PackageDependency): void { - dep.depType = 'required_version'; - dep.datasource = GithubReleasesDatasource.id; - dep.depName = 'hashicorp/terraform'; - dep.extractVersion = 'v(?<version>.*)$'; -} diff --git a/lib/modules/manager/terraform/resources.ts b/lib/modules/manager/terraform/resources.ts deleted file mode 100644 index cd58ba0961..0000000000 --- a/lib/modules/manager/terraform/resources.ts +++ /dev/null @@ -1,163 +0,0 @@ -import is from '@sindresorhus/is'; -import { logger } from '../../../logger'; -import { regEx } from '../../../util/regex'; -import { HelmDatasource } from '../../datasource/helm'; -import { getDep } from '../dockerfile/extract'; -import type { PackageDependency } from '../types'; -import { TerraformResourceTypes } from './common'; -import { extractTerraformKubernetesResource } from './extract/kubernetes'; -import { analyseTerraformVersion } from './required-version'; -import type { ExtractionResult, ResourceManagerData } from './types'; -import { - 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; - const line = lines[lineNumber]; - const deps: PackageDependency<ResourceManagerData>[] = []; - const managerData: ResourceManagerData = { - terraformDependencyType: 'resource', - }; - const dep: PackageDependency<ResourceManagerData> = { - managerData, - }; - - const typeMatch = resourceTypeExtractionRegex.exec(line); - - // Sets the resourceType, e.g., 'resource "helm_release" "test_release"' -> helm_release - const resourceType = typeMatch?.groups?.type; - - const isKnownType = - resourceType && - Object.keys(TerraformResourceTypes).some((key) => { - return TerraformResourceTypes[key].includes(resourceType); - }); - - if (isKnownType && resourceType.startsWith('kubernetes_')) { - return extractTerraformKubernetesResource( - startingLine, - lines, - resourceType - ); - } - - managerData.resourceType = isKnownType - ? resourceType - : TerraformResourceTypes.unknown[0]; - - /** - * Iterates over all lines of the resource to extract the relevant key value pairs, - * e.g. the chart name for helm charts or the terraform_version for tfe_workspace - */ - let braceCounter = 0; - do { - // istanbul ignore if - if (lineNumber > lines.length - 1) { - logger.debug(`Malformed Terraform file detected.`); - } - - const line = lines[lineNumber]; - - // istanbul ignore else - if (is.string(line)) { - // `{` will be counted with +1 and `}` with -1. Therefore if we reach braceCounter == 0. We have found the end of the terraform block - const openBrackets = (line.match(regEx(/\{/g)) ?? []).length; - const closedBrackets = (line.match(regEx(/\}/g)) ?? []).length; - braceCounter = braceCounter + openBrackets - closedBrackets; - - const kvMatch = keyValueExtractionRegex.exec(line); - if (kvMatch?.groups) { - switch (kvMatch.groups.key) { - case 'chart': - case 'image': - case 'name': - case 'repository': - managerData[kvMatch.groups.key] = kvMatch.groups.value; - break; - case 'version': - case 'terraform_version': - dep.currentValue = kvMatch.groups.value; - break; - default: - /* istanbul ignore next */ - break; - } - } - } else { - // stop - something went wrong - braceCounter = 0; - } - lineNumber += 1; - } while (braceCounter !== 0); - deps.push(dep); - - // remove last lineNumber addition to not skip a line after the last bracket - lineNumber -= 1; - return { lineNumber, dependencies: deps }; -} - -export function analyseTerraformResource( - dep: PackageDependency<ResourceManagerData> -): void { - switch (dep.managerData?.resourceType) { - case TerraformResourceTypes.generic_image_resource.find( - (key) => key === dep.managerData?.resourceType - ): - if (dep.managerData.image) { - applyDockerDependency(dep, dep.managerData.image); - dep.depType = dep.managerData.resourceType; - } else { - dep.skipReason = 'invalid-dependency-specification'; - } - break; - - case TerraformResourceTypes.docker_image[0]: - if (dep.managerData.name) { - applyDockerDependency(dep, dep.managerData.name); - dep.depType = 'docker_image'; - } else { - dep.skipReason = 'invalid-dependency-specification'; - } - break; - - case TerraformResourceTypes.helm_release[0]: - if (!dep.managerData.chart) { - dep.skipReason = 'invalid-name'; - } else if (checkIfStringIsPath(dep.managerData.chart)) { - dep.skipReason = 'local-chart'; - } - dep.depType = 'helm_release'; - // TODO #7154 - dep.registryUrls = [dep.managerData.repository!]; - dep.depName = dep.managerData.chart; - dep.datasource = HelmDatasource.id; - break; - - case TerraformResourceTypes.tfe_workspace[0]: - if (dep.currentValue) { - analyseTerraformVersion(dep); - dep.depType = 'tfe_workspace'; - } else { - dep.skipReason = 'no-version'; - } - break; - - default: - dep.skipReason = 'invalid-value'; - break; - } -} diff --git a/lib/modules/manager/terraform/types.ts b/lib/modules/manager/terraform/types.ts index bfe592b1c5..923e9ca630 100644 --- a/lib/modules/manager/terraform/types.ts +++ b/lib/modules/manager/terraform/types.ts @@ -1,22 +1,4 @@ -import type { PackageDependency } from '../types'; -import type { TerraformDependencyTypes } from './common'; - -export interface ExtractionResult { - lineNumber: number; - dependencies: PackageDependency<TerraformManagerData>[]; -} - -export interface TerraformManagerData { - moduleName?: string; - source?: string; - sourceLine?: number; - terraformDependencyType: TerraformDependencyTypes; -} - -export interface ResourceManagerData extends TerraformManagerData { - resourceType?: string; - chart?: string; - image?: string; - name?: string; - repository?: string; +export interface GenericImageResourceDef { + type: string; + path: string[]; } diff --git a/lib/modules/manager/terraform/util.spec.ts b/lib/modules/manager/terraform/util.spec.ts deleted file mode 100644 index a1109e6e3b..0000000000 --- a/lib/modules/manager/terraform/util.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { getTerraformDependencyType } from './util'; - -describe('modules/manager/terraform/util', () => { - describe('getTerraformDependencyType()', () => { - it('returns module', () => { - expect(getTerraformDependencyType('module')).toBe('module'); - }); - - it('returns provider', () => { - expect(getTerraformDependencyType('provider')).toBe('provider'); - }); - - it('returns unknown', () => { - expect(getTerraformDependencyType('unknown')).toBe('unknown'); - }); - - it('returns required_providers', () => { - expect(getTerraformDependencyType('required_providers')).toBe( - 'required_providers' - ); - }); - - it('returns unknown on empty string', () => { - expect(getTerraformDependencyType('')).toBe('unknown'); - }); - - it('returns unknown on string with random chars', () => { - expect(getTerraformDependencyType('sdfsgdsfadfhfghfhgdfsdf')).toBe( - 'unknown' - ); - }); - }); -}); diff --git a/lib/modules/manager/terraform/util.ts b/lib/modules/manager/terraform/util.ts index 696f775bca..b20fd36812 100644 --- a/lib/modules/manager/terraform/util.ts +++ b/lib/modules/manager/terraform/util.ts @@ -1,40 +1,9 @@ +import is from '@sindresorhus/is'; import { regEx } from '../../../util/regex'; import { TerraformProviderDatasource } from '../../datasource/terraform-provider'; import type { PackageDependency } from '../types'; -import type { TerraformDependencyTypes } from './common'; import type { ProviderLock } from './lockfile/types'; - -export const keyValueExtractionRegex = regEx( - /^\s*(?<key>[^\s]+)\s+=\s+"(?<value>[^"]+)"\s*$/ -); -export const resourceTypeExtractionRegex = regEx( - /^\s*resource\s+"(?<type>[^\s]+)"\s+"(?<name>[^"]+)"\s*{/ -); - -export function getTerraformDependencyType( - value: string -): TerraformDependencyTypes { - switch (value) { - case 'module': { - return 'module'; - } - case 'provider': { - return 'provider'; - } - case 'required_providers': { - return 'required_providers'; - } - case 'resource': { - return 'resource'; - } - case 'terraform': { - return 'terraform_version'; - } - default: { - return 'unknown'; - } - } -} +import { extractLocks, findLockFile, readLockFile } from './lockfile/util'; export function checkFileContainsDependency( content: string, @@ -81,3 +50,20 @@ export function getLockedVersion( } return undefined; } + +export async function extractLocksForPackageFile( + fileName: string +): Promise<ProviderLock[]> { + 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); + } + } + } + return locks; +} diff --git a/package.json b/package.json index b1af463153..7ef8ce9e2b 100644 --- a/package.json +++ b/package.json @@ -166,6 +166,7 @@ "auth-header": "1.0.0", "aws4": "1.11.0", "azure-devops-node-api": "11.2.0", + "hcl2-parser": "1.0.3", "bunyan": "1.8.15", "cacache": "17.0.4", "cacheable-lookup": "5.0.4", diff --git a/yarn.lock b/yarn.lock index 705ce01da8..a788b289f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5738,6 +5738,11 @@ hasha@5.2.2: is-stream "^2.0.0" type-fest "^0.8.0" +hcl2-parser@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/hcl2-parser/-/hcl2-parser-1.0.3.tgz#096d0ff5a3c46707ace54fcb7571317f5828ff0e" + integrity sha512-NQUm/BFF+2nrBfeqDhhsy4DxxiLHgkeE3FywtjFiXnjSUaio3w4Tz1MQ3vGJBUhyArzOXJ24pO7JwE5LAn7Ncg== + he@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" -- GitLab