From a7faacc027aa4475ca44501082ca9c63393482af Mon Sep 17 00:00:00 2001
From: Sergei Zharinov <zharinov@users.noreply.github.com>
Date: Thu, 2 Nov 2023 09:46:16 -0300
Subject: [PATCH] fix(terraform): Reduce constraints changes in lockfiles
 (#25430)

Co-authored-by: Rhys Arkins <rhys@arkins.net>
---
 .../manager/terraform/lockfile/index.spec.ts  | 179 ++++++++++++++++++
 .../manager/terraform/lockfile/index.ts       |  59 +++++-
 2 files changed, 235 insertions(+), 3 deletions(-)

diff --git a/lib/modules/manager/terraform/lockfile/index.spec.ts b/lib/modules/manager/terraform/lockfile/index.spec.ts
index 669754fb2e..9db80efa40 100644
--- a/lib/modules/manager/terraform/lockfile/index.spec.ts
+++ b/lib/modules/manager/terraform/lockfile/index.spec.ts
@@ -910,4 +910,183 @@ describe('modules/manager/terraform/lockfile/index', () => {
     });
     expect(result).toBeNull();
   });
+
+  it('preserves constraints when current value and new value are same', async () => {
+    fs.readLocalFile.mockResolvedValueOnce(codeBlock`
+      provider "registry.terraform.io/hashicorp/aws" {
+        version     = "3.0.0"
+        constraints = "~> 3.0.0"
+        hashes = [
+          "aaa",
+          "bbb",
+          "ccc",
+        ]
+      }
+    `);
+    fs.findLocalSiblingOrParent.mockResolvedValueOnce('.terraform.lock.hcl');
+
+    mockHash.mockResolvedValueOnce([
+      'h1:lDsKRxDRXPEzA4AxkK4t+lJd3IQIP2UoaplJGjQSp2s=',
+      'h1:6zB2hX7YIOW26OrKsLJn0uLMnjqbPNxcz9RhlWEuuSY=',
+    ]);
+
+    const result = await updateArtifacts({
+      packageFileName: 'main.tf',
+      updatedDeps: [
+        {
+          depName: 'aws',
+          depType: 'provider',
+          packageName: 'hashicorp/aws',
+          registryUrls: ['https://registry.example.com'],
+          newVersion: '3.36.1',
+          currentValue: '~> 3.36',
+          newValue: '~> 3.36',
+        },
+      ],
+      newPackageFileContent: '',
+      config,
+    });
+
+    expect(result).toEqual([
+      {
+        file: {
+          contents: codeBlock`
+            provider "registry.terraform.io/hashicorp/aws" {
+              version     = "3.36.1"
+              constraints = "~> 3.0.0"
+              hashes = [
+                "h1:lDsKRxDRXPEzA4AxkK4t+lJd3IQIP2UoaplJGjQSp2s=",
+                "h1:6zB2hX7YIOW26OrKsLJn0uLMnjqbPNxcz9RhlWEuuSY=",
+              ]
+            }
+          `,
+          path: '.terraform.lock.hcl',
+          type: 'addition',
+        },
+      },
+    ]);
+
+    expect(mockHash.mock.calls).toEqual([
+      ['https://registry.example.com', 'hashicorp/aws', '3.36.1'],
+    ]);
+  });
+
+  it('replaces current value to new version within a constraint', async () => {
+    fs.readLocalFile.mockResolvedValueOnce(codeBlock`
+      provider "registry.terraform.io/hashicorp/aws" {
+        version     = "3.0.0"
+        constraints = "~> 3.0.0"
+        hashes = [
+          "aaa",
+          "bbb",
+          "ccc",
+        ]
+      }
+    `);
+    fs.findLocalSiblingOrParent.mockResolvedValueOnce('.terraform.lock.hcl');
+
+    mockHash.mockResolvedValueOnce([
+      'h1:lDsKRxDRXPEzA4AxkK4t+lJd3IQIP2UoaplJGjQSp2s=',
+      'h1:6zB2hX7YIOW26OrKsLJn0uLMnjqbPNxcz9RhlWEuuSY=',
+    ]);
+
+    const result = await updateArtifacts({
+      packageFileName: 'main.tf',
+      updatedDeps: [
+        {
+          depName: 'aws',
+          depType: 'provider',
+          packageName: 'hashicorp/aws',
+          registryUrls: ['https://registry.example.com'],
+          newVersion: '3.37.0',
+          currentValue: '~> 3.0.0',
+          newValue: '~> 3.37.0',
+        },
+      ],
+      newPackageFileContent: '',
+      config,
+    });
+
+    expect(result).toEqual([
+      {
+        file: {
+          contents: codeBlock`
+            provider "registry.terraform.io/hashicorp/aws" {
+              version     = "3.37.0"
+              constraints = "~> 3.37.0"
+              hashes = [
+                "h1:lDsKRxDRXPEzA4AxkK4t+lJd3IQIP2UoaplJGjQSp2s=",
+                "h1:6zB2hX7YIOW26OrKsLJn0uLMnjqbPNxcz9RhlWEuuSY=",
+              ]
+            }
+          `,
+          path: '.terraform.lock.hcl',
+          type: 'addition',
+        },
+      },
+    ]);
+
+    expect(mockHash.mock.calls).toEqual([
+      ['https://registry.example.com', 'hashicorp/aws', '3.37.0'],
+    ]);
+  });
+
+  it('replaces current version to new version within a constraint', async () => {
+    fs.readLocalFile.mockResolvedValueOnce(codeBlock`
+      provider "registry.terraform.io/hashicorp/aws" {
+        version     = "3.0.0"
+        constraints = "~> 3.0.0"
+        hashes = [
+          "aaa",
+          "bbb",
+          "ccc",
+        ]
+      }
+    `);
+    fs.findLocalSiblingOrParent.mockResolvedValueOnce('.terraform.lock.hcl');
+
+    mockHash.mockResolvedValueOnce([
+      'h1:lDsKRxDRXPEzA4AxkK4t+lJd3IQIP2UoaplJGjQSp2s=',
+      'h1:6zB2hX7YIOW26OrKsLJn0uLMnjqbPNxcz9RhlWEuuSY=',
+    ]);
+
+    const result = await updateArtifacts({
+      packageFileName: 'main.tf',
+      updatedDeps: [
+        {
+          depName: 'aws',
+          depType: 'provider',
+          packageName: 'hashicorp/aws',
+          registryUrls: ['https://registry.example.com'],
+          newVersion: '3.37.0',
+          currentVersion: '3.0.0',
+        },
+      ],
+      newPackageFileContent: '',
+      config,
+    });
+
+    expect(result).toEqual([
+      {
+        file: {
+          contents: codeBlock`
+            provider "registry.terraform.io/hashicorp/aws" {
+              version     = "3.37.0"
+              constraints = "~> 3.37.0"
+              hashes = [
+                "h1:lDsKRxDRXPEzA4AxkK4t+lJd3IQIP2UoaplJGjQSp2s=",
+                "h1:6zB2hX7YIOW26OrKsLJn0uLMnjqbPNxcz9RhlWEuuSY=",
+              ]
+            }
+          `,
+          path: '.terraform.lock.hcl',
+          type: 'addition',
+        },
+      },
+    ]);
+
+    expect(mockHash.mock.calls).toEqual([
+      ['https://registry.example.com', 'hashicorp/aws', '3.37.0'],
+    ]);
+  });
 });
