packages/aws-cdk/lib/cli/cli.ts (507 lines of code) (raw):
import * as cxapi from '@aws-cdk/cx-api';
import * as chalk from 'chalk';
import { CdkToolkit, AssetBuildTime } from './cdk-toolkit';
import { ciSystemIsStdErrSafe } from './ci-systems';
import type { IoMessageLevel } from './io-host';
import { CliIoHost } from './io-host';
import { parseCommandLineArguments } from './parse-command-line-arguments';
import { checkForPlatformWarnings } from './platform-warnings';
import { prettyPrintError } from './pretty-print-error';
import type { Command } from './user-configuration';
import { Configuration } from './user-configuration';
import * as version from './version';
import { ToolkitError } from '../../../@aws-cdk/toolkit-lib/lib/api';
import { asIoHelper } from '../../../@aws-cdk/toolkit-lib/lib/api/io/private';
import { SdkProvider, SdkToCliLogger, setSdkTracing } from '../api/aws-auth';
import type { BootstrapSource } from '../api/bootstrap';
import { Bootstrapper } from '../api/bootstrap';
import type { DeploymentMethod } from '../api/deployments';
import { Deployments } from '../api/deployments';
import { HotswapMode } from '../api/hotswap';
import { Notices } from '../api/notices';
import type { IReadLock } from '../api/rwlock';
import type { Settings } from '../api/settings';
import { ToolkitInfo } from '../api/toolkit-info';
import { contextHandler as context } from '../commands/context';
import { docs } from '../commands/docs';
import { doctor } from '../commands/doctor';
import { cliInit, printAvailableTemplates } from '../commands/init';
import { getMigrateScanType } from '../commands/migrate';
import { execProgram, CloudExecutable } from '../cxapp';
import type { StackSelector, Synthesizer } from '../cxapp';
import { GLOBAL_PLUGIN_HOST } from './singleton-plugin-host';
import { makeRequestHandler } from '../../../@aws-cdk/toolkit-lib/lib/api/shared-private';
/* eslint-disable max-len */
/* eslint-disable @typescript-eslint/no-shadow */ // yargs
if (!process.stdout.isTTY) {
// Disable chalk color highlighting
process.env.FORCE_COLOR = '0';
}
export async function exec(args: string[], synthesizer?: Synthesizer): Promise<number | void> {
const argv = await parseCommandLineArguments(args);
const cmd = argv._[0];
// if one -v, log at a DEBUG level
// if 2 -v, log at a TRACE level
let ioMessageLevel: IoMessageLevel = 'info';
if (argv.verbose) {
switch (argv.verbose) {
case 1:
ioMessageLevel = 'debug';
break;
case 2:
default:
ioMessageLevel = 'trace';
break;
}
}
const ioHost = CliIoHost.instance({
logLevel: ioMessageLevel,
isTTY: process.stdout.isTTY,
isCI: Boolean(argv.ci),
currentAction: cmd,
stackProgress: argv.progress,
}, true);
// Debug should always imply tracing
if (argv.debug || argv.verbose > 2) {
setSdkTracing(true);
} else {
// cli-lib-alpha needs to explicitly set in case it was enabled before
setSdkTracing(false);
}
try {
await checkForPlatformWarnings();
} catch (e) {
ioHost.defaults.debug(`Error while checking for platform warnings: ${e}`);
}
ioHost.defaults.debug('CDK Toolkit CLI version:', version.displayVersion());
ioHost.defaults.debug('Command line arguments:', argv);
const configuration = new Configuration({
commandLineArguments: {
...argv,
_: argv._ as [Command, ...string[]], // TypeScript at its best
},
});
await configuration.load();
const shouldDisplayNotices = configuration.settings.get(['notices']);
if (shouldDisplayNotices !== undefined) {
// Notices either go to stderr, or nowhere
ioHost.noticesDestination = shouldDisplayNotices ? 'stderr' : 'drop';
} else {
// If the user didn't supply either `--notices` or `--no-notices`, we do
// autodetection. The autodetection currently is: do write notices if we are
// not on CI, or are on a CI system where we know that writing to stderr is
// safe. We fail "closed"; that is, we decide to NOT print for unknown CI
// systems, even though technically we maybe could.
const safeToWriteToStdErr = !argv.ci || Boolean(ciSystemIsStdErrSafe());
ioHost.noticesDestination = safeToWriteToStdErr ? 'stderr' : 'drop';
}
const notices = Notices.create({
ioHost,
context: configuration.context,
output: configuration.settings.get(['outdir']),
includeAcknowledged: cmd === 'notices' ? !argv.unacknowledged : false,
httpOptions: {
proxyAddress: configuration.settings.get(['proxy']),
caBundlePath: configuration.settings.get(['caBundlePath']),
},
cliVersion: version.versionNumber(),
});
await notices.refresh();
const ioHelper = asIoHelper(ioHost, ioHost.currentAction as any);
const sdkProvider = await SdkProvider.withAwsCliCompatibleDefaults({
ioHelper,
profile: configuration.settings.get(['profile']),
requestHandler: await makeRequestHandler(ioHelper, {
proxyAddress: argv.proxy,
caBundlePath: argv['ca-bundle-path'],
}),
logger: new SdkToCliLogger(asIoHelper(ioHost, ioHost.currentAction as any)),
pluginHost: GLOBAL_PLUGIN_HOST,
});
let outDirLock: IReadLock | undefined;
const cloudExecutable = new CloudExecutable({
configuration,
sdkProvider,
synthesizer:
synthesizer ??
(async (aws, config) => {
// Invoke 'execProgram', and copy the lock for the directory in the global
// variable here. It will be released when the CLI exits. Locks are not re-entrant
// so release it if we have to synthesize more than once (because of context lookups).
await outDirLock?.release();
const { assembly, lock } = await execProgram(aws, ioHost.asIoHelper(), config);
outDirLock = lock;
return assembly;
}),
ioHelper: ioHost.asIoHelper(),
});
/** Function to load plug-ins, using configurations additively. */
function loadPlugins(...settings: Settings[]) {
for (const source of settings) {
const plugins: string[] = source.get(['plugin']) || [];
for (const plugin of plugins) {
GLOBAL_PLUGIN_HOST.load(plugin, ioHost);
}
}
}
loadPlugins(configuration.settings);
if (typeof(cmd) !== 'string') {
throw new ToolkitError(`First argument should be a string. Got: ${cmd} (${typeof(cmd)})`);
}
try {
return await main(cmd, argv);
} finally {
// If we locked the 'cdk.out' directory, release it here.
await outDirLock?.release();
// Do PSAs here
await version.displayVersionMessage();
if (cmd === 'notices') {
await notices.refresh({ force: true });
notices.display({ showTotal: argv.unacknowledged });
} else if (cmd !== 'version') {
await notices.refresh();
notices.display();
}
}
async function main(command: string, args: any): Promise<number | void> {
ioHost.currentAction = command as any;
const toolkitStackName: string = ToolkitInfo.determineName(configuration.settings.get(['toolkitStackName']));
ioHost.defaults.debug(`Toolkit stack: ${chalk.bold(toolkitStackName)}`);
const cloudFormation = new Deployments({
sdkProvider,
toolkitStackName,
ioHelper: asIoHelper(ioHost, ioHost.currentAction as any),
});
if (args.all && args.STACKS) {
throw new ToolkitError('You must either specify a list of Stacks or the `--all` argument');
}
args.STACKS = args.STACKS ?? (args.STACK ? [args.STACK] : []);
args.ENVIRONMENTS = args.ENVIRONMENTS ?? [];
const selector: StackSelector = {
allTopLevel: args.all,
patterns: args.STACKS,
};
const cli = new CdkToolkit({
ioHost,
cloudExecutable,
toolkitStackName,
deployments: cloudFormation,
verbose: argv.trace || argv.verbose > 0,
ignoreErrors: argv['ignore-errors'],
strict: argv.strict,
configuration,
sdkProvider,
});
switch (command) {
case 'context':
ioHost.currentAction = 'context';
return context({
context: configuration.context,
clear: argv.clear,
json: argv.json,
force: argv.force,
reset: argv.reset,
});
case 'docs':
case 'doc':
ioHost.currentAction = 'docs';
return docs({ browser: configuration.settings.get(['browser']) });
case 'doctor':
ioHost.currentAction = 'doctor';
return doctor();
case 'ls':
case 'list':
ioHost.currentAction = 'list';
return cli.list(args.STACKS, {
long: args.long,
json: argv.json,
showDeps: args.showDependencies,
});
case 'diff':
ioHost.currentAction = 'diff';
const enableDiffNoFail = isFeatureEnabled(configuration, cxapi.ENABLE_DIFF_NO_FAIL_CONTEXT);
return cli.diff({
stackNames: args.STACKS,
exclusively: args.exclusively,
templatePath: args.template,
strict: args.strict,
contextLines: args.contextLines,
securityOnly: args.securityOnly,
fail: args.fail != null ? args.fail : !enableDiffNoFail,
compareAgainstProcessedTemplate: args.processed,
quiet: args.quiet,
changeSet: args['change-set'],
toolkitStackName: toolkitStackName,
});
case 'refactor':
if (!configuration.settings.get(['unstable']).includes('refactor')) {
throw new ToolkitError('Unstable feature use: \'refactor\' is unstable. It must be opted in via \'--unstable\', e.g. \'cdk refactor --unstable=refactor\'');
}
ioHost.currentAction = 'refactor';
return cli.refactor({
dryRun: args.dryRun,
selector,
excludeFile: args.excludeFile,
});
case 'bootstrap':
ioHost.currentAction = 'bootstrap';
const source: BootstrapSource = determineBootstrapVersion(ioHost, args);
if (args.showTemplate) {
const bootstrapper = new Bootstrapper(source, asIoHelper(ioHost, ioHost.currentAction));
return bootstrapper.showTemplate(args.json);
}
return cli.bootstrap(args.ENVIRONMENTS, {
source,
roleArn: args.roleArn,
forceDeployment: argv.force,
toolkitStackName: toolkitStackName,
execute: args.execute,
tags: configuration.settings.get(['tags']),
terminationProtection: args.terminationProtection,
usePreviousParameters: args['previous-parameters'],
parameters: {
bucketName: configuration.settings.get(['toolkitBucket', 'bucketName']),
kmsKeyId: configuration.settings.get(['toolkitBucket', 'kmsKeyId']),
createCustomerMasterKey: args.bootstrapCustomerKey,
qualifier: args.qualifier ?? configuration.context.get('@aws-cdk/core:bootstrapQualifier'),
publicAccessBlockConfiguration: args.publicAccessBlockConfiguration,
examplePermissionsBoundary: argv.examplePermissionsBoundary,
customPermissionsBoundary: argv.customPermissionsBoundary,
trustedAccounts: arrayFromYargs(args.trust),
trustedAccountsForLookup: arrayFromYargs(args.trustForLookup),
untrustedAccounts: arrayFromYargs(args.untrust),
cloudFormationExecutionPolicies: arrayFromYargs(args.cloudformationExecutionPolicies),
},
});
case 'deploy':
ioHost.currentAction = 'deploy';
const parameterMap: { [name: string]: string | undefined } = {};
for (const parameter of args.parameters) {
if (typeof parameter === 'string') {
const keyValue = (parameter as string).split('=');
parameterMap[keyValue[0]] = keyValue.slice(1).join('=');
}
}
if (args.execute !== undefined && args.method !== undefined) {
throw new ToolkitError('Can not supply both --[no-]execute and --method at the same time');
}
let deploymentMethod: DeploymentMethod | undefined;
switch (args.method) {
case 'direct':
if (args.changeSetName) {
throw new ToolkitError('--change-set-name cannot be used with method=direct');
}
if (args.importExistingResources) {
throw new ToolkitError('--import-existing-resources cannot be enabled with method=direct');
}
deploymentMethod = { method: 'direct' };
break;
case 'change-set':
deploymentMethod = {
method: 'change-set',
execute: true,
changeSetName: args.changeSetName,
importExistingResources: args.importExistingResources,
};
break;
case 'prepare-change-set':
deploymentMethod = {
method: 'change-set',
execute: false,
changeSetName: args.changeSetName,
importExistingResources: args.importExistingResources,
};
break;
case undefined:
deploymentMethod = {
method: 'change-set',
execute: args.execute ?? true,
changeSetName: args.changeSetName,
importExistingResources: args.importExistingResources,
};
break;
}
return cli.deploy({
selector,
exclusively: args.exclusively,
toolkitStackName,
roleArn: args.roleArn,
notificationArns: args.notificationArns,
requireApproval: configuration.settings.get(['requireApproval']),
reuseAssets: args['build-exclude'],
tags: configuration.settings.get(['tags']),
deploymentMethod,
force: args.force,
parameters: parameterMap,
usePreviousParameters: args['previous-parameters'],
outputsFile: configuration.settings.get(['outputsFile']),
progress: configuration.settings.get(['progress']),
ci: args.ci,
rollback: configuration.settings.get(['rollback']),
hotswap: determineHotswapMode(args.hotswap, args.hotswapFallback),
watch: args.watch,
traceLogs: args.logs,
concurrency: args.concurrency,
assetParallelism: configuration.settings.get(['assetParallelism']),
assetBuildTime: configuration.settings.get(['assetPrebuild'])
? AssetBuildTime.ALL_BEFORE_DEPLOY
: AssetBuildTime.JUST_IN_TIME,
ignoreNoStacks: args.ignoreNoStacks,
});
case 'rollback':
ioHost.currentAction = 'rollback';
return cli.rollback({
selector,
toolkitStackName,
roleArn: args.roleArn,
force: args.force,
validateBootstrapStackVersion: args['validate-bootstrap-version'],
orphanLogicalIds: args.orphan,
});
case 'import':
ioHost.currentAction = 'import';
return cli.import({
selector,
toolkitStackName,
roleArn: args.roleArn,
deploymentMethod: {
method: 'change-set',
execute: args.execute,
changeSetName: args.changeSetName,
},
progress: configuration.settings.get(['progress']),
rollback: configuration.settings.get(['rollback']),
recordResourceMapping: args['record-resource-mapping'],
resourceMappingFile: args['resource-mapping'],
force: args.force,
});
case 'watch':
ioHost.currentAction = 'watch';
await cli.watch({
selector,
exclusively: args.exclusively,
toolkitStackName,
roleArn: args.roleArn,
reuseAssets: args['build-exclude'],
deploymentMethod: {
method: 'change-set',
changeSetName: args.changeSetName,
},
force: args.force,
progress: configuration.settings.get(['progress']),
rollback: configuration.settings.get(['rollback']),
hotswap: determineHotswapMode(args.hotswap, args.hotswapFallback, true),
traceLogs: args.logs,
concurrency: args.concurrency,
});
return;
case 'destroy':
ioHost.currentAction = 'destroy';
return cli.destroy({
selector,
exclusively: args.exclusively,
force: args.force,
roleArn: args.roleArn,
});
case 'gc':
ioHost.currentAction = 'gc';
if (!configuration.settings.get(['unstable']).includes('gc')) {
throw new ToolkitError('Unstable feature use: \'gc\' is unstable. It must be opted in via \'--unstable\', e.g. \'cdk gc --unstable=gc\'');
}
return cli.garbageCollect(args.ENVIRONMENTS, {
action: args.action,
type: args.type,
rollbackBufferDays: args['rollback-buffer-days'],
createdBufferDays: args['created-buffer-days'],
bootstrapStackName: args.bootstrapStackName,
confirm: args.confirm,
});
case 'synthesize':
case 'synth':
ioHost.currentAction = 'synth';
const quiet = configuration.settings.get(['quiet']) ?? args.quiet;
if (args.exclusively) {
return cli.synth(args.STACKS, args.exclusively, quiet, args.validation, argv.json);
} else {
return cli.synth(args.STACKS, true, quiet, args.validation, argv.json);
}
case 'notices':
ioHost.currentAction = 'notices';
// If the user explicitly asks for notices, they are now the primary output
// of the command and they should go to stdout.
ioHost.noticesDestination = 'stdout';
// This is a valid command, but we're postponing its execution because displaying
// notices automatically happens after every command.
return;
case 'metadata':
ioHost.currentAction = 'metadata';
return cli.metadata(args.STACK, argv.json);
case 'acknowledge':
case 'ack':
ioHost.currentAction = 'notices';
return cli.acknowledge(args.ID);
case 'init':
ioHost.currentAction = 'init';
const language = configuration.settings.get(['language']);
if (args.list) {
return printAvailableTemplates(language);
} else {
return cliInit({
type: args.TEMPLATE,
language,
canUseNetwork: undefined,
generateOnly: args.generateOnly,
libVersion: args.libVersion,
});
}
case 'migrate':
ioHost.currentAction = 'migrate';
return cli.migrate({
stackName: args['stack-name'],
fromPath: args['from-path'],
fromStack: args['from-stack'],
language: args.language,
outputPath: args['output-path'],
fromScan: getMigrateScanType(args['from-scan']),
filter: args.filter,
account: args.account,
region: args.region,
compress: args.compress,
});
case 'version':
ioHost.currentAction = 'version';
return ioHost.defaults.result(version.displayVersion());
default:
throw new ToolkitError('Unknown command: ' + command);
}
}
}
/**
* Determine which version of bootstrapping
*/
function determineBootstrapVersion(ioHost: CliIoHost, args: { template?: string }): BootstrapSource {
let source: BootstrapSource;
if (args.template) {
ioHost.defaults.info(`Using bootstrapping template from ${args.template}`);
source = { source: 'custom', templateFile: args.template };
} else if (process.env.CDK_LEGACY_BOOTSTRAP) {
ioHost.defaults.info('CDK_LEGACY_BOOTSTRAP set, using legacy-style bootstrapping');
source = { source: 'legacy' };
} else {
// in V2, the "new" bootstrapping is the default
source = { source: 'default' };
}
return source;
}
function isFeatureEnabled(configuration: Configuration, featureFlag: string) {
return configuration.context.get(featureFlag) ?? cxapi.futureFlagDefault(featureFlag);
}
/**
* Translate a Yargs input array to something that makes more sense in a programming language
* model (telling the difference between absence and an empty array)
*
* - An empty array is the default case, meaning the user didn't pass any arguments. We return
* undefined.
* - If the user passed a single empty string, they did something like `--array=`, which we'll
* take to mean they passed an empty array.
*/
function arrayFromYargs(xs: string[]): string[] | undefined {
if (xs.length === 0) {
return undefined;
}
return xs.filter((x) => x !== '');
}
function determineHotswapMode(hotswap?: boolean, hotswapFallback?: boolean, watch?: boolean): HotswapMode {
if (hotswap && hotswapFallback) {
throw new ToolkitError('Can not supply both --hotswap and --hotswap-fallback at the same time');
} else if (!hotswap && !hotswapFallback) {
if (hotswap === undefined && hotswapFallback === undefined) {
return watch ? HotswapMode.HOTSWAP_ONLY : HotswapMode.FULL_DEPLOYMENT;
} else if (hotswap === false || hotswapFallback === false) {
return HotswapMode.FULL_DEPLOYMENT;
}
}
let hotswapMode: HotswapMode;
if (hotswap) {
hotswapMode = HotswapMode.HOTSWAP_ONLY;
/* if (hotswapFallback)*/
} else {
hotswapMode = HotswapMode.FALL_BACK;
}
return hotswapMode;
}
/* c8 ignore start */ // we never call this in unit tests
export function cli(args: string[] = process.argv.slice(2)) {
exec(args)
.then(async (value) => {
if (typeof value === 'number') {
process.exitCode = value;
}
})
.catch((err) => {
// Log the stack trace if we're on a developer workstation. Otherwise this will be into a minified
// file and the printed code line and stack trace are huge and useless.
prettyPrintError(err, version.isDeveloperBuild());
process.exitCode = 1;
});
}
/* c8 ignore stop */