packages/@aws-cdk/cli-lib-alpha/lib/cli.ts (217 lines of code) (raw):

import type { SharedOptions, DeployOptions, DestroyOptions, BootstrapOptions, SynthOptions, ListOptions } from './commands'; import { StackActivityProgress, HotswapMode } from './commands'; import { exec as runCli } from '../../../aws-cdk/lib'; import { prepareContext, prepareDefaultEnvironment } from '../../../aws-cdk/lib/api/cloud-assembly'; import { createAssembly } from '../../../aws-cdk/lib/cxapp'; import { debug } from '../../../aws-cdk/lib/legacy-exports'; const debugFn = async (msg: string) => void debug(msg); /** * AWS CDK CLI operations */ export interface IAwsCdkCli { /** * cdk list */ list(options?: ListOptions): Promise<void>; /** * cdk synth */ synth(options?: SynthOptions): Promise<void>; /** * cdk bootstrap */ bootstrap(options?: BootstrapOptions): Promise<void>; /** * cdk deploy */ deploy(options?: DeployOptions): Promise<void>; /** * cdk destroy */ destroy(options?: DestroyOptions): Promise<void>; } /** * Configuration for creating a CLI from an AWS CDK App directory */ export interface CdkAppDirectoryProps { /** * Command-line for executing your app or a cloud assembly directory * e.g. "node bin/my-app.js" * or * "cdk.out" * * @default - read from cdk.json */ readonly app?: string; /** * Emits the synthesized cloud assembly into a directory * * @default cdk.out */ readonly output?: string; } /** * A class returning the path to a Cloud Assembly Directory when its `produce` method is invoked with the current context * * AWS CDK apps might need to be synthesized multiple times with additional context values before they are ready. * When running the CLI from inside a directory, this is implemented by invoking the app multiple times. * Here the `produce()` method provides this multi-pass ability. */ export interface ICloudAssemblyDirectoryProducer { /** * The working directory used to run the Cloud Assembly from. * This is were a `cdk.context.json` files will be written. * * @default - current working directory */ workingDirectory?: string; /** * Synthesize a Cloud Assembly directory for a given context. * * For all features to work correctly, `cdk.App()` must be instantiated with the received context values in the method body. * Usually obtained similar to this: * ```ts fixture=imports * class MyProducer implements ICloudAssemblyDirectoryProducer { * async produce(context: Record<string, any>) { * const app = new cdk.App({ context }); * // create stacks here * return app.synth().directory; * } * } * ``` */ produce(context: Record<string, any>): Promise<string>; } /** * Provides a programmatic interface for interacting with the AWS CDK CLI */ export class AwsCdkCli implements IAwsCdkCli { /** * Create the CLI from a directory containing an AWS CDK app * @param directory the directory of the AWS CDK app. Defaults to the current working directory. * @param props additional configuration properties * @returns an instance of `AwsCdkCli` */ public static fromCdkAppDirectory(directory?: string, props: CdkAppDirectoryProps = {}) { return new AwsCdkCli(async (args) => changeDir( () => { if (props.app) { args.push('--app', props.app); } if (props.output) { args.push('--output', props.output); } return runCli(args); }, directory, )); } /** * Create the CLI from a CloudAssemblyDirectoryProducer */ public static fromCloudAssemblyDirectoryProducer(producer: ICloudAssemblyDirectoryProducer) { return new AwsCdkCli(async (args) => changeDir( () => runCli(args, async (sdk, config) => { const env = await prepareDefaultEnvironment(sdk, debugFn); const context = await prepareContext(config.settings, config.context.all, env, debugFn); return withEnv(async() => createAssembly(await producer.produce(context)), env); }), producer.workingDirectory, )); } private constructor( private readonly cli: (args: string[]) => Promise<number | void>, ) { } /** * Execute the CLI with a list of arguments */ private async exec(args: string[]) { return this.cli(args); } /** * cdk list */ public async list(options: ListOptions = {}) { const listCommandArgs: string[] = [ ...renderBooleanArg('long', options.long), ...this.createDefaultArguments(options), ]; await this.exec(['ls', ...listCommandArgs]); } /** * cdk synth */ public async synth(options: SynthOptions = {}) { const synthCommandArgs: string[] = [ ...renderBooleanArg('validation', options.validation), ...renderBooleanArg('quiet', options.quiet), ...renderBooleanArg('exclusively', options.exclusively), ...this.createDefaultArguments(options), ]; await this.exec(['synth', ...synthCommandArgs]); } /** * cdk bootstrap */ public async bootstrap(options: BootstrapOptions = {}) { const envs = options.environments ?? []; const bootstrapCommandArgs: string[] = [ ...envs, ...renderBooleanArg('force', options.force), ...renderBooleanArg('show-template', options.showTemplate), ...renderBooleanArg('terminationProtection', options.terminationProtection), ...renderBooleanArg('example-permissions-boundary', options.examplePermissionsBoundary), ...renderBooleanArg('terminationProtection', options.usePreviousParameters), ...renderBooleanArg('execute', options.execute), ...options.toolkitStackName ? ['--toolkit-stack-name', options.toolkitStackName] : [], ...options.bootstrapBucketName ? ['--bootstrap-bucket-name', options.bootstrapBucketName] : [], ...options.cfnExecutionPolicy ? ['--cloudformation-execution-policies', options.cfnExecutionPolicy] : [], ...options.template ? ['--template', options.template] : [], ...options.customPermissionsBoundary ? ['--custom-permissions-boundary', options.customPermissionsBoundary] : [], ...options.qualifier ? ['--qualifier', options.qualifier] : [], ...options.trust ? ['--trust', options.trust] : [], ...options.trustForLookup ? ['--trust-for-lookup', options.trustForLookup] : [], ...options.bootstrapKmsKeyId ? ['--bootstrap-kms-key-id', options.bootstrapKmsKeyId] : [], ...options.bootstrapCustomerKey ? ['--bootstrap-customer-key', options.bootstrapCustomerKey] : [], ...options.publicAccessBlockConfiguration ? ['--public-access-block-configuration', options.publicAccessBlockConfiguration] : [], ...this.createDefaultArguments(options), ]; await this.exec(['bootstrap', ...bootstrapCommandArgs]); } /** * cdk deploy */ public async deploy(options: DeployOptions = {}) { const deployCommandArgs: string[] = [ ...renderBooleanArg('ci', options.ci), ...renderBooleanArg('execute', options.execute), ...renderBooleanArg('exclusively', options.exclusively), ...renderBooleanArg('force', options.force), ...renderBooleanArg('previous-parameters', options.usePreviousParameters), ...renderBooleanArg('rollback', options.rollback), ...renderBooleanArg('staging', options.staging), ...renderBooleanArg('asset-parallelism', options.assetParallelism), ...renderBooleanArg('asset-prebuild', options.assetPrebuild), ...renderNumberArg('concurrency', options.concurrency), ...renderHotswapArg(options.hotswap), ...options.reuseAssets ? renderArrayArg('--reuse-assets', options.reuseAssets) : [], ...options.notificationArns ? renderArrayArg('--notification-arns', options.notificationArns) : [], ...options.parameters ? renderMapArrayArg('--parameters', options.parameters) : [], ...options.outputsFile ? ['--outputs-file', options.outputsFile] : [], ...options.requireApproval ? ['--require-approval', options.requireApproval] : [], ...options.changeSetName ? ['--change-set-name', options.changeSetName] : [], ...options.toolkitStackName ? ['--toolkit-stack-name', options.toolkitStackName] : [], ...options.progress ? ['--progress', options.progress] : ['--progress', StackActivityProgress.EVENTS], ...this.createDefaultArguments(options), ]; await this.exec(['deploy', ...deployCommandArgs]); } /** * cdk destroy */ public async destroy(options: DestroyOptions = {}) { const destroyCommandArgs: string[] = [ ...options.requireApproval ? [] : ['--force'], ...renderBooleanArg('exclusively', options.exclusively), ...this.createDefaultArguments(options), ]; await this.exec(['destroy', ...destroyCommandArgs]); } /** * Configure default arguments shared by all commands */ private createDefaultArguments(options: SharedOptions): string[] { const stacks = options.stacks ?? ['--all']; return [ ...renderBooleanArg('strict', options.strict), ...renderBooleanArg('trace', options.trace), ...renderBooleanArg('lookups', options.lookups), ...renderBooleanArg('ignore-errors', options.ignoreErrors), ...renderBooleanArg('json', options.json), ...renderBooleanArg('verbose', options.verbose), ...renderBooleanArg('debug', options.debug), ...renderBooleanArg('ec2creds', options.ec2Creds), ...renderBooleanArg('version-reporting', options.versionReporting), ...renderBooleanArg('path-metadata', options.pathMetadata), ...renderBooleanArg('asset-metadata', options.assetMetadata), ...renderBooleanArg('notices', options.notices), ...renderBooleanArg('color', options.color ?? (process.env.NO_COLOR ? false : undefined)), ...options.context ? renderMapArrayArg('--context', options.context) : [], ...options.profile ? ['--profile', options.profile] : [], ...options.proxy ? ['--proxy', options.proxy] : [], ...options.caBundlePath ? ['--ca-bundle-path', options.caBundlePath] : [], ...options.roleArn ? ['--role-arn', options.roleArn] : [], ...stacks, ]; } } function renderHotswapArg(hotswapMode: HotswapMode | undefined): string[] { switch (hotswapMode) { case HotswapMode.FALL_BACK: return ['--hotswap-fallback']; case HotswapMode.HOTSWAP_ONLY: return ['--hotswap']; default: return []; } } function renderMapArrayArg(flag: string, parameters: { [name: string]: string | undefined }): string[] { const params: string[] = []; for (const [key, value] of Object.entries(parameters)) { params.push(`${key}=${value}`); } return renderArrayArg(flag, params); } function renderArrayArg(flag: string, values?: string[]): string[] { let args: string[] = []; for (const value of values ?? []) { args.push(flag, value); } return args; } function renderBooleanArg(arg: string, value?: boolean): string[] { if (value) { return [`--${arg}`]; } else if (value === undefined) { return []; } else { return [`--no-${arg}`]; } } function renderNumberArg(arg: string, value?: number): string[] { if (typeof value === 'undefined') { return []; } return [`--${arg}`, value.toString(10)]; } /** * Run code from a different working directory */ async function changeDir(block: () => Promise<any>, workingDir?: string) { const originalWorkingDir = process.cwd(); try { if (workingDir) { process.chdir(workingDir); } return await block(); } finally { if (workingDir) { process.chdir(originalWorkingDir); } } } /** * Run code with additional environment variables */ async function withEnv(block: () => Promise<any>, env: Record<string, string> = {}) { const originalEnv = process.env; try { process.env = { ...originalEnv, ...env, }; return await block(); } finally { process.env = originalEnv; } }