diff --git a/lib/modules/manager/terraform/lockfile/index.ts b/lib/modules/manager/terraform/lockfile/index.ts
index 58c72065e2..287df2bff9 100644
--- a/lib/modules/manager/terraform/lockfile/index.ts
+++ b/lib/modules/manager/terraform/lockfile/index.ts
@@ -4,7 +4,11 @@ import * as p from '../../../../util/promises';
 import { GetPkgReleasesConfig, getPkgReleases } from '../../../datasource';
 import { TerraformProviderDatasource } from '../../../datasource/terraform-provider';
 import { get as getVersioning } from '../../../versioning';
-import type { UpdateArtifact, UpdateArtifactsResult } from '../../types';
+import type {
+  UpdateArtifact,
+  UpdateArtifactsResult,
+  Upgrade,
+} from '../../types';
 import { massageProviderLookupName } from '../util';
 import { TerraformProviderHash } from './hash';
 import type { ProviderLock, ProviderLockUpdate } from './types';
@@ -61,6 +65,55 @@ async function updateAllLocks(
   return updates.filter(is.truthy);
 }
 
+function getNewConstraint(
+  dep: Upgrade<Record<string, unknown>>,
+  oldConstraint: string | undefined
+): string | undefined {
+  const { currentValue, currentVersion, newValue, newVersion, packageName } =
+    dep;
+
+  if (oldConstraint && currentValue && newValue && currentValue === newValue) {
+    logger.debug(
+      `Leaving constraints "${oldConstraint}" unchanged for "${packageName}" as current and new values are the same`
+    );
+    return oldConstraint;
+  }
+
+  if (
+    oldConstraint &&
+    currentValue &&
+    newValue &&
+    oldConstraint.includes(currentValue)
+  ) {
+    logger.debug(
+      `Updating constraint "${oldConstraint}" to replace "${currentValue}" with "${newValue}" for "${packageName}"`
+    );
+    return oldConstraint.replace(currentValue, newValue);
+  }
+
+  if (
+    oldConstraint &&
+    currentVersion &&
+    newVersion &&
+    oldConstraint.includes(currentVersion)
+  ) {
+    logger.debug(
+      `Updating constraint "${oldConstraint}" to replace "${currentVersion}" with "${newVersion}" for "${packageName}"`
+    );
+    return oldConstraint.replace(currentVersion, newVersion);
+  }
+
+  if (isPinnedVersion(newValue)) {
+    logger.debug(`Pinning constraint for "${packageName}" to "${newVersion}"`);
+    return newVersion;
+  }
+
+  logger.debug(
+    `Could not detect constraint to update for "${packageName}" so setting to newValue "${newValue}"`
+  );
+  return newValue;
+}
+
 export async function updateArtifacts({
   packageFileName,
   updatedDeps,
@@ -99,12 +152,11 @@ export async function updateArtifacts({
       );
       for (const dep of providerDeps) {
         massageProviderLookupName(dep);
-        const { registryUrls, newVersion, newValue, packageName } = dep;
+        const { registryUrls, newVersion, packageName } = dep;
 
         const registryUrl = registryUrls
           ? registryUrls[0]
           : TerraformProviderDatasource.defaultRegistryUrls[0];
-        const newConstraint = isPinnedVersion(newValue) ? newVersion : newValue;
         const updateLock = locks.find(
           (value) => value.packageName === packageName
         );
@@ -112,6 +164,7 @@ export async function updateArtifacts({
         if (!updateLock) {
           continue;
         }
+        const newConstraint = getNewConstraint(dep, updateLock.constraints);
         const update: ProviderLockUpdate = {
           // TODO #22198
           newVersion: newVersion!,
-- 
GitLab