packages/aws-cdk/lib/cli/cdk-toolkit.ts (1,221 lines of code) (raw):
import * as path from 'path';
import { format } from 'util';
import * as cxapi from '@aws-cdk/cx-api';
import * as chalk from 'chalk';
import * as chokidar from 'chokidar';
import * as fs from 'fs-extra';
import * as promptly from 'promptly';
import * as uuid from 'uuid';
import { CliIoHost } from './io-host';
import type { Configuration } from './user-configuration';
import { PROJECT_CONFIG } from './user-configuration';
import type { ToolkitAction } from '../../../@aws-cdk/toolkit-lib/lib/api';
import { StackSelectionStrategy, ToolkitError } from '../../../@aws-cdk/toolkit-lib/lib/api';
import { asIoHelper } from '../../../@aws-cdk/toolkit-lib/lib/api/io/private';
import { PermissionChangeType } from '../../../@aws-cdk/toolkit-lib/lib/payloads';
import type { ToolkitOptions } from '../../../@aws-cdk/toolkit-lib/lib/toolkit';
import { Toolkit } from '../../../@aws-cdk/toolkit-lib/lib/toolkit';
import { DEFAULT_TOOLKIT_STACK_NAME } from '../api';
import type { SdkProvider } from '../api/aws-auth';
import type { BootstrapEnvironmentOptions } from '../api/bootstrap';
import { Bootstrapper } from '../api/bootstrap';
import { ExtendedStackSelection, StackCollection } from '../api/cloud-assembly';
import type { DeploymentMethod, Deployments, SuccessfulDeployStackResult } from '../api/deployments';
import { GarbageCollector } from '../api/garbage-collection';
import { EcsHotswapProperties, HotswapMode, HotswapPropertyOverrides } from '../api/hotswap';
import { CloudWatchLogEventMonitor, findCloudWatchLogGroups } from '../api/logs-monitor';
import { removeNonImportResources, ResourceImporter, ResourceMigrator } from '../api/resource-import';
import { type Tag, tagsForStack } from '../api/tags';
import type { AssetBuildNode, AssetPublishNode, Concurrency, StackNode, WorkGraph } from '../api/work-graph';
import { WorkGraphBuilder } from '../api/work-graph';
import { cfnApi } from '../api-private';
import { StackActivityProgress } from '../commands/deploy';
import { DiffFormatter, RequireApproval } from '../commands/diff';
import { listStacks } from '../commands/list-stacks';
import type { FromScan, GenerateTemplateOutput } from '../commands/migrate';
import {
appendWarningsToReadme,
buildCfnClient,
buildGenertedTemplateOutput,
CfnTemplateGeneratorProvider,
generateCdkApp,
generateStack,
generateTemplate,
isThereAWarning,
parseSourceOptions,
readFromPath,
readFromStack,
setEnvironment,
TemplateSourceOptions,
writeMigrateJsonFile,
} from '../commands/migrate';
import type { CloudAssembly, CloudExecutable, StackSelector } from '../cxapp';
import { DefaultSelection, environmentsFromDescriptors, globEnvironmentsFromStacks, looksLikeGlob } from '../cxapp';
import { debug, error, highlight, info, result as logResult, success, warning } from '../logging';
import {
deserializeStructure,
formatErrorMessage,
formatTime,
obscureTemplate,
partition,
serializeStructure,
validateSnsTopicArn,
} from '../util';
// Must use a require() otherwise esbuild complains about calling a namespace
// eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/consistent-type-imports
const pLimit: typeof import('p-limit') = require('p-limit');
let TESTING = false;
export function markTesting() {
TESTING = true;
}
export interface CdkToolkitProps {
/**
* The Cloud Executable
*/
cloudExecutable: CloudExecutable;
/**
* The provisioning engine used to apply changes to the cloud
*/
deployments: Deployments;
/**
* The CliIoHost that's used for I/O operations
*/
ioHost?: CliIoHost;
/**
* Name of the toolkit stack to use/deploy
*
* @default CDKToolkit
*/
toolkitStackName?: string;
/**
* Whether to be verbose
*
* @default false
*/
verbose?: boolean;
/**
* Don't stop on error metadata
*
* @default false
*/
ignoreErrors?: boolean;
/**
* Treat warnings in metadata as errors
*
* @default false
*/
strict?: boolean;
/**
* Application configuration (settings and context)
*/
configuration: Configuration;
/**
* AWS object (used by synthesizer and contextprovider)
*/
sdkProvider: SdkProvider;
}
/**
* When to build assets
*/
export enum AssetBuildTime {
/**
* Build all assets before deploying the first stack
*
* This is intended for expensive Docker image builds; so that if the Docker image build
* fails, no stacks are unnecessarily deployed (with the attendant wait time).
*/
ALL_BEFORE_DEPLOY = 'all-before-deploy',
/**
* Build assets just-in-time, before publishing
*/
JUST_IN_TIME = 'just-in-time',
}
class InternalToolkit extends Toolkit {
private readonly _sdkProvider: SdkProvider;
public constructor(sdkProvider: SdkProvider, options: ToolkitOptions) {
super(options);
this._sdkProvider = sdkProvider;
}
/**
* Access to the AWS SDK
* @internal
*/
protected async sdkProvider(_action: ToolkitAction): Promise<SdkProvider> {
return this._sdkProvider;
}
}
/**
* Toolkit logic
*
* The toolkit runs the `cloudExecutable` to obtain a cloud assembly and
* deploys applies them to `cloudFormation`.
*/
export class CdkToolkit {
private ioHost: CliIoHost;
private toolkitStackName: string;
private toolkit: InternalToolkit;
constructor(private readonly props: CdkToolkitProps) {
this.ioHost = props.ioHost ?? CliIoHost.instance();
this.toolkitStackName = props.toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME;
this.toolkit = new InternalToolkit(props.sdkProvider, {
assemblyFailureAt: this.validateMetadataFailAt(),
color: true,
emojis: true,
ioHost: this.ioHost,
sdkConfig: {},
toolkitStackName: this.toolkitStackName,
});
this.toolkit; // aritifical use of this.toolkit to satisfy TS, we want to prepare usage of the new toolkit without using it just yet
}
public async metadata(stackName: string, json: boolean) {
const stacks = await this.selectSingleStackByName(stackName);
printSerializedObject(stacks.firstStack.manifest.metadata ?? {}, json);
}
public async acknowledge(noticeId: string) {
const acks = this.props.configuration.context.get('acknowledged-issue-numbers') ?? [];
acks.push(Number(noticeId));
this.props.configuration.context.set('acknowledged-issue-numbers', acks);
await this.props.configuration.saveContext();
}
public async diff(options: DiffOptions): Promise<number> {
const stacks = await this.selectStacksForDiff(options.stackNames, options.exclusively);
const strict = !!options.strict;
const contextLines = options.contextLines || 3;
const quiet = options.quiet || false;
let diffs = 0;
const parameterMap = buildParameterMap(options.parameters);
if (options.templatePath !== undefined) {
// Compare single stack against fixed template
if (stacks.stackCount !== 1) {
throw new ToolkitError(
'Can only select one stack when comparing to fixed template. Use --exclusively to avoid selecting multiple stacks.',
);
}
if (!(await fs.pathExists(options.templatePath))) {
throw new ToolkitError(`There is no file at ${options.templatePath}`);
}
const template = deserializeStructure(await fs.readFile(options.templatePath, { encoding: 'UTF-8' }));
const formatter = new DiffFormatter({
ioHelper: asIoHelper(this.ioHost, 'diff'),
templateInfo: {
oldTemplate: template,
newTemplate: stacks.firstStack,
},
});
if (options.securityOnly) {
const securityDiff = formatter.formatSecurityDiff();
// Warn, count, and display the diff only if the reported changes are broadening permissions
if (securityDiff.permissionChangeType === PermissionChangeType.BROADENING) {
warning('This deployment will make potentially sensitive changes according to your current security approval level.\nPlease confirm you intend to make the following modifications:\n');
info(securityDiff.formattedDiff);
diffs += 1;
}
} else {
const diff = formatter.formatStackDiff({
strict,
context: contextLines,
quiet,
});
diffs = diff.numStacksWithChanges;
info(diff.formattedDiff);
}
} else {
// Compare N stacks against deployed templates
for (const stack of stacks.stackArtifacts) {
const templateWithNestedStacks = await this.props.deployments.readCurrentTemplateWithNestedStacks(
stack,
options.compareAgainstProcessedTemplate,
);
const currentTemplate = templateWithNestedStacks.deployedRootTemplate;
const nestedStacks = templateWithNestedStacks.nestedStacks;
const migrator = new ResourceMigrator({
deployments: this.props.deployments,
ioHelper: asIoHelper(this.ioHost, 'diff'),
});
const resourcesToImport = await migrator.tryGetResources(await this.props.deployments.resolveEnvironment(stack));
if (resourcesToImport) {
removeNonImportResources(stack);
}
let changeSet = undefined;
if (options.changeSet) {
let stackExists = false;
try {
stackExists = await this.props.deployments.stackExists({
stack,
deployName: stack.stackName,
tryLookupRole: true,
});
} catch (e: any) {
debug(formatErrorMessage(e));
if (!quiet) {
info(
`Checking if the stack ${stack.stackName} exists before creating the changeset has failed, will base the diff on template differences (run again with -v to see the reason)\n`,
);
}
stackExists = false;
}
if (stackExists) {
changeSet = await cfnApi.createDiffChangeSet(asIoHelper(this.ioHost, 'diff'), {
stack,
uuid: uuid.v4(),
deployments: this.props.deployments,
willExecute: false,
sdkProvider: this.props.sdkProvider,
parameters: Object.assign({}, parameterMap['*'], parameterMap[stack.stackName]),
resourcesToImport,
});
} else {
debug(
`the stack '${stack.stackName}' has not been deployed to CloudFormation or describeStacks call failed, skipping changeset creation.`,
);
}
}
const formatter = new DiffFormatter({
ioHelper: asIoHelper(this.ioHost, 'diff'),
templateInfo: {
oldTemplate: currentTemplate,
newTemplate: stack,
changeSet,
isImport: !!resourcesToImport,
nestedStacks,
},
});
if (options.securityOnly) {
const securityDiff = formatter.formatSecurityDiff();
// Warn, count, and display the diff only if the reported changes are broadening permissions
if (securityDiff.permissionChangeType === PermissionChangeType.BROADENING) {
warning('This deployment will make potentially sensitive changes according to your current security approval level.\nPlease confirm you intend to make the following modifications:\n');
info(securityDiff.formattedDiff);
diffs += 1;
}
} else {
const diff = formatter.formatStackDiff({
strict,
context: contextLines,
quiet,
});
info(diff.formattedDiff);
diffs += diff.numStacksWithChanges;
}
}
}
info(format('\n✨ Number of stacks with differences: %s\n', diffs));
return diffs && options.fail ? 1 : 0;
}
public async deploy(options: DeployOptions) {
if (options.watch) {
return this.watch(options);
}
// set progress from options, this includes user and app config
if (options.progress) {
this.ioHost.stackProgress = options.progress;
}
const startSynthTime = new Date().getTime();
const stackCollection = await this.selectStacksForDeploy(
options.selector,
options.exclusively,
options.cacheCloudAssembly,
options.ignoreNoStacks,
);
const elapsedSynthTime = new Date().getTime() - startSynthTime;
info(`\n✨ Synthesis time: ${formatTime(elapsedSynthTime)}s\n`);
if (stackCollection.stackCount === 0) {
error('This app contains no stacks');
return;
}
const migrator = new ResourceMigrator({
deployments: this.props.deployments,
ioHelper: asIoHelper(this.ioHost, 'deploy'),
});
await migrator.tryMigrateResources(stackCollection, {
toolkitStackName: this.toolkitStackName,
...options,
});
const requireApproval = options.requireApproval ?? RequireApproval.BROADENING;
const parameterMap = buildParameterMap(options.parameters);
if (options.hotswap !== HotswapMode.FULL_DEPLOYMENT) {
warning(
'⚠️ The --hotswap and --hotswap-fallback flags deliberately introduce CloudFormation drift to speed up deployments',
);
warning('⚠️ They should only be used for development - never use them for your production Stacks!\n');
}
let hotswapPropertiesFromSettings = this.props.configuration.settings.get(['hotswap']) || {};
let hotswapPropertyOverrides = new HotswapPropertyOverrides();
hotswapPropertyOverrides.ecsHotswapProperties = new EcsHotswapProperties(
hotswapPropertiesFromSettings.ecs?.minimumHealthyPercent,
hotswapPropertiesFromSettings.ecs?.maximumHealthyPercent,
);
const stacks = stackCollection.stackArtifacts;
const stackOutputs: { [key: string]: any } = {};
const outputsFile = options.outputsFile;
const buildAsset = async (assetNode: AssetBuildNode) => {
await this.props.deployments.buildSingleAsset(
assetNode.assetManifestArtifact,
assetNode.assetManifest,
assetNode.asset,
{
stack: assetNode.parentStack,
roleArn: options.roleArn,
stackName: assetNode.parentStack.stackName,
},
);
};
const publishAsset = async (assetNode: AssetPublishNode) => {
await this.props.deployments.publishSingleAsset(assetNode.assetManifest, assetNode.asset, {
stack: assetNode.parentStack,
roleArn: options.roleArn,
stackName: assetNode.parentStack.stackName,
forcePublish: options.force,
});
};
const deployStack = async (stackNode: StackNode) => {
const stack = stackNode.stack;
if (stackCollection.stackCount !== 1) {
highlight(stack.displayName);
}
if (!stack.environment) {
// eslint-disable-next-line max-len
throw new ToolkitError(
`Stack ${stack.displayName} does not define an environment, and AWS credentials could not be obtained from standard locations or no region was configured.`,
);
}
if (Object.keys(stack.template.Resources || {}).length === 0) {
// The generated stack has no resources
if (!(await this.props.deployments.stackExists({ stack }))) {
warning('%s: stack has no resources, skipping deployment.', chalk.bold(stack.displayName));
} else {
warning('%s: stack has no resources, deleting existing stack.', chalk.bold(stack.displayName));
await this.destroy({
selector: { patterns: [stack.hierarchicalId] },
exclusively: true,
force: true,
roleArn: options.roleArn,
fromDeploy: true,
});
}
return;
}
if (requireApproval !== RequireApproval.NEVER) {
const currentTemplate = await this.props.deployments.readCurrentTemplate(stack);
const formatter = new DiffFormatter({
ioHelper: asIoHelper(this.ioHost, 'deploy'),
templateInfo: {
oldTemplate: currentTemplate,
newTemplate: stack,
},
});
const securityDiff = formatter.formatSecurityDiff();
if (requiresApproval(requireApproval, securityDiff.permissionChangeType)) {
info(securityDiff.formattedDiff);
await askUserConfirmation(
this.ioHost,
concurrency,
'"--require-approval" is enabled and stack includes security-sensitive updates',
'Do you wish to deploy these changes',
);
}
}
// Following are the same semantics we apply with respect to Notification ARNs (dictated by the SDK)
//
// - undefined => cdk ignores it, as if it wasn't supported (allows external management).
// - []: => cdk manages it, and the user wants to wipe it out.
// - ['arn-1'] => cdk manages it, and the user wants to set it to ['arn-1'].
const notificationArns = (!!options.notificationArns || !!stack.notificationArns)
? (options.notificationArns ?? []).concat(stack.notificationArns ?? [])
: undefined;
for (const notificationArn of notificationArns ?? []) {
if (!validateSnsTopicArn(notificationArn)) {
throw new ToolkitError(`Notification arn ${notificationArn} is not a valid arn for an SNS topic`);
}
}
const stackIndex = stacks.indexOf(stack) + 1;
info(`${chalk.bold(stack.displayName)}: deploying... [${stackIndex}/${stackCollection.stackCount}]`);
const startDeployTime = new Date().getTime();
let tags = options.tags;
if (!tags || tags.length === 0) {
tags = tagsForStack(stack);
}
let elapsedDeployTime = 0;
try {
let deployResult: SuccessfulDeployStackResult | undefined;
let rollback = options.rollback;
let iteration = 0;
while (!deployResult) {
if (++iteration > 2) {
throw new ToolkitError('This loop should have stabilized in 2 iterations, but didn\'t. If you are seeing this error, please report it at https://github.com/aws/aws-cdk/issues/new/choose');
}
const r = await this.props.deployments.deployStack({
stack,
deployName: stack.stackName,
roleArn: options.roleArn,
toolkitStackName: options.toolkitStackName,
reuseAssets: options.reuseAssets,
notificationArns,
tags,
execute: options.execute,
changeSetName: options.changeSetName,
deploymentMethod: options.deploymentMethod,
forceDeployment: options.force,
parameters: Object.assign({}, parameterMap['*'], parameterMap[stack.stackName]),
usePreviousParameters: options.usePreviousParameters,
rollback,
hotswap: options.hotswap,
hotswapPropertyOverrides: hotswapPropertyOverrides,
extraUserAgent: options.extraUserAgent,
assetParallelism: options.assetParallelism,
ignoreNoStacks: options.ignoreNoStacks,
});
switch (r.type) {
case 'did-deploy-stack':
deployResult = r;
break;
case 'failpaused-need-rollback-first': {
const motivation = r.reason === 'replacement'
? `Stack is in a paused fail state (${r.status}) and change includes a replacement which cannot be deployed with "--no-rollback"`
: `Stack is in a paused fail state (${r.status}) and command line arguments do not include "--no-rollback"`;
if (options.force) {
warning(`${motivation}. Rolling back first (--force).`);
} else {
await askUserConfirmation(
this.ioHost,
concurrency,
motivation,
`${motivation}. Roll back first and then proceed with deployment`,
);
}
// Perform a rollback
await this.rollback({
selector: { patterns: [stack.hierarchicalId] },
toolkitStackName: options.toolkitStackName,
force: options.force,
});
// Go around through the 'while' loop again but switch rollback to true.
rollback = true;
break;
}
case 'replacement-requires-rollback': {
const motivation = 'Change includes a replacement which cannot be deployed with "--no-rollback"';
if (options.force) {
warning(`${motivation}. Proceeding with regular deployment (--force).`);
} else {
await askUserConfirmation(
this.ioHost,
concurrency,
motivation,
`${motivation}. Perform a regular deployment`,
);
}
// Go around through the 'while' loop again but switch rollback to true.
rollback = true;
break;
}
default:
throw new ToolkitError(`Unexpected result type from deployStack: ${JSON.stringify(r)}. If you are seeing this error, please report it at https://github.com/aws/aws-cdk/issues/new/choose`);
}
}
const message = deployResult.noOp
? ' ✅ %s (no changes)'
: ' ✅ %s';
success('\n' + message, stack.displayName);
elapsedDeployTime = new Date().getTime() - startDeployTime;
info(`\n✨ Deployment time: ${formatTime(elapsedDeployTime)}s\n`);
if (Object.keys(deployResult.outputs).length > 0) {
info('Outputs:');
stackOutputs[stack.stackName] = deployResult.outputs;
}
for (const name of Object.keys(deployResult.outputs).sort()) {
const value = deployResult.outputs[name];
info(`${chalk.cyan(stack.id)}.${chalk.cyan(name)} = ${chalk.underline(chalk.cyan(value))}`);
}
info('Stack ARN:');
logResult(deployResult.stackArn);
} catch (e: any) {
// It has to be exactly this string because an integration test tests for
// "bold(stackname) failed: ResourceNotReady: <error>"
throw new ToolkitError(
[`❌ ${chalk.bold(stack.stackName)} failed:`, ...(e.name ? [`${e.name}:`] : []), formatErrorMessage(e)].join(' '),
);
} finally {
if (options.cloudWatchLogMonitor) {
const foundLogGroupsResult = await findCloudWatchLogGroups(this.props.sdkProvider, asIoHelper(this.ioHost, 'deploy'), stack);
options.cloudWatchLogMonitor.addLogGroups(
foundLogGroupsResult.env,
foundLogGroupsResult.sdk,
foundLogGroupsResult.logGroupNames,
);
}
// If an outputs file has been specified, create the file path and write stack outputs to it once.
// Outputs are written after all stacks have been deployed. If a stack deployment fails,
// all of the outputs from successfully deployed stacks before the failure will still be written.
if (outputsFile) {
fs.ensureFileSync(outputsFile);
await fs.writeJson(outputsFile, stackOutputs, {
spaces: 2,
encoding: 'utf8',
});
}
}
info(`\n✨ Total time: ${formatTime(elapsedSynthTime + elapsedDeployTime)}s\n`);
};
const assetBuildTime = options.assetBuildTime ?? AssetBuildTime.ALL_BEFORE_DEPLOY;
const prebuildAssets = assetBuildTime === AssetBuildTime.ALL_BEFORE_DEPLOY;
const concurrency = options.concurrency || 1;
if (concurrency > 1) {
// always force "events" progress output when we have concurrency
this.ioHost.stackProgress = StackActivityProgress.EVENTS;
// ...but only warn if the user explicitly requested "bar" progress
if (options.progress && options.progress != StackActivityProgress.EVENTS) {
warning('⚠️ The --concurrency flag only supports --progress "events". Switching to "events".');
}
}
const stacksAndTheirAssetManifests = stacks.flatMap((stack) => [
stack,
...stack.dependencies.filter(x => cxapi.AssetManifestArtifact.isAssetManifestArtifact(x)),
]);
const workGraph = new WorkGraphBuilder(
asIoHelper(this.ioHost, 'deploy'),
prebuildAssets,
).build(stacksAndTheirAssetManifests);
// Unless we are running with '--force', skip already published assets
if (!options.force) {
await this.removePublishedAssets(workGraph, options);
}
const graphConcurrency: Concurrency = {
'stack': concurrency,
'asset-build': 1, // This will be CPU-bound/memory bound, mostly matters for Docker builds
'asset-publish': (options.assetParallelism ?? true) ? 8 : 1, // This will be I/O-bound, 8 in parallel seems reasonable
};
await workGraph.doParallel(graphConcurrency, {
deployStack,
buildAsset,
publishAsset,
});
}
/**
* Roll back the given stack or stacks.
*/
public async rollback(options: RollbackOptions) {
const startSynthTime = new Date().getTime();
const stackCollection = await this.selectStacksForDeploy(options.selector, true);
const elapsedSynthTime = new Date().getTime() - startSynthTime;
info(`\n✨ Synthesis time: ${formatTime(elapsedSynthTime)}s\n`);
if (stackCollection.stackCount === 0) {
error('No stacks selected');
return;
}
let anyRollbackable = false;
for (const stack of stackCollection.stackArtifacts) {
info('Rolling back %s', chalk.bold(stack.displayName));
const startRollbackTime = new Date().getTime();
try {
const result = await this.props.deployments.rollbackStack({
stack,
roleArn: options.roleArn,
toolkitStackName: options.toolkitStackName,
orphanFailedResources: options.force,
validateBootstrapStackVersion: options.validateBootstrapStackVersion,
orphanLogicalIds: options.orphanLogicalIds,
});
if (!result.notInRollbackableState) {
anyRollbackable = true;
}
const elapsedRollbackTime = new Date().getTime() - startRollbackTime;
info(`\n✨ Rollback time: ${formatTime(elapsedRollbackTime).toString()}s\n`);
} catch (e: any) {
error('\n ❌ %s failed: %s', chalk.bold(stack.displayName), formatErrorMessage(e));
throw new ToolkitError('Rollback failed (use --force to orphan failing resources)');
}
}
if (!anyRollbackable) {
throw new ToolkitError('No stacks were in a state that could be rolled back');
}
}
public async watch(options: WatchOptions) {
const rootDir = path.dirname(path.resolve(PROJECT_CONFIG));
const ioHelper = asIoHelper(this.ioHost, 'watch');
debug("root directory used for 'watch' is: %s", rootDir);
const watchSettings: { include?: string | string[]; exclude: string | string[] } | undefined =
this.props.configuration.settings.get(['watch']);
if (!watchSettings) {
throw new ToolkitError(
"Cannot use the 'watch' command without specifying at least one directory to monitor. " +
'Make sure to add a "watch" key to your cdk.json',
);
}
// For the "include" subkey under the "watch" key, the behavior is:
// 1. No "watch" setting? We error out.
// 2. "watch" setting without an "include" key? We default to observing "./**".
// 3. "watch" setting with an empty "include" key? We default to observing "./**".
// 4. Non-empty "include" key? Just use the "include" key.
const watchIncludes = this.patternsArrayForWatch(watchSettings.include, {
rootDir,
returnRootDirIfEmpty: true,
});
debug("'include' patterns for 'watch': %s", watchIncludes);
// For the "exclude" subkey under the "watch" key,
// the behavior is to add some default excludes in addition to the ones specified by the user:
// 1. The CDK output directory.
// 2. Any file whose name starts with a dot.
// 3. Any directory's content whose name starts with a dot.
// 4. Any node_modules and its content (even if it's not a JS/TS project, you might be using a local aws-cli package)
const outputDir = this.props.configuration.settings.get(['output']);
const watchExcludes = this.patternsArrayForWatch(watchSettings.exclude, {
rootDir,
returnRootDirIfEmpty: false,
}).concat(`${outputDir}/**`, '**/.*', '**/.*/**', '**/node_modules/**');
debug("'exclude' patterns for 'watch': %s", watchExcludes);
// Since 'cdk deploy' is a relatively slow operation for a 'watch' process,
// introduce a concurrency latch that tracks the state.
// This way, if file change events arrive when a 'cdk deploy' is still executing,
// we will batch them, and trigger another 'cdk deploy' after the current one finishes,
// making sure 'cdk deploy's always execute one at a time.
// Here's a diagram showing the state transitions:
// -------------- -------- file changed -------------- file changed -------------- file changed
// | | ready event | | ------------------> | | ------------------> | | --------------|
// | pre-ready | -------------> | open | | deploying | | queued | |
// | | | | <------------------ | | <------------------ | | <-------------|
// -------------- -------- 'cdk deploy' done -------------- 'cdk deploy' done --------------
let latch: 'pre-ready' | 'open' | 'deploying' | 'queued' = 'pre-ready';
const cloudWatchLogMonitor = options.traceLogs ? new CloudWatchLogEventMonitor({
ioHelper,
}) : undefined;
const deployAndWatch = async () => {
latch = 'deploying';
await cloudWatchLogMonitor?.deactivate();
await this.invokeDeployFromWatch(options, cloudWatchLogMonitor);
// If latch is still 'deploying' after the 'await', that's fine,
// but if it's 'queued', that means we need to deploy again
while ((latch as 'deploying' | 'queued') === 'queued') {
// TypeScript doesn't realize latch can change between 'awaits',
// and thinks the above 'while' condition is always 'false' without the cast
latch = 'deploying';
info("Detected file changes during deployment. Invoking 'cdk deploy' again");
await this.invokeDeployFromWatch(options, cloudWatchLogMonitor);
}
latch = 'open';
await cloudWatchLogMonitor?.activate();
};
chokidar
.watch(watchIncludes, {
ignored: watchExcludes,
cwd: rootDir,
})
.on('ready', async () => {
latch = 'open';
debug("'watch' received the 'ready' event. From now on, all file changes will trigger a deployment");
info("Triggering initial 'cdk deploy'");
await deployAndWatch();
})
.on('all', async (event: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', filePath?: string) => {
if (latch === 'pre-ready') {
info(`'watch' is observing ${event === 'addDir' ? 'directory' : 'the file'} '%s' for changes`, filePath);
} else if (latch === 'open') {
info("Detected change to '%s' (type: %s). Triggering 'cdk deploy'", filePath, event);
await deployAndWatch();
} else {
// this means latch is either 'deploying' or 'queued'
latch = 'queued';
info(
"Detected change to '%s' (type: %s) while 'cdk deploy' is still running. " +
'Will queue for another deployment after this one finishes',
filePath,
event,
);
}
});
}
public async import(options: ImportOptions) {
const stacks = await this.selectStacksForDeploy(options.selector, true, true, false);
// set progress from options, this includes user and app config
if (options.progress) {
this.ioHost.stackProgress = options.progress;
}
if (stacks.stackCount > 1) {
throw new ToolkitError(
`Stack selection is ambiguous, please choose a specific stack for import [${stacks.stackArtifacts.map((x) => x.id).join(', ')}]`,
);
}
if (!process.stdout.isTTY && !options.resourceMappingFile) {
throw new ToolkitError('--resource-mapping is required when input is not a terminal');
}
const stack = stacks.stackArtifacts[0];
highlight(stack.displayName);
const resourceImporter = new ResourceImporter(stack, {
deployments: this.props.deployments,
ioHelper: asIoHelper(this.ioHost, 'import'),
});
const { additions, hasNonAdditions } = await resourceImporter.discoverImportableResources(options.force);
if (additions.length === 0) {
warning(
'%s: no new resources compared to the currently deployed stack, skipping import.',
chalk.bold(stack.displayName),
);
return;
}
// Prepare a mapping of physical resources to CDK constructs
const actualImport = !options.resourceMappingFile
? await resourceImporter.askForResourceIdentifiers(additions)
: await resourceImporter.loadResourceIdentifiers(additions, options.resourceMappingFile);
if (actualImport.importResources.length === 0) {
warning('No resources selected for import.');
return;
}
// If "--create-resource-mapping" option was passed, write the resource mapping to the given file and exit
if (options.recordResourceMapping) {
const outputFile = options.recordResourceMapping;
fs.ensureFileSync(outputFile);
await fs.writeJson(outputFile, actualImport.resourceMap, {
spaces: 2,
encoding: 'utf8',
});
info('%s: mapping file written.', outputFile);
return;
}
// Import the resources according to the given mapping
info('%s: importing resources into stack...', chalk.bold(stack.displayName));
const tags = tagsForStack(stack);
await resourceImporter.importResourcesFromMap(actualImport, {
roleArn: options.roleArn,
tags,
deploymentMethod: options.deploymentMethod,
usePreviousParameters: true,
rollback: options.rollback,
});
// Notify user of next steps
info(
`Import operation complete. We recommend you run a ${chalk.blueBright('drift detection')} operation ` +
'to confirm your CDK app resource definitions are up-to-date. Read more here: ' +
chalk.underline.blueBright(
'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/detect-drift-stack.html',
),
);
if (actualImport.importResources.length < additions.length) {
info('');
warning(
`Some resources were skipped. Run another ${chalk.blueBright('cdk import')} or a ${chalk.blueBright('cdk deploy')} to bring the stack up-to-date with your CDK app definition.`,
);
} else if (hasNonAdditions) {
info('');
warning(
`Your app has pending updates or deletes excluded from this import operation. Run a ${chalk.blueBright('cdk deploy')} to bring the stack up-to-date with your CDK app definition.`,
);
}
}
public async destroy(options: DestroyOptions) {
let stacks = await this.selectStacksForDestroy(options.selector, options.exclusively);
// The stacks will have been ordered for deployment, so reverse them for deletion.
stacks = stacks.reversed();
if (!options.force) {
// eslint-disable-next-line max-len
const confirmed = await promptly.confirm(
`Are you sure you want to delete: ${chalk.blue(stacks.stackArtifacts.map((s) => s.hierarchicalId).join(', '))} (y/n)?`,
);
if (!confirmed) {
return;
}
}
const action = options.fromDeploy ? 'deploy' : 'destroy';
for (const [index, stack] of stacks.stackArtifacts.entries()) {
success('%s: destroying... [%s/%s]', chalk.blue(stack.displayName), index + 1, stacks.stackCount);
try {
await this.props.deployments.destroyStack({
stack,
deployName: stack.stackName,
roleArn: options.roleArn,
});
success(`\n ✅ %s: ${action}ed`, chalk.blue(stack.displayName));
} catch (e) {
error(`\n ❌ %s: ${action} failed`, chalk.blue(stack.displayName), e);
throw e;
}
}
}
public async list(
selectors: string[],
options: { long?: boolean; json?: boolean; showDeps?: boolean } = {},
): Promise<number> {
const stacks = await listStacks(this, {
selectors: selectors,
});
if (options.long && options.showDeps) {
printSerializedObject(stacks, options.json ?? false);
return 0;
}
if (options.showDeps) {
const stackDeps = [];
for (const stack of stacks) {
stackDeps.push({
id: stack.id,
dependencies: stack.dependencies,
});
}
printSerializedObject(stackDeps, options.json ?? false);
return 0;
}
if (options.long) {
const long = [];
for (const stack of stacks) {
long.push({
id: stack.id,
name: stack.name,
environment: stack.environment,
});
}
printSerializedObject(long, options.json ?? false);
return 0;
}
// just print stack IDs
for (const stack of stacks) {
logResult(stack.id);
}
return 0; // exit-code
}
/**
* Synthesize the given set of stacks (called when the user runs 'cdk synth')
*
* INPUT: Stack names can be supplied using a glob filter. If no stacks are
* given, all stacks from the application are implicitly selected.
*
* OUTPUT: If more than one stack ends up being selected, an output directory
* should be supplied, where the templates will be written.
*/
public async synth(
stackNames: string[],
exclusively: boolean,
quiet: boolean,
autoValidate?: boolean,
json?: boolean,
): Promise<any> {
const stacks = await this.selectStacksForDiff(stackNames, exclusively, autoValidate);
// if we have a single stack, print it to STDOUT
if (stacks.stackCount === 1) {
if (!quiet) {
printSerializedObject(obscureTemplate(stacks.firstStack.template), json ?? false);
}
return undefined;
}
// not outputting template to stdout, let's explain things to the user a little bit...
success(`Successfully synthesized to ${chalk.blue(path.resolve(stacks.assembly.directory))}`);
info(
`Supply a stack id (${stacks.stackArtifacts.map((s) => chalk.green(s.hierarchicalId)).join(', ')}) to display its template.`,
);
return undefined;
}
/**
* Bootstrap the CDK Toolkit stack in the accounts used by the specified stack(s).
*
* @param userEnvironmentSpecs environment names that need to have toolkit support
* provisioned, as a glob filter. If none is provided, all stacks are implicitly selected.
* @param options The name, role ARN, bootstrapping parameters, etc. to be used for the CDK Toolkit stack.
*/
public async bootstrap(
userEnvironmentSpecs: string[],
options: BootstrapEnvironmentOptions,
): Promise<void> {
const bootstrapper = new Bootstrapper(options.source, asIoHelper(this.ioHost, 'bootstrap'));
// If there is an '--app' argument and an environment looks like a glob, we
// select the environments from the app. Otherwise, use what the user said.
const environments = await this.defineEnvironments(userEnvironmentSpecs);
const limit = pLimit(20);
// eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism
await Promise.all(environments.map((environment) => limit(async () => {
success(' ⏳ Bootstrapping environment %s...', chalk.blue(environment.name));
try {
const result = await bootstrapper.bootstrapEnvironment(environment, this.props.sdkProvider, options);
const message = result.noOp
? ' ✅ Environment %s bootstrapped (no changes).'
: ' ✅ Environment %s bootstrapped.';
success(message, chalk.blue(environment.name));
} catch (e) {
error(' ❌ Environment %s failed bootstrapping: %s', chalk.blue(environment.name), e);
throw e;
}
})));
}
/**
* Garbage collects assets from a CDK app's environment
* @param options Options for Garbage Collection
*/
public async garbageCollect(userEnvironmentSpecs: string[], options: GarbageCollectionOptions) {
const environments = await this.defineEnvironments(userEnvironmentSpecs);
for (const environment of environments) {
success(' ⏳ Garbage Collecting environment %s...', chalk.blue(environment.name));
const gc = new GarbageCollector({
sdkProvider: this.props.sdkProvider,
ioHelper: asIoHelper(this.ioHost, 'gc'),
resolvedEnvironment: environment,
bootstrapStackName: options.bootstrapStackName,
rollbackBufferDays: options.rollbackBufferDays,
createdBufferDays: options.createdBufferDays,
action: options.action ?? 'full',
type: options.type ?? 'all',
confirm: options.confirm ?? true,
});
await gc.garbageCollect();
}
}
private async defineEnvironments(userEnvironmentSpecs: string[]): Promise<cxapi.Environment[]> {
// By default, glob for everything
const environmentSpecs = userEnvironmentSpecs.length > 0 ? [...userEnvironmentSpecs] : ['**'];
// Partition into globs and non-globs (this will mutate environmentSpecs).
const globSpecs = partition(environmentSpecs, looksLikeGlob);
if (globSpecs.length > 0 && !this.props.cloudExecutable.hasApp) {
if (userEnvironmentSpecs.length > 0) {
// User did request this glob
throw new ToolkitError(
`'${globSpecs}' is not an environment name. Specify an environment name like 'aws://123456789012/us-east-1', or run in a directory with 'cdk.json' to use wildcards.`,
);
} else {
// User did not request anything
throw new ToolkitError(
"Specify an environment name like 'aws://123456789012/us-east-1', or run in a directory with 'cdk.json'.",
);
}
}
const environments: cxapi.Environment[] = [...environmentsFromDescriptors(environmentSpecs)];
// If there is an '--app' argument, select the environments from the app.
if (this.props.cloudExecutable.hasApp) {
environments.push(
...(await globEnvironmentsFromStacks(await this.selectStacksForList([]), globSpecs, this.props.sdkProvider)),
);
}
return environments;
}
/**
* Migrates a CloudFormation stack/template to a CDK app
* @param options Options for CDK app creation
*/
public async migrate(options: MigrateOptions): Promise<void> {
warning('This command is an experimental feature.');
const language = options.language?.toLowerCase() ?? 'typescript';
const environment = setEnvironment(options.account, options.region);
let generateTemplateOutput: GenerateTemplateOutput | undefined;
let cfn: CfnTemplateGeneratorProvider | undefined;
let templateToDelete: string | undefined;
try {
// if neither fromPath nor fromStack is provided, generate a template using cloudformation
const scanType = parseSourceOptions(options.fromPath, options.fromStack, options.stackName).source;
if (scanType == TemplateSourceOptions.SCAN) {
generateTemplateOutput = await generateTemplate({
stackName: options.stackName,
filters: options.filter,
fromScan: options.fromScan,
sdkProvider: this.props.sdkProvider,
environment: environment,
});
templateToDelete = generateTemplateOutput.templateId;
} else if (scanType == TemplateSourceOptions.PATH) {
const templateBody = readFromPath(options.fromPath!);
const parsedTemplate = deserializeStructure(templateBody);
const templateId = parsedTemplate.Metadata?.TemplateId?.toString();
if (templateId) {
// if we have a template id, we can call describe generated template to get the resource identifiers
// resource metadata, and template source to generate the template
cfn = new CfnTemplateGeneratorProvider(await buildCfnClient(this.props.sdkProvider, environment));
const generatedTemplateSummary = await cfn.describeGeneratedTemplate(templateId);
generateTemplateOutput = buildGenertedTemplateOutput(
generatedTemplateSummary,
templateBody,
generatedTemplateSummary.GeneratedTemplateId!,
);
} else {
generateTemplateOutput = {
migrateJson: {
templateBody: templateBody,
source: 'localfile',
},
};
}
} else if (scanType == TemplateSourceOptions.STACK) {
const template = await readFromStack(options.stackName, this.props.sdkProvider, environment);
if (!template) {
throw new ToolkitError(`No template found for stack-name: ${options.stackName}`);
}
generateTemplateOutput = {
migrateJson: {
templateBody: template,
source: options.stackName,
},
};
} else {
// We shouldn't ever get here, but just in case.
throw new ToolkitError(`Invalid source option provided: ${scanType}`);
}
const stack = generateStack(generateTemplateOutput.migrateJson.templateBody, options.stackName, language);
success(' ⏳ Generating CDK app for %s...', chalk.blue(options.stackName));
await generateCdkApp(options.stackName, stack!, language, options.outputPath, options.compress);
if (generateTemplateOutput) {
writeMigrateJsonFile(options.outputPath, options.stackName, generateTemplateOutput.migrateJson);
}
if (isThereAWarning(generateTemplateOutput)) {
warning(
' ⚠️ Some resources could not be migrated completely. Please review the README.md file for more information.',
);
appendWarningsToReadme(
`${path.join(options.outputPath ?? process.cwd(), options.stackName)}/README.md`,
generateTemplateOutput.resources!,
);
}
} catch (e) {
error(' ❌ Migrate failed for `%s`: %s', options.stackName, (e as Error).message);
throw e;
} finally {
if (templateToDelete) {
if (!cfn) {
cfn = new CfnTemplateGeneratorProvider(await buildCfnClient(this.props.sdkProvider, environment));
}
if (!process.env.MIGRATE_INTEG_TEST) {
await cfn.deleteGeneratedTemplate(templateToDelete);
}
}
}
}
public async refactor(options: RefactorOptions): Promise<number> {
let exclude: string[] = [];
if (options.excludeFile != null) {
if (!(await fs.pathExists(options.excludeFile))) {
throw new ToolkitError(`The exclude file '${options.excludeFile}' does not exist`);
}
exclude = fs.readFileSync(options.excludeFile).toString('utf-8').split('\n');
}
try {
await this.toolkit.refactor(this.props.cloudExecutable, {
dryRun: options.dryRun,
exclude,
stacks: {
patterns: options.selector.patterns,
strategy: options.selector.patterns.length > 0 ? StackSelectionStrategy.PATTERN_MATCH : StackSelectionStrategy.ALL_STACKS,
},
});
} catch (e) {
error((e as Error).message);
return 1;
}
return 0;
}
private async selectStacksForList(patterns: string[]) {
const assembly = await this.assembly();
const stacks = await assembly.selectStacks({ patterns }, { defaultBehavior: DefaultSelection.AllStacks });
// No validation
return stacks;
}
private async selectStacksForDeploy(
selector: StackSelector,
exclusively?: boolean,
cacheCloudAssembly?: boolean,
ignoreNoStacks?: boolean,
): Promise<StackCollection> {
const assembly = await this.assembly(cacheCloudAssembly);
const stacks = await assembly.selectStacks(selector, {
extend: exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Upstream,
defaultBehavior: DefaultSelection.OnlySingle,
ignoreNoStacks,
});
this.validateStacksSelected(stacks, selector.patterns);
await this.validateStacks(stacks);
return stacks;
}
private async selectStacksForDiff(
stackNames: string[],
exclusively?: boolean,
autoValidate?: boolean,
): Promise<StackCollection> {
const assembly = await this.assembly();
const selectedForDiff = await assembly.selectStacks(
{ patterns: stackNames },
{
extend: exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Upstream,
defaultBehavior: DefaultSelection.MainAssembly,
},
);
const allStacks = await this.selectStacksForList([]);
const autoValidateStacks = autoValidate
? allStacks.filter((art) => art.validateOnSynth ?? false)
: new StackCollection(assembly, []);
this.validateStacksSelected(selectedForDiff.concat(autoValidateStacks), stackNames);
await this.validateStacks(selectedForDiff.concat(autoValidateStacks));
return selectedForDiff;
}
private async selectStacksForDestroy(selector: StackSelector, exclusively?: boolean) {
const assembly = await this.assembly();
const stacks = await assembly.selectStacks(selector, {
extend: exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Downstream,
defaultBehavior: DefaultSelection.OnlySingle,
});
// No validation
return stacks;
}
/**
* Validate the stacks for errors and warnings according to the CLI's current settings
*/
private async validateStacks(stacks: StackCollection) {
const failAt = this.validateMetadataFailAt();
await stacks.validateMetadata(failAt, stackMetadataLogger(this.props.verbose));
}
private validateMetadataFailAt(): 'warn' | 'error' | 'none' {
let failAt: 'warn' | 'error' | 'none' = 'error';
if (this.props.ignoreErrors) {
failAt = 'none';
}
if (this.props.strict) {
failAt = 'warn';
}
return failAt;
}
/**
* Validate that if a user specified a stack name there exists at least 1 stack selected
*/
private validateStacksSelected(stacks: StackCollection, stackNames: string[]) {
if (stackNames.length != 0 && stacks.stackCount == 0) {
throw new ToolkitError(`No stacks match the name(s) ${stackNames}`);
}
}
/**
* Select a single stack by its name
*/
private async selectSingleStackByName(stackName: string) {
const assembly = await this.assembly();
const stacks = await assembly.selectStacks(
{ patterns: [stackName] },
{
extend: ExtendedStackSelection.None,
defaultBehavior: DefaultSelection.None,
},
);
// Could have been a glob so check that we evaluated to exactly one
if (stacks.stackCount > 1) {
throw new ToolkitError(`This command requires exactly one stack and we matched more than one: ${stacks.stackIds}`);
}
return assembly.stackById(stacks.firstStack.id);
}
public assembly(cacheCloudAssembly?: boolean): Promise<CloudAssembly> {
return this.props.cloudExecutable.synthesize(cacheCloudAssembly);
}
private patternsArrayForWatch(
patterns: string | string[] | undefined,
options: { rootDir: string; returnRootDirIfEmpty: boolean },
): string[] {
const patternsArray: string[] = patterns !== undefined ? (Array.isArray(patterns) ? patterns : [patterns]) : [];
return patternsArray.length > 0 ? patternsArray : options.returnRootDirIfEmpty ? [options.rootDir] : [];
}
private async invokeDeployFromWatch(
options: WatchOptions,
cloudWatchLogMonitor?: CloudWatchLogEventMonitor,
): Promise<void> {
const deployOptions: DeployOptions = {
...options,
requireApproval: RequireApproval.NEVER,
// if 'watch' is called by invoking 'cdk deploy --watch',
// we need to make sure to not call 'deploy' with 'watch' again,
// as that would lead to a cycle
watch: false,
cloudWatchLogMonitor,
cacheCloudAssembly: false,
hotswap: options.hotswap,
extraUserAgent: `cdk-watch/hotswap-${options.hotswap !== HotswapMode.FALL_BACK ? 'on' : 'off'}`,
concurrency: options.concurrency,
};
try {
await this.deploy(deployOptions);
} catch {
// just continue - deploy will show the error
}
}
/**
* Remove the asset publishing and building from the work graph for assets that are already in place
*/
private async removePublishedAssets(graph: WorkGraph, options: DeployOptions) {
await graph.removeUnnecessaryAssets(assetNode => this.props.deployments.isSingleAssetPublished(assetNode.assetManifest, assetNode.asset, {
stack: assetNode.parentStack,
roleArn: options.roleArn,
stackName: assetNode.parentStack.stackName,
}));
}
}
/**
* Print a serialized object (YAML or JSON) to stdout.
*/
function printSerializedObject(obj: any, json: boolean) {
logResult(serializeStructure(obj, json));
}
/**
* Options for the diff command
*/
export interface DiffOptions {
/**
* Stack names to diff
*/
readonly stackNames: string[];
/**
* Name of the toolkit stack, if not the default name
*
* @default 'CDKToolkit'
*/
readonly toolkitStackName?: string;
/**
* Only select the given stack
*
* @default false
*/
readonly exclusively?: boolean;
/**
* Used a template from disk instead of from the server
*
* @default Use from the server
*/
readonly templatePath?: string;
/**
* Strict diff mode
*
* @default false
*/
readonly strict?: boolean;
/**
* How many lines of context to show in the diff
*
* @default 3
*/
readonly contextLines?: number;
/**
* Whether to fail with exit code 1 in case of diff
*
* @default false
*/
readonly fail?: boolean;
/**
* Only run diff on broadened security changes
*
* @default false
*/
readonly securityOnly?: boolean;
/**
* Whether to run the diff against the template after the CloudFormation Transforms inside it have been executed
* (as opposed to the original template, the default, which contains the unprocessed Transforms).
*
* @default false
*/
readonly compareAgainstProcessedTemplate?: boolean;
/*
* Run diff in quiet mode without printing the diff statuses
*
* @default false
*/
readonly quiet?: boolean;
/**
* Additional parameters for CloudFormation at diff time, used to create a change set
* @default {}
*/
readonly parameters?: { [name: string]: string | undefined };
/**
* Whether or not to create, analyze, and subsequently delete a changeset
*
* @default true
*/
readonly changeSet?: boolean;
}
interface CfnDeployOptions {
/**
* Criteria for selecting stacks to deploy
*/
selector: StackSelector;
/**
* Name of the toolkit stack to use/deploy
*
* @default CDKToolkit
*/
toolkitStackName?: string;
/**
* Role to pass to CloudFormation for deployment
*/
roleArn?: string;
/**
* Optional name to use for the CloudFormation change set.
* If not provided, a name will be generated automatically.
*
* @deprecated Use 'deploymentMethod' instead
*/
changeSetName?: string;
/**
* Whether to execute the ChangeSet
* Not providing `execute` parameter will result in execution of ChangeSet
*
* @default true
* @deprecated Use 'deploymentMethod' instead
*/
execute?: boolean;
/**
* Deployment method
*/
readonly deploymentMethod?: DeploymentMethod;
/**
* Display mode for stack deployment progress.
*
* @default - StackActivityProgress.Bar - stack events will be displayed for
* the resource currently being deployed.
*/
progress?: StackActivityProgress;
/**
* Rollback failed deployments
*
* @default true
*/
readonly rollback?: boolean;
}
interface WatchOptions extends Omit<CfnDeployOptions, 'execute'> {
/**
* Only select the given stack
*
* @default false
*/
exclusively?: boolean;
/**
* Reuse the assets with the given asset IDs
*/
reuseAssets?: string[];
/**
* Always deploy, even if templates are identical.
* @default false
*/
force?: boolean;
/**
* Whether to perform a 'hotswap' deployment.
* A 'hotswap' deployment will attempt to short-circuit CloudFormation
* and update the affected resources like Lambda functions directly.
*
* @default - `HotswapMode.FALL_BACK` for regular deployments, `HotswapMode.HOTSWAP_ONLY` for 'watch' deployments
*/
readonly hotswap: HotswapMode;
/**
* The extra string to append to the User-Agent header when performing AWS SDK calls.
*
* @default - nothing extra is appended to the User-Agent header
*/
readonly extraUserAgent?: string;
/**
* Whether to show CloudWatch logs for hotswapped resources
* locally in the users terminal
*
* @default - false
*/
readonly traceLogs?: boolean;
/**
* Maximum number of simultaneous deployments (dependency permitting) to execute.
* The default is '1', which executes all deployments serially.
*
* @default 1
*/
readonly concurrency?: number;
}
export interface DeployOptions extends CfnDeployOptions, WatchOptions {
/**
* ARNs of SNS topics that CloudFormation will notify with stack related events
*/
notificationArns?: string[];
/**
* What kind of security changes require approval
*
* @default RequireApproval.Broadening
*/
requireApproval?: RequireApproval;
/**
* Tags to pass to CloudFormation for deployment
*/
tags?: Tag[];
/**
* Additional parameters for CloudFormation at deploy time
* @default {}
*/
parameters?: { [name: string]: string | undefined };
/**
* Use previous values for unspecified parameters
*
* If not set, all parameters must be specified for every deployment.
*
* @default true
*/
usePreviousParameters?: boolean;
/**
* Path to file where stack outputs will be written after a successful deploy as JSON
* @default - Outputs are not written to any file
*/
outputsFile?: string;
/**
* Whether we are on a CI system
*
* @default false
*/
readonly ci?: boolean;
/**
* Whether this 'deploy' command should actually delegate to the 'watch' command.
*
* @default false
*/
readonly watch?: boolean;
/**
* Whether we should cache the Cloud Assembly after the first time it has been synthesized.
* The default is 'true', we only don't want to do it in case the deployment is triggered by
* 'cdk watch'.
*
* @default true
*/
readonly cacheCloudAssembly?: boolean;
/**
* Allows adding CloudWatch log groups to the log monitor via
* cloudWatchLogMonitor.setLogGroups();
*
* @default - not monitoring CloudWatch logs
*/
readonly cloudWatchLogMonitor?: CloudWatchLogEventMonitor;
/**
* Maximum number of simultaneous deployments (dependency permitting) to execute.
* The default is '1', which executes all deployments serially.
*
* @default 1
*/
readonly concurrency?: number;
/**
* Build/publish assets for a single stack in parallel
*
* Independent of whether stacks are being done in parallel or no.
*
* @default true
*/
readonly assetParallelism?: boolean;
/**
* When to build assets
*
* The default is the Docker-friendly default.
*
* @default AssetBuildTime.ALL_BEFORE_DEPLOY
*/
readonly assetBuildTime?: AssetBuildTime;
/**
* Whether to deploy if the app contains no stacks.
*
* @default false
*/
readonly ignoreNoStacks?: boolean;
}
export interface RollbackOptions {
/**
* Criteria for selecting stacks to deploy
*/
readonly selector: StackSelector;
/**
* Name of the toolkit stack to use/deploy
*
* @default CDKToolkit
*/
readonly toolkitStackName?: string;
/**
* Role to pass to CloudFormation for deployment
*
* @default - Default stack role
*/
readonly roleArn?: string;
/**
* Whether to force the rollback or not
*
* @default false
*/
readonly force?: boolean;
/**
* Logical IDs of resources to orphan
*
* @default - No orphaning
*/
readonly orphanLogicalIds?: string[];
/**
* Whether to validate the version of the bootstrap stack permissions
*
* @default true
*/
readonly validateBootstrapStackVersion?: boolean;
}
export interface ImportOptions extends CfnDeployOptions {
/**
* Build a physical resource mapping and write it to the given file, without performing the actual import operation
*
* @default - No file
*/
readonly recordResourceMapping?: string;
/**
* Path to a file with the physical resource mapping to CDK constructs in JSON format
*
* @default - No mapping file
*/
readonly resourceMappingFile?: string;
/**
* Allow non-addition changes to the template
*
* @default false
*/
readonly force?: boolean;
}
export interface DestroyOptions {
/**
* Criteria for selecting stacks to deploy
*/
selector: StackSelector;
/**
* Whether to exclude stacks that depend on the stacks to be deleted
*/
exclusively: boolean;
/**
* Whether to skip prompting for confirmation
*/
force: boolean;
/**
* The arn of the IAM role to use
*/
roleArn?: string;
/**
* Whether the destroy request came from a deploy.
*/
fromDeploy?: boolean;
}
/**
* Options for the garbage collection
*/
export interface GarbageCollectionOptions {
/**
* The action to perform.
*
* @default 'full'
*/
readonly action: 'print' | 'tag' | 'delete-tagged' | 'full';
/**
* The type of the assets to be garbage collected.
*
* @default 'all'
*/
readonly type: 's3' | 'ecr' | 'all';
/**
* Elapsed time between an asset being marked as isolated and actually deleted.
*
* @default 0
*/
readonly rollbackBufferDays: number;
/**
* Refuse deletion of any assets younger than this number of days.
*/
readonly createdBufferDays: number;
/**
* The stack name of the bootstrap stack.
*
* @default DEFAULT_TOOLKIT_STACK_NAME
*/
readonly bootstrapStackName?: string;
/**
* Skips the prompt before actual deletion begins
*
* @default false
*/
readonly confirm?: boolean;
}
export interface MigrateOptions {
/**
* The name assigned to the generated stack. This is also used to get
* the stack from the user's account if `--from-stack` is used.
*/
readonly stackName: string;
/**
* The target language for the generated the CDK app.
*
* @default typescript
*/
readonly language?: string;
/**
* The local path of the template used to generate the CDK app.
*
* @default - Local path is not used for the template source.
*/
readonly fromPath?: string;
/**
* Whether to get the template from an existing CloudFormation stack.
*
* @default false
*/
readonly fromStack?: boolean;
/**
* The output path at which to create the CDK app.
*
* @default - The current directory
*/
readonly outputPath?: string;
/**
* The account from which to retrieve the template of the CloudFormation stack.
*
* @default - Uses the account for the credentials in use by the user.
*/
readonly account?: string;
/**
* The region from which to retrieve the template of the CloudFormation stack.
*
* @default - Uses the default region for the credentials in use by the user.
*/
readonly region?: string;
/**
* Filtering criteria used to select the resources to be included in the generated CDK app.
*
* @default - Include all resources
*/
readonly filter?: string[];
/**
* Whether to initiate a new account scan for generating the CDK app.
*
* @default false
*/
readonly fromScan?: FromScan;
/**
* Whether to zip the generated cdk app folder.
*
* @default false
*/
readonly compress?: boolean;
}
export interface RefactorOptions {
/**
* Whether to only show the proposed refactor, without applying it
*/
readonly dryRun: boolean;
/**
* Criteria for selecting stacks to deploy
*/
selector: StackSelector;
/**
* The absolute path to a file that contains a list of resources that
* should be excluded during the refactor. This file should contain a
* newline separated list of _destination_ locations to exclude, i.e.,
* the location to which a resource would be moved if the refactor
* were to happen.
*
* The format of the locations in the file can be either:
*
* - Stack name and logical ID (e.g. `Stack1.MyQueue`)
* - A construct path (e.g. `Stack1/Foo/Bar/Resource`).
*/
excludeFile?: string;
}
function buildParameterMap(
parameters:
| {
[name: string]: string | undefined;
}
| undefined,
): { [name: string]: { [name: string]: string | undefined } } {
const parameterMap: {
[name: string]: { [name: string]: string | undefined };
} = { '*': {} };
for (const key in parameters) {
if (parameters.hasOwnProperty(key)) {
const [stack, parameter] = key.split(':', 2);
if (!parameter) {
parameterMap['*'][stack] = parameters[key];
} else {
if (!parameterMap[stack]) {
parameterMap[stack] = {};
}
parameterMap[stack][parameter] = parameters[key];
}
}
}
return parameterMap;
}
/**
* Ask the user for a yes/no confirmation
*
* Automatically fail the confirmation in case we're in a situation where the confirmation
* cannot be interactively obtained from a human at the keyboard.
*/
async function askUserConfirmation(
ioHost: CliIoHost,
concurrency: number,
motivation: string,
question: string,
) {
await ioHost.withCorkedLogging(async () => {
// only talk to user if STDIN is a terminal (otherwise, fail)
if (!TESTING && !process.stdin.isTTY) {
throw new ToolkitError(`${motivation}, but terminal (TTY) is not attached so we are unable to get a confirmation from the user`);
}
// only talk to user if concurrency is 1 (otherwise, fail)
if (concurrency > 1) {
throw new ToolkitError(`${motivation}, but concurrency is greater than 1 so we are unable to get a confirmation from the user`);
}
const confirmed = await promptly.confirm(`${chalk.cyan(question)} (y/n)?`);
if (!confirmed) {
throw new ToolkitError('Aborted by user');
}
});
}
/**
* Logger for processing stack metadata
*/
function stackMetadataLogger(verbose?: boolean): (level: 'info' | 'error' | 'warn', msg: cxapi.SynthesisMessage) => Promise<void> {
const makeLogger = (level: string): [logger: (m: string) => void, prefix: string] => {
switch (level) {
case 'error':
return [error, 'Error'];
case 'warn':
return [warning, 'Warning'];
default:
return [info, 'Info'];
}
};
return async (level, msg) => {
const [logFn, prefix] = makeLogger(level);
logFn(`[${prefix} at ${msg.id}] ${msg.entry.data}`);
if (verbose && msg.entry.trace) {
logFn(` ${msg.entry.trace.join('\n ')}`);
}
};
}
/**
* Determine if manual approval is required or not. Requires approval for
* - RequireApproval.ANY_CHANGE
* - RequireApproval.BROADENING and the changes are indeed broadening permissions
*/
function requiresApproval(requireApproval: RequireApproval, permissionChangeType: PermissionChangeType) {
return requireApproval === RequireApproval.ANY_CHANGE ||
requireApproval === RequireApproval.BROADENING && permissionChangeType === PermissionChangeType.BROADENING;
}