From 938d896d31c553085baf4ad4a91a260db83fd8c3 Mon Sep 17 00:00:00 2001
From: Michael Kriese <michael.kriese@visualon.de>
Date: Tue, 28 Jul 2020 12:27:17 +0200
Subject: [PATCH] feat(terraform): support docker provider (#6866)

---
 lib/manager/terraform/__fixtures__/1.tf       |  58 +++++++++-
 .../__snapshots__/extract.spec.ts.snap        |  42 ++++++++
 lib/manager/terraform/extract.spec.ts         |   4 +-
 lib/manager/terraform/extract.ts              |   4 +-
 lib/manager/terraform/resources.ts            | 100 ++++++++++++++----
 lib/manager/terraform/util.ts                 |  33 ++++++
 6 files changed, 217 insertions(+), 24 deletions(-)

diff --git a/lib/manager/terraform/__fixtures__/1.tf b/lib/manager/terraform/__fixtures__/1.tf
index ccea08fff7..db6f8e68aa 100644
--- a/lib/manager/terraform/__fixtures__/1.tf
+++ b/lib/manager/terraform/__fixtures__/1.tf
@@ -168,7 +168,63 @@ terraform {
 
 terraform {
   required_providers {
-    aws = ">= 2.5.0"
+    aws     = ">= 2.5.0"
     azurerm = ">= 2.0.0"
   }
 }
+
+
+# docker_image resources
+# https://www.terraform.io/docs/providers/docker/r/image.html
+resource "docker_image" "nginx" {
+  name = "nginx:1.7.8"
+}
+
+resource "docker_image" "invalid" {
+}
+
+resource "docker_image" "ignore_variable" {
+  name          = "${data.docker_registry_image.ubuntu.name}"
+  pull_triggers = ["${data.docker_registry_image.ubuntu.sha256_digest}"]
+}
+
+
+# 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"
+    }
+  }
+}
+
+resource "docker_service" "invalid" {
+}
+
+# unsupported resources
+resource "not_supported_resource" "foo" {
+  name  = "foo"
+  image = "nginx:1.7.8"
+  dummy = "true"
+}
diff --git a/lib/manager/terraform/__snapshots__/extract.spec.ts.snap b/lib/manager/terraform/__snapshots__/extract.spec.ts.snap
index 6f4f5802a0..b4aac1bbb7 100644
--- a/lib/manager/terraform/__snapshots__/extract.spec.ts.snap
+++ b/lib/manager/terraform/__snapshots__/extract.spec.ts.snap
@@ -297,6 +297,48 @@ Object {
       "depNameShort": "azurerm",
       "depType": "terraform",
     },
