diff --git a/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/index.ts b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/index.ts index a3999647411cb..4fd6dcfdd4e94 100644 --- a/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/index.ts +++ b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/index.ts @@ -1,5 +1,4 @@ export * from './cached-source'; -export * from './context-aware-source'; export * from './identity-source'; export * from './stack-assembly'; export * from './stack-selector'; diff --git a/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/context-aware-source.ts b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/context-aware-source.ts similarity index 81% rename from packages/@aws-cdk/toolkit/lib/api/cloud-assembly/context-aware-source.ts rename to packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/context-aware-source.ts index 0519a020d6673..950b19b42b418 100644 --- a/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/context-aware-source.ts +++ b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/context-aware-source.ts @@ -1,17 +1,18 @@ import type { MissingContext } from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; -import { SdkProvider } from 'aws-cdk/lib/api/aws-auth'; import * as contextproviders from 'aws-cdk/lib/context-providers'; -import { debug } from 'aws-cdk/lib/logging'; import { Context, PROJECT_CONTEXT } from 'aws-cdk/lib/settings'; -import { ICloudAssemblySource } from './types'; -import { ToolkitError } from '../errors'; +import { ToolkitError } from '../../errors'; +import { ActionAwareIoHost, debug } from '../../io/private'; +import { ToolkitServices } from '../../toolkit/private'; +import { ICloudAssemblySource } from '../types'; -export interface CloudExecutableProps { +export interface ContextAwareCloudAssemblyProps { /** * AWS object (used by contextprovider) + * @deprecated context should be moved to the toolkit itself */ - readonly sdkProvider: SdkProvider; + readonly services: ToolkitServices; /** * Application context @@ -42,11 +43,13 @@ export class ContextAwareCloudAssembly implements ICloudAssemblySource { private canLookup: boolean; private context: Context; private contextFile: string; + private ioHost: ActionAwareIoHost; - constructor(private readonly source: ICloudAssemblySource, private readonly props: CloudExecutableProps) { + constructor(private readonly source: ICloudAssemblySource, private readonly props: ContextAwareCloudAssemblyProps) { this.canLookup = props.lookups ?? true; this.context = props.context; this.contextFile = props.contextFile ?? PROJECT_CONTEXT; // @todo new feature not needed right now + this.ioHost = props.services.ioHost; } /** @@ -73,19 +76,18 @@ export class ContextAwareCloudAssembly implements ICloudAssemblySource { let tryLookup = true; if (previouslyMissingKeys && equalSets(missingKeys, previouslyMissingKeys)) { - debug('Not making progress trying to resolve environmental context. Giving up.'); + await this.ioHost.notify(debug('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.ioHost.notify(debug('Some context information is missing. Fetching...')); await contextproviders.provideContextValues( assembly.manifest.missing, this.context, - this.props.sdkProvider, + this.props.services.sdkProvider, ); // Cache the new context to disk diff --git a/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/exec.ts b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/exec.ts new file mode 100644 index 0000000000000..7c49e98635f8a --- /dev/null +++ b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/exec.ts @@ -0,0 +1,45 @@ +import * as child_process from 'node:child_process'; +import { ToolkitError } from '../../errors'; + +interface ExecOptions { + extraEnv?: { [key: string]: string | undefined }; + cwd?: string; +} + +/** + * Execute a command and args in a child process + */ +export async function execInChildProcess(commandAndArgs: string, options: ExecOptions = {}) { + 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 = child_process.spawn(commandAndArgs, { + stdio: ['ignore', 'inherit', 'inherit'], + detached: false, + shell: true, + cwd: options.cwd, + env: { + ...process.env, + ...(options.extraEnv ?? {}), + }, + }); + + proc.on('error', fail); + + proc.on('exit', code => { + if (code === 0) { + return ok(); + } else { + return fail(new ToolkitError(`Subprocess exited with error ${code}`)); + } + }); + }); +} diff --git a/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/from-app.ts b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/from-app.ts deleted file mode 100644 index a99e0aa51f4ff..0000000000000 --- a/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/from-app.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { ToolkitError } from '../../errors'; -import { ContextAwareCloudAssembly } from '../context-aware-source'; -import { ICloudAssemblySource } from '../types'; - -/** - * Configuration for creating a CLI from an AWS CDK App directory - */ -export interface FromCdkAppProps { -/** - * @default - current working directory - */ - readonly workingDirectory?: string; - - /** - * Emits the synthesized cloud assembly into a directory - * - * @default cdk.out - */ - readonly output?: string; -} - -/** - * Use a directory containing an AWS CDK app as source. - * @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` - */ -export function fromCdkApp(app: string, props: FromCdkAppProps = {}): ICloudAssemblySource { - return new ContextAwareCloudAssembly( - { - produce: async () => { - await this.lock?.release(); - - try { - const build = this.props.configuration.settings.get(['build']); - if (build) { - await exec(build, { cwd: props.workingDirectory }); - } - - const commandLine = await guessExecutable(app); - const outdir = props.output ?? 'cdk.out'; - - try { - fs.mkdirpSync(outdir); - } catch (e: any) { - throw new ToolkitError(`Could not create output directory at '${outdir}' (${e.message}).`); - } - - this.lock = await new RWLock(outdir).acquireWrite(); - - const env = await prepareDefaultEnvironment(this.props.sdkProvider, { outdir }); - return await withContext(env, this.props.configuration, async (envWithContext, _context) => { - await exec(commandLine.join(' '), { extraEnv: envWithContext, cwd: props.workingDirectory }); - return createAssembly(outdir); - }); - } finally { - await this.lock?.release(); - } - }, - }, - this.propsForContextAssembly, - ); -} diff --git a/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/prepare-source.ts b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/prepare-source.ts new file mode 100644 index 0000000000000..f48d4a3217fc0 --- /dev/null +++ b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/prepare-source.ts @@ -0,0 +1,179 @@ +import * as os from 'node:os'; +import * as path from 'node:path'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as cxapi from '@aws-cdk/cx-api'; +import { prepareDefaultEnvironment as oldPrepare, prepareContext, spaceAvailableForContext } from 'aws-cdk/lib/api/cxapp/exec'; +import { Settings } from 'aws-cdk/lib/settings'; +import { loadTree, some } from 'aws-cdk/lib/tree'; +import { splitBySize } from 'aws-cdk/lib/util'; +import { versionNumber } from 'aws-cdk/lib/version'; +import * as fs from 'fs-extra'; +import { lte } from 'semver'; +import type { AppSynthOptions } from './source-builder'; +import { ToolkitError } from '../../errors'; +import { ActionAwareIoHost, asLogger, error, warn } from '../../io/private'; +import { ToolkitServices } from '../../toolkit/private'; + +export { guessExecutable } from 'aws-cdk/lib/api/cxapp/exec'; + +type Env = { [key: string]: string }; +type Context = { [key: string]: any }; + +/** + * 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 = asLogger(services.ioHost, 'ASSEMBLY').debug; + const env = await oldPrepare(services.sdkProvider, logFn); + + if (props.outdir) { + env[cxapi.OUTDIR_ENV] = props.outdir; + await logFn('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); + } + + return await block(); + + } finally { + if (workingDir) { + process.chdir(originalWorkingDir); + } + } +} + +/** + * 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; + } +} + +/** + * 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; + + 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)); + } + } +} + +/** + * Checks if a given assembly supports context overflow, warn otherwise. + * + * @param assembly the assembly to check + */ +export async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly, ioHost: ActionAwareIoHost): Promise { + const logFn = asLogger(ioHost, 'ASSEMBLY').warn; + const tree = loadTree(assembly); + const frameworkDoesNotSupportContextOverflow = some(tree, node => { + const fqn = node.constructInfo?.fqn; + const version = node.constructInfo?.version; + return (fqn === 'aws-cdk-lib.App' && version != null && lte(version, '2.38.0')) // v2 + || fqn === '@aws-cdk/core.App'; // v1 + }); + + // 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) { + await logFn('Part of the context could not be sent to the application. Please update the AWS CDK library to the latest version.'); + } +} + +/** + * Safely create an assembly from a cloud assembly directory + */ +export async function assemblyFromDirectory(assemblyDir: string, ioHost: ActionAwareIoHost) { + try { + const assembly = new cxapi.CloudAssembly(assemblyDir, { + // We sort as we deploy + topoSort: false, + }); + await checkContextOverflowSupport(assembly, ioHost); + 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(error(message, 'CDK_ASSEMBLY_E1111', { error: err.message })); + throw new ToolkitError(`${message}\n(${err.message}`); + } + throw err; + } +} +function synthOptsDefaults(synthOpts: AppSynthOptions = {}): Settings { + return new Settings({ + debug: false, + pathMetadata: true, + versionReporting: true, + assetMetadata: true, + assetStaging: true, + ...synthOpts, + }, true); +} + diff --git a/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/source-builder.ts b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/source-builder.ts new file mode 100644 index 0000000000000..bca83104779be --- /dev/null +++ b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/source-builder.ts @@ -0,0 +1,193 @@ +import * as cxapi from '@aws-cdk/cx-api'; +import { ILock, RWLock } from 'aws-cdk/lib/api/util/rwlock'; +import * as fs from 'fs-extra'; +import type { ICloudAssemblySource } from '../'; +import { ContextAwareCloudAssembly, ContextAwareCloudAssemblyProps } from './context-aware-source'; +import { execInChildProcess } from './exec'; +import { assemblyFromDirectory, changeDir, guessExecutable, prepareDefaultEnvironment, withContext, withEnv } from './prepare-source'; +import { ToolkitError } from '../../errors'; +import { debug } from '../../io/private'; +import { ToolkitServices } from '../../toolkit/private'; + +/** + * Configuration for creating a CLI from an AWS CDK App directory + */ +export interface FromCdkAppProps { + /** + * @default - current working directory + */ + readonly workingDirectory?: string; + + /** + * Emits the synthesized cloud assembly into a directory + * + * @default cdk.out + */ + readonly output?: string; + + /** + * Options that are passed through the context to a CDK app on synth + */ + readonly synthOptions?: AppSynthOptions; +} + +export type AssemblyBuilder = (context: Record) => Promise; + +export abstract class CloudAssemblySourceBuilder { + /** + * Create a Cloud Assembly from a Cloud Assembly builder function. + */ + public fromAssemblyBuilder( + builder: AssemblyBuilder, + props: FromCdkAppProps = {}, + ): ICloudAssemblySource { + const context: Context; + const services: ToolkitServices; + const contextAssemblyProps: ContextAwareCloudAssemblyProps = {}; + + return new ContextAwareCloudAssembly( + { + produce: async () => { + const env = await prepareDefaultEnvironment(services, { outdir: props.output }); + return changeDir(async () => + withContext(context.all, env, props.synthOptions ?? {}, async (envWithContext, ctx) => + withEnv(envWithContext, () => builder(ctx)), + ), props.workingDirectory); + }, + }, + contextAssemblyProps, + ); + } + + /** + * Creates a Cloud Assembly from an existing assembly directory. + * @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 fromAssemblyDirectory(directory: string): ICloudAssemblySource { + const contextAssemblyProps: ContextAwareCloudAssemblyProps = {}; + const services: ToolkitServices; + + return new ContextAwareCloudAssembly( + { + produce: async () => { + // @todo build + await services.ioHost.notify(debug('--app points to a cloud assembly, so we bypass synth')); + return assemblyFromDirectory(directory, services.ioHost); + + }, + }, + contextAssemblyProps, + ); + } + /** + * Use a directory containing an AWS CDK app as source. + * @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 fromCdkApp(app: string, props: FromCdkAppProps = {}): ICloudAssemblySource { + const context: Context; + const services: ToolkitServices; + const contextAssemblyProps: ContextAwareCloudAssemblyProps = {}; + + return new ContextAwareCloudAssembly( + { + produce: async () => { + let lock: ILock | undefined = undefined; + try { + // @todo build + // const build = this.props.configuration.settings.get(['build']); + // if (build) { + // await execInChildProcess(build, { cwd: props.workingDirectory }); + // } + + const commandLine = await guessExecutable(app); + const outdir = props.output ?? 'cdk.out'; + + try { + fs.mkdirpSync(outdir); + } catch (e: any) { + throw new ToolkitError(`Could not create output directory at '${outdir}' (${e.message}).`); + } + + lock = await new RWLock(outdir).acquireWrite(); + + const env = await prepareDefaultEnvironment(services, { outdir }); + return await withContext(context.all, env, props.synthOptions, async (envWithContext, _ctx) => { + await execInChildProcess(commandLine.join(' '), { extraEnv: envWithContext, cwd: props.workingDirectory }); + return assemblyFromDirectory(outdir, services.ioHost); + }); + } finally { + await lock?.release(); + } + }, + }, + contextAssemblyProps, + ); + } +} + +/** + * Settings that are passed to a CDK app via the context + */ +export interface AppSynthOptions { + /** + * Debug the CDK app. + * Logs additional information during synthesis, such as creation stack traces of tokens. + * This also sets the `CDK_DEBUG` env variable and will slow down synthesis. + * + * @default false + */ + readonly debug?: boolean; + + /** + * Enables the embedding of the "aws:cdk:path" in CloudFormation template metadata. + * + * @default true + */ + readonly pathMetadata?: boolean; + + /** + * Enable the collection and reporting of version information. + * + * @default true + */ + readonly versionReporting?: boolean; + + /** + * Whe enabled, `aws:asset:xxx` metadata entries are added to the template. + * + * Disabling this can be useful in certain cases like integration tests. + * + * @default true + */ + readonly assetMetadata?: boolean; + + /** + * Enable asset staging. + * + * Disabling asset staging means that copyable assets will not be copied to the + * output directory and will be referenced with absolute paths. + * + * Not copied to the output directory: this is so users can iterate on the + * Lambda source and run SAM CLI without having to re-run CDK (note: we + * cannot achieve this for bundled assets, if assets are bundled they + * will have to re-run CDK CLI to re-bundle updated versions). + * + * Absolute path: SAM CLI expects `cwd`-relative paths in a resource's + * `aws:asset:path` metadata. In order to be predictable, we will always output + * absolute paths. + * + * @default true + */ + readonly assetStaging?: boolean; + + /** + * Select which stacks should have asset bundling enabled + * + * @default ["**"] - all stacks + */ + readonly bundlingForStacks?: string; +} diff --git a/packages/@aws-cdk/toolkit/lib/api/io/private/logger.ts b/packages/@aws-cdk/toolkit/lib/api/io/private/logger.ts index b80e095347b01..ff8cb28dc7c9c 100644 --- a/packages/@aws-cdk/toolkit/lib/api/io/private/logger.ts +++ b/packages/@aws-cdk/toolkit/lib/api/io/private/logger.ts @@ -1,7 +1,9 @@ +import * as util from 'node:util'; import type { Logger } from '@smithy/types'; import { formatSdkLoggerContent } from 'aws-cdk/lib/api/aws-auth/sdk-logger'; -import type { IIoHost, IoMessage, IoRequest } from '../io-host'; -import { trace } from './messages'; +import type { IIoHost, IoMessage, IoMessageCodeCategory, IoMessageLevel, IoRequest } from '../io-host'; +import { debug, error, info, messageCode, trace, warn } from './messages'; +import { ActionAwareIoHost } from './types'; import type { ToolkitAction } from '../../../toolkit'; export function withAction(ioHost: IIoHost, action: ToolkitAction) { @@ -21,6 +23,7 @@ export function withAction(ioHost: IIoHost, action: ToolkitAction) { }; } +// @todo these cannot be awaited WTF export function asSdkLogger(ioHost: IIoHost, action: ToolkitAction): Logger { return new class implements Logger { // This is too much detail for our logs @@ -109,3 +112,28 @@ export function asSdkLogger(ioHost: IIoHost, action: ToolkitAction): Logger { } }; } + +/** + * Turn an ActionAwareIoHost into a logger that is compatible with older code, but doesn't support data + */ +export function asLogger(ioHost: ActionAwareIoHost, category?: IoMessageCodeCategory) { + const code = (level: IoMessageLevel) => messageCode(level, category); + + return { + trace: async (msg: string, ...args: any[]) => { + await ioHost.notify(trace(util.format(msg, args), code('trace'))); + }, + debug: async (msg: string, ...args: any[]) => { + await ioHost.notify(debug(util.format(msg, args), code('debug'))); + }, + info: async (msg: string, ...args: any[]) => { + await ioHost.notify(info(util.format(msg, args), code('info'))); + }, + warn: async (msg: string, ...args: any[]) => { + await ioHost.notify(warn(util.format(msg, args), code('warn'))); + }, + error: async (msg: string, ...args: any[]) => { + await ioHost.notify(error(util.format(msg, args), code('error'))); + }, + }; +} diff --git a/packages/@aws-cdk/toolkit/lib/api/io/private/messages.ts b/packages/@aws-cdk/toolkit/lib/api/io/private/messages.ts index 98e479253a7ce..55cfde9ef7248 100644 --- a/packages/@aws-cdk/toolkit/lib/api/io/private/messages.ts +++ b/packages/@aws-cdk/toolkit/lib/api/io/private/messages.ts @@ -7,11 +7,11 @@ import type { ActionLessMessage, ActionLessRequest, Optional, SimplifiedMessage * Handles string interpolation, format strings, and object parameter styles. * Applies optional styling and prepares the final message for logging. */ -function formatMessage(msg: Optional, 'code'>): ActionLessMessage { +export function formatMessage(msg: Optional, 'code'>, category: IoMessageCodeCategory = 'TOOLKIT'): ActionLessMessage { return { time: new Date(), level: msg.level, - code: msg.code ?? messageCode(msg.level), + code: msg.code ?? messageCode(msg.level, category), message: msg.message, data: msg.data, }; @@ -20,7 +20,7 @@ function formatMessage(msg: Optional, 'code'>): ActionLe /** * Build a message code from level and category */ -function messageCode(level: IoMessageLevel, category: IoMessageCodeCategory = 'TOOLKIT', number?: `${number}${number}${number}${number}`): IoMessageCode { +export function messageCode(level: IoMessageLevel, category: IoMessageCodeCategory = 'TOOLKIT', number?: `${number}${number}${number}${number}`): IoMessageCode { const levelIndicator = level === 'error' ? 'E' : level === 'warn' ? 'W' : 'I'; @@ -76,7 +76,7 @@ export const error = (message: string, code?: IoMessageCode, payload?: T) => /** * Logs an warning level message. */ -export const warning = (message: string, code?: IoMessageCode, payload?: T) => { +export const warn = (message: string, code?: IoMessageCode, payload?: T) => { return formatMessage({ level: 'warn', code, diff --git a/packages/@aws-cdk/toolkit/lib/api/io/private/types.ts b/packages/@aws-cdk/toolkit/lib/api/io/private/types.ts index 2009b442dbdd7..27f14c55e8d0a 100644 --- a/packages/@aws-cdk/toolkit/lib/api/io/private/types.ts +++ b/packages/@aws-cdk/toolkit/lib/api/io/private/types.ts @@ -8,7 +8,7 @@ export type ActionLessRequest = Omit, 'action'>; /** * Helper type for IoHosts that are action aware */ -export interface ActionlessIoHost extends IIoHost { +export interface ActionAwareIoHost extends IIoHost { notify(msg: ActionLessMessage): Promise; requestResponse(msg: ActionLessRequest): Promise; } diff --git a/packages/@aws-cdk/toolkit/lib/api/toolkit/private/index.ts b/packages/@aws-cdk/toolkit/lib/api/toolkit/private/index.ts new file mode 100644 index 0000000000000..0b856394ab15b --- /dev/null +++ b/packages/@aws-cdk/toolkit/lib/api/toolkit/private/index.ts @@ -0,0 +1,10 @@ +import { SdkProvider } from 'aws-cdk/lib'; +import { ActionAwareIoHost } from '../../io/private'; + +/** + * Helper struct to pass internal services around. + */ +export interface ToolkitServices { + sdkProvider: SdkProvider; + ioHost: ActionAwareIoHost; +} diff --git a/packages/@aws-cdk/toolkit/lib/toolkit.ts b/packages/@aws-cdk/toolkit/lib/toolkit.ts index 7d407e200762d..b3a2f4b4d9977 100644 --- a/packages/@aws-cdk/toolkit/lib/toolkit.ts +++ b/packages/@aws-cdk/toolkit/lib/toolkit.ts @@ -29,6 +29,7 @@ import { StackSelectionStrategy } from './api/cloud-assembly/stack-selector'; import { ToolkitError } from './api/errors'; import { IIoHost, IoMessageCode, IoMessageLevel } from './api/io'; import { asSdkLogger, withAction, ActionAwareIoHost, Timer, confirm, data, error, highlight, info, success, warn } from './api/io/private'; +import { Configuration } from 'aws-cdk/lib/settings'; /** * The current action being performed by the CLI. 'none' represents the absence of an action. @@ -73,19 +74,31 @@ export interface ToolkitOptions { * The AWS CDK Programmatic Toolkit */ export class Toolkit { + + /** + * The toolkit stack name used for bootstrapping resources. + */ + public readonly toolkitStackName: string; + + /** + * @todo should probably be public in one way or the other. + */ private readonly ioHost: IIoHost; private _sdkProvider?: SdkProvider; - private toolkitStackName: string; + private _configuration?: Configuration; public constructor(private readonly props: ToolkitOptions) { this.ioHost = CliIoHost.getIoHost(); // this.ioHost = options.ioHost; // @todo open this up this.toolkitStackName = props.toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME; - } + } + /** + * Access to the AWS SDK + */ private async sdkProvider(action: ToolkitAction): Promise { - // @todo this needs to be different per action + // @todo this needs to be different instance per action if (!this._sdkProvider) { this._sdkProvider = await SdkProvider.withAwsCliCompatibleDefaults({ ...this.props.sdkOptions, @@ -96,6 +109,19 @@ export class Toolkit { return this._sdkProvider; } + /** + * The internal toolkit configuration + * @deprecated + */ + private async configuration(): Promise { + if (!this._configuration) { + this._configuration = new Configuration(); + await this._configuration.load(); + } + + return this._configuration; + } + /** * Synth Action */ diff --git a/packages/aws-cdk/lib/api/cxapp/exec.ts b/packages/aws-cdk/lib/api/cxapp/exec.ts index b02ad38445a07..f17d0c11453df 100644 --- a/packages/aws-cdk/lib/api/cxapp/exec.ts +++ b/packages/aws-cdk/lib/api/cxapp/exec.ts @@ -6,7 +6,7 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; import * as semver from 'semver'; import { debug, warning } from '../../logging'; -import { Configuration, PROJECT_CONFIG, USER_DEFAULTS } from '../../settings'; +import { Configuration, PROJECT_CONFIG, Settings, USER_DEFAULTS } from '../../settings'; import { ToolkitError } from '../../toolkit/error'; import { loadTree, some } from '../../tree'; import { splitBySize } from '../../util/objects'; @@ -22,7 +22,7 @@ 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, env); + const context = await prepareContext(config.settings, config.context.all, env); const build = config.settings.get(['build']); if (build) { @@ -44,7 +44,7 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom return { assembly: createAssembly(app), lock }; } - const commandLine = await guessExecutable(appToArray(app)); + const commandLine = await guessExecutable(app); const outdir = config.settings.get(['output']); if (!outdir) { @@ -166,16 +166,19 @@ export function createAssembly(appDir: string) { * * @param context The context key/value bash. */ -export async function prepareDefaultEnvironment(aws: SdkProvider): Promise<{ [key: string]: string }> { +export async function prepareDefaultEnvironment( + aws: SdkProvider, + logFn: (msg: string, ...args: any) => any = debug, +): Promise<{ [key: string]: string }> { const env: { [key: string]: string } = { }; env[cxapi.DEFAULT_REGION_ENV] = aws.defaultRegion; - debug(`Setting "${cxapi.DEFAULT_REGION_ENV}" environment variable to`, env[cxapi.DEFAULT_REGION_ENV]); + await logFn(`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; - debug(`Setting "${cxapi.DEFAULT_ACCOUNT_ENV}" environment variable to`, env[cxapi.DEFAULT_ACCOUNT_ENV]); + await logFn(`Setting "${cxapi.DEFAULT_ACCOUNT_ENV}" environment variable to`, env[cxapi.DEFAULT_ACCOUNT_ENV]); } return env; @@ -186,35 +189,33 @@ export async function prepareDefaultEnvironment(aws: SdkProvider): Promise<{ [ke * 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(config: Configuration, env: { [key: string]: string | undefined}) { - const context = config.context.all; - - const debugMode: boolean = config.settings.get(['debug']) ?? true; +export async function prepareContext(settings: Settings, context: {[key: string]: any}, env: { [key: string]: string | undefined}) { + const debugMode: boolean = settings.get(['debug']) ?? true; if (debugMode) { env.CDK_DEBUG = 'true'; } - const pathMetadata: boolean = config.settings.get(['pathMetadata']) ?? true; + const pathMetadata: boolean = settings.get(['pathMetadata']) ?? true; if (pathMetadata) { context[cxapi.PATH_METADATA_ENABLE_CONTEXT] = true; } - const assetMetadata: boolean = config.settings.get(['assetMetadata']) ?? true; + const assetMetadata: boolean = settings.get(['assetMetadata']) ?? true; if (assetMetadata) { context[cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT] = true; } - const versionReporting: boolean = config.settings.get(['versionReporting']) ?? true; + const versionReporting: boolean = settings.get(['versionReporting']) ?? true; if (versionReporting) { context[cxapi.ANALYTICS_REPORTING_ENABLED_CONTEXT] = true; } // We need to keep on doing this for framework version from before this flag was deprecated. if (!versionReporting) { context['aws:cdk:disable-version-reporting'] = true; } - const stagingEnabled = config.settings.get(['staging']) ?? true; + const stagingEnabled = settings.get(['staging']) ?? true; if (!stagingEnabled) { context[cxapi.DISABLE_ASSET_STAGING_CONTEXT] = true; } - const bundlingStacks = config.settings.get(['bundlingStacks']) ?? ['**']; + const bundlingStacks = settings.get(['bundlingStacks']) ?? ['**']; context[cxapi.BUNDLING_STACKS] = bundlingStacks; debug('context:', context); @@ -257,7 +258,8 @@ 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. */ -async function guessExecutable(commandLine: string[]) { +export async function guessExecutable(app: string) { + const commandLine = appToArray(app); if (commandLine.length === 1) { let fstat; @@ -300,7 +302,7 @@ function contextOverflowCleanup(location: string | undefined, assembly: cxapi.Cl } } -function spaceAvailableForContext(env: { [key: string]: string }, limit: number) { +export function spaceAvailableForContext(env: { [key: string]: string }, limit: number) { const size = (value: string) => value != null ? Buffer.byteLength(value) : 0; const usedSpace = Object.entries(env) diff --git a/packages/aws-cdk/lib/settings.ts b/packages/aws-cdk/lib/settings.ts index 67ef8dabfa728..a14b712f05a3a 100644 --- a/packages/aws-cdk/lib/settings.ts +++ b/packages/aws-cdk/lib/settings.ts @@ -126,6 +126,7 @@ export class Configuration { this._projectConfig = await loadAndLog(PROJECT_CONFIG); this._projectContext = await loadAndLog(PROJECT_CONTEXT); + // @todo cannot currently be disabled by cli users const readUserContext = this.props.readUserContext ?? true; if (userConfig.get(['build'])) {