Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions nx.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
"outputs": ["{projectRoot}/dist"],
"cache": true
},
"package:js": { "dependsOn": ["build"] },
"package:python": { "dependsOn": ["build"] },
"package:java": { "dependsOn": ["build"] },
"package:dotnet": { "dependsOn": ["build"] },
"package:go": { "dependsOn": ["build"] },
"test": {
"dependsOn": [
"build",
Expand Down
55 changes: 8 additions & 47 deletions packages/@cdktn/cli-core/src/lib/cdktf-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ import {
tryReadGeneratedConfigurationFile,
tryRemoveGeneratedConfigurationFile,
} from "./models/terraform-cli";
import { ProviderConstraint } from "./dependencies/dependency-manager";
import { terraformJsonSchema, TerraformStack } from "./terraform-json";
import { TerraformProviderLock } from "./terraform-provider-lock";
import {
TerraformProviderLock,
needsLockfileUpdate,
needsUpgrade,
} from "./terraform-provider-lock";
import { convertConfigurationFile } from "./convert";

export type StackUpdate =
Expand Down Expand Up @@ -299,15 +302,7 @@ export class CdktfStack {
private requiredProviders() {
// Read required providers from the stack output
const requiredProviders = this.parsedContent.terraform?.required_providers;

return Object.values(requiredProviders || {}).reduce(
(acc, obj) => {
const constraint = new ProviderConstraint(obj.source, obj.version);
acc[constraint.source] = constraint;
return acc;
},
{} as Record<string, ProviderConstraint>,
);
return Object.values(requiredProviders || {});
}

private async checkNeedsLockfileUpdate(): Promise<boolean> {
Expand All @@ -323,46 +318,12 @@ export class CdktfStack {
return true;
}

const requiredProviders = this.requiredProviders();

for (const provider of Object.values(requiredProviders)) {
const hasProvider = await lock.hasMatchingProvider(provider);
if (!hasProvider) {
// If we don't have a provider or version doesn't match, we need to init
return true;
}
}

return false;
return needsLockfileUpdate(this.requiredProviders(), lock);
}

private async checkNeedsUpgrade(): Promise<boolean> {
const lock = new TerraformProviderLock(this.stack.workingDirectory);
const allProviders = this.requiredProviders();
const lockedProviders = Object.values(await lock.providers());

// Check if any provider contained in `providers` violates constraints in `lockedProviders`
// Upgrade if some provider constraint not met
// If a provider wasn't preset in lockedProviders, that's fine; it will just get added
return lockedProviders.some((lockedProvider) => {
const lockedConstraint = lockedProvider.constraints;
if (!lockedConstraint) {
// Provider lock doesn't have a constraint specified, so we can't check.
// This shouldn't happen
logger.debug(
`Provider lock doesn't have a constraint for ${lockedProvider.name}`,
);
return false;
}

const provider = allProviders[lockedConstraint.source];
if (!provider) {
// else no longer using this provider, so won't cause problems
return;
}

return !lockedConstraint.matchesVersion(provider.version ?? ">0");
});
return needsUpgrade(this.requiredProviders(), lock);
}

private async run(cb: () => Promise<void>) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,62 @@
// Copyright (c) HashiCorp, Inc
// SPDX-License-Identifier: MPL-2.0
import {
Registry,
TerraformDependencyConstraint,
registryForTargetVersions,
} from "@cdktn/commons";
import { CdktfConfig } from "../cdktf-config";
import { ProviderConstraint } from "./dependency-manager";

/**
* Drops the host when it is the project's own registry, so cdktf.json keeps the
* short form. A source qualified with any other host stays qualified.
*/
function simplifySource(source: string, registry: Registry): string {
const prefix = `${registry.hostname}/`;
return source.startsWith(prefix) ? source.slice(prefix.length) : source;
}

/**
* facilitates adding provider dependencies to cdktf.json
*/
export class CdktfConfigManager {
private config: CdktfConfig = CdktfConfig.read();

/** Registry an unqualified entry in this cdktf.json resolves against. */
private get registry(): Registry {
return registryForTargetVersions(this.config.targetVersions);
}

public async hasProvider(constraint: ProviderConstraint): Promise<boolean> {
const registry = this.registry;
return this.config.terraformProviders.some(
(provider) =>
ProviderConstraint.fromConfigEntry(provider).source ===
ProviderConstraint.fromConfigEntry(provider, registry).source ===
constraint.source,
);
}

private async setProvider(constraint: ProviderConstraint): Promise<void> {
const registry = this.registry;
const isSame = (provider: string | TerraformDependencyConstraint) =>
ProviderConstraint.fromConfigEntry(provider, registry).source ===
constraint.source;

// An update keeps the entry's own spelling, so a host the user wrote stays.
const existing = this.config.terraformProviders.find(isSame);
const source =
typeof existing === "string"
? existing.split("@")[0].trim()
: simplifySource(constraint.source, registry);

// excluding the constraint to be added (if it already existed)
const currentProviders = this.config.terraformProviders.filter(
(provider) =>
ProviderConstraint.fromConfigEntry(provider).source !==
constraint.source,
);

const simplifiedSource = constraint.source.replace(
"registry.terraform.io/",
"",
(provider) => !isSame(provider),
);

const provider =
simplifiedSource + (constraint.version ? `@${constraint.version}` : "");
source + (constraint.version ? `@${constraint.version}` : "");

currentProviders.push(provider);
this.config.writeTerraformProviders(currentProviders);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,24 @@ export class ProviderConstraint {
// TODO: add examples to cli command description (i.e. =,~>.> etc.)
// if no version constraint is specified, we assume the latest version
// if specific version is specified without e.g. =, we allow patch level increments (e.g. ~>2.12 for "2.12")
/** Whether the source as given named a host, rather than gaining one here. */
private readonly namedHost: boolean;

constructor(
source: string,
public readonly version: string | undefined,
registry: Registry = TERRAFORM_REGISTRY,
) {
this.source = normalizeProviderSource(source, registry);
this.namedHost = source.split("/").length === 3;
}

/**
* Source for a `required_providers` block. Keeps a host the user wrote; drops
* one added by normalization, since a host pins the CLI to that registry.
*/
public get requiredProvidersSource(): string {
return this.namedHost ? this.source : `${this.namespace}/${this.name}`;
}

static fromConfigEntry(
Expand Down
19 changes: 10 additions & 9 deletions packages/@cdktn/cli-core/src/lib/local-provider-versions.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
// Copyright (c) HashiCorp, Inc
// SPDX-License-Identifier: MPL-2.0
import { Errors, logger } from "@cdktn/commons";
import { Errors, logger, registryForHostname } from "@cdktn/commons";
import fs from "fs-extra";
import path from "path";
import { CdktfConfig } from "./cdktf-config";
import {
DEFAULT_HOSTNAME,
DEFAULT_NAMESPACE,
} from "./dependencies/dependency-manager";
import { DEFAULT_NAMESPACE } from "./dependencies/dependency-manager";

// TODO: move this to @cdktn/commons
// tracked here https://github.com/hashicorp/terraform-cdk/issues/1814
Expand Down Expand Up @@ -46,11 +43,15 @@ export class LocalProviderVersions {

this.versions = Object.fromEntries(
Object.entries(providerVersions).map(([providerFqn, versions]) => {
// Any public registry host, not just Terraform's: the host recorded here
// is whichever registry the fetching CLI used.
const [maybeHost, ...rest] = providerFqn.split("/");
const withoutHost = registryForHostname(maybeHost)
? rest.join("/")
: providerFqn;

return [
providerFqn
.replace(`${DEFAULT_HOSTNAME}/`, "")
.replace(`${DEFAULT_NAMESPACE}/`, "")
.toLowerCase(),
withoutHost.replace(`${DEFAULT_NAMESPACE}/`, "").toLowerCase(),
versions,
];
}),
Expand Down
69 changes: 68 additions & 1 deletion packages/@cdktn/cli-core/src/lib/terraform-provider-lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import path from "path";
import fs from "fs/promises";
import { ProviderConstraint } from "./dependencies/dependency-manager";
import { logger } from "@cdktn/commons";
import { PUBLIC_REGISTRIES, logger } from "@cdktn/commons";

const TerraformLockFileName = ".terraform.lock.hcl";

Expand All @@ -22,6 +22,73 @@ type TerraformProviderLockFileData = {
};
};

type RequiredProvider = { source: string; version?: string };

// A source that names a host is locked under exactly that host. A bare one is
// locked under whichever public registry the CLI resolved it from, so either
// counts: OpenTofu migrates a Terraform entry itself on init.
function lockAddressesFor(
source: string,
version?: string,
): ProviderConstraint[] {
if (source.split("/").length === 3) {
return [new ProviderConstraint(source, version)];
}
return PUBLIC_REGISTRIES.map(
(registry) => new ProviderConstraint(source, version, registry),
);
}

/** Whether `init` should be followed by a `providers lock` refresh. */
export async function needsLockfileUpdate(
required: RequiredProvider[],
lock: TerraformProviderLock,
): Promise<boolean> {
for (const { source, version } of required) {
let locked = false;
for (const address of lockAddressesFor(source, version)) {
if (await lock.hasMatchingProvider(address)) {
locked = true;
break;
}
}
if (!locked) {
return true;
}
}
return false;
}

/** Whether a locked version no longer satisfies a requirement, needing `init -upgrade`. */
export async function needsUpgrade(
required: RequiredProvider[],
lock: TerraformProviderLock,
): Promise<boolean> {
const lockedProviders = Object.values(await lock.providers());
return lockedProviders.some((lockedProvider) => {
const lockedConstraint = lockedProvider.constraints;
if (!lockedConstraint) {
// Provider lock doesn't have a constraint specified, so we can't check.
logger.debug(
`Provider lock doesn't have a constraint for ${lockedProvider.name}`,
);
return false;
}

const provider = required.find(({ source, version }) =>
lockAddressesFor(source, version).some(
(address) => address.source === lockedConstraint.source,
),
);
// No longer required by this stack, so it can't block the upgrade.
if (!provider) {
return false;
}

return !lockedConstraint.matchesVersion(provider.version ?? ">0");
});
}

export class TerraformProviderLock {
private _providerLockData: TerraformProviderLockFileData | null;
constructor(private stackWorkingDirectory: string) {
Expand Down
Loading
Loading