Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(manager/terraform): support range strategy update lockfile #11720

Merged
2 changes: 1 addition & 1 deletion docs/usage/configuration-options.md
Expand Up @@ -1860,7 +1860,7 @@ Behavior:
- `bump` = e.g. bump the range even if the new version satisfies the existing range, e.g. `^1.0.0` -> `^1.1.0`
- `replace` = Replace the range with a newer one if the new version falls outside it, e.g. `^1.0.0` -> `^2.0.0`
- `widen` = Widen the range with newer one, e.g. `^1.0.0` -> `^1.0.0 || ^2.0.0`
- `update-lockfile` = Update the lock file when in-range updates are available, otherwise `replace` for updates out of range. Works for `bundler`, `composer`, `npm`, `yarn` and `poetry` so far
- `update-lockfile` = Update the lock file when in-range updates are available, otherwise `replace` for updates out of range. Works for `bundler`, `composer`, `npm`, `yarn`, `terraform` and `poetry` so far

Renovate's `"auto"` strategy works like this for npm:

Expand Down
15 changes: 15 additions & 0 deletions lib/manager/terraform/__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"
}
}
}
32 changes: 32 additions & 0 deletions lib/manager/terraform/__fixtures__/rangeStrategy.hcl
@@ -0,0 +1,32 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/aws" {
version = "3.1.0"
constraints = "~> 3.0"
hashes = [
"h1:ULKfwySvQ4pDhy027ryRhLxDhg640wsojYc+7NHMFBU=",
"zh:df568a69087831c1780fac4395630a2cfb3cdf67b7dffbfe16bd78c64770bb75",
"zh:fce1b69dd673aace19508640b0b9b7eb1ef7e746d76cb846b49e7d52e0f5fb7e",
]
}

provider "registry.terraform.io/hashicorp/azurerm" {
version = "2.50.0"
constraints = "~> 2.50.0"
hashes = [
"h1:Vr6WUm88s9hXGkyVjHtHsP2Jmc2ypQXn6ww7dXtvk1M=",
"zh:e98f1d178d1e111b3f3449e27d305ce263071226fad3d86272e1bd161c26fd43",
"zh:eb76ec000c9c49a0bf730370c8880f671597bc01f7b7401ab301df7124c049ec",
]
}

