From a4205761c8ccb6a177b08f15e8647e27d35d62ae Mon Sep 17 00:00:00 2001
From: Johannes Feichtner <Churro@users.noreply.github.com>
Date: Tue, 14 Jun 2022 21:51:11 +0200
Subject: [PATCH] feat(terraform): kubernetes image resources support (#16029)

* add tests

* add support for kubernetes image resources

* Update lib/modules/manager/terraform/extract.ts

Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com>

* added unknown resource

* added Kubernetes to Readme

* missed one spot in the Readme

* Update lib/modules/manager/terraform/readme.md

Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com>

* switch to toMatchObject() in tests

Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com>
Co-authored-by: Rhys Arkins <rhys@arkins.net>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---
 .../manager/terraform/__fixtures__/docker.tf  |   6 +-
 .../terraform/__fixtures__/kubernetes.tf      | 255 ++++++++++++++++++
 lib/modules/manager/terraform/common.ts       |  54 ++--
 lib/modules/manager/terraform/extract.spec.ts |  85 ++++++
 lib/modules/manager/terraform/extract.ts      |   4 +-
 lib/modules/manager/terraform/readme.md       |  42 ++-
 lib/modules/manager/terraform/resources.ts    |  43 ++-
 lib/modules/manager/terraform/types.ts        |   7 +-
 8 files changed, 427 insertions(+), 69 deletions(-)
 create mode 100644 lib/modules/manager/terraform/__fixtures__/kubernetes.tf

diff --git a/lib/modules/manager/terraform/__fixtures__/docker.tf b/lib/modules/manager/terraform/__fixtures__/docker.tf
index 9111419102..de04d0cd91 100644
--- a/lib/modules/manager/terraform/__fixtures__/docker.tf
+++ b/lib/modules/manager/terraform/__fixtures__/docker.tf
@@ -1,5 +1,5 @@
 # docker_image resources
-# https://www.terraform.io/docs/providers/docker/r/image.html
+# https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs/resources/image
 resource "docker_image" "nginx" {
   name = "nginx:1.7.8"
 }
@@ -14,7 +14,7 @@ resource "docker_image" "ignore_variable" {
 
 
 # docker_container resources
-# https://www.terraform.io/docs/providers/docker/r/container.html
+# https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs/resources/container
 resource "docker_container" "foo" {
   name  = "foo"
   image = "nginx:1.7.8"
@@ -26,7 +26,7 @@ resource "docker_container" "invalid" {
 
 
 # docker_service resources
-# https://www.terraform.io/docs/providers/docker/r/service.html
+# https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs/resources/service
 resource "docker_service" "foo" {
   name = "foo-service"
 
diff --git a/lib/modules/manager/terraform/__fixtures__/kubernetes.tf b/lib/modules/manager/terraform/__fixtures__/kubernetes.tf
new file mode 100644
index 0000000000..bd8e5886b4
--- /dev/null
+++ b/lib/modules/manager/terraform/__fixtures__/kubernetes.tf
@@ -0,0 +1,255 @@
+resource "kubernetes_cron_job_v1" "demo" {
+  metadata {}
+  spec {
+    job_template {
+      metadata {}
+      spec {
+        template {
+          metadata {}
+          spec {
+            container {
+              name    = "kaniko"
+              image   = "gcr.io/kaniko-project/executor:v1.7.0@sha256:8504bde9a9a8c9c4e9a4fe659703d265697a36ff13607b7669a4caa4407baa52"
+            }
+          }
+        }
+      }
+    }
+    schedule = ""
+  }
+}
+
+resource "kubernetes_cron_job" "demo" {
+  metadata {}
+  spec {
+    job_template {
+      metadata {}
+      spec {
+        template {
+          metadata {}
+          spec {
+            container {
+              name    = "kaniko"
+              image   = "gcr.io/kaniko-project/executor:v1.8.0@sha256:8504bde9a9a8c9c4e9a4fe659703d265697a36ff13607b7669a4caa4407baa52"
+            }
+          }
+        }
+      }
+    }
+    schedule = ""
+  }
+}
+
+resource "kubernetes_daemon_set_v1" "example" {
+  metadata {}
+  spec {
+    template {
+      metadata {}
+      spec {
+        container {
+          image = "nginx:1.21.1"
+          name  = "example1"
+        }
+      }
+    }
+  }
+}
+
+resource "kubernetes_daemonset" "example" {
+  metadata {}
+  spec {
+    template {
+      metadata {}
+      spec {
+        container {
+          image = "nginx:1.21.2"
+          name  = "example2"
+        }
+      }
+    }
+  }
+}
+
+resource "kubernetes_deployment" "example" {
+  metadata {}
+  spec {
+    template {
+      metadata {}
+      spec {
+        container {
+          image = "nginx:1.21.3"
+          name  = "example3"
+        }
+      }
+    }
+  }
+}
+
+resource "kubernetes_deployment_v1" "example" {
+  metadata {}
+  spec {
+    template {
+      metadata {}
+      spec {
+        container {
+          image = "nginx:1.21.4"
+          name  = "example4"
+        }
+      }
+    }
+  }
+}
+
+resource "kubernetes_job" "demo" {
+  metadata {}
+  spec {
+    template {
+      metadata {}
+      spec {
+        container {
+          name    = "example5"
+          image   = "nginx:1.21.5"
+        }
+      }
+    }
+  }
+}
+
+resource "kubernetes_job" "demo_invalid" {
+  metadata {}
+  spec {
+    template {
+      metadata {}
+      spec {
+        container {
+          name    = "example5-invalid"
+        }
+      }
+    }
+  }
+}
+
+resource "kubernetes_job_invalid" "demo_invalid2" {
+  metadata {}
+  spec {
+    template {
+      metadata {}
+      spec {
+        container {
+          name    = "example5"
+          image   = "nginx:1.21.6"
+        }
+      }
+    }
+  }
+}
+
+resource "kubernetes_job_v1" "demo" {
+  metadata {}
+  spec {
+    template {
+      metadata {}
+      spec {
+        container {
+          name    = "example6"
+          image   = "nginx:1.21.6"
+        }
+      }
+    }
+  }
+}
+
+resource "kubernetes_pod" "test" {
+  metadata {}
+  spec {
+    container {
+      image = "nginx:1.21.7"
+      name  = "example7"
+    }
+  }
+}
+
+resource "kubernetes_pod_v1" "test" {
+  metadata {}
+  spec {
+    container {
+      image = "nginx:1.21.8"
+      name  = "example8"
+    }
+  }
+}
+
+resource "kubernetes_replication_controller" "example" {
+  metadata {}
+  spec {
+    selector = {}
+    template {
+      metadata {}
+      spec {
+        container {
+          image = "nginx:1.21.9"
+          name  = "example9"
+        }
+      }
+    }
+  }
+}
+
+resource "kubernetes_replication_controller_v1" "example" {
+  metadata {}
+  spec {
+    selector = {}
+    template {
+      metadata {}
+      spec {
+        container {
+          image = "nginx:1.21.10"
+          name  = "example10"
+        }
+      }
+    }
+  }
+}
+
+resource "kubernetes_stateful_set" "prometheus" {
+  metadata {}
+  spec {
+    template {
+      metadata {}
+      spec {
+        init_container {
+          name              = "example11"
+          image             = "nginx:1.21.11"
+        }
+        container {
+          name              = "prometheus-server1"
+          image             = "prom/prometheus:v2.2.1"
+        }
+      }
+    }
+    service_name = ""
+    selector {}
+  }
+}
+
+resource "kubernetes_stateful_set_v1" "prometheus" {
+  metadata {}
+  spec {
+    template {
+      metadata {}
+      spec {
+        init_container {
+          name              = "example12"
+          image             = "nginx:1.21.12"
+        }
+
+        container {
+          name              = "prometheus-server2"
+          image             = "prom/prometheus:v2.2.2"
+        }
+      }
+    }
+    service_name = ""
+    selector {}
+  }
+}
diff --git a/lib/modules/manager/terraform/common.ts b/lib/modules/manager/terraform/common.ts
index 8e168d7d02..8e123299a0 100644
--- a/lib/modules/manager/terraform/common.ts
+++ b/lib/modules/manager/terraform/common.ts
@@ -11,27 +11,33 @@ export enum TerraformDependencyTypes {
   terraform_version = 'terraform_version',
 }
 
-// eslint-disable-next-line typescript-enum/no-enum
-export enum TerraformResourceTypes {
-  unknown = 'unknown',
-  /**
-   * https://www.terraform.io/docs/providers/docker/r/container.html
-   */
-  docker_container = 'docker_container',
-  /**
-   * https://www.terraform.io/docs/providers/docker/r/image.html
-   */
-  docker_image = 'docker_image',
-  /**
-   * https://www.terraform.io/docs/providers/docker/r/service.html
-   */
-  docker_service = 'docker_service',
-  /**
-   * https://www.terraform.io/docs/providers/helm/r/release.html
-   */
-  helm_release = 'helm_release',
-  /**
-   * https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/resources/workspace
-   */
-  tfe_workspace = 'tfe_workspace',
-}
+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 d1d82d8556..e876b5bc0a 100644
--- a/lib/modules/manager/terraform/extract.spec.ts
+++ b/lib/modules/manager/terraform/extract.spec.ts
@@ -10,6 +10,7 @@ const bitbucketModules = Fixtures.get('bitbucketModules.tf');
 const azureDevOpsModules = Fixtures.get('azureDevOpsModules.tf');
 const providers = Fixtures.get('providers.tf');
 const docker = Fixtures.get('docker.tf');
+const kubernetes = Fixtures.get('kubernetes.tf');
 
 const tf2 = `module "relative" {
   source = "../fe"
@@ -104,6 +105,90 @@ describe('modules/manager/terraform/extract', () => {
       expect(res).toMatchSnapshot();
     });
 
+    it('extracts kubernetes resources', async () => {
+      const res = await extractPackageFile(kubernetes, 'kubernetes.tf', {});
+      expect(res.deps).toHaveLength(16);
+      expect(res.deps.filter((dep) => dep.skipReason)).toHaveLength(2);
+      expect(res.deps).toMatchObject([
+        {
+          depName: 'gcr.io/kaniko-project/executor',
+          currentValue: 'v1.7.0',
+          currentDigest:
+            'sha256:8504bde9a9a8c9c4e9a4fe659703d265697a36ff13607b7669a4caa4407baa52',
+          depType: 'kubernetes_cron_job_v1',
+        },
+        {
+          depName: 'gcr.io/kaniko-project/executor',
+          currentValue: 'v1.8.0',
+          currentDigest:
+            'sha256:8504bde9a9a8c9c4e9a4fe659703d265697a36ff13607b7669a4caa4407baa52',
+          depType: 'kubernetes_cron_job',
+        },
+        {
+          depName: 'nginx',
+          currentValue: '1.21.1',
+          depType: 'kubernetes_daemon_set_v1',
+        },
+        {
+          depName: 'nginx',
+          currentValue: '1.21.2',
+          depType: 'kubernetes_daemonset',
+        },
+        {
+          depName: 'nginx',
+          currentValue: '1.21.3',
+          depType: 'kubernetes_deployment',
+        },
+        {
+          depName: 'nginx',
+          currentValue: '1.21.4',
+          depType: 'kubernetes_deployment_v1',
+        },
+        {
+          depName: 'nginx',
+          currentValue: '1.21.5',
+          depType: 'kubernetes_job',
+        },
+        { skipReason: 'invalid-dependency-specification' },
+        { skipReason: 'invalid-value' },
+        {
+          depName: 'nginx',
+          currentValue: '1.21.6',
+          depType: 'kubernetes_job_v1',
+        },
+        {
+          depName: 'nginx',
+          currentValue: '1.21.7',
+          depType: 'kubernetes_pod',
+        },
+        {
+          depName: 'nginx',
+          currentValue: '1.21.8',
+          depType: 'kubernetes_pod_v1',
+        },
+        {
+          depName: 'nginx',
+          currentValue: '1.21.9',
+          depType: 'kubernetes_replication_controller',
+        },
+        {
+          depName: 'nginx',
+          currentValue: '1.21.10',
+          depType: 'kubernetes_replication_controller_v1',
+        },
+        {
+          depName: 'prom/prometheus',
+          currentValue: 'v2.2.1',
+          depType: 'kubernetes_stateful_set',
+        },
+        {
+          depName: 'prom/prometheus',
+          currentValue: 'v2.2.2',
+          depType: 'kubernetes_stateful_set_v1',
+        },
+      ]);
+    });
+
     it('returns null if only local deps', async () => {
       expect(await extractPackageFile(tf2, '2.tf', {})).toBeNull();
     });
diff --git a/lib/modules/manager/terraform/extract.ts b/lib/modules/manager/terraform/extract.ts
index 1f2aae7a14..73df9d7b29 100644
--- a/lib/modules/manager/terraform/extract.ts
+++ b/lib/modules/manager/terraform/extract.ts
@@ -34,11 +34,13 @@ const dependencyBlockExtractionRegex = regEx(
 const contentCheckList = [
   'module "',
   'provider "',
+  '"docker_',
+  '"kubernetes_',
   'required_providers ',
   ' "helm_release" ',
   ' "docker_image" ',
   'required_version',
-  'terraform_version', // part of  tfe_workspace
+  'terraform_version', // part of tfe_workspace
 ];
 
 export async function extractPackageFile(
diff --git a/lib/modules/manager/terraform/readme.md b/lib/modules/manager/terraform/readme.md
index d158f3e080..405ada1aec 100644
--- a/lib/modules/manager/terraform/readme.md
+++ b/lib/modules/manager/terraform/readme.md
@@ -1,4 +1,4 @@
-Currently, Terraform supports renovating the following dependencies, where sub points represent hosting options of the dependencies:
+Currently, Terraform supports renovating the following dependencies, where sub-points represent hosting options of the dependencies:
 
 - modules
   - GitTags
@@ -13,6 +13,8 @@ Currently, Terraform supports renovating the following dependencies, where sub p
   - chart repository ( Public and Private )
 - docker\_\*
   - Docker registry ( Public and Private )
+- kubernetes\_\*
+  - Docker registry ( Public and Private )
 - [tfe_workspace](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/resources/workspace) ( `terraform_version` argument )
 
 Terraform range constraints are supported:
@@ -23,18 +25,32 @@ Terraform range constraints are supported:
 - `~> 1.2`: any non-beta version >= 1.2.0 and < 2.0.0, e.g. 1.X.Y
 - `>= 1.0.0, <= 2.0.0`: any version between 1.0.0 and 2.0.0 inclusive
 
-For fine-grained control, e.g. to turn off only parts of this manager, you can use the following `depTypes`:
+For fine-grained control, e.g., to turn off only parts of this manager, you can use the following `depTypes`:
 
-| resource                    |       depType       |                                   Notes                                    |
-| --------------------------- | :-----------------: | :------------------------------------------------------------------------: |
-| Terraform provider          |     `provider`      |                                                                            |
-| required Terraform provider | `required_provider` |                                                                            |
-| required Terraform version  | `required_version`  |          This handles the `required_version` in terraform blocks           |
-| TFE workspace               |   `tfe_workspace`   | This handles the `terraform_version` argument in `tfe_workspace` resources |
-| Terraform module            |      `module`       |                                                                            |
-| Helm release                |   `helm_release`    |                                                                            |
-| Docker container            | `docker_container`  |                                                                            |
-| Docker image                |   `docker_image`    |                                                                            |
-| Docker service              |  `docker_service`   |                                                                            |
+| resource                             |                depType                 |                                   Notes                                    |
+| ------------------------------------ | :------------------------------------: | :------------------------------------------------------------------------: |
+| Terraform provider                   |               `provider`               |                                                                            |
+| required Terraform provider          |          `required_provider`           |                                                                            |
+| required Terraform version           |           `required_version`           |          This handles the `required_version` in terraform blocks           |
+| TFE workspace                        |            `tfe_workspace`             | This handles the `terraform_version` argument in `tfe_workspace` resources |
+| Terraform module                     |                `module`                |                                                                            |
+| Helm release                         |             `helm_release`             |                                                                            |
+| Docker container                     |           `docker_container`           |                                                                            |
+| Docker image                         |             `docker_image`             |                                                                            |
+| Docker service                       |            `docker_service`            |                                                                            |
+| Kubernetes CronJob                   |         `kubernetes_cron_job`          |                                                                            |
+| Kubernetes CronJob v1                |        `kubernetes_cron_job_v1`        |                                                                            |
+| Kubernetes DaemonSet                 |        `kubernetes_daemon_set`         |                                                                            |
+| Kubernetes DaemonSet v1              |       `kubernetes_daemon_set_v1`       |                                                                            |
+| Kubernetes Deployment                |        `kubernetes_deployment`         |                                                                            |
+| Kubernetes Deployment v1             |       `kubernetes_deployment_v1`       |                                                                            |
+| Kubernetes Job                       |            `kubernetes_job`            |                                                                            |
+| Kubernetes Job v1                    |          `kubernetes_job_v1`           |                                                                            |
+| Kubernetes Pod                       |            `kubernetes_pod`            |                                                                            |
+| Kubernetes Pod v1                    |          `kubernetes_pod_v1`           |                                                                            |
+| Kubernetes Replication Controller    |  `kubernetes_replication_controller`   |                                                                            |
+| Kubernetes Replication Controller v1 | `kubernetes_replication_controller_v1` |                                                                            |
+| Kubernetes StatefulSet               |       `kubernetes_stateful_set`        |                                                                            |
+| Kubernetes StatefulSet v1            |      `kubernetes_stateful_set_v1`      |                                                                            |
 
 If you need to change the versioning format, read the [versioning](https://docs.renovatebot.com/modules/versioning/) documentation to learn more.
diff --git a/lib/modules/manager/terraform/resources.ts b/lib/modules/manager/terraform/resources.ts
index 587b52aac0..9811c8f104 100644
--- a/lib/modules/manager/terraform/resources.ts
+++ b/lib/modules/manager/terraform/resources.ts
@@ -37,10 +37,18 @@ export function extractTerraformResource(
 
   const typeMatch = resourceTypeExtractionRegex.exec(line);
 
-  // Sets the resourceType, e.g. "helm_release" 'resource "helm_release" "test_release"'
-  managerData.resourceType =
-    TerraformResourceTypes[typeMatch?.groups?.type as TerraformResourceTypes] ??
-    TerraformResourceTypes.unknown;
+  // 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);
+    });
+
+  managerData.resourceType = isKnownType
+    ? resourceType
+    : TerraformResourceTypes.unknown[0];
 
   /**
    * Iterates over all lines of the resource to extract the relevant key value pairs,
@@ -96,21 +104,19 @@ export function extractTerraformResource(
 export function analyseTerraformResource(
   dep: PackageDependency<ResourceManagerData>
 ): void {
-  // istanbul ignore if: should tested?
-  if (!dep.managerData) {
-    return;
-  }
-  switch (dep.managerData.resourceType) {
-    case TerraformResourceTypes.docker_container:
+  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 = 'docker_container';
+        dep.depType = dep.managerData.resourceType;
       } else {
         dep.skipReason = 'invalid-dependency-specification';
       }
       break;
 
-    case TerraformResourceTypes.docker_image:
+    case TerraformResourceTypes.docker_image[0]:
       if (dep.managerData.name) {
         applyDockerDependency(dep, dep.managerData.name);
         dep.depType = 'docker_image';
@@ -119,16 +125,7 @@ export function analyseTerraformResource(
       }
       break;
 
-    case TerraformResourceTypes.docker_service:
-      if (dep.managerData.image) {
-        applyDockerDependency(dep, dep.managerData.image);
-        dep.depType = 'docker_service';
-      } else {
-        dep.skipReason = 'invalid-dependency-specification';
-      }
-      break;
-
-    case TerraformResourceTypes.helm_release:
+    case TerraformResourceTypes.helm_release[0]:
       if (!dep.managerData.chart) {
         dep.skipReason = 'invalid-name';
       } else if (checkIfStringIsPath(dep.managerData.chart)) {
@@ -141,7 +138,7 @@ export function analyseTerraformResource(
       dep.datasource = HelmDatasource.id;
       break;
 
-    case TerraformResourceTypes.tfe_workspace:
+    case TerraformResourceTypes.tfe_workspace[0]:
       if (dep.currentValue) {
         analyseTerraformVersion(dep);
         dep.depType = 'tfe_workspace';
diff --git a/lib/modules/manager/terraform/types.ts b/lib/modules/manager/terraform/types.ts
index 584ac9f08d..bfe592b1c5 100644
--- a/lib/modules/manager/terraform/types.ts
+++ b/lib/modules/manager/terraform/types.ts
@@ -1,8 +1,5 @@
 import type { PackageDependency } from '../types';
-import type {
-  TerraformDependencyTypes,
-  TerraformResourceTypes,
-} from './common';
+import type { TerraformDependencyTypes } from './common';
 
 export interface ExtractionResult {
   lineNumber: number;
@@ -17,7 +14,7 @@ export interface TerraformManagerData {
 }
 
 export interface ResourceManagerData extends TerraformManagerData {
-  resourceType?: TerraformResourceTypes;
+  resourceType?: string;
   chart?: string;
   image?: string;
   name?: string;
-- 
GitLab