Skip to content

Commit

Permalink
feat(manager/terraform): support range strategy update lockfile (#11720)
Browse files Browse the repository at this point in the history
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
  • Loading branch information
secustor and viceice committed Sep 15, 2021
1 parent d05137f commit 12ae0d1
Show file tree
Hide file tree
Showing 12 changed files with 257 additions and 56 deletions.
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",
],
},
],
}
`;
54 changes: 44 additions & 10 deletions lib/manager/terraform/extract.spec.ts
@@ -1,32 +1,66 @@
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 () => {
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 () => {
expect(await extractPackageFile(tf2, '2.tf', {})).toBeNull();
});
it('extract helm releases', () => {
const res = extractPackageFile(helm);

it('extract helm releases', async () => {
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);
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 type { ExtractConfig } from '../types';
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);
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

0 comments on commit 12ae0d1

Please sign in to comment.