UNPKG

aws-cdk

Version:

AWS CDK CLI, the command line tool for CDK apps

903 lines 282 kB
"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,