packages/@aws-cdk/toolkit-lib/lib/api/bootstrap/deploy-bootstrap.ts (131 lines of code) (raw):
import * as os from 'os';
import * as path from 'path';
import { ArtifactType } from '@aws-cdk/cloud-assembly-schema';
import type { Environment } from '@aws-cdk/cx-api';
import { CloudAssemblyBuilder, EnvironmentUtils } from '@aws-cdk/cx-api';
import * as fs from 'fs-extra';
import type { BootstrapEnvironmentOptions } from './bootstrap-props';
import {
BOOTSTRAP_VARIANT_PARAMETER,
BOOTSTRAP_VERSION_OUTPUT,
BOOTSTRAP_VERSION_RESOURCE,
DEFAULT_BOOTSTRAP_VARIANT,
} from './bootstrap-props';
import type { SDK, SdkProvider } from '../aws-auth/private';
import type { SuccessfulDeployStackResult } from '../deployments';
import { assertIsSuccessfulDeployStackResult } from '../deployments';
import { deployStack } from '../deployments/deploy-stack';
import { NoBootstrapStackEnvironmentResources } from '../environment';
import { IO, type IoHelper } from '../io/private';
import { Mode } from '../plugin';
import { DEFAULT_TOOLKIT_STACK_NAME, ToolkitInfo } from '../toolkit-info';
/**
* A class to hold state around stack bootstrapping
*
* This class exists so we can break bootstrapping into 2 phases:
*
* ```ts
* const current = BootstrapStack.lookup(...);
* // ...
* current.update(newTemplate, ...);
* ```
*
* And do something in between the two phases (such as look at the
* current bootstrap stack and doing something intelligent).
*/
export class BootstrapStack {
public static async lookup(sdkProvider: SdkProvider, environment: Environment, toolkitStackName: string, ioHelper: IoHelper) {
toolkitStackName = toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME;
const resolvedEnvironment = await sdkProvider.resolveEnvironment(environment);
const sdk = (await sdkProvider.forEnvironment(resolvedEnvironment, Mode.ForWriting)).sdk;
const currentToolkitInfo = await ToolkitInfo.lookup(resolvedEnvironment, sdk, ioHelper, toolkitStackName);
return new BootstrapStack(sdkProvider, sdk, resolvedEnvironment, toolkitStackName, currentToolkitInfo, ioHelper);
}
protected constructor(
private readonly sdkProvider: SdkProvider,
private readonly sdk: SDK,
private readonly resolvedEnvironment: Environment,
private readonly toolkitStackName: string,
private readonly currentToolkitInfo: ToolkitInfo,
private readonly ioHelper: IoHelper,
) {
}
public get parameters(): Record<string, string> {
return this.currentToolkitInfo.found ? this.currentToolkitInfo.bootstrapStack.parameters : {};
}
public get terminationProtection() {
return this.currentToolkitInfo.found ? this.currentToolkitInfo.bootstrapStack.terminationProtection : undefined;
}
public async partition(): Promise<string> {
return (await this.sdk.currentAccount()).partition;
}
/**
* Perform the actual deployment of a bootstrap stack, given a template and some parameters
*/
public async update(
template: any,
parameters: Record<string, string | undefined>,
options: Omit<BootstrapEnvironmentOptions, 'parameters'>,
): Promise<SuccessfulDeployStackResult> {
if (this.currentToolkitInfo.found && !options.forceDeployment) {
// Safety checks
const abortResponse = {
type: 'did-deploy-stack',
noOp: true,
outputs: {},
stackArn: this.currentToolkitInfo.bootstrapStack.stackId,
} satisfies SuccessfulDeployStackResult;
// Validate that the bootstrap stack we're trying to replace is from the same variant as the one we're trying to deploy
const currentVariant = this.currentToolkitInfo.variant;
const newVariant = bootstrapVariantFromTemplate(template);
if (currentVariant !== newVariant) {
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_WARN.msg(
`Bootstrap stack already exists, containing '${currentVariant}'. Not overwriting it with a template containing '${newVariant}' (use --force if you intend to overwrite)`,
));
return abortResponse;
}
// Validate that we're not downgrading the bootstrap stack
const newVersion = bootstrapVersionFromTemplate(template);
const currentVersion = this.currentToolkitInfo.version;
if (newVersion < currentVersion) {
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_WARN.msg(
`Bootstrap stack already at version ${currentVersion}. Not downgrading it to version ${newVersion} (use --force if you intend to downgrade)`,
));
if (newVersion === 0) {
// A downgrade with 0 as target version means we probably have a new-style bootstrap in the account,
// and an old-style bootstrap as current target, which means the user probably forgot to put this flag in.
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_WARN.msg(
"(Did you set the '@aws-cdk/core:newStyleStackSynthesis' feature flag in cdk.json?)",
));
}
return abortResponse;
}
}
const outdir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-bootstrap'));
const builder = new CloudAssemblyBuilder(outdir);
const templateFile = `${this.toolkitStackName}.template.json`;
await fs.writeJson(path.join(builder.outdir, templateFile), template, {
spaces: 2,
});
builder.addArtifact(this.toolkitStackName, {
type: ArtifactType.AWS_CLOUDFORMATION_STACK,
environment: EnvironmentUtils.format(this.resolvedEnvironment.account, this.resolvedEnvironment.region),
properties: {
templateFile,
terminationProtection: options.terminationProtection ?? false,
},
});
const assembly = builder.buildAssembly();
const ret = await deployStack({
stack: assembly.getStackByName(this.toolkitStackName),
resolvedEnvironment: this.resolvedEnvironment,
sdk: this.sdk,
sdkProvider: this.sdkProvider,
forceDeployment: options.forceDeployment,
roleArn: options.roleArn,
tags: options.tags,
deploymentMethod: { method: 'change-set', execute: options.execute },
parameters,
usePreviousParameters: options.usePreviousParameters ?? true,
// Obviously we can't need a bootstrap stack to deploy a bootstrap stack
envResources: new NoBootstrapStackEnvironmentResources(this.resolvedEnvironment, this.sdk, this.ioHelper),
}, this.ioHelper);
assertIsSuccessfulDeployStackResult(ret);
return ret;
}
}
export function bootstrapVersionFromTemplate(template: any): number {
const versionSources = [
template.Outputs?.[BOOTSTRAP_VERSION_OUTPUT]?.Value,
template.Resources?.[BOOTSTRAP_VERSION_RESOURCE]?.Properties?.Value,
];
for (const vs of versionSources) {
if (typeof vs === 'number') {
return vs;
}
if (typeof vs === 'string' && !isNaN(parseInt(vs, 10))) {
return parseInt(vs, 10);
}
}
return 0;
}
export function bootstrapVariantFromTemplate(template: any): string {
return template.Parameters?.[BOOTSTRAP_VARIANT_PARAMETER]?.Default ?? DEFAULT_BOOTSTRAP_VARIANT;
}