diff --git a/packages/@aws-cdk/cli-lib-alpha/lib/cli.ts b/packages/@aws-cdk/cli-lib-alpha/lib/cli.ts index 72e9e4a78..4dce533fd 100644 --- a/packages/@aws-cdk/cli-lib-alpha/lib/cli.ts +++ b/packages/@aws-cdk/cli-lib-alpha/lib/cli.ts @@ -1,9 +1,10 @@ -// eslint-disable-next-line import/no-extraneous-dependencies import type { SharedOptions, DeployOptions, DestroyOptions, BootstrapOptions, SynthOptions, ListOptions } from './commands'; import { StackActivityProgress, HotswapMode } from './commands'; import { exec as runCli } from '../../../aws-cdk/lib'; -// eslint-disable-next-line import/no-extraneous-dependencies import { createAssembly, prepareContext, prepareDefaultEnvironment } from '../../../aws-cdk/lib/api/cxapp/exec'; +import { debug } from '../../../aws-cdk/lib/legacy-exports'; + +const debugFn = async (msg: string) => void debug(msg); /** * AWS CDK CLI operations @@ -123,8 +124,8 @@ export class AwsCdkCli implements IAwsCdkCli { public static fromCloudAssemblyDirectoryProducer(producer: ICloudAssemblyDirectoryProducer) { return new AwsCdkCli(async (args) => changeDir( () => runCli(args, async (sdk, config) => { - const env = await prepareDefaultEnvironment(sdk); - const context = await prepareContext(config.settings, config.context.all, env); + 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); }), diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/messages.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/messages.ts index 438db2738..e952833fb 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/messages.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/messages.ts @@ -383,6 +383,14 @@ export const IO = { code: 'CDK_ASSEMBLY_I0000', description: 'Default debug messages emitted from Cloud Assembly operations', }), + DEFAULT_ASSEMBLY_INFO: make.info({ + code: 'CDK_ASSEMBLY_I0000', + description: 'Default info messages emitted from Cloud Assembly operations', + }), + DEFAULT_ASSEMBLY_WARN: make.warn({ + code: 'CDK_ASSEMBLY_W0000', + description: 'Default warning messages emitted from Cloud Assembly operations', + }), CDK_ASSEMBLY_I0010: make.debug({ code: 'CDK_ASSEMBLY_I0010', diff --git a/packages/@aws-cdk/toolkit-lib/docs/message-registry.md b/packages/@aws-cdk/toolkit-lib/docs/message-registry.md index 351c5bb14..efbc44a04 100644 --- a/packages/@aws-cdk/toolkit-lib/docs/message-registry.md +++ b/packages/@aws-cdk/toolkit-lib/docs/message-registry.md @@ -75,6 +75,8 @@ group: Documents | `CDK_TOOLKIT_I0101` | A notice that is marked as informational | `info` | n/a | | `CDK_ASSEMBLY_I0000` | Default trace messages emitted from Cloud Assembly operations | `trace` | n/a | | `CDK_ASSEMBLY_I0000` | Default debug messages emitted from Cloud Assembly operations | `debug` | n/a | +| `CDK_ASSEMBLY_I0000` | Default info messages emitted from Cloud Assembly operations | `info` | n/a | +| `CDK_ASSEMBLY_W0000` | Default warning messages emitted from Cloud Assembly operations | `warn` | n/a | | `CDK_ASSEMBLY_I0010` | Generic environment preparation debug messages | `debug` | n/a | | `CDK_ASSEMBLY_W0010` | Emitted if the found framework version does not support context overflow | `warn` | n/a | | `CDK_ASSEMBLY_I0042` | Writing updated context | `debug` | {@link UpdatedContext} | diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts index 4297d5433..7d5c2bce0 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts @@ -1,8 +1,9 @@ import type * as cxapi from '@aws-cdk/cx-api'; import { environmentsFromDescriptors } from './private'; import type { Tag } from '../../api/aws-cdk'; -import type { ICloudAssemblySource } from '../../api/cloud-assembly'; +import type { ICloudAssemblySource, IIoHost } from '../../api/cloud-assembly'; import { ALL_STACKS } from '../../api/cloud-assembly/private'; +import { asIoHelper } from '../../api/shared-private'; import { assemblyFromSource } from '../../toolkit/private'; /** @@ -21,21 +22,28 @@ export class BootstrapEnvironments { * Create from a cloud assembly source */ static fromCloudAssemblySource(cx: ICloudAssemblySource): BootstrapEnvironments { - return new BootstrapEnvironments(async () => { - const assembly = await assemblyFromSource(cx); - const stackCollection = assembly.selectStacksV2(ALL_STACKS); + return new BootstrapEnvironments(async (ioHost: IIoHost) => { + const ioHelper = asIoHelper(ioHost, 'bootstrap'); + const assembly = await assemblyFromSource(ioHelper, cx); + const stackCollection = await assembly.selectStacksV2(ALL_STACKS); return stackCollection.stackArtifacts.map(stack => stack.environment); }); } - private constructor(private readonly envProvider: cxapi.Environment[] | (() => Promise)) { + private constructor(private readonly envProvider: cxapi.Environment[] | ((ioHost: IIoHost) => Promise)) { + } - async getEnvironments(): Promise { + /** + * Compute the bootstrap enviornments + * + * @internal + */ + async getEnvironments(ioHost: IIoHost): Promise { if (Array.isArray(this.envProvider)) { return this.envProvider; } - return this.envProvider(); + return this.envProvider(ioHost); } } diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/prepare-source.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/prepare-source.ts index 1b3193d3c..282af4d12 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/prepare-source.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/prepare-source.ts @@ -5,7 +5,8 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; import { lte } from 'semver'; -import { prepareDefaultEnvironment as oldPrepare, prepareContext, spaceAvailableForContext, Settings, loadTree, some, versionNumber } from '../../../api/aws-cdk'; +import type { SdkProvider } from '../../../api/aws-cdk'; +import { prepareDefaultEnvironment as oldPrepare, prepareContext, spaceAvailableForContext, Settings, loadTree, some, versionNumber, guessExecutable } from '../../../api/aws-cdk'; import { splitBySize } from '../../../private/util'; import type { ToolkitServices } from '../../../toolkit/private'; import { IO } from '../../io/private'; @@ -13,114 +14,140 @@ import type { IoHelper } from '../../shared-private'; import { ToolkitError } from '../../shared-public'; import type { AppSynthOptions, LoadAssemblyOptions } from '../source-builder'; -export { guessExecutable } from '../../../api/aws-cdk'; - type Env = { [key: string]: string }; type Context = { [key: string]: any }; -/** - * Turn the given optional output directory into a fixed output directory - */ -export function determineOutputDirectory(outdir?: string) { - return outdir ?? fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'cdk.out')); -} - -/** - * If we don't have region/account defined in context, we fall back to the default SDK behavior - * where region is retrieved from ~/.aws/config and account is based on default credentials provider - * chain and then STS is queried. - * - * This is done opportunistically: for example, if we can't access STS for some reason or the region - * is not configured, the context value will be 'null' and there could failures down the line. In - * some cases, synthesis does not require region/account information at all, so that might be perfectly - * fine in certain scenarios. - * - * @param context The context key/value bash. - */ -export async function prepareDefaultEnvironment(services: ToolkitServices, props: { outdir?: string } = {}): Promise { - const logFn = (msg: string, ...args: any) => services.ioHelper.notify(IO.CDK_ASSEMBLY_I0010.msg(format(msg, ...args))); - const env = await oldPrepare(services.sdkProvider, logFn); - - if (props.outdir) { - env[cxapi.OUTDIR_ENV] = props.outdir; - await logFn('outdir:', props.outdir); +export class ExecutionEnvironment { + private readonly ioHelper: IoHelper; + private readonly sdkProvider: SdkProvider; + private readonly debugFn: (msg: string) => Promise; + private _outdir: string | undefined; + + public constructor(services: ToolkitServices, props: { outdir?: string } = {}) { + this.ioHelper = services.ioHelper; + this.sdkProvider = services.sdkProvider; + this.debugFn = (msg: string) => this.ioHelper.notify(IO.DEFAULT_ASSEMBLY_DEBUG.msg(msg)); + this._outdir = props.outdir; } - // CLI version information - env[cxapi.CLI_ASM_VERSION_ENV] = cxschema.Manifest.version(); - env[cxapi.CLI_VERSION_ENV] = versionNumber(); - - await logFn('env:', env); - return env; -} - -/** - * Run code from a different working directory - */ -export async function changeDir(block: () => Promise, workingDir?: string) { - const originalWorkingDir = process.cwd(); - try { - if (workingDir) { - process.chdir(workingDir); + /** + * Turn the given optional output directory into a fixed output directory + */ + public get outdir(): string { + if (!this._outdir) { + const outdir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'cdk.out')); + this._outdir = outdir; } + return this._outdir; + } - return await block(); - } finally { - if (workingDir) { - process.chdir(originalWorkingDir); - } + /** + * Guess the executable from the command-line argument + * + * Only do this if the file is NOT marked as executable. If it is, + * we'll defer to the shebang inside the file itself. + * + * If we're on Windows, we ALWAYS take the handler, since it's hard to + * verify if registry associations have or have not been set up for this + * file type, so we'll assume the worst and take control. + */ + public guessExecutable(app: string) { + return guessExecutable(app, this.debugFn); } -} -/** - * Run code with additional environment variables - */ -export async function withEnv(env: Env = {}, block: () => Promise) { - const originalEnv = process.env; - try { - process.env = { - ...originalEnv, - ...env, - }; - - return await block(); - } finally { - process.env = originalEnv; + /** + * If we don't have region/account defined in context, we fall back to the default SDK behavior + * where region is retrieved from ~/.aws/config and account is based on default credentials provider + * chain and then STS is queried. + * + * This is done opportunistically: for example, if we can't access STS for some reason or the region + * is not configured, the context value will be 'null' and there could failures down the line. In + * some cases, synthesis does not require region/account information at all, so that might be perfectly + * fine in certain scenarios. + */ + public async defaultEnvVars(): Promise { + const debugFn = (msg: string) => this.ioHelper.notify(IO.CDK_ASSEMBLY_I0010.msg(msg)); + const env = await oldPrepare(this.sdkProvider, debugFn); + + env[cxapi.OUTDIR_ENV] = this.outdir; + await debugFn(format('outdir:', this.outdir)); + + // CLI version information + env[cxapi.CLI_ASM_VERSION_ENV] = cxschema.Manifest.version(); + env[cxapi.CLI_VERSION_ENV] = versionNumber(); + + await debugFn(format('env:', env)); + return env; } -} -/** - * Run code with context setup inside the environment - */ -export async function withContext( - inputContext: Context, - env: Env, - synthOpts: AppSynthOptions = {}, - block: (env: Env, context: Context) => Promise, -) { - const context = await prepareContext(synthOptsDefaults(synthOpts), inputContext, env); - let contextOverflowLocation = null; + /** + * Run code from a different working directory + */ + public async changeDir(block: () => Promise, workingDir?: string) { + const originalWorkingDir = process.cwd(); + try { + if (workingDir) { + process.chdir(workingDir); + } + + return await block(); + } finally { + if (workingDir) { + process.chdir(originalWorkingDir); + } + } + } - try { - const envVariableSizeLimit = os.platform() === 'win32' ? 32760 : 131072; - const [smallContext, overflow] = splitBySize(context, spaceAvailableForContext(env, envVariableSizeLimit)); - - // Store the safe part in the environment variable - env[cxapi.CONTEXT_ENV] = JSON.stringify(smallContext); - - // If there was any overflow, write it to a temporary file - if (Object.keys(overflow ?? {}).length > 0) { - const contextDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-context')); - contextOverflowLocation = path.join(contextDir, 'context-overflow.json'); - fs.writeJSONSync(contextOverflowLocation, overflow); - env[cxapi.CONTEXT_OVERFLOW_LOCATION_ENV] = contextOverflowLocation; + /** + * Run code with additional environment variables + */ + public async withEnv(env: Env = {}, block: () => Promise) { + const originalEnv = process.env; + try { + process.env = { + ...originalEnv, + ...env, + }; + + return await block(); + } finally { + process.env = originalEnv; } + } - // call the block code with new environment - return await block(env, context); - } finally { - if (contextOverflowLocation) { - fs.removeSync(path.dirname(contextOverflowLocation)); + /** + * Run code with context setup inside the environment + */ + public async withContext( + inputContext: Context, + env: Env, + synthOpts: AppSynthOptions = {}, + block: (env: Env, context: Context) => Promise, + ) { + const context = await prepareContext(synthOptsDefaults(synthOpts), inputContext, env, this.debugFn); + let contextOverflowLocation = null; + + try { + const envVariableSizeLimit = os.platform() === 'win32' ? 32760 : 131072; + const [smallContext, overflow] = splitBySize(context, spaceAvailableForContext(env, envVariableSizeLimit)); + + // Store the safe part in the environment variable + env[cxapi.CONTEXT_ENV] = JSON.stringify(smallContext); + + // If there was any overflow, write it to a temporary file + if (Object.keys(overflow ?? {}).length > 0) { + const contextDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-context')); + contextOverflowLocation = path.join(contextDir, 'context-overflow.json'); + fs.writeJSONSync(contextOverflowLocation, overflow); + env[cxapi.CONTEXT_OVERFLOW_LOCATION_ENV] = contextOverflowLocation; + } + + // call the block code with new environment + return await block(env, context); + } finally { + if (contextOverflowLocation) { + fs.removeSync(path.dirname(contextOverflowLocation)); + } } } } @@ -130,8 +157,9 @@ export async function withContext( * * @param assembly the assembly to check */ -export async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly, ioHelper: IoHelper): Promise { - const tree = loadTree(assembly, (msg: string) => void ioHelper.notify(IO.DEFAULT_ASSEMBLY_TRACE.msg(msg))); +async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly, ioHelper: IoHelper): Promise { + const traceFn = (msg: string) => ioHelper.notify(IO.DEFAULT_ASSEMBLY_TRACE.msg(msg)); + const tree = await loadTree(assembly, traceFn); const frameworkDoesNotSupportContextOverflow = some(tree, node => { const fqn = node.constructInfo?.fqn; const version = node.constructInfo?.version; @@ -149,7 +177,7 @@ export async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly, /** * Safely create an assembly from a cloud assembly directory */ -export async function assemblyFromDirectory(assemblyDir: string, ioHost: IoHelper, loadOptions: LoadAssemblyOptions = {}) { +export async function assemblyFromDirectory(assemblyDir: string, ioHelper: IoHelper, loadOptions: LoadAssemblyOptions = {}) { try { const assembly = new cxapi.CloudAssembly(assemblyDir, { skipVersionCheck: !(loadOptions.checkVersion ?? true), @@ -157,14 +185,14 @@ export async function assemblyFromDirectory(assemblyDir: string, ioHost: IoHelpe // We sort as we deploy topoSort: false, }); - await checkContextOverflowSupport(assembly, ioHost); + await checkContextOverflowSupport(assembly, ioHelper); return assembly; } catch (err: any) { if (err.message.includes(cxschema.VERSION_MISMATCH)) { // this means the CLI version is too old. // we instruct the user to upgrade. const message = 'This AWS CDK Toolkit is not compatible with the AWS CDK library used by your application. Please upgrade to the latest version.'; - await ioHost.notify(IO.CDK_ASSEMBLY_E1111.msg(message, { error: err })); + await ioHelper.notify(IO.CDK_ASSEMBLY_E1111.msg(message, { error: err })); throw new ToolkitError(`${message}\n(${err.message}`); } throw err; diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts index b36bbfeb6..d20ba6d12 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts @@ -4,7 +4,7 @@ import type { AssemblyDirectoryProps, AssemblySourceProps, ICloudAssemblySource import type { ContextAwareCloudAssemblyProps } from './context-aware-source'; import { ContextAwareCloudAssembly } from './context-aware-source'; import { execInChildProcess } from './exec'; -import { assemblyFromDirectory, changeDir, determineOutputDirectory, guessExecutable, prepareDefaultEnvironment, withContext, withEnv } from './prepare-source'; +import { ExecutionEnvironment, assemblyFromDirectory } from './prepare-source'; import type { ToolkitServices } from '../../../toolkit/private'; import type { ILock } from '../../aws-cdk'; import { Context, RWLock, Settings } from '../../aws-cdk'; @@ -40,14 +40,14 @@ export abstract class CloudAssemblySourceBuilder { return new ContextAwareCloudAssembly( { produce: async () => { - const outdir = determineOutputDirectory(props.outdir); - const env = await prepareDefaultEnvironment(services, { outdir }); - const assembly = await changeDir(async () => - withContext(context.all, env, props.synthOptions ?? {}, async (envWithContext, ctx) => - withEnv(envWithContext, () => { + const execution = new ExecutionEnvironment(services, { outdir: props.outdir }); + const env = await execution.defaultEnvVars(); + const assembly = await execution.changeDir(async () => + execution.withContext(context.all, env, props.synthOptions ?? {}, async (envWithContext, ctx) => + execution.withEnv(envWithContext, () => { try { return builder({ - outdir, + outdir: execution.outdir, context: ctx, }); } catch (error: unknown) { @@ -122,9 +122,7 @@ export abstract class CloudAssemblySourceBuilder { // await execInChildProcess(build, { cwd: props.workingDirectory }); // } - const commandLine = await guessExecutable(app); const outdir = props.outdir ?? 'cdk.out'; - try { fs.mkdirpSync(outdir); } catch (e: any) { @@ -133,8 +131,10 @@ export abstract class CloudAssemblySourceBuilder { lock = await new RWLock(outdir).acquireWrite(); - const env = await prepareDefaultEnvironment(services, { outdir }); - return await withContext(context.all, env, props.synthOptions, async (envWithContext, _ctx) => { + const execution = new ExecutionEnvironment(services, { outdir }); + const commandLine = await execution.guessExecutable(app); + const env = await execution.defaultEnvVars(); + return await execution.withContext(context.all, env, props.synthOptions, async (envWithContext, _ctx) => { await execInChildProcess(commandLine.join(' '), { eventPublisher: async (type, line) => { switch (type) { diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/stack-assembly.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/stack-assembly.ts index aa77946f9..62418ee8f 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/stack-assembly.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/stack-assembly.ts @@ -19,7 +19,7 @@ export class StackAssembly extends CloudAssembly implements ICloudAssemblySource * @throws when the assembly does not contain any stacks, unless `selector.failOnEmpty` is `false` * @throws when individual selection strategies are not satisfied */ - public selectStacksV2(selector: StackSelector): StackCollection { + public async selectStacksV2(selector: StackSelector): Promise { const asm = this.assembly; const topLevelStacks = asm.stacks; const allStacks = major(asm.version) < 10 ? asm.stacks : asm.stacksRecursively; @@ -48,7 +48,7 @@ export class StackAssembly extends CloudAssembly implements ICloudAssemblySource } return new StackCollection(this, topLevelStacks); default: - const matched = this.selectMatchingStacks(allStacks, patterns, extend); + const matched = await this.selectMatchingStacks(allStacks, patterns, extend); if ( selector.strategy === StackSelectionStrategy.PATTERN_MUST_MATCH_SINGLE && matched.stackCount !== 1 diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/index.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/index.ts index ece7a961b..e0565d09a 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/index.ts @@ -18,14 +18,14 @@ export interface ToolkitServices { * @param cache if the assembly should be cached, default: `true` * @returns the CloudAssembly object */ -export async function assemblyFromSource(assemblySource: ICloudAssemblySource, cache: boolean = true): Promise { +export async function assemblyFromSource(ioHelper: IoHelper, assemblySource: ICloudAssemblySource, cache: boolean = true): Promise { if (assemblySource instanceof StackAssembly) { return assemblySource; } if (cache) { - return new StackAssembly(await new CachedCloudAssemblySource(assemblySource).produce()); + return new StackAssembly(await new CachedCloudAssemblySource(assemblySource).produce(), ioHelper); } - return new StackAssembly(await assemblySource.produce()); + return new StackAssembly(await assemblySource.produce(), ioHelper); } diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 077adf7aa..36610768f 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -152,7 +152,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const results: EnvironmentBootstrapResult[] = []; const ioHelper = asIoHelper(this.ioHost, 'bootstrap'); - const bootstrapEnvironments = await environments.getEnvironments(); + const bootstrapEnvironments = await environments.getEnvironments(this.ioHost); const source = options.source ?? BootstrapSource.default(); const parameters = options.parameters; const bootstrapper = new Bootstrapper(source, ioHelper); @@ -212,8 +212,8 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const ioHelper = asIoHelper(this.ioHost, 'synth'); const selectStacks = options.stacks ?? ALL_STACKS; const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); - const assembly = await assemblyFromSource(cx); - const stacks = assembly.selectStacksV2(selectStacks); + const assembly = await assemblyFromSource(ioHelper, cx); + const stacks = await assembly.selectStacksV2(selectStacks); const autoValidateStacks = options.validateStacks ? [assembly.selectStacksForValidation()] : []; await this.validateStacksMetadata(stacks.concat(...autoValidateStacks), ioHelper); await synthSpan.end(); @@ -258,7 +258,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const ioHelper = asIoHelper(this.ioHost, 'list'); const selectStacks = options.stacks ?? ALL_STACKS; const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); - const assembly = await assemblyFromSource(cx); + const assembly = await assemblyFromSource(ioHelper, cx); const stackCollection = await assembly.selectStacksV2(selectStacks); await synthSpan.end(); @@ -275,7 +275,8 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab * Deploys the selected stacks into an AWS account */ public async deploy(cx: ICloudAssemblySource, options: DeployOptions = {}): Promise { - const assembly = await assemblyFromSource(cx); + const ioHelper = asIoHelper(this.ioHost, 'deploy'); + const assembly = await assemblyFromSource(ioHelper, cx); return this._deploy(assembly, 'deploy', options); } @@ -286,7 +287,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const ioHelper = asIoHelper(this.ioHost, action); const selectStacks = options.stacks ?? ALL_STACKS; const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); - const stackCollection = assembly.selectStacksV2(selectStacks); + const stackCollection = await assembly.selectStacksV2(selectStacks); await this.validateStacksMetadata(stackCollection, ioHelper); const synthDuration = await synthSpan.end(); @@ -594,8 +595,8 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab * Implies hotswap deployments. */ public async watch(cx: ICloudAssemblySource, options: WatchOptions): Promise { - const assembly = await assemblyFromSource(cx, false); const ioHelper = asIoHelper(this.ioHost, 'watch'); + const assembly = await assemblyFromSource(ioHelper, cx, false); const rootDir = options.watchDir ?? process.cwd(); if (options.include === undefined && options.exclude === undefined) { @@ -710,7 +711,8 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab * Rolls back the selected stacks. */ public async rollback(cx: ICloudAssemblySource, options: RollbackOptions): Promise { - const assembly = await assemblyFromSource(cx); + const ioHelper = asIoHelper(this.ioHost, 'rollback'); + const assembly = await assemblyFromSource(ioHelper, cx); return this._rollback(assembly, 'rollback', options); } @@ -720,7 +722,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab private async _rollback(assembly: StackAssembly, action: 'rollback' | 'deploy' | 'watch', options: RollbackOptions): Promise { const ioHelper = asIoHelper(this.ioHost, action); const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: options.stacks }); - const stacks = assembly.selectStacksV2(options.stacks); + const stacks = await assembly.selectStacksV2(options.stacks); await this.validateStacksMetadata(stacks, ioHelper); await synthSpan.end(); @@ -767,7 +769,8 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab * Destroys the selected Stacks. */ public async destroy(cx: ICloudAssemblySource, options: DestroyOptions): Promise { - const assembly = await assemblyFromSource(cx); + const ioHelper = asIoHelper(this.ioHost, 'destroy'); + const assembly = await assemblyFromSource(ioHelper, cx); return this._destroy(assembly, 'destroy', options); } @@ -778,7 +781,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab const ioHelper = asIoHelper(this.ioHost, action); const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: options.stacks }); // The stacks will have been ordered for deployment, so reverse them for deletion. - const stacks = await assembly.selectStacksV2(options.stacks).reversed(); + const stacks = (await assembly.selectStacksV2(options.stacks)).reversed(); await synthSpan.end(); const motivation = 'Destroying stacks is an irreversible action'; diff --git a/packages/@aws-cdk/toolkit-lib/test/_helpers/index.ts b/packages/@aws-cdk/toolkit-lib/test/_helpers/index.ts index 7b58fe19a..d8817ad78 100644 --- a/packages/@aws-cdk/toolkit-lib/test/_helpers/index.ts +++ b/packages/@aws-cdk/toolkit-lib/test/_helpers/index.ts @@ -1,8 +1,8 @@ import * as fs from 'node:fs'; +import * as os from 'node:os'; import * as path from 'node:path'; import type { AssemblyDirectoryProps, Toolkit } from '../../lib'; import { ToolkitError } from '../../lib'; -import { determineOutputDirectory } from '../../lib/api/cloud-assembly/private'; export * from '../../lib/api/shared-private'; export * from './test-cloud-assembly-source'; @@ -18,7 +18,7 @@ export async function appFixture(toolkit: Toolkit, name: string, context?: { [ke } const app = `cat ${appPath} | node --input-type=module`; return toolkit.fromCdkApp(app, { - outdir: determineOutputDirectory(), + outdir: tmpOutdir(), context, }); } @@ -27,7 +27,7 @@ export function builderFixture(toolkit: Toolkit, name: string, context?: { [key: // eslint-disable-next-line @typescript-eslint/no-require-imports const builder = require(path.join(__dirname, '..', '_fixtures', name)).default; return toolkit.fromAssemblyBuilder(builder, { - outdir: determineOutputDirectory(), + outdir: tmpOutdir(), context, }); } @@ -39,3 +39,7 @@ export function cdkOutFixture(toolkit: Toolkit, name: string, props: AssemblyDir } return toolkit.fromAssemblyDirectory(outdir, props); } + +function tmpOutdir(): string { + return fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'cdk.out')); +} diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index 9ae64af3c..4d678ee9b 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -1,11 +1,11 @@ import type * as cxapi from '@aws-cdk/cx-api'; import { SynthesisMessageLevel } from '@aws-cdk/cx-api'; -import { type StackDetails } from '@aws-cdk/tmp-toolkit-helpers'; import * as chalk from 'chalk'; import { minimatch } from 'minimatch'; import * as semver from 'semver'; +import { type StackDetails } from '../../../../@aws-cdk/tmp-toolkit-helpers'; import { AssemblyError, ToolkitError } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api'; -import { info } from '../../logging'; +import { IO, type IoHelper } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private'; import { flatten } from '../../util'; export enum DefaultSelection { @@ -97,8 +97,11 @@ export class CloudAssembly { */ public readonly directory: string; - constructor(public readonly assembly: cxapi.CloudAssembly) { + private readonly ioHelper: IoHelper; + + constructor(public readonly assembly: cxapi.CloudAssembly, ioHelper: IoHelper) { this.directory = assembly.directory; + this.ioHelper = ioHelper; } public async selectStacks(selector: StackSelector, options: SelectStacksOptions): Promise { @@ -124,11 +127,11 @@ export class CloudAssembly { } } - private selectTopLevelStacks( + private async selectTopLevelStacks( stacks: cxapi.CloudFormationStackArtifact[], topLevelStacks: cxapi.CloudFormationStackArtifact[], extend: ExtendedStackSelection = ExtendedStackSelection.None, - ): StackCollection { + ): Promise { if (topLevelStacks.length > 0) { return this.extendStacks(topLevelStacks, stacks, extend); } else { @@ -136,11 +139,11 @@ export class CloudAssembly { } } - protected selectMatchingStacks( + protected async selectMatchingStacks( stacks: cxapi.CloudFormationStackArtifact[], patterns: string[], extend: ExtendedStackSelection = ExtendedStackSelection.None, - ): StackCollection { + ): Promise { const matchingPattern = (pattern: string) => (stack: cxapi.CloudFormationStackArtifact) => minimatch(stack.hierarchicalId, pattern); const matchedStacks = flatten(patterns.map(pattern => stacks.filter(matchingPattern(pattern)))); @@ -171,7 +174,7 @@ export class CloudAssembly { } } - protected extendStacks( + protected async extendStacks( matched: cxapi.CloudFormationStackArtifact[], all: cxapi.CloudFormationStackArtifact[], extend: ExtendedStackSelection = ExtendedStackSelection.None, @@ -185,10 +188,10 @@ export class CloudAssembly { switch (extend) { case ExtendedStackSelection.Downstream: - includeDownstreamStacks(index, allStacks); + await includeDownstreamStacks(this.ioHelper, index, allStacks); break; case ExtendedStackSelection.Upstream: - includeUpstreamStacks(index, allStacks); + await includeUpstreamStacks(this.ioHelper, index, allStacks); break; } @@ -366,9 +369,11 @@ function indexByHierarchicalId(stacks: cxapi.CloudFormationStackArtifact[]): Map * * Modifies `selectedStacks` in-place. */ -function includeDownstreamStacks( +async function includeDownstreamStacks( + ioHelper: IoHelper, selectedStacks: Map, - allStacks: Map) { + allStacks: Map, +) { const added = new Array(); let madeProgress; @@ -386,7 +391,7 @@ function includeDownstreamStacks( } while (madeProgress); if (added.length > 0) { - info('Including depending stacks: %s', chalk.bold(added.join(', '))); + await ioHelper.notify(IO.DEFAULT_ASSEMBLY_INFO.msg(`Including depending stacks: ${chalk.bold(added.join(', '))}`)); } } @@ -395,9 +400,11 @@ function includeDownstreamStacks( * * Modifies `selectedStacks` in-place. */ -function includeUpstreamStacks( +async function includeUpstreamStacks( + ioHelper: IoHelper, selectedStacks: Map, - allStacks: Map) { + allStacks: Map, +) { const added = new Array(); let madeProgress = true; while (madeProgress) { @@ -416,7 +423,7 @@ function includeUpstreamStacks( } if (added.length > 0) { - info('Including dependency stacks: %s', chalk.bold(added.join(', '))); + await ioHelper.notify(IO.DEFAULT_ASSEMBLY_INFO.msg(`Including dependency stacks: ${chalk.bold(added.join(', '))}`)); } } diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-executable.ts b/packages/aws-cdk/lib/api/cxapp/cloud-executable.ts index c45d51ca3..db8775043 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-executable.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-executable.ts @@ -1,9 +1,9 @@ import type * as cxapi from '@aws-cdk/cx-api'; import { CloudAssembly } from './cloud-assembly'; import { ToolkitError } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api'; +import { IO, type IoHelper } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private'; import type { Configuration } from '../../cli/user-configuration'; import * as contextproviders from '../../context-providers'; -import { debug } from '../../logging'; import type { SdkProvider } from '../aws-auth'; /** @@ -22,6 +22,11 @@ export interface CloudExecutableProps { */ sdkProvider: SdkProvider; + /** + * Messaging helper + */ + ioHelper: IoHelper; + /** * Callback invoked to synthesize the actual stacks */ @@ -79,19 +84,20 @@ export class CloudExecutable { let tryLookup = true; if (previouslyMissingKeys && setsEqual(missingKeys, previouslyMissingKeys)) { - debug('Not making progress trying to resolve environmental context. Giving up.'); + await this.props.ioHelper.notify(IO.DEFAULT_ASSEMBLY_DEBUG.msg('Not making progress trying to resolve environmental context. Giving up.')); tryLookup = false; } previouslyMissingKeys = missingKeys; if (tryLookup) { - debug('Some context information is missing. Fetching...'); + await this.props.ioHelper.notify(IO.DEFAULT_ASSEMBLY_DEBUG.msg('Some context information is missing. Fetching...')); await contextproviders.provideContextValues( assembly.manifest.missing, this.props.configuration.context, this.props.sdkProvider, + this.props.ioHelper, ); // Cache the new context to disk @@ -102,7 +108,7 @@ export class CloudExecutable { } } - return new CloudAssembly(assembly); + return new CloudAssembly(assembly, this.props.ioHelper); } } diff --git a/packages/aws-cdk/lib/api/cxapp/exec.ts b/packages/aws-cdk/lib/api/cxapp/exec.ts index 4e415c597..faaa75c30 100644 --- a/packages/aws-cdk/lib/api/cxapp/exec.ts +++ b/packages/aws-cdk/lib/api/cxapp/exec.ts @@ -1,16 +1,17 @@ import * as childProcess from 'child_process'; import * as os from 'os'; import * as path from 'path'; +import { format } from 'util'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; import * as semver from 'semver'; import { ToolkitError } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api'; +import { IO, type IoHelper } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private'; import { loadTree, some } from '../../api/tree'; import type { Configuration } from '../../cli/user-configuration'; import { PROJECT_CONFIG, USER_DEFAULTS } from '../../cli/user-configuration'; import { versionNumber } from '../../cli/version'; -import { debug, trace, warning } from '../../logging'; import { splitBySize } from '../../util'; import type { SdkProvider } from '../aws-auth'; import type { Settings } from '../settings'; @@ -23,9 +24,10 @@ export interface ExecProgramResult { } /** Invokes the cloud executable and returns JSON output */ -export async function execProgram(aws: SdkProvider, config: Configuration): Promise { - const env = await prepareDefaultEnvironment(aws); - const context = await prepareContext(config.settings, config.context.all, env); +export async function execProgram(aws: SdkProvider, ioHelper: IoHelper, config: Configuration): Promise { + const debugFn = (msg: string) => ioHelper.notify(IO.DEFAULT_ASSEMBLY_DEBUG.msg(msg)); + const env = await prepareDefaultEnvironment(aws, debugFn); + const context = await prepareContext(config.settings, config.context.all, env, debugFn); const build = config.settings.get(['build']); if (build) { @@ -39,7 +41,7 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom // bypass "synth" if app points to a cloud assembly if (await fs.pathExists(app) && (await fs.stat(app)).isDirectory()) { - debug('--app points to a cloud assembly, so we bypass synth'); + await debugFn('--app points to a cloud assembly, so we bypass synth'); // Acquire a read lock on this directory const lock = await new RWLock(app).acquireRead(); @@ -47,7 +49,7 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom return { assembly: createAssembly(app), lock }; } - const commandLine = await guessExecutable(app); + const commandLine = await guessExecutable(app, debugFn); const outdir = config.settings.get(['output']); if (!outdir) { @@ -62,7 +64,7 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom throw new ToolkitError(`Could not create output directory ${outdir} (${error.message})`); } - debug('outdir:', outdir); + await debugFn(`outdir: ${outdir}`); env[cxapi.OUTDIR_ENV] = outdir; // Acquire a lock on the output directory @@ -73,7 +75,7 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom env[cxapi.CLI_ASM_VERSION_ENV] = cxschema.Manifest.version(); env[cxapi.CLI_VERSION_ENV] = versionNumber(); - debug('env:', env); + await debugFn(format('env:', env)); const envVariableSizeLimit = os.platform() === 'win32' ? 32760 : 131072; const [smallContext, overflow] = splitBySize(context, spaceAvailableForContext(env, envVariableSizeLimit)); @@ -94,7 +96,7 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom const assembly = createAssembly(outdir); - contextOverflowCleanup(contextOverflowLocation, assembly); + await contextOverflowCleanup(contextOverflowLocation, assembly, ioHelper); return { assembly, lock: await writerLock.convertToReaderLock() }; } catch (e) { @@ -103,38 +105,42 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom } async function exec(commandAndArgs: string) { - return new Promise((ok, fail) => { - // We use a slightly lower-level interface to: - // - // - Pass arguments in an array instead of a string, to get around a - // number of quoting issues introduced by the intermediate shell layer - // (which would be different between Linux and Windows). - // - // - Inherit stderr from controlling terminal. We don't use the captured value - // anyway, and if the subprocess is printing to it for debugging purposes the - // user gets to see it sooner. Plus, capturing doesn't interact nicely with some - // processes like Maven. - const proc = childProcess.spawn(commandAndArgs, { - stdio: ['ignore', 'inherit', 'inherit'], - detached: false, - shell: true, - env: { - ...process.env, - ...env, - }, - }); - - proc.on('error', fail); - - proc.on('exit', code => { - if (code === 0) { - return ok(); - } else { - debug('failed command:', commandAndArgs); - return fail(new ToolkitError(`Subprocess exited with error ${code}`)); - } + try { + await new Promise((ok, fail) => { + // We use a slightly lower-level interface to: + // + // - Pass arguments in an array instead of a string, to get around a + // number of quoting issues introduced by the intermediate shell layer + // (which would be different between Linux and Windows). + // + // - Inherit stderr from controlling terminal. We don't use the captured value + // anyway, and if the subprocess is printing to it for debugging purposes the + // user gets to see it sooner. Plus, capturing doesn't interact nicely with some + // processes like Maven. + const proc = childProcess.spawn(commandAndArgs, { + stdio: ['ignore', 'inherit', 'inherit'], + detached: false, + shell: true, + env: { + ...process.env, + ...env, + }, + }); + + proc.on('error', fail); + + proc.on('exit', code => { + if (code === 0) { + return ok(); + } else { + return fail(new ToolkitError(`Subprocess exited with error ${code}`)); + } + }); }); - }); + } catch (e: any) { + await debugFn(`failed command: ${commandAndArgs}`); + throw e; + } } } @@ -171,17 +177,17 @@ export function createAssembly(appDir: string) { */ export async function prepareDefaultEnvironment( aws: SdkProvider, - logFn: (msg: string, ...args: any) => any = debug, + debugFn: (msg: string) => Promise, ): Promise<{ [key: string]: string }> { const env: { [key: string]: string } = { }; env[cxapi.DEFAULT_REGION_ENV] = aws.defaultRegion; - await logFn(`Setting "${cxapi.DEFAULT_REGION_ENV}" environment variable to`, env[cxapi.DEFAULT_REGION_ENV]); + await debugFn(`Setting "${cxapi.DEFAULT_REGION_ENV}" environment variable to ${env[cxapi.DEFAULT_REGION_ENV]}`); const accountId = (await aws.defaultAccount())?.accountId; if (accountId) { env[cxapi.DEFAULT_ACCOUNT_ENV] = accountId; - await logFn(`Setting "${cxapi.DEFAULT_ACCOUNT_ENV}" environment variable to`, env[cxapi.DEFAULT_ACCOUNT_ENV]); + await debugFn(`Setting "${cxapi.DEFAULT_ACCOUNT_ENV}" environment variable to ${env[cxapi.DEFAULT_ACCOUNT_ENV]}`); } return env; @@ -192,7 +198,12 @@ export async function prepareDefaultEnvironment( * The merging of various configuration sources like cli args or cdk.json has already happened. * We now need to set the final values to the context. */ -export async function prepareContext(settings: Settings, context: {[key: string]: any}, env: { [key: string]: string | undefined}) { +export async function prepareContext( + settings: Settings, + context: {[key: string]: any}, + env: { [key: string]: string | undefined}, + debugFn: (msg: string) => Promise, +) { const debugMode: boolean = settings.get(['debug']) ?? true; if (debugMode) { env.CDK_DEBUG = 'true'; @@ -225,7 +236,7 @@ export async function prepareContext(settings: Settings, context: {[key: string] const bundlingStacks = settings.get(['bundlingStacks']) ?? ['**']; context[cxapi.BUNDLING_STACKS] = bundlingStacks; - debug('context:', context); + await debugFn(format('context:', context)); return context; } @@ -265,7 +276,7 @@ const EXTENSION_MAP = new Map([ * verify if registry associations have or have not been set up for this * file type, so we'll assume the worst and take control. */ -export async function guessExecutable(app: string) { +export async function guessExecutable(app: string, debugFn: (msg: string) => Promise) { const commandLine = appToArray(app); if (commandLine.length === 1) { let fstat; @@ -273,7 +284,7 @@ export async function guessExecutable(app: string) { try { fstat = await fs.stat(commandLine[0]); } catch { - debug(`Not a file: '${commandLine[0]}'. Using '${commandLine}' as command-line`); + await debugFn(`Not a file: '${commandLine[0]}'. Using '${commandLine}' as command-line`); return commandLine; } @@ -289,11 +300,15 @@ export async function guessExecutable(app: string) { return commandLine; } -function contextOverflowCleanup(location: string | undefined, assembly: cxapi.CloudAssembly) { +async function contextOverflowCleanup( + location: string | undefined, + assembly: cxapi.CloudAssembly, + ioHelper: IoHelper, +) { if (location) { fs.removeSync(path.dirname(location)); - const tree = loadTree(assembly, trace); + const tree = await loadTree(assembly, (msg: string) => ioHelper.notify(IO.DEFAULT_ASSEMBLY_TRACE.msg(msg))); const frameworkDoesNotSupportContextOverflow = some(tree, node => { const fqn = node.constructInfo?.fqn; const version = node.constructInfo?.version; @@ -304,7 +319,7 @@ function contextOverflowCleanup(location: string | undefined, assembly: cxapi.Cl // We're dealing with an old version of the framework here. It is unaware of the temporary // file, which means that it will ignore the context overflow. if (frameworkDoesNotSupportContextOverflow) { - warning('Part of the context could not be sent to the application. Please update the AWS CDK library to the latest version.'); + await ioHelper.notify(IO.DEFAULT_ASSEMBLY_WARN.msg('Part of the context could not be sent to the application. Please update the AWS CDK library to the latest version.')); } } } diff --git a/packages/aws-cdk/lib/api/tree.ts b/packages/aws-cdk/lib/api/tree.ts index 1f1a74a46..5d360304c 100644 --- a/packages/aws-cdk/lib/api/tree.ts +++ b/packages/aws-cdk/lib/api/tree.ts @@ -36,13 +36,13 @@ export function some(node: ConstructTreeNode | undefined, predicate: (n: Constru } } -export function loadTree(assembly: CloudAssembly, trace: (msg: string) => void): ConstructTreeNode | undefined { +export async function loadTree(assembly: CloudAssembly, trace: (msg: string) => Promise): Promise { try { const outdir = assembly.directory; const fileName = assembly.tree()?.file; - return fileName ? fs.readJSONSync(path.join(outdir, fileName)).tree : {}; + return fileName ? fs.readJSONSync(path.join(outdir, fileName)).tree : ({} as ConstructTreeNode); } catch (e) { - trace(`Failed to get tree.json file: ${e}. Proceeding with empty tree.`); + await trace(`Failed to get tree.json file: ${e}. Proceeding with empty tree.`); return undefined; } } diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index 2f14e7a17..19641dba2 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -140,10 +140,11 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise Promise.resolve(testAssembly(assembly)), + ioHelper: asIoHelper(mockIoHost, 'deploy'), }); this.configuration = configuration; diff --git a/packages/aws-cdk/test/api/cxapp/exec.test.ts b/packages/aws-cdk/test/api/cxapp/exec.test.ts index 76bb323ab..cfc68c826 100644 --- a/packages/aws-cdk/test/api/cxapp/exec.test.ts +++ b/packages/aws-cdk/test/api/cxapp/exec.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable import/order */ jest.mock('child_process'); import bockfs from '../../_helpers/bockfs'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; @@ -13,12 +12,16 @@ import { mockSpawn } from '../../util/mock-child_process'; import { MockSdkProvider } from '../../util/mock-sdk'; import { RWLock } from '../../../lib/api/util/rwlock'; import { rewriteManifestVersion } from './assembly-versions'; -import { CliIoHost } from '../../../lib/cli/io-host'; +import { asIoHelper, TestIoHost } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private'; let sdkProvider: MockSdkProvider; let config: Configuration; +const ioHost = new TestIoHost(); +const ioHelper = asIoHelper(ioHost, 'synth'); + beforeEach(() => { - CliIoHost.instance().logLevel = 'debug'; + ioHost.notifySpy.mockClear(); + ioHost.requestSpy.mockClear(); sdkProvider = new MockSdkProvider(); config = new Configuration(); @@ -37,8 +40,6 @@ beforeEach(() => { }); afterEach(() => { - CliIoHost.instance().logLevel = 'info'; - sinon.restore(); bockfs.restore(); }); @@ -84,7 +85,7 @@ test('cli throws when manifest version > schema version', async () => { config.settings.set(['app'], 'cdk.out'); - await expect(execProgram(sdkProvider, config)).rejects.toEqual(new Error(expectedError)); + await expect(execProgram(sdkProvider, ioHelper, config)).rejects.toEqual(new Error(expectedError)); }, TEN_SECOND_TIMEOUT); @@ -97,7 +98,7 @@ test('cli does not throw when manifest version = schema version', async () => { config.settings.set(['app'], 'cdk.out'); - const { lock } = await execProgram(sdkProvider, config); + const { lock } = await execProgram(sdkProvider, ioHelper, config); await lock.release(); }, TEN_SECOND_TIMEOUT); @@ -124,7 +125,7 @@ test.skip('cli does not throw when manifest version < schema version', async () // greater that the version created in the manifest, which is what we are testing for. const mockVersionNumber = ImportMock.mockFunction(cxschema.Manifest, 'version', semver.inc(currentSchemaVersion, 'major')); try { - const { lock } = await execProgram(sdkProvider, config); + const { lock } = await execProgram(sdkProvider, ioHelper, config); await lock.release(); } finally { mockVersionNumber.restore(); @@ -134,7 +135,7 @@ test.skip('cli does not throw when manifest version < schema version', async () test('validates --app key is present', async () => { // GIVEN no config key for `app` - await expect(execProgram(sdkProvider, config)).rejects.toThrow( + await expect(execProgram(sdkProvider, ioHelper, config)).rejects.toThrow( '--app is required either in command-line, in cdk.json or in ~/.cdk.json', ); @@ -147,7 +148,7 @@ test('bypasses synth when app points to a cloud assembly', async () => { rewriteManifestVersionToOurs(); // WHEN - const { assembly: cloudAssembly, lock } = await execProgram(sdkProvider, config); + const { assembly: cloudAssembly, lock } = await execProgram(sdkProvider, ioHelper, config); expect(cloudAssembly.artifacts).toEqual([]); expect(cloudAssembly.directory).toEqual('cdk.out'); @@ -163,7 +164,7 @@ test('the application set in --app is executed', async () => { }); // WHEN - const { lock } = await execProgram(sdkProvider, config); + const { lock } = await execProgram(sdkProvider, ioHelper, config); await lock.release(); }); @@ -176,7 +177,7 @@ test('the application set in --app is executed as-is if it contains a filename t }); // WHEN - const { lock } = await execProgram(sdkProvider, config); + const { lock } = await execProgram(sdkProvider, ioHelper, config); await lock.release(); }); @@ -189,7 +190,7 @@ test('the application set in --app is executed with arguments', async () => { }); // WHEN - const { lock } = await execProgram(sdkProvider, config); + const { lock } = await execProgram(sdkProvider, ioHelper, config); await lock.release(); }); @@ -203,7 +204,7 @@ test('application set in --app as `*.js` always uses handler on windows', async }); // WHEN - const { lock } = await execProgram(sdkProvider, config); + const { lock } = await execProgram(sdkProvider, ioHelper, config); await lock.release(); }); @@ -216,7 +217,7 @@ test('application set in --app is `*.js` and executable', async () => { }); // WHEN - const { lock } = await execProgram(sdkProvider, config); + const { lock } = await execProgram(sdkProvider, ioHelper, config); await lock.release(); }); @@ -229,7 +230,7 @@ test('cli throws when the `build` script fails', async () => { }); // WHEN - await expect(execProgram(sdkProvider, config)).rejects.toEqual(new Error('Subprocess exited with error 127')); + await expect(execProgram(sdkProvider, ioHelper, config)).rejects.toEqual(new Error('Subprocess exited with error 127')); }, TEN_SECOND_TIMEOUT); test('cli does not throw when the `build` script succeeds', async () => { @@ -246,7 +247,7 @@ test('cli does not throw when the `build` script succeeds', async () => { }); // WHEN - const { lock } = await execProgram(sdkProvider, config); + const { lock } = await execProgram(sdkProvider, ioHelper, config); await lock.release(); }, TEN_SECOND_TIMEOUT); @@ -259,7 +260,7 @@ test('cli releases the outdir lock when execProgram throws', async () => { }); // WHEN - await expect(execProgram(sdkProvider, config)).rejects.toThrow(); + await expect(execProgram(sdkProvider, ioHelper, config)).rejects.toThrow(); const output = config.settings.get(['output']); expect(output).toBeDefined(); diff --git a/packages/aws-cdk/test/commands/diff.test.ts b/packages/aws-cdk/test/commands/diff.test.ts index 7cd22f39f..390252828 100644 --- a/packages/aws-cdk/test/commands/diff.test.ts +++ b/packages/aws-cdk/test/commands/diff.test.ts @@ -76,7 +76,7 @@ describe('fixed template', () => { }, }, ], - }); + }, undefined, ioHost); toolkit = new CdkToolkit({ cloudExecutable, @@ -172,7 +172,7 @@ describe('imports', () => { }, }, ], - }); + }, undefined, ioHost); cloudFormation = instanceMockFrom(Deployments); @@ -290,7 +290,7 @@ describe('non-nested stacks', () => { template: { resource: 'D' }, }, ], - }); + }, undefined, ioHost); cloudFormation = instanceMockFrom(Deployments); @@ -358,7 +358,7 @@ describe('non-nested stacks', () => { template: { resourceC: 'C' }, }, ], - }); + }, undefined, ioHost); toolkit = new CdkToolkit({ cloudExecutable, @@ -500,7 +500,7 @@ describe('stack exists checks', () => { template: { resource: 'D' }, }, ], - }); + }, undefined, ioHost); cloudFormation = instanceMockFrom(Deployments); @@ -607,7 +607,7 @@ describe('nested stacks', () => { template: {}, }, ], - }); + }, undefined, ioHost); cloudFormation = instanceMockFrom(Deployments); @@ -1092,7 +1092,7 @@ describe('--strict', () => { }, }, ], - }); + }, undefined, ioHost); toolkit = new CdkToolkit({ cloudExecutable,