From 3f0303c16783dcf64cba128b250a0d9b8b3c6598 Mon Sep 17 00:00:00 2001
From: Sebastian Poxhofer <secustor@users.noreply.github.com>
Date: Sun, 2 Oct 2022 07:14:59 +0200
Subject: [PATCH] fix(datasource/terraform): support absolute URLs in service
 discovery (#18040)

---
 .../datasource/terraform-module/index.ts      | 18 +++-
 .../datasource/terraform-module/types.ts      |  2 +
 .../datasource/terraform-module/utils.spec.ts | 85 +++++++++++++++++++
 .../datasource/terraform-module/utils.ts      | 19 +++++
 .../datasource/terraform-provider/index.ts    | 22 ++++-
 5 files changed, 139 insertions(+), 7 deletions(-)
 create mode 100644 lib/modules/datasource/terraform-module/utils.spec.ts
 create mode 100644 lib/modules/datasource/terraform-module/utils.ts

diff --git a/lib/modules/datasource/terraform-module/index.ts b/lib/modules/datasource/terraform-module/index.ts
index 7e412935d5..eda82254ae 100644
--- a/lib/modules/datasource/terraform-module/index.ts
+++ b/lib/modules/datasource/terraform-module/index.ts
@@ -10,6 +10,7 @@ import type {
   TerraformModuleVersions,
   TerraformRelease,
 } from './types';
+import { createSDBackendURL } from './utils';
 
 export class TerraformModuleDatasource extends TerraformDatasource {
   static override readonly id = 'terraform-module';
@@ -81,8 +82,13 @@ export class TerraformModuleDatasource extends TerraformDatasource {
 
     try {
       // TODO: types (#7154)
-      // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-      pkgUrl = `${registryUrl}${serviceDiscovery['modules.v1']}${repository}`;
+
+      pkgUrl = createSDBackendURL(
+        registryUrl,
+        'modules.v1',
+        serviceDiscovery,
+        repository
+      );
       res = (await this.http.getJson<TerraformRelease>(pkgUrl)).body;
       const returnedName = res.namespace + '/' + res.name + '/' + res.provider;
       if (returnedName !== repository) {
@@ -126,8 +132,12 @@ export class TerraformModuleDatasource extends TerraformDatasource {
     let pkgUrl: string;
     try {
       // TODO: types (#7154)
-      // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-      pkgUrl = `${registryUrl}${serviceDiscovery['modules.v1']}${repository}/versions`;
+      pkgUrl = createSDBackendURL(
+        registryUrl,
+        'modules.v1',
+        serviceDiscovery,
+        `${repository}/versions`
+      );
       res = (await this.http.getJson<TerraformModuleVersions>(pkgUrl)).body;
       if (res.modules.length < 1) {
         logger.warn({ pkgUrl }, 'Terraform registry result mismatch');
diff --git a/lib/modules/datasource/terraform-module/types.ts b/lib/modules/datasource/terraform-module/types.ts
index 7e32a0b8f6..c394ee55a4 100644
--- a/lib/modules/datasource/terraform-module/types.ts
+++ b/lib/modules/datasource/terraform-module/types.ts
@@ -32,3 +32,5 @@ export interface ServiceDiscoveryResult {
   'modules.v1'?: string;
   'providers.v1'?: string;
 }
+
+export type ServiceDiscoveryEndpointType = 'modules.v1' | 'providers.v1';
diff --git a/lib/modules/datasource/terraform-module/utils.spec.ts b/lib/modules/datasource/terraform-module/utils.spec.ts
new file mode 100644
index 0000000000..5477b62473
--- /dev/null
+++ b/lib/modules/datasource/terraform-module/utils.spec.ts
@@ -0,0 +1,85 @@
+import { createSDBackendURL } from './utils';
+
+describe('modules/datasource/terraform-module/utils', () => {
+  describe('createSDBackendURL', () => {
+    const defaultRegistryURL = 'https://registry.example.com';
+
+    it('returns URL with relative SD for modules', () => {
+      const result = createSDBackendURL(
+        defaultRegistryURL,
+        'modules.v1',
+        {
+          'modules.v1': '/v1/modules/',
+        },
+        'hashicorp/consul/aws'
+      );
+      expect(result).toBe(
+        'https://registry.example.com/v1/modules/hashicorp/consul/aws'
+      );
+    });
+
+    it('returns URL with relative SD for providers', () => {
+      const result = createSDBackendURL(
+        defaultRegistryURL,
+        'providers.v1',
+        {
+          'providers.v1': '/v1/providers/',
+        },
+        'hashicorp/azure'
+      );
+      expect(result).toBe(
+        'https://registry.example.com/v1/providers/hashicorp/azure'
+      );
+    });
+
+    it('returns URL with absolute SD  for modules', () => {
+      const result = createSDBackendURL(
+        defaultRegistryURL,
+        'modules.v1',
+        {
+          'modules.v1': 'https://other.example.com/v1/modules/',
+        },
+        'hashicorp/consul/aws'
+      );
+      expect(result).toBe(
+        'https://other.example.com/v1/modules/hashicorp/consul/aws'
+      );
+    });
+
+    it('returns URL with absolute SD for providers and missing trailing slash', () => {
+      const result = createSDBackendURL(
+        defaultRegistryURL,
+        'providers.v1',
+        {
+          'providers.v1': 'https://other.example.com/providers',
+        },
+        'hashicorp/azure'
+      );
+      expect(result).toBe(
+        'https://other.example.com/providers/hashicorp/azure'
+      );
+    });
+
+    it('returns URL with with empty SD', () => {
+      const result = createSDBackendURL(
+        defaultRegistryURL,
+        'providers.v1',
+        {
+          'providers.v1': '',
+        },
+        'hashicorp/azure'
+      );
+      expect(result).toBe('https://registry.example.com/hashicorp/azure');
+    });
+
+    it('returns URL with with missing SD', () => {
+      const result = createSDBackendURL(
+        defaultRegistryURL,
+        'providers.v1',
+        {},
+        'hashicorp/azure'
+      );
+      expect(result).toBe('https://registry.example.com/hashicorp/azure');
+    });
+  });
+});
diff --git a/lib/modules/datasource/terraform-module/utils.ts b/lib/modules/datasource/terraform-module/utils.ts
new file mode 100644
index 0000000000..02195cd825
--- /dev/null
+++ b/lib/modules/datasource/terraform-module/utils.ts
@@ -0,0 +1,19 @@
+import { joinUrlParts, validateUrl } from '../../../util/url';
+import type {
+  ServiceDiscoveryEndpointType,
+  ServiceDiscoveryResult,
+} from './types';
+
+export function createSDBackendURL(
+  registryURL: string,
+  sdType: ServiceDiscoveryEndpointType,
+  sdResult: ServiceDiscoveryResult,
+  subPath: string
+): string {
+  const sdEndpoint = sdResult[sdType] ?? '';
+  const fullPath = joinUrlParts(sdEndpoint, subPath);
+  if (validateUrl(fullPath)) {
+    return fullPath;
+  }
+  return joinUrlParts(registryURL, fullPath);
+}
diff --git a/lib/modules/datasource/terraform-provider/index.ts b/lib/modules/datasource/terraform-provider/index.ts
index c968a5eecf..018549f4a1 100644
--- a/lib/modules/datasource/terraform-provider/index.ts
+++ b/lib/modules/datasource/terraform-provider/index.ts
@@ -9,6 +9,7 @@ import { regEx } from '../../../util/regex';
 import * as hashicorpVersioning from '../../versioning/hashicorp';
 import { TerraformDatasource } from '../terraform-module/base';
 import type { ServiceDiscoveryResult } from '../terraform-module/types';
+import { createSDBackendURL } from '../terraform-module/utils';
 import type { GetReleasesConfig, ReleaseResult } from '../types';
 import type {
   TerraformBuild,
@@ -97,7 +98,12 @@ export class TerraformProviderDatasource extends TerraformDatasource {
     registryUrl: string,
     repository: string
   ): Promise<ReleaseResult> {
-    const backendURL = `${registryUrl}${serviceDiscovery['providers.v1']}${repository}`;
+    const backendURL = createSDBackendURL(
+      registryUrl,
+      'providers.v1',
+      serviceDiscovery,
+      repository
+    );
     const res = (await this.http.getJson<TerraformProvider>(backendURL)).body;
     const dep: ReleaseResult = {
       releases: res.versions.map((version) => ({
@@ -128,7 +134,12 @@ export class TerraformProviderDatasource extends TerraformDatasource {
     registryUrl: string,
     repository: string
   ): Promise<ReleaseResult> {
-    const backendURL = `${registryUrl}${serviceDiscovery['providers.v1']}${repository}/versions`;
+    const backendURL = createSDBackendURL(
+      registryUrl,
+      'providers.v1',
+      serviceDiscovery,
+      `${repository}/versions`
+    );
     const res = (await this.http.getJson<TerraformProviderVersions>(backendURL))
       .body;
     const dep: ReleaseResult = {
@@ -211,7 +222,12 @@ export class TerraformProviderDatasource extends TerraformDatasource {
       logger.trace(`Failed to retrieve service discovery from ${registryURL}`);
       return null;
     }
-    const backendURL = `${registryURL}${serviceDiscovery['providers.v1']}${repository}`;
+    const backendURL = createSDBackendURL(
+      registryURL,
+      'providers.v1',
+      serviceDiscovery,
+      repository
+    );
     const versionsResponse = (
       await this.http.getJson<TerraformRegistryVersions>(
         `${backendURL}/versions`
-- 
GitLab