provider "https://terraform.example.com/example/kubernetes" {
version = "1.5.0"
constraints = ">= 1.0"
hashes = [
"h1:Vr6WUm88s9hXGkyVjHtHsP2Jmc2ypQXn6ww7dXtvk1M=",
"zh:e98f1d178d1e111b3f3449e27d305ce263071226fad3d86272e1bd161c26fd43",
"zh:eb76ec000c9c49a0bf730370c8880f671597bc01f7b7401ab301df7124c049ec",
]
}
57 changes: 56 additions & 1 deletion lib/manager/terraform/__snapshots__/extract.spec.ts.snap
Expand Up @@ -187,29 +187,39 @@ Object {
"datasource": "terraform-provider",
"depName": "azurerm",
"depType": "provider",
"lockedVersion": undefined,
"lookupName": "hashicorp/azurerm",
},
Object {
"currentValue": "=2.4",
"datasource": "terraform-provider",
"depName": "gitlab",
"depType": "provider",
"lockedVersion": undefined,
"lookupName": "hashicorp/gitlab",
},
Object {
"currentValue": "=1.3",
"datasource": "terraform-provider",
"depName": "gitlab",
"depType": "provider",
"lockedVersion": undefined,
"lookupName": "hashicorp/gitlab",
},
Object {
"datasource": "terraform-provider",
"depName": "helm",
"depType": "provider",
"lockedVersion": undefined,
"lookupName": "hashicorp/helm",
},
Object {
"currentValue": "V1.9",
"datasource": "terraform-provider",
"depName": "newrelic",
"depType": "provider",
"lockedVersion": undefined,
"lookupName": "hashicorp/newrelic",
},
Object {
"currentValue": "v1.0.0",
Expand Down Expand Up @@ -258,12 +268,16 @@ Object {
"datasource": "terraform-provider",
"depName": "aws",
"depType": "required_provider",
"lockedVersion": undefined,
"lookupName": "hashicorp/aws",
},
Object {
"currentValue": ">= 2.0.0",
"datasource": "terraform-provider",
"depName": "azurerm",
"depType": "required_provider",
"lockedVersion": undefined,
"lookupName": "hashicorp/azurerm",
},
Object {
"currentValue": ">= 0.13",
Expand All @@ -278,6 +292,8 @@ Object {
"datasource": "terraform-provider",
"depName": "docker",
"depType": "required_provider",
"lockedVersion": undefined,
"lookupName": "hashicorp/docker",
"registryUrls": Array [
"https://releases.hashicorp.com",
],
Expand All @@ -287,13 +303,16 @@ Object {
"datasource": "terraform-provider",
"depName": "aws",
"depType": "required_provider",
"lookupName": "aws",
"lockedVersion": undefined,
"lookupName": "hashicorp/aws",
},
Object {
"currentValue": "=2.27.0",
"datasource": "terraform-provider",
"depName": "azurerm",
"depType": "required_provider",
"lockedVersion": undefined,
"lookupName": "hashicorp/azurerm",
},
Object {
"currentValue": "1.2.4",
Expand All @@ -307,13 +326,15 @@ Object {
"datasource": "terraform-provider",
"depName": "helm",
"depType": "required_provider",
"lockedVersion": undefined,
"lookupName": "hashicorp/helm",
},
Object {
"currentValue": ">= 1.0",
"datasource": "terraform-provider",
"depName": "kubernetes",
"depType": "required_provider",
"lockedVersion": undefined,
"lookupName": "hashicorp/kubernetes",
"registryUrls": Array [
"https://terraform.example.com",
Expand Down Expand Up @@ -368,3 +389,37 @@ Object {
],
}
`;

exports[`manager/terraform/extract extractPackageFile() update lockfile constraints with range strategy update-lockfile 1`] = `
Object {
"deps": Array [
Object {
"currentValue": "~> 3.0",
"datasource": "terraform-provider",
"depName": "aws",
"depType": "required_provider",
"lockedVersion": "3.1.0",
"lookupName": "hashicorp/aws",
},
Object {
"currentValue": "~> 2.50.0",
"datasource": "terraform-provider",
"depName": "azurerm",
"depType": "required_provider",
"lockedVersion": "2.50.0",
"lookupName": "hashicorp/azurerm",
},
Object {
"currentValue": ">= 1.0",
"datasource": "terraform-provider",
"depName": "kubernetes",
"depType": "required_provider",
"lockedVersion": undefined,
"lookupName": "example/kubernetes",
"registryUrls": Array [
"https://terraform.example.com",
],
},
],
}
`;
51 changes: 41 additions & 10 deletions lib/manager/terraform/extract.spec.ts
@@ -1,32 +1,63 @@
import { loadFixture } from '../../../test/util';
import { extractPackageFile } from './extract';
import { join } from 'upath';
import { fs, loadFixture } from '../../../test/util';
import { setGlobalConfig } from '../../config/global';
import type { RepoGlobalConfig } from '../../config/types';
import { extractPackageFile } from '.';

const tf1 = loadFixture('1.tf');
const tf2 = `module "relative" {
source = "../../modules/fe"
}
`;
const helm = loadFixture('helm.tf');
const lockedVersion = loadFixture('lockedVersion.tf');
const lockedVersionLockfile = loadFixture('rangeStrategy.hcl');

const adminConfig: RepoGlobalConfig = {
// `join` fixes Windows CI
localDir: join('/tmp/github/some/repo'),
cacheDir: join('/tmp/cache'),
};

// auto-mock fs
jest.mock('../../util/fs');

describe('manager/terraform/extract', () => {
beforeEach(() => {
setGlobalConfig(adminConfig);
});
describe('extractPackageFile()', () => {
it('returns null for empty', () => {
expect(extractPackageFile('nothing here')).toBeNull();
it('returns null for empty', async () => {
expect(await extractPackageFile('nothing here', '1.tf', {})).toBeNull();
});
it('extracts', () => {
const res = extractPackageFile(tf1);
it('extracts', async () => {
viceice marked this conversation as resolved.
Show resolved Hide resolved
const res = await extractPackageFile(tf1, '1.tf', {});
expect(res).toMatchSnapshot();
expect(res.deps).toHaveLength(46);
expect(res.deps.filter((dep) => dep.skipReason)).toHaveLength(8);
});
it('returns null if only local deps', () => {
expect(extractPackageFile(tf2)).toBeNull();
it('returns null if only local deps', async () => {
viceice marked this conversation as resolved.
Show resolved Hide resolved
expect(await extractPackageFile(tf2, '2.tf', {})).toBeNull();
});
it('extract helm releases', () => {
const res = extractPackageFile(helm);
it('extract helm releases', async () => {
viceice marked this conversation as resolved.
Show resolved Hide resolved
const res = await extractPackageFile(helm, 'helm.tf', {});
expect(res).toMatchSnapshot();
expect(res.deps).toHaveLength(6);
expect(res.deps.filter((dep) => dep.skipReason)).toHaveLength(2);
});

it('update lockfile constraints with range strategy update-lockfile', async () => {
fs.readLocalFile.mockResolvedValueOnce(lockedVersionLockfile as any);
secustor marked this conversation as resolved.
Show resolved Hide resolved
fs.getSiblingFileName.mockReturnValueOnce('aLockFile.hcl');

const res = await extractPackageFile(
lockedVersion,
'lockedVersion.tf',
{}
);
expect(res).toMatchSnapshot();
expect(res.deps).toHaveLength(3);
expect(res.deps.filter((dep) => dep.skipReason)).toHaveLength(0);
});
});
});
23 changes: 20 additions & 3 deletions lib/manager/terraform/extract.ts
@@ -1,6 +1,8 @@
import { logger } from '../../logger';
import { ExtractConfig } from '../types';
secustor marked this conversation as resolved.
Show resolved Hide resolved
import type { PackageDependency, PackageFile } from '../types';
import { TerraformDependencyTypes } from './common';
import { extractLocks, findLockFile, readLockFile } from './lockfile/util';
import { analyseTerraformModule, extractTerraformModule } from './modules';
import {
analyzeTerraformProvider,
Expand Down Expand Up @@ -34,7 +36,11 @@ const contentCheckList = [
' "docker_image" ',
];

export function extractPackageFile(content: string): PackageFile | null {
export async function extractPackageFile(
content: string,
fileName: string,
config: ExtractConfig
): Promise<PackageFile | null> {
logger.trace({ content }, 'terraform.extractPackageFile()');
if (!checkFileContainsDependency(content, contentCheckList)) {
return null;
Expand Down Expand Up @@ -99,13 +105,24 @@ export function extractPackageFile(content: string): PackageFile | null {
} catch (err) /* istanbul ignore next */ {
logger.warn({ err }, 'Error extracting terraform plugins');
}

const locks = [];
const lockFilePath = findLockFile(fileName);
if (lockFilePath) {
const lockFileContent = await readLockFile(lockFilePath);
if (lockFileContent) {
const extractedLocks = extractLocks(lockFileContent);
locks.push(...extractedLocks);
}
}

deps.forEach((dep) => {
switch (dep.managerData.terraformDependencyType) {
case TerraformDependencyTypes.required_providers:
analyzeTerraformRequiredProvider(dep);
analyzeTerraformRequiredProvider(dep, locks);
break;
case TerraformDependencyTypes.provider:
analyzeTerraformProvider(dep);
analyzeTerraformProvider(dep, locks);
break;
case TerraformDependencyTypes.module:
analyseTerraformModule(dep);
Expand Down
26 changes: 10 additions & 16 deletions lib/manager/terraform/lockfile/index.ts
Expand Up @@ -4,6 +4,7 @@ import { TerraformProviderDatasource } from '../../../datasource/terraform-provi
import { logger } from '../../../logger';
import { get as getVersioning } from '../../../versioning';
import type { UpdateArtifact, UpdateArtifactsResult } from '../../types';
import { massageProviderLookupName } from '../util';
import { TerraformProviderHash } from './hash';
import type { ProviderLock, ProviderLockUpdate } from './types';
import {
Expand Down Expand Up @@ -85,30 +86,23 @@ export async function updateArtifacts({
['provider', 'required_provider'].includes(dep.depType)
);
for (const dep of providerDeps) {
const lookupName = dep.lookupName ?? dep.depName;
massageProviderLookupName(dep);
viceice marked this conversation as resolved.
Show resolved Hide resolved
const { registryUrls, newVersion, newValue, lookupName } = dep;

// handle cases like `Telmate/proxmox`
const massagedLookupName = lookupName.toLowerCase();

const repository = massagedLookupName.includes('/')
? massagedLookupName
: `hashicorp/${massagedLookupName}`;
const registryUrl = dep.registryUrls
? dep.registryUrls[0]
const registryUrl = registryUrls
? registryUrls[0]
: TerraformProviderDatasource.defaultRegistryUrls[0];
const newConstraint = isPinnedVersion(dep.newValue)
? dep.newVersion
: dep.newValue;
const newConstraint = isPinnedVersion(newValue) ? newVersion : newValue;
const updateLock = locks.find(
(value) => value.lookupName === repository
(value) => value.lookupName === lookupName
);
const update: ProviderLockUpdate = {
newVersion: dep.newVersion,
newVersion,
newConstraint,
newHashes: await TerraformProviderHash.createHashes(
registryUrl,
repository,
dep.newVersion
lookupName,
newVersion
),
...updateLock,
};
Expand Down