aws-cdk
Version:
AWS CDK CLI, the command line tool for CDK apps
922 lines • 202 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CdkToolkit = exports.AssetBuildTime = void 0;
exports.markTesting = markTesting;
const path = require("path");
const util_1 = require("util");
const cloudformation_diff_1 = require("@aws-cdk/cloudformation-diff");
const cxapi = require("@aws-cdk/cx-api");
const chalk = require("chalk");
const chokidar = require("chokidar");
const fs = require("fs-extra");
const promptly = require("promptly");
const uuid = require("uuid");
const io_host_1 = require("./io-host");
const user_configuration_1 = require("./user-configuration");
const api_1 = require("../../../@aws-cdk/tmp-toolkit-helpers/src/api");
const private_1 = require("../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private");
const refactoring_1 = require("../../../@aws-cdk/tmp-toolkit-helpers/src/api/refactoring");
const toolkit_1 = require("../../../@aws-cdk/toolkit-lib/lib/toolkit");
const api_2 = require("../api");
const bootstrap_1 = require("../api/bootstrap");
const cloud_assembly_1 = require("../api/cloud-assembly");
const garbage_collection_1 = require("../api/garbage-collection");
const hotswap_1 = require("../api/hotswap");
const logs_monitor_1 = require("../api/logs-monitor");
const resource_import_1 = require("../api/resource-import");
const tags_1 = require("../api/tags");
const work_graph_1 = require("../api/work-graph");
const api_private_1 = require("../api-private");
const deploy_1 = require("../commands/deploy");
const diff_1 = require("../commands/diff");
const list_stacks_1 = require("../commands/list-stacks");
const migrate_1 = require("../commands/migrate");
const cxapp_1 = require("../cxapp");
const logging_1 = require("../logging");
const util_2 = require("../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 = require('p-limit');
let TESTING = false;
function markTesting() {
TESTING = true;
}
/**
* 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 = {}));
class InternalToolkit extends toolkit_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_2.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
}
async metadata(stackName, json) {
const stacks = await this.selectSingleStackByName(stackName);
printSerializedObject(stacks.firstStack.manifest.metadata ?? {}, json);
}
async acknowledge(noticeId) {
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();
}
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 api_1.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 api_1.ToolkitError(`There is no file at ${options.templatePath}`);
}
const template = (0, util_2.deserializeStructure)(await fs.readFile(options.templatePath, { encoding: 'UTF-8' }));
const formatter = new diff_1.DiffFormatter({
ioHelper: (0, private_1.asIoHelper)(this.ioHost, 'diff'),
templateInfo: {
oldTemplate: template,
newTemplate: stacks.firstStack,
},
});
if (options.securityOnly) {
const securityDiff = formatter.formatSecurityDiff({
requireApproval: diff_1.RequireApproval.BROADENING,
});
if (securityDiff.formattedDiff) {
(0, logging_1.info)(securityDiff.formattedDiff);
diffs += 1;
}
}
else {
const diff = formatter.formatStackDiff({
strict,
context: contextLines,
quiet,
});
diffs = diff.numStacksWithChanges;
(0, logging_1.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 resource_import_1.ResourceMigrator({
deployments: this.props.deployments,
ioHelper: (0, private_1.asIoHelper)(this.ioHost, 'diff'),
});
const resourcesToImport = await migrator.tryGetResources(await this.props.deployments.resolveEnvironment(stack));
if (resourcesToImport) {
(0, resource_import_1.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) {
(0, logging_1.debug)((0, util_2.formatErrorMessage)(e));
if (!quiet) {
(0, logging_1.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 api_private_1.cfnApi.createDiffChangeSet((0, private_1.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 {
(0, logging_1.debug)(`the stack '${stack.stackName}' has not been deployed to CloudFormation or describeStacks call failed, skipping changeset creation.`);
}
}
const formatter = new diff_1.DiffFormatter({
ioHelper: (0, private_1.asIoHelper)(this.ioHost, 'diff'),
templateInfo: {
oldTemplate: currentTemplate,
newTemplate: stack,
changeSet,
isImport: !!resourcesToImport,
nestedStacks,
},
});
if (options.securityOnly) {
const securityDiff = formatter.formatSecurityDiff({
requireApproval: diff_1.RequireApproval.BROADENING,
});
if (securityDiff.formattedDiff) {
(0, logging_1.info)(securityDiff.formattedDiff);
diffs += 1;
}
}
else {
const diff = formatter.formatStackDiff({
strict,
context: contextLines,
quiet,
});
(0, logging_1.info)(diff.formattedDiff);
diffs += diff.numStacksWithChanges;
}
}
}
(0, logging_1.info)((0, util_1.format)('\n✨ Number of stacks with differences: %s\n', diffs));
return diffs && options.fail ? 1 : 0;
}
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;
}
const startSynthTime = new Date().getTime();
const stackCollection = await this.selectStacksForDeploy(options.selector, options.exclusively, options.cacheCloudAssembly, options.ignoreNoStacks);
const elapsedSynthTime = new Date().getTime() - startSynthTime;
(0, logging_1.info)(`\n✨ Synthesis time: ${(0, util_2.formatTime)(elapsedSynthTime)}s\n`);
if (stackCollection.stackCount === 0) {
(0, logging_1.error)('This app contains no stacks');
return;
}
const migrator = new resource_import_1.ResourceMigrator({
deployments: this.props.deployments,
ioHelper: (0, private_1.asIoHelper)(this.ioHost, 'deploy'),
});
await migrator.tryMigrateResources(stackCollection, {
toolkitStackName: this.toolkitStackName,
...options,
});
const requireApproval = options.requireApproval ?? diff_1.RequireApproval.BROADENING;
const parameterMap = buildParameterMap(options.parameters);
if (options.hotswap !== hotswap_1.HotswapMode.FULL_DEPLOYMENT) {
(0, logging_1.warning)('⚠️ The --hotswap and --hotswap-fallback flags deliberately introduce CloudFormation drift to speed up deployments');
(0, logging_1.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 hotswap_1.HotswapPropertyOverrides();
hotswapPropertyOverrides.ecsHotswapProperties = new hotswap_1.EcsHotswapProperties(hotswapPropertiesFromSettings.ecs?.minimumHealthyPercent, hotswapPropertiesFromSettings.ecs?.maximumHealthyPercent);
const stacks = stackCollection.stackArtifacts;
const stackOutputs = {};
const outputsFile = options.outputsFile;
const buildAsset = async (assetNode) => {
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) => {
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) => {
const stack = stackNode.stack;
if (stackCollection.stackCount !== 1) {
(0, logging_1.highlight)(stack.displayName);
}
if (!stack.environment) {
// eslint-disable-next-line max-len
throw new api_1.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 }))) {
(0, logging_1.warning)('%s: stack has no resources, skipping deployment.', chalk.bold(stack.displayName));
}
else {
(0, logging_1.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 !== diff_1.RequireApproval.NEVER) {
const currentTemplate = await this.props.deployments.readCurrentTemplate(stack);
const formatter = new diff_1.DiffFormatter({
ioHelper: (0, private_1.asIoHelper)(this.ioHost, 'deploy'),
templateInfo: {
oldTemplate: currentTemplate,
newTemplate: stack,
},
});
const securityDiff = formatter.formatSecurityDiff({
requireApproval,
});
if (securityDiff.formattedDiff) {
(0, logging_1.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 (!(0, util_2.validateSnsTopicArn)(notificationArn)) {
throw new api_1.ToolkitError(`Notification arn ${notificationArn} is not a valid arn for an SNS topic`);
}
}
const stackIndex = stacks.indexOf(stack) + 1;
(0, logging_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 = (0, tags_1.tagsForStack)(stack);
}
let elapsedDeployTime = 0;
try {
let deployResult;
let rollback = options.rollback;
let iteration = 0;
while (!deployResult) {
if (++iteration > 2) {
throw new api_1.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) {
(0, logging_1.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) {
(0, logging_1.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 api_1.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';
(0, logging_1.success)('\n' + message, stack.displayName);
elapsedDeployTime = new Date().getTime() - startDeployTime;
(0, logging_1.info)(`\n✨ Deployment time: ${(0, util_2.formatTime)(elapsedDeployTime)}s\n`);
if (Object.keys(deployResult.outputs).length > 0) {
(0, logging_1.info)('Outputs:');
stackOutputs[stack.stackName] = deployResult.outputs;
}
for (const name of Object.keys(deployResult.outputs).sort()) {
const value = deployResult.outputs[name];
(0, logging_1.info)(`${chalk.cyan(stack.id)}.${chalk.cyan(name)} = ${chalk.underline(chalk.cyan(value))}`);
}
(0, logging_1.info)('Stack ARN:');
(0, logging_1.result)(deployResult.stackArn);
}
catch (e) {
// It has to be exactly this string because an integration test tests for
// "bold(stackname) failed: ResourceNotReady: <error>"
throw new api_1.ToolkitError([`❌ ${chalk.bold(stack.stackName)} failed:`, ...(e.name ? [`${e.name}:`] : []), (0, util_2.formatErrorMessage)(e)].join(' '));
}
finally {
if (options.cloudWatchLogMonitor) {
const foundLogGroupsResult = await (0, logs_monitor_1.findCloudWatchLogGroups)(this.props.sdkProvider, (0, private_1.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',
});
}
}
(0, logging_1.info)(`\n✨ Total time: ${(0, util_2.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 = deploy_1.StackActivityProgress.EVENTS;
// ...but only warn if the user explicitly requested "bar" progress
if (options.progress && options.progress != deploy_1.StackActivityProgress.EVENTS) {
(0, logging_1.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 work_graph_1.WorkGraphBuilder((0, 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': 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.
*/
async rollback(options) {
const startSynthTime = new Date().getTime();
const stackCollection = await this.selectStacksForDeploy(options.selector, true);
const elapsedSynthTime = new Date().getTime() - startSynthTime;
(0, logging_1.info)(`\n✨ Synthesis time: ${(0, util_2.formatTime)(elapsedSynthTime)}s\n`);
if (stackCollection.stackCount === 0) {
(0, logging_1.error)('No stacks selected');
return;
}
let anyRollbackable = false;
for (const stack of stackCollection.stackArtifacts) {
(0, logging_1.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;
(0, logging_1.info)(`\n✨ Rollback time: ${(0, util_2.formatTime)(elapsedRollbackTime).toString()}s\n`);
}
catch (e) {
(0, logging_1.error)('\n ❌ %s failed: %s', chalk.bold(stack.displayName), (0, util_2.formatErrorMessage)(e));
throw new api_1.ToolkitError('Rollback failed (use --force to orphan failing resources)');
}
}
if (!anyRollbackable) {
throw new api_1.ToolkitError('No stacks were in a state that could be rolled back');
}
}
async watch(options) {
const rootDir = path.dirname(path.resolve(user_configuration_1.PROJECT_CONFIG));
const ioHelper = (0, private_1.asIoHelper)(this.ioHost, 'watch');
(0, logging_1.debug)("root directory used for 'watch' is: %s", rootDir);
const watchSettings = this.props.configuration.settings.get(['watch']);
if (!watchSettings) {
throw new api_1.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,
});
(0, logging_1.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/**');
(0, logging_1.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 logs_monitor_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';
(0, logging_1.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';
(0, logging_1.debug)("'watch' received the 'ready' event. From now on, all file changes will trigger a deployment");
(0, logging_1.info)("Triggering initial 'cdk deploy'");
await deployAndWatch();
})
.on('all', async (event, filePath) => {
if (latch === 'pre-ready') {
(0, logging_1.info)(`'watch' is observing ${event === 'addDir' ? 'directory' : 'the file'} '%s' for changes`, filePath);
}
else if (latch === 'open') {
(0, logging_1.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';
(0, logging_1.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 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 api_1.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 api_1.ToolkitError('--resource-mapping is required when input is not a terminal');
}
const stack = stacks.stackArtifacts[0];
(0, logging_1.highlight)(stack.displayName);
const resourceImporter = new resource_import_1.ResourceImporter(stack, {
deployments: this.props.deployments,
ioHelper: (0, private_1.asIoHelper)(this.ioHost, 'import'),
});
const { additions, hasNonAdditions } = await resourceImporter.discoverImportableResources(options.force);
if (additions.length === 0) {
(0, logging_1.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) {
(0, logging_1.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',
});
(0, logging_1.info)('%s: mapping file written.', outputFile);
return;
}
// Import the resources according to the given mapping
(0, logging_1.info)('%s: importing resources into stack...', chalk.bold(stack.displayName));
const tags = (0, tags_1.tagsForStack)(stack);
await resourceImporter.importResourcesFromMap(actualImport, {
roleArn: options.roleArn,
tags,
deploymentMethod: options.deploymentMethod,
usePreviousParameters: true,
rollback: options.rollback,
});
// Notify user of next steps
(0, logging_1.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) {
(0, logging_1.info)('');
(0, logging_1.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) {
(0, logging_1.info)('');
(0, logging_1.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.`);
}
}
async destroy(options) {
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()) {
(0, logging_1.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,
});
(0, logging_1.success)(`\n ✅ %s: ${action}ed`, chalk.blue(stack.displayName));
}
catch (e) {
(0, logging_1.error)(`\n ❌ %s: ${action} failed`, chalk.blue(stack.displayName), e);
throw e;
}
}
}
async list(selectors, options = {}) {
const stacks = await (0, list_stacks_1.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) {
(0, logging_1.result)(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.
*/
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) {
printSerializedObject((0, util_2.obscureTemplate)(stacks.firstStack.template), json ?? false);
}
return undefined;
}
// not outputting template to stdout, let's explain things to the user a little bit...
(0, logging_1.success)(`Successfully synthesized to ${chalk.blue(path.resolve(stacks.assembly.directory))}`);
(0, logging_1.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.
*/
async bootstrap(userEnvironmentSpecs, options) {
const bootstrapper = new bootstrap_1.Bootstrapper(options.source, (0, 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 () => {
(0, logging_1.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.';
(0, logging_1.success)(message, chalk.blue(environment.name));
}
catch (e) {
(0, logging_1.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) {
(0, logging_1.success)(' ⏳ Garbage Collecting environment %s...', chalk.blue(environment.name));
const gc = new garbage_collection_1.GarbageCollector({
sdkProvider: this.props.sdkProvider,
ioHelper: (0, 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_2.partition)(environmentSpecs, cxapp_1.looksLikeGlob);
if (globSpecs.length > 0 && !this.props.cloudExecutable.hasApp) {
if (userEnvironmentSpecs.length > 0) {
// User did request this glob
throw new api_1.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 api_1.ToolkitError("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) {
(0, logging_1.warning)('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,
filters: options.filter,
fromScan: options.fromScan,
sdkProvider: this.props.sdkProvider,
environment: environment,
});
templateToDelete = generateTemplateOutput.templateId;
}
else if (scanType == migrate_1.TemplateSourceOptions.PATH) {
const templateBody = (0, migrate_1.readFromPath)(options.fromPath);
const parsedTemplate = (0, util_2.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 migrate_1.CfnTemplateGeneratorProvider(await (0, migrate_1.buildCfnClient)(this.props.sdkProvider, environment));
const generatedTemplateSummary = await cfn.describeGeneratedTemplate(templateId);
generateTemplateOutput = (0, migrate_1.buildGenertedTemplateOutput)(generatedTemplateSummary, templateBody, generatedTemplateSummary.GeneratedTemplateId);
}
else {
generateTemplateOutput = {
migrateJson: {
templateBody: templateBody,
source: 'localfile',
},
};
}
}
else if (scanType == migrate_1.TemplateSourceOptions.STACK) {
const template = await (0, migrate_1.readFromStack)(options.stackName, this.props.sdkProvider, environment);
if (!template) {
throw new api_1.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 api_1.ToolkitError(`Invalid source option provided: ${scanType}`);
}
const stack = (0, migrate_1.generateStack)(generateTemplateOutput.migrateJson.templateBody, options.stackName, language);
(0, logging_1.success)(' ⏳ Generating CDK app for %s...', chalk.blue(options.stackName));
await (0, migrate_1.generateCdkApp)(options.stackName, stack, language, options.outputPath, options.compress);
if (generateTemplateOutput) {
(0, migrate_1.writeMigrateJsonFile)(options.outputPath, options.stackName, generateTemplateOutput.migrateJson);
}
if ((0, migrate_1.isThereAWarning)(generateTemplateOutput)) {
(0, logging_1.warning)(' ⚠️ Some resources could not be migrated completely. Please review the README.md file for more information.');
(0, migrate_1.appendWarningsToReadme)(`${path.join(options.outputPath ?? process.cwd(), options.stackName)}/README.md`, generateTemplateOutput.resources);
}
}
catch (e) {
(0, logging_1.error)(' ❌ Migrate failed for `%s`: %s', options.stackName, e.message);
throw e;
}
finally {
if (templateToDelete) {
if (!cfn) {
cfn = new migrate_1.CfnTemplateGeneratorProvider(await (0, migrate_1.buildCfnClient)(this.props.sdkProvider, environment));
}
if (!process.env.MIGRATE_INTEG_TEST) {
await cfn.deleteGeneratedTemplate(templateToDelete);
}
}
}
}
async refa