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

import type { ChildProcess } from 'child_process'; import type { DefaultCdkOptions, DeployOptions, DestroyOptions, SynthOptions, ListOptions } from './commands'; import { StackActivityProgress, HotswapMode } from './commands'; import { exec, watch } from './utils'; /** * AWS CDK CLI operations */ export interface ICdk { /** * cdk deploy */ deploy(options: DeployOptions): void; /** * cdk synth */ synth(options: SynthOptions): void; /** * cdk destroy */ destroy(options: DestroyOptions): void; /** * cdk list */ list(options: ListOptions): string; /** * cdk synth fast */ synthFast(options: SynthFastOptions): void; /** * cdk watch */ watch(options: DeployOptions): ChildProcess; } /** * Options for synthing and bypassing the CDK CLI */ export interface SynthFastOptions { /** * The command to use to execute the app. * This is typically the same thing that normally * gets passed to `--app` * * e.g. "node 'bin/my-app.ts'" * or 'go run main.go' */ readonly execCmd: string[]; /** * Emits the synthesized cloud assembly into a directory * * @default cdk.out */ readonly output?: string; /** * Additional context * * @default - no additional context */ readonly context?: Record<string, string>; /** * Additional environment variables to set in the * execution environment * * @default - no additional env */ readonly env?: { [name: string]: string }; } /** * Additional environment variables to set in the execution environment * * @deprecated Use raw property bags instead (object literals, `Map<String,Object>`, etc... ) */ export interface Environment { /** * This index signature is not usable in non-TypeScript/JavaScript languages. * * @jsii ignore */ [key: string]: string | undefined; } /** * AWS CDK client that provides an API to programatically execute the CDK CLI * by wrapping calls to exec the CLI */ export interface CdkCliWrapperOptions { /** * The directory to run the cdk commands from */ readonly directory: string; /** * Additional environment variables to set * in the execution environment that will be running * the cdk commands * * @default - no additional env vars */ readonly env?: { [name: string]: string }; /** * The path to the cdk executable * * @default 'aws-cdk/bin/cdk' */ readonly cdkExecutable?: string; /** * Show the output from running the CDK CLI * * @default false */ readonly showOutput?: boolean; } /** * Provides a programmatic interface for interacting with the CDK CLI by * wrapping the CLI with exec */ export class CdkCliWrapper implements ICdk { private readonly directory: string; private readonly env?: { [name: string]: string | undefined }; private readonly cdk: string; private readonly showOutput: boolean; constructor(options: CdkCliWrapperOptions) { this.directory = options.directory; this.env = options.env; this.showOutput = options.showOutput ?? false; try { this.cdk = options.cdkExecutable ?? 'cdk'; } catch { throw new Error(`could not resolve path to cdk executable: "${options.cdkExecutable ?? 'cdk'}"`); } } private validateArgs(options: DefaultCdkOptions): void { if (!options.stacks && !options.all) { throw new Error('one of "app" or "stacks" must be provided'); } } public list(options: ListOptions): string { const listCommandArgs: string[] = [ ...renderBooleanArg('long', options.long), ...this.createDefaultArguments(options), ]; return exec([this.cdk, 'ls', ...listCommandArgs], { cwd: this.directory, verbose: this.showOutput, env: this.env, }); } /** * cdk deploy */ public deploy(options: DeployOptions): void { 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), ...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], ...options.deploymentMethod ? ['--method', options.deploymentMethod] : [], ...options.concurrency ? ['--concurrency', options.concurrency.toString()] : [], ...this.createDefaultArguments(options), ]; exec([this.cdk, 'deploy', ...deployCommandArgs], { cwd: this.directory, verbose: this.showOutput, env: this.env, }); } public watch(options: DeployOptions): ChildProcess { let hotswap: string; switch (options.hotswap) { case HotswapMode.FALL_BACK: hotswap = '--hotswap-fallback'; break; case HotswapMode.HOTSWAP_ONLY: hotswap = '--hotswap'; break; default: hotswap = '--hotswap-fallback'; break; } const deployCommandArgs: string[] = [ '--watch', ...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('logs', options.traceLogs), 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], ...options.deploymentMethod ? ['--method', options.deploymentMethod] : [], ...this.createDefaultArguments(options), ]; return watch([this.cdk, 'deploy', ...deployCommandArgs], { cwd: this.directory, verbose: this.showOutput, env: this.env, }); } /** * cdk destroy */ public destroy(options: DestroyOptions): void { const destroyCommandArgs: string[] = [ ...renderBooleanArg('force', options.force), ...renderBooleanArg('exclusively', options.exclusively), ...this.createDefaultArguments(options), ]; exec([this.cdk, 'destroy', ...destroyCommandArgs], { cwd: this.directory, verbose: this.showOutput, env: this.env, }); } /** * cdk synth */ public synth(options: SynthOptions): void { const synthCommandArgs: string[] = [ ...renderBooleanArg('validation', options.validation), ...renderBooleanArg('quiet', options.quiet), ...renderBooleanArg('exclusively', options.exclusively), ...this.createDefaultArguments(options), ]; exec([this.cdk, 'synth', ...synthCommandArgs], { cwd: this.directory, verbose: this.showOutput, env: this.env, }); } /** * Do a CDK synth, mimicking the CLI (without actually using it) * * The CLI has a pretty slow startup time because of all the modules it needs to load, * Bypass it to be quicker! */ public synthFast(options: SynthFastOptions): void { exec(options.execCmd, { cwd: this.directory, verbose: this.showOutput, env: { CDK_CONTEXT_JSON: JSON.stringify(options.context), CDK_OUTDIR: options.output, ...this.env, ...options.env, }, }); } private createDefaultArguments(options: DefaultCdkOptions): string[] { this.validateArgs(options); const stacks = options.stacks ?? []; return [ ...options.app ? ['--app', options.app] : [], ...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), ...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] : [], ...options.output ? ['--output', options.output] : [], ...stacks, ...options.all ? ['--all'] : [], ]; } } 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(val: string, arg?: boolean): string[] { if (arg) { return [`--${val}`]; } else if (arg === undefined) { return []; } else { return [`--no-${val}`]; } }