+    Object {
+      "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}",
+      "currentDigest": undefined,
+      "currentValue": "1.7.8",
+      "datasource": "docker",
+      "depName": "nginx",
+      "replaceString": "nginx:1.7.8",
+    },
+    Object {
+      "skipReason": "invalid-dependency-specification",
+    },
+    Object {
+      "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}",
+      "datasource": "docker",
+      "replaceString": "\${data.docker_registry_image.ubuntu.name}",
+      "skipReason": "contains-variable",
+    },
+    Object {
+      "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}",
+      "currentDigest": undefined,
+      "currentValue": "1.7.8",
+      "datasource": "docker",
+      "depName": "nginx",
+      "replaceString": "nginx:1.7.8",
+    },
+    Object {
+      "skipReason": "invalid-dependency-specification",
+    },
+    Object {
+      "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}",
+      "currentDigest": undefined,
+      "currentValue": "v1",
+      "datasource": "docker",
+      "depName": "repo.mycompany.com:8080/foo-service",
+      "replaceString": "repo.mycompany.com:8080/foo-service:v1",
+    },
+    Object {
+      "skipReason": "invalid-dependency-specification",
+    },
+    Object {
+      "skipReason": "unsupported-value",
+    },
   ],
 }
 `;
diff --git a/lib/manager/terraform/extract.spec.ts b/lib/manager/terraform/extract.spec.ts
index 23019135fb..6cd7d5285d 100644
--- a/lib/manager/terraform/extract.spec.ts
+++ b/lib/manager/terraform/extract.spec.ts
@@ -16,8 +16,8 @@ describe('lib/manager/terraform/extract', () => {
     it('extracts', () => {
       const res = extractPackageFile(tf1);
       expect(res).toMatchSnapshot();
-      expect(res.deps).toHaveLength(30);
-      expect(res.deps.filter((dep) => dep.skipReason)).toHaveLength(6);
+      expect(res.deps).toHaveLength(38);
+      expect(res.deps.filter((dep) => dep.skipReason)).toHaveLength(11);
     });
     it('returns null if only local deps', () => {
       expect(extractPackageFile(tf2)).toBeNull();
diff --git a/lib/manager/terraform/extract.ts b/lib/manager/terraform/extract.ts
index ec979a9277..5c55438fd6 100644
--- a/lib/manager/terraform/extract.ts
+++ b/lib/manager/terraform/extract.ts
@@ -12,6 +12,7 @@ import {
 } from './resources';
 import {
   TerraformDependencyTypes,
+  TerraformManagerData,
   checkFileContainsDependency,
   getTerraformDependencyType,
 } from './util';
@@ -22,6 +23,7 @@ const contentCheckList = [
   'provider "',
   'required_providers ',
   ' "helm_release" ',
+  ' "docker_image" ',
 ];
 
 export function extractPackageFile(content: string): PackageFile | null {
@@ -29,7 +31,7 @@ export function extractPackageFile(content: string): PackageFile | null {
   if (!checkFileContainsDependency(content, contentCheckList)) {
     return null;
   }
-  let deps: PackageDependency[] = [];
+  let deps: PackageDependency<TerraformManagerData>[] = [];
   try {
     const lines = content.split('\n');
     for (let lineNumber = 0; lineNumber < lines.length; lineNumber += 1) {
diff --git a/lib/manager/terraform/resources.ts b/lib/manager/terraform/resources.ts
index 793b4ced50..d5ac8ba51c 100644
--- a/lib/manager/terraform/resources.ts
+++ b/lib/manager/terraform/resources.ts
@@ -2,37 +2,62 @@ import * as datasourceHelm from '../../datasource/helm';
 import { SkipReason } from '../../types';
 import { isValid } from '../../versioning/hashicorp';
 import { PackageDependency } from '../common';
+import { getDep } from '../dockerfile/extract';
 import {
   ExtractionResult,
+  ResourceManagerData,
   TerraformDependencyTypes,
+  TerraformResourceTypes,
   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;
-  let line;
+  let line = lines[lineNumber];
   const deps: PackageDependency[] = [];
-  const dep: PackageDependency = {
+  const dep: PackageDependency<ResourceManagerData> = {
     managerData: {
       terraformDependencyType: TerraformDependencyTypes.resource,
     },
   };
 
+  const typeMatch = resourceTypeExtractionRegex.exec(line);
+
+  dep.managerData.resourceType =
+    TerraformResourceTypes[typeMatch?.groups?.type] ??
+    TerraformResourceTypes.unknown;
+
   do {
     lineNumber += 1;
     line = lines[lineNumber];
     const kvMatch = keyValueExtractionRegex.exec(line);
     if (kvMatch) {
-      if (kvMatch.groups.key === 'version') {
-        dep.currentValue = kvMatch.groups.value;
-      } else if (kvMatch.groups.key === 'chart') {
-        dep.managerData.chart = kvMatch.groups.value;
-      } else if (kvMatch.groups.key === 'repository') {
-        dep.managerData.repository = kvMatch.groups.value;
+      switch (kvMatch.groups.key) {
+        case 'chart':
+        case 'image':
+        case 'name':
+        case 'repository':
+          dep.managerData[kvMatch.groups.key] = kvMatch.groups.value;
+          break;
+        case 'version':
+          dep.currentValue = kvMatch.groups.value;
+          break;
+        default:
+          /* istanbul ignore next */
+          break;
       }
     }
   } while (line.trim() !== '}');
@@ -40,19 +65,54 @@ export function extractTerraformResource(
   return { lineNumber, dependencies: deps };
 }
 
-export function analyseTerraformResource(dep: PackageDependency): void {
+export function analyseTerraformResource(
+  dep: PackageDependency<ResourceManagerData>
+): void {
   /* eslint-disable no-param-reassign */
-  if (dep.managerData.chart == null) {
-    dep.skipReason = SkipReason.InvalidName;
-  } else if (checkIfStringIsPath(dep.managerData.chart)) {
-    dep.skipReason = SkipReason.LocalChart;
-  } else if (!isValid(dep.currentValue)) {
-    dep.skipReason = SkipReason.UnsupportedVersion;
+
+  switch (dep.managerData.resourceType) {
+    case TerraformResourceTypes.docker_container:
+      if (!dep.managerData.image) {
+        dep.skipReason = SkipReason.InvalidDependencySpecification;
+      } else {
+        applyDockerDependency(dep, dep.managerData.image);
+      }
+      break;
+
+    case TerraformResourceTypes.docker_image:
+      if (!dep.managerData.name) {
+        dep.skipReason = SkipReason.InvalidDependencySpecification;
+      } else {
+        applyDockerDependency(dep, dep.managerData.name);
+      }
+      break;
+
+    case TerraformResourceTypes.docker_service:
+      if (!dep.managerData.image) {
+        dep.skipReason = SkipReason.InvalidDependencySpecification;
+      } else {
+        applyDockerDependency(dep, dep.managerData.image);
+      }
+      break;
+
+    case TerraformResourceTypes.helm_release:
+      if (dep.managerData.chart == null) {
+        dep.skipReason = SkipReason.InvalidName;
+      } else if (checkIfStringIsPath(dep.managerData.chart)) {
+        dep.skipReason = SkipReason.LocalChart;
+      } else if (!isValid(dep.currentValue)) {
+        dep.skipReason = SkipReason.UnsupportedVersion;
+      }
+      dep.depType = 'helm';
+      dep.registryUrls = [dep.managerData.repository];
+      dep.depName = dep.managerData.chart;
+      dep.depNameShort = dep.managerData.chart;
+      dep.datasource = datasourceHelm.id;
+      break;
+
+    default:
+      dep.skipReason = SkipReason.UnsupportedValue;
+      break;
   }
-  dep.depType = 'helm';
-  dep.registryUrls = [dep.managerData.repository];
-  dep.depName = dep.managerData.chart;
-  dep.depNameShort = dep.managerData.chart;
-  dep.datasource = datasourceHelm.id;
   /* eslint-enable no-param-reassign */
 }
diff --git a/lib/manager/terraform/util.ts b/lib/manager/terraform/util.ts
index c2fd55855b..80dfada2cf 100644
--- a/lib/manager/terraform/util.ts
+++ b/lib/manager/terraform/util.ts
@@ -1,6 +1,7 @@
 import { PackageDependency } from '../common';
 
 export const keyValueExtractionRegex = /^\s*(?<key>[^\s]+)\s+=\s+"(?<value>[^"]+)"\s*$/;
+export const resourceTypeExtractionRegex = /^\s*resource\s+"(?<type>[^\s]+)"\s+"(?<name>[^"]+)"\s*{/;
 
 export interface ExtractionResult {
   lineNumber: number;
@@ -15,6 +16,38 @@ export enum TerraformDependencyTypes {
   resource = 'resource',
 }
 
+export interface TerraformManagerData {
+  terraformDependencyType: TerraformDependencyTypes;
+}
+
+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',
+}
+
+export interface ResourceManagerData extends TerraformManagerData {
+  resourceType?: TerraformResourceTypes;
+  chart?: string;
+  image?: string;
+  name?: string;
+  repository?: string;
+}
+
 export function getTerraformDependencyType(
   value: string
 ): TerraformDependencyTypes {
-- 
GitLab