aws-cdk
Version:
AWS CDK CLI, the command line tool for CDK apps
903 lines • 282 kB
JavaScript
"use strict";
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
if (value !== null && value !== void 0) {
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
var dispose, inner;
if (async) {
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
dispose = value[Symbol.asyncDispose];
}
if (dispose === void 0) {
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
dispose = value[Symbol.dispose];
if (async) inner = dispose;
}
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
env.stack.push({ value: value, dispose: dispose, async: async });
}
else if (async) {
env.stack.push({ async: true });
}
return value;
};
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
return function (env) {
function fail(e) {
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
env.hasError = true;
}
var r, s = 0;
function next() {
while (r = env.stack.pop()) {
try {
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
if (r.dispose) {
var result = r.dispose.call(r.value);
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
}
else s |= 1;
}
catch (e) {
fail(e);
}
}
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
if (env.hasError) throw env.error;
}
return next();
};
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
});
Object.defineProperty(exports, "__esModule", { value: true });
exports.CdkToolkit = exports.AssetBuildTime = void 0;
exports.displayFlagsMessage = displayFlagsMessage;
const node_crypto_1 = require("node:crypto");
const path = require("node:path");
const node_util_1 = require("node:util");
const cxapi = require("@aws-cdk/cloud-assembly-api");
const cloud_assembly_schema_1 = require("@aws-cdk/cloud-assembly-schema");
const toolkit_lib_1 = require("@aws-cdk/toolkit-lib");
const chalk = require("chalk");
const chokidar = require("chokidar");
const handler_js_1 = require("chokidar/handler.js");
const fs = require("fs-extra");
const io_host_1 = require("./io-host");
const user_configuration_1 = require("./user-configuration");
const api_private_1 = require("../../lib/api-private");
const api_1 = require("../api");
const bootstrap_1 = require("../api/bootstrap");
const cloud_assembly_1 = require("../api/cloud-assembly");
const deploy_private_1 = require("../api/deploy-private");
const refactor_1 = require("../api/refactor");
const deploy_1 = require("../commands/deploy");
const list_stacks_1 = require("../commands/list-stacks");
const migrate_1 = require("../commands/migrate");
const cxapp_1 = require("../cxapp");
const util_1 = require("../util");
const collect_telemetry_1 = require("./telemetry/collect-telemetry");
const error_1 = require("./telemetry/error");
const messages_1 = require("./telemetry/messages");
const operations_1 = require("../commands/flags/operations");
// 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 = require('p-limit');
/**
* File events that we care about from chokidar.
* In chokidar v4, EventName includes additional events like 'error', 'raw', 'ready', 'all'
* that we need to filter out in the 'all' handler.
*/
const FILE_EVENTS = [handler_js_1.EVENTS.ADD, handler_js_1.EVENTS.ADD_DIR, handler_js_1.EVENTS.CHANGE, handler_js_1.EVENTS.UNLINK, handler_js_1.EVENTS.UNLINK_DIR];
/**
* Type guard to check if an event is a file event we should process.
*/
function isFileEvent(event) {
return FILE_EVENTS.includes(event);
}
/**
* When to build assets
*/
var AssetBuildTime;
(function (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).
*/
AssetBuildTime["ALL_BEFORE_DEPLOY"] = "all-before-deploy";
/**
* Build assets just-in-time, before publishing
*/
AssetBuildTime["JUST_IN_TIME"] = "just-in-time";
})(AssetBuildTime || (exports.AssetBuildTime = AssetBuildTime = {}));
/**
* Custom implementation of the public Toolkit to integrate with the legacy CdkToolkit
*
* This overwrites how an sdkProvider is acquired
* in favor of the one provided directly to CdkToolkit.
*/
class InternalToolkit extends toolkit_lib_1.Toolkit {
constructor(sdkProvider, options) {
super(options);
this._sdkProvider = sdkProvider;
}
/**
* Access to the AWS SDK
* @internal
*/
async sdkProvider(_action) {
return this._sdkProvider;
}
}
/**
* Toolkit logic
*
* The toolkit runs the `cloudExecutable` to obtain a cloud assembly and
* deploys applies them to `cloudFormation`.
*/
class CdkToolkit {
constructor(props) {
this.props = props;
this.ioHost = props.ioHost ?? io_host_1.CliIoHost.instance();
this.toolkitStackName = props.toolkitStackName ?? api_1.DEFAULT_TOOLKIT_STACK_NAME;
// We don't have the toolkit check unstable features. Instead, the error
// messages the CLI give are slightly different, and so they need to be checked
// separately. Instead, the Toolkit we use has all unstable features automatically
// enabled.
const iPromiseUnstablenessIsCheckedByTheCli = {
'publish-assets': true,
'diagnose': true,
'flags': true,
'orphan': true,
'refactor': true,
'validate': true,
};
this.toolkit = new InternalToolkit(props.sdkProvider, {
assemblyFailureAt: this.validateMetadataFailAt(),
color: true,
emojis: true,
ioHost: this.ioHost,
toolkitStackName: this.toolkitStackName,
unstableFeatures: Object.keys(iPromiseUnstablenessIsCheckedByTheCli),
});
}
async metadata(stackName, json) {
const stacks = await this.selectSingleStackByName(stackName);
await printSerializedObject(this.ioHost.asIoHelper(), stacks.firstStack.metadata ?? {}, json);
}
async acknowledge(noticeId) {
const acks = new Set(this.props.configuration.context.get('acknowledged-issue-numbers') ?? []);
acks.add(Number(noticeId));
this.props.configuration.context.set('acknowledged-issue-numbers', Array.from(acks));
await this.props.configuration.saveContext();
}
async cliTelemetryStatus(args) {
const canCollect = (0, collect_telemetry_1.canCollectTelemetry)(args, this.props.configuration.context);
if (canCollect) {
await this.ioHost.asIoHelper().defaults.info('CLI Telemetry is enabled. See https://docs.aws.amazon.com/cdk/v2/guide/cli-telemetry.html for ways to disable.');
}
else {
await this.ioHost.asIoHelper().defaults.info('CLI Telemetry is disabled. See https://docs.aws.amazon.com/cdk/v2/guide/cli-telemetry.html for ways to enable.');
}
}
async cliTelemetry(enable) {
this.props.configuration.context.set('cli-telemetry', enable);
await this.props.configuration.saveContext();
await this.ioHost.asIoHelper().defaults.info(`Telemetry ${enable ? 'enabled' : 'disabled'}`);
}
async diff(options) {
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 toolkit_lib_1.ToolkitError('SingleStackRequired', '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 toolkit_lib_1.ToolkitError('TemplateNotFound', `There is no file at ${options.templatePath}`);
}
if (options.importExistingResources) {
throw new toolkit_lib_1.ToolkitError('ImportWithTemplatePath', 'Can only use --import-existing-resources flag when comparing against deployed stacks.');
}
const template = (0, util_1.deserializeStructure)(await fs.readFile(options.templatePath, { encoding: 'utf-8' }));
const formatter = new api_1.DiffFormatter({
templateInfo: {
oldTemplate: template,
newTemplate: stacks.firstStack,
},
});
if (options.securityOnly) {
const securityDiff = formatter.formatSecurityDiff({ quiet });
// Warn, count, and display the diff only if the reported changes are broadening permissions
if (securityDiff.permissionChangeType === toolkit_lib_1.PermissionChangeType.BROADENING) {
await this.ioHost.asIoHelper().defaults.warn('This deployment will make potentially sensitive changes according to your current security approval level.\nPlease confirm you intend to make the following modifications:\n');
await this.ioHost.asIoHelper().defaults.info(securityDiff.formattedDiff);
diffs += securityDiff.numStacksWithChanges;
}
}
else {
const diff = formatter.formatStackDiff({
strict,
contextLines,
quiet,
});
diffs = diff.numStacksWithChanges;
await this.ioHost.asIoHelper().defaults.info(diff.formattedDiff);
}
}
else {
const allMappings = options.includeMoves
? await (0, refactor_1.mappingsByEnvironment)(stacks.stackArtifacts, this.props.sdkProvider, true)
: [];
// 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 api_1.ResourceMigrator({
deployments: this.props.deployments,
ioHelper: (0, api_private_1.asIoHelper)(this.ioHost, 'diff'),
});
const resourcesToImport = await migrator.tryGetResources(await this.props.deployments.resolveEnvironment(stack));
if (resourcesToImport) {
(0, api_1.removeNonImportResources)(stack);
}
const changeSet = (options.method !== 'template')
? await this.tryCreateDiffChangeSet(stack, options, parameterMap, resourcesToImport, quiet)
: undefined;
const mappings = allMappings.find(m => m.environment.region === stack.environment.region && m.environment.account === stack.environment.account)?.mappings ?? {};
const formatter = new api_1.DiffFormatter({
templateInfo: {
oldTemplate: currentTemplate,
newTemplate: stack,
changeSet,
isImport: !!resourcesToImport,
nestedStacks,
mappings,
},
});
if (options.securityOnly) {
const securityDiff = formatter.formatSecurityDiff({ quiet });
// Warn, count, and display the diff only if the reported changes are broadening permissions
if (securityDiff.permissionChangeType === toolkit_lib_1.PermissionChangeType.BROADENING) {
await this.ioHost.asIoHelper().defaults.warn('This deployment will make potentially sensitive changes according to your current security approval level.\nPlease confirm you intend to make the following modifications:\n');
await this.ioHost.asIoHelper().defaults.info(securityDiff.formattedDiff);
diffs += securityDiff.numStacksWithChanges;
}
}
else {
const diff = formatter.formatStackDiff({
strict,
contextLines,
quiet,
});
await this.ioHost.asIoHelper().defaults.info(diff.formattedDiff);
diffs += diff.numStacksWithChanges;
}
}
}
await this.ioHost.asIoHelper().defaults.info((0, node_util_1.format)('\n✨ Number of stacks with differences: %s\n', diffs));
return diffs && options.fail ? 1 : 0;
}
/**
* Try to create a diff changeset for the given stack.
* Returns undefined if the stack cannot be accessed and changeSetOnly is not set.
*/
async tryCreateDiffChangeSet(stack, options, parameterMap, resourcesToImport, quiet) {
try {
// we don't actually need to know if the stack exists here
// we just use this to flush our any permissions issues and drop the result
void await this.props.deployments.stackExists({
stack,
deployName: stack.stackName,
tryLookupRole: true,
});
}
catch (e) {
if (options.method === 'change-set') {
throw toolkit_lib_1.ToolkitError.withCause('DescribeStacksFailed', `Could not access stack '${stack.stackName}'. Please check your permissions or use '--method=auto' to allow falling back to a template diff.`, e);
}
await this.ioHost.asIoHelper().defaults.debug((0, util_1.formatErrorMessage)(e));
if (!quiet) {
await this.ioHost.asIoHelper().defaults.info(`Could not access stack '${stack.stackName}', falling back to template diff. Use '--method=change-set' to fail instead. Run with -v to see the reason.\n`);
}
return undefined;
}
return api_private_1.cfnApi.createDiffChangeSet((0, api_private_1.asIoHelper)(this.ioHost, 'diff'), {
stack,
uuid: (0, node_crypto_1.randomUUID)(),
deployments: this.props.deployments,
willExecute: false,
sdkProvider: this.props.sdkProvider,
parameters: Object.assign({}, parameterMap['*'], parameterMap[stack.stackName]),
resourcesToImport,
importExistingResources: options.importExistingResources,
failOnError: options.method === 'change-set',
});
}
async deploy(options) {
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;
}
// the ioHost uses this internally to determine if a confirmation
// is actually needed, so it needs the same value we determine here.
const requireApproval = options.requireApproval ?? cloud_assembly_schema_1.RequireApproval.BROADENING;
this.ioHost.requireDeployApproval = requireApproval;
// execute-change-set is a new flow that we can just delegate to toolkit-lib
if (options.deploymentMethod?.method === 'execute-change-set') {
await this.toolkit.deploy(this.props.cloudExecutable, {
deploymentMethod: options.deploymentMethod,
stacks: {
patterns: options.selector.patterns,
strategy: api_1.StackSelectionStrategy.PATTERN_MUST_MATCH_SINGLE,
expand: cloud_assembly_1.ExpandStackSelection.NONE,
},
roleArn: options.roleArn,
forceDeployment: options.force,
rollback: options.rollback,
reuseAssets: options.reuseAssets,
concurrency: options.concurrency,
traceLogs: options.traceLogs,
notificationArns: options.notificationArns,
tags: options.tags,
outputsFile: options.outputsFile,
assetParallelism: options.assetParallelism,
assetBuildConcurrency: options.assetBuildConcurrency,
assetBuildTime: options.assetBuildTime,
parameters: undefined, // parameters are only set during change set creation, so this is explicitly unset because change set already exists
});
return;
}
const startSynthTime = new Date().getTime();
const stackCollection = await this.selectStacksForDeploy(options.selector, options.exclusively, options.cacheCloudAssembly, options.ignoreNoStacks);
const elapsedSynthTime = new Date().getTime() - startSynthTime;
await this.ioHost.asIoHelper().defaults.info(`\n✨ Synthesis time: ${(0, util_1.formatTime)(elapsedSynthTime)}s\n`);
if (stackCollection.stackCount === 0) {
await this.ioHost.asIoHelper().defaults.error('This app contains no stacks');
return;
}
const migrator = new api_1.ResourceMigrator({
deployments: this.props.deployments,
ioHelper: (0, api_private_1.asIoHelper)(this.ioHost, 'deploy'),
});
await migrator.tryMigrateResources(stackCollection, {
toolkitStackName: this.toolkitStackName,
...options,
});
if (options.deploymentMethod?.method === 'hotswap') {
await this.ioHost.asIoHelper().defaults.warn('⚠️ The --hotswap and --hotswap-fallback flags deliberately introduce CloudFormation drift to speed up deployments');
await this.ioHost.asIoHelper().defaults.warn('⚠️ They should only be used for development - never use them for your production Stacks!\n');
}
const stacks = stackCollection.stackArtifacts;
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 = deploy_1.StackActivityProgress.EVENTS;
// ...but only warn if the user explicitly requested "bar" progress
if (options.progress && options.progress != deploy_1.StackActivityProgress.EVENTS) {
await this.ioHost.asIoHelper().defaults.warn('⚠️ 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 api_1.WorkGraphBuilder((0, api_private_1.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 = {
'stack': concurrency,
'asset-build': (options.assetParallelism ?? true) ? options.assetBuildConcurrency ?? 1 : 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
'marker': 1,
};
const deploymentActions = new WorkGraphDeploymentActions(this.props.deployments, this.ioHost, this, {
roleArn: options.roleArn,
force: options.force,
stackCount: stackCollection.stackCount,
notificationArns: options.notificationArns,
deploymentMethod: options.deploymentMethod,
toolkitStackName: this.toolkitStackName,
reuseAssets: options.reuseAssets,
tags: options.tags,
parameters: options.parameters,
usePreviousParameters: options.usePreviousParameters,
rollback: options.rollback,
concurrency,
requireApproval,
assetParallelism: options.assetParallelism,
extraUserAgent: options.extraUserAgent,
cloudWatchLogMonitor: options.cloudWatchLogMonitor,
sdkProvider: this.props.sdkProvider,
});
const startDeployTime = Date.now();
await workGraph.doParallel(graphConcurrency, deploymentActions);
if (options.outputsFile) {
// 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.
await deploymentActions.writeOutputs(options.outputsFile);
}
// Add a timer on the COMMAND span for the full deployment wait time (not the same as the sum of all DEPLOY
// spans because of parallelism).
this.ioHost.telemetry?.commandSpan?.addTimer('totalDeployTime', Date.now() - startDeployTime);
await this.ioHost.asIoHelper().defaults.info(`\n✨ Total time: ${(0, util_1.formatTime)(Date.now() - startSynthTime)}s\n`);
}
/**
* Detect infrastructure drift for the given stack(s)
*/
async drift(options) {
const driftResults = await this.toolkit.drift(this.props.cloudExecutable, {
stacks: {
patterns: options.selector.patterns,
strategy: options.selector.patterns.length > 0 ? api_1.StackSelectionStrategy.PATTERN_MATCH : api_1.StackSelectionStrategy.ALL_STACKS,
},
});
const totalDrifts = Object.values(driftResults).reduce((total, current) => total + (current.numResourcesWithDrift ?? 0), 0);
return totalDrifts > 0 && options.fail ? 1 : 0;
}
/**
* Validate synthesized templates against policy rules
*/
async validate(options) {
const result = await this.toolkit.validate(this.props.cloudExecutable, options);
return result.conclusion === 'failure' ? 1 : 0;
}
/**
* Diagnose errors
*/
async diagnose(options) {
const results = await this.toolkit.diagnose(this.props.cloudExecutable, options);
if (results.stacks.some(s => s.result.type !== 'no-problem')) {
return 1;
}
return 0;
}
/**
* Roll back the given stack or stacks.
*/
async rollback(options) {
const startSynthTime = new Date().getTime();
const stackCollection = await this.selectStacksForDeploy(options.selector, true);
const elapsedSynthTime = new Date().getTime() - startSynthTime;
await this.ioHost.asIoHelper().defaults.info(`\n✨ Synthesis time: ${(0, util_1.formatTime)(elapsedSynthTime)}s\n`);
if (stackCollection.stackCount === 0) {
await this.ioHost.asIoHelper().defaults.error('No stacks selected');
return;
}
let anyRollbackable = false;
for (const stack of stackCollection.stackArtifacts) {
await this.ioHost.asIoHelper().defaults.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;
await this.ioHost.asIoHelper().defaults.info(`\n✨ Rollback time: ${(0, util_1.formatTime)(elapsedRollbackTime).toString()}s\n`);
}
catch (e) {
await this.ioHost.asIoHelper().defaults.error('\n ❌ %s failed: %s', chalk.bold(stack.displayName), (0, util_1.formatErrorMessage)(e));
throw new toolkit_lib_1.ToolkitError('RollbackFailed', 'Rollback failed (use --force to orphan failing resources)');
}
}
if (!anyRollbackable) {
throw new toolkit_lib_1.ToolkitError('NoRollbackableStacks', 'No stacks were in a state that could be rolled back');
}
}
async publishAssets(options) {
await this.toolkit.publishAssets(this.props.cloudExecutable, options);
}
async watch(options) {
const rootDir = path.dirname(path.resolve(user_configuration_1.PROJECT_CONFIG));
const ioHelper = (0, api_private_1.asIoHelper)(this.ioHost, 'watch');
await this.ioHost.asIoHelper().defaults.debug("root directory used for 'watch' is: %s", rootDir);
const watchSettings = this.props.configuration.settings.get(['watch']);
if (!watchSettings) {
throw new toolkit_lib_1.ToolkitError('WatchConfigMissing', "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.
// Note: We use '**' as the default pattern (not rootDir) because chokidar reports
// file paths relative to cwd, and the ignored function uses picomatch which expects
// glob patterns, not absolute paths.
const watchIncludes = this.patternsArrayForWatch(watchSettings.include, {
defaultPattern: '**',
returnDefaultIfEmpty: true,
});
await this.ioHost.asIoHelper().defaults.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, {
defaultPattern: '',
returnDefaultIfEmpty: false,
}).concat(`${outputDir}/**`, '**/.*', '**/.*/**', '**/node_modules/**');
await this.ioHost.asIoHelper().defaults.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';
const cloudWatchLogMonitor = options.traceLogs ? new api_1.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 === 'queued') {
// TypeScript doesn't realize latch can change between 'awaits',
// and thinks the above 'while' condition is always 'false' without the cast
latch = 'deploying';
await this.ioHost.asIoHelper().defaults.info("Detected file changes during deployment. Invoking 'cdk deploy' again");
await this.invokeDeployFromWatch(options, cloudWatchLogMonitor);
}
latch = 'open';
await cloudWatchLogMonitor?.activate();
};
// Create ignore matcher for chokidar v4 compatibility
// Chokidar v4 removed glob pattern support, so we use picomatch to filter files
// We pass rootDir because chokidar v4 passes absolute paths to the ignored callback
const shouldIgnore = (0, api_private_1.createIgnoreMatcher)({
include: watchIncludes,
exclude: watchExcludes,
rootDir,
});
chokidar
.watch('.', {
ignored: shouldIgnore,
cwd: rootDir,
})
.on('ready', async () => {
latch = 'open';
await this.ioHost.asIoHelper().defaults.debug("'watch' received the 'ready' event. From now on, all file changes will trigger a deployment");
await this.ioHost.asIoHelper().defaults.info("Triggering initial 'cdk deploy'");
await deployAndWatch();
})
.on('all', async (event, filePath) => {
if (!isFileEvent(event)) {
return; // Ignore non-file events like 'error', 'raw', 'ready', 'all'
}
if (latch === 'pre-ready') {
await this.ioHost.asIoHelper().defaults.info(`'watch' is observing ${event === 'addDir' ? 'directory' : 'the file'} '%s' for changes`, filePath);
}
else if (latch === 'open') {
await this.ioHost.asIoHelper().defaults.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';
await this.ioHost.asIoHelper().defaults.info("Detected change to '%s' (type: %s) while 'cdk deploy' is still running. " +
'Will queue for another deployment after this one finishes', filePath, event);
}
});
}
async orphan(options) {
await this.toolkit.orphan(this.props.cloudExecutable, {
constructPaths: options.constructPath,
roleArn: options.roleArn,
toolkitStackName: options.toolkitStackName,
});
}
async import(options) {
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 toolkit_lib_1.ToolkitError('AmbiguousStackSelection', `Stack selection is ambiguous, please choose a specific stack for import [${stacks.stackArtifacts.map((x) => x.id).join(', ')}]`);
}
if (!process.stdout.isTTY && !options.resourceMappingFile && !options.resourceMappingInline) {
throw new toolkit_lib_1.ToolkitError('ResourceMappingRequired', '--resource-mapping or --resource-mapping-inline is required when input is not a terminal');
}
const stack = stacks.stackArtifacts[0];
await this.ioHost.asIoHelper().defaults.info(chalk.bold(stack.displayName));
const resourceImporter = new api_1.ResourceImporter(stack, {
deployments: this.props.deployments,
ioHelper: (0, api_private_1.asIoHelper)(this.ioHost, 'import'),
});
const { additions, hasNonAdditions, diffFormatter } = await resourceImporter.discoverImportableResources(options.force);
// If there are non-addition changes (e.g. after orphan, hardcoded refs differ from Fn::GetAtt),
// warn the user and ask for confirmation unless --force was given.
if (hasNonAdditions && !options.force) {
const ioHelper = this.ioHost.asIoHelper();
await ioHelper.defaults.info(`The following resources have pending updates that will be reconciled with a ${chalk.blueBright('cdk deploy')} after import:`);
const { formattedDiff } = diffFormatter.formatStackDiff();
await ioHelper.defaults.info(formattedDiff);
const confirmed = await ioHelper.requestResponse(api_private_1.IO.CDK_TOOLKIT_I7010.req('Perform import?', { motivation: 'Confirm import with pending drift' }));
if (!confirmed) {
await ioHelper.defaults.info('Import cancelled.');
return;
}
}
if (additions.length === 0) {
await this.ioHost.asIoHelper().defaults.warn('%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
let actualImport;
if (options.resourceMappingInline) {
actualImport = await resourceImporter.loadResourceIdentifiers(additions, options.resourceMappingInline);
}
else if (options.resourceMappingFile) {
actualImport = await resourceImporter.loadResourceIdentifiersFromFile(additions, options.resourceMappingFile);
}
else {
actualImport = await resourceImporter.askForResourceIdentifiers(additions);
}
if (actualImport.importResources.length === 0) {
await this.ioHost.asIoHelper().defaults.warn('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',
});
await this.ioHost.asIoHelper().defaults.info('%s: mapping file written.', outputFile);
return;
}
// Import the resources according to the given mapping
await this.ioHost.asIoHelper().defaults.info('%s: importing resources into stack...', chalk.bold(stack.displayName));
const tags = (0, api_private_1.tagsForStack)(stack);
await resourceImporter.importResourcesFromMap(actualImport, {
roleArn: options.roleArn,
tags,
deploymentMethod: options.deploymentMethod,
usePreviousParameters: true,
rollback: options.rollback,
});
// Notify user of next steps
if (actualImport.importResources.length < additions.length) {
await this.ioHost.asIoHelper().defaults.warn(`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) {
// After orphan→import, the deployed template still has hardcoded values that differ from
// the synth'd template's Fn::GetAtt/Ref intrinsics. A deploy updates the template to match the CDK app.
if (options.force) {
await this.ioHost.asIoHelper().defaults.info(`Import complete. Run ${chalk.blueBright('cdk deploy')} to update the stack to match your CDK app.`);
}
else {
const deployNow = await this.ioHost.asIoHelper().requestResponse(api_private_1.IO.CDK_TOOLKIT_I7010.req(`Finish with a ${chalk.blueBright('cdk deploy')} now?`, { motivation: 'Update stack to match CDK app after import' }));
if (deployNow) {
await this.deploy({
selector: options.selector,
toolkitStackName: options.toolkitStackName,
roleArn: options.roleArn,
deploymentMethod: options.deploymentMethod,
});
}
else {
await this.ioHost.asIoHelper().defaults.info(`Import complete. Remember to run ${chalk.blueBright('cdk deploy')} to update the stack to match your CDK app.`);
}
}
}
else {
await this.ioHost.asIoHelper().defaults.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'));
}
}
async destroy(options) {
const ioHelper = this.ioHost.asIoHelper();
const stacks = await this.selectStacksForDestroy(options.selector, options.exclusively);
if (!options.force) {
const motivation = 'Destroying stacks is an irreversible action';
const question = `Are you sure you want to delete: ${chalk.blue(stacks.stackArtifacts.map((s) => s.hierarchicalId).join(', '))}`;
try {
await ioHelper.requestResponse(api_private_1.IO.CDK_TOOLKIT_I7010.req(question, { motivation }));
}
catch (err) {
if (!toolkit_lib_1.ToolkitError.isToolkitError(err) || err.message != 'Aborted by user') {
throw err; // unexpected error
}
await ioHelper.notify(api_private_1.IO.CDK_TOOLKIT_E7010.msg(err.message));
return;
}
}
const concurrency = options.concurrency || 1;
const action = options.fromDeploy ? 'deploy' : 'destroy';
let destroyCount = 0;
if (concurrency > 1) {
this.ioHost.stackProgress = deploy_1.StackActivityProgress.EVENTS;
}
const destroyStack = async (stackNode) => {
const stack = stackNode.stack;
destroyCount++;
await ioHelper.defaults.info(chalk.green('%s: destroying... [%s/%s]'), chalk.blue(stack.displayName), destroyCount, stacks.stackCount);
try {
await this.props.deployments.destroyStack({
stack,
deployName: stack.stackName,
roleArn: options.roleArn,
});
await ioHelper.defaults.info(chalk.green(`\n ✅ %s: ${action}ed`), chalk.blue(stack.displayName));
}
catch (e) {
await ioHelper.defaults.error(`\n ❌ %s: ${action} failed`, chalk.blue(stack.displayName), e);
throw e;
}
};
const workGraph = (0, api_1.buildDestroyWorkGraph)(stacks.stackArtifacts, ioHelper);
await workGraph.processStacks(concurrency, destroyStack);
}
async list(selectors, options = {}) {
this.ioHost.rewriteOnce(api_private_1.IO.CDK_TOOLKIT_I2901, (msg) => (0, list_stacks_1.formatStackList)(msg.data.stacks, options));
// With `--json`, stdout must stay machine-parsable, so suppress the synth-time line (I1000).
if (options.json) {
this.ioHost.once(api_private_1.IO.CDK_TOOLKIT_I1000, () => ({ preventDefault: true }));
}
await this.toolkit.list(this.props.cloudExecutable, {
stacks: selectors.length > 0
? { patterns: selectors, strategy: api_1.StackSelectionStrategy.PATTERN_MATCH, expand: cloud_assembly_1.ExpandStackSelection.UPSTREAM }
: undefined,
});
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.
*/
async synth(stackNames, exclusively, quiet, autoValidate, json) {
const stacks = await this.selectStacksForDiff(stackNames, exclusively, autoValidate);
// if we have a single stack, print it to STDOUT
if (stacks.stackCount === 1) {
if (!quiet) {
await printSerializedObject(this.ioHost.asIoHelper(), (0, util_1.obscureTemplate)(stacks.firstStack.template), json ?? false);
}
// In CI mode, non-error messages go to stdout. When we just printed the
// template to stdout, skip the flags message to preserve the contract that
// `cdk synth` output is valid YAML. When quiet (no template printed) or
// non-CI (flags go to stderr), it's safe to show.
if (quiet || !this.ioHost.isCI) {
await displayFlagsMessage(this.ioHost.asIoHelper(), this.toolkit, this.props.cloudExecutable);
}
return undefined;
}
// not outputting template to stdout, let's explain things to the user a little bit...
await this.ioHost.asIoHelper().defaults.info(chalk.green(`Successfully synthesized to ${chalk.blue(path.resolve(stacks.assembly.directory))}`));
await this.ioHost.asIoHelper().defaults.info(`Supply a stack id (${stacks.stackArtifacts.map((s) => chalk.green(s.hierarchicalId)).join(', ')}) to display its template.`);
await displayFlagsMessage(this.ioHost.asIoHelper(), this.toolkit, this.props.cloudExecutable);
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.
*/
async bootstrap(userEnvironmentSpecs, options) {
const bootstrapper = new bootstrap_1.Bootstrapper(options.source, (0, api_private_1.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 () => {
await this.ioHost.asIoHelper().defaults.info(chalk.green(' ⏳ 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.';
await this.ioHost.asIoHelper().defaults.info(chalk.green(message), chalk.blue(environment.name));
}
catch (e) {
await this.ioHost.asIoHelper().defaults.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
*/
async garbageCollect(userEnvironmentSpecs, options) {
const environments = await this.defineEnvironments(userEnvironmentSpecs);
for (const environment of environments) {
await this.ioHost.asIoHelper().defaults.info(chalk.green(' ⏳ Garbage Collecting environment %s...'), chalk.blue(environment.name));
const gc = new api_1.GarbageCollector({
sdkProvider: this.props.sdkProvider,
ioHelper: (0, api_private_1.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();
}
}
async defineEnvironments(userEnvironmentSpecs) {
// By default, glob for everything
const environmentSpecs = userEnvironmentSpecs.length > 0 ? [...userEnvironmentSpecs] : ['**'];
// Partition into globs and non-globs (this will mutate environmentSpecs).
const globSpecs = (0, util_1.partition)(environmentSpecs, cxapp_1.looksLikeGlob);
if (globSpecs.length > 0 && !this.props.cloudExecutable.hasApp) {
if (userEnvironmentSpecs.length > 0) {
// User did request this glob
throw new toolkit_lib_1.ToolkitError('InvalidEnvironmentGlob', `'${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 toolkit_lib_1.ToolkitError('EnvironmentRequired', "Specify an environment name like 'aws://123456789012/us-east-1', or run in a directory with 'cdk.json'.");
}
}
const environments = [...(0, cxapp_1.environmentsFromDescriptors)(environmentSpecs)];
// If there is an '--app' argument, select the environments from the app.
if (this.props.cloudExecutable.hasApp) {
environments.push(...(await (0, cxapp_1.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
*/
async migrate(options) {
await this.ioHost.asIoHelper().defaults.warn('This command is an experimental feature.');
const language = options.language?.toLowerCase() ?? 'typescript';
const environment = (0, migrate_1.setEnvironment)(options.account, options.region);
let generateTemplateOutput;
let cfn;
let templateToDelete;
try {
// if neither fromPath nor fromStack is provided, generate a template using cloudformation
const scanType = (0, migrate_1.parseSourceOptions)(options.fromPath, options.fromStack, options.stackName).source;
if (scanType == migrate_1.TemplateSourceOptions.SCAN) {
generateTemplateOutput = await (0, migrate_1.generateTemplate)({
stackName: options.stackName,