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