diff --git a/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/context-aware-source.ts b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/context-aware-source.ts index dad63b05170be..60d9724fd6121 100644 --- a/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/context-aware-source.ts +++ b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/context-aware-source.ts @@ -55,8 +55,8 @@ export class ContextAwareCloudAssembly implements ICloudAssemblySource { * Produce a Cloud Assembly, i.e. a set of stacks */ public async produce(): Promise { - // We may need to run the cloud executable multiple times in order to satisfy all missing context - // (When the executable runs, it will tell us about context it wants to use + // We may need to run the cloud assembly source multiple times in order to satisfy all missing context + // (When the source producer runs, it will tell us about context it wants to use // but it missing. We'll then look up the context and run the executable again, and // again, until it doesn't complain anymore or we've stopped making progress). let previouslyMissingKeys: Set | undefined; @@ -82,7 +82,9 @@ export class ContextAwareCloudAssembly implements ICloudAssemblySource { previouslyMissingKeys = missingKeys; if (tryLookup) { - await this.ioHost.notify(debug('Some context information is missing. Fetching...')); + await this.ioHost.notify(debug('Some context information is missing. Fetching...', 'CDK_ASSEMBLY_I0241', { + missingKeys: Array.from(missingKeys), + })); await contextproviders.provideContextValues( assembly.manifest.missing, this.context, @@ -90,6 +92,10 @@ export class ContextAwareCloudAssembly implements ICloudAssemblySource { ); // Cache the new context to disk + await this.ioHost.notify(debug(`Writing updated context to ${this.contextFile}...`, 'CDK_ASSEMBLY_I0042', { + contextFile: this.contextFile, + context: this.context.all, + })); await this.context.save(this.contextFile); // Execute again 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 index ba1715a105042..d7f5374edda66 100644 --- 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 @@ -169,6 +169,7 @@ export async function assemblyFromDirectory(assemblyDir: string, ioHost: ActionA throw err; } } + function synthOptsDefaults(synthOpts: AppSynthOptions = {}): Settings { return new Settings({ debug: false, @@ -179,4 +180,3 @@ function synthOptsDefaults(synthOpts: AppSynthOptions = {}): Settings { ...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 index 9fe3966d4c5a6..bcd2b4f807091 100644 --- 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 @@ -4,7 +4,7 @@ import { ContextAwareCloudAssembly, ContextAwareCloudAssemblyProps } from './con import { execInChildProcess } from './exec'; import { assemblyFromDirectory, changeDir, determineOutputDirectory, guessExecutable, prepareDefaultEnvironment, withContext, withEnv } from './prepare-source'; import { ToolkitServices } from '../../../toolkit/private'; -import { Context, ILock, RWLock } from '../../aws-cdk'; +import { Context, ILock, RWLock, Settings } from '../../aws-cdk'; import { ToolkitError } from '../../errors'; import { debug } from '../../io/private'; import { AssemblyBuilder, CdkAppSourceProps } from '../source-builder'; @@ -28,7 +28,7 @@ export abstract class CloudAssemblySourceBuilder { props: CdkAppSourceProps = {}, ): Promise { const services = await this.toolkitServices(); - const context = new Context(); // @todo check if this needs to read anything + const context = new Context({ bag: new Settings(props.context ?? {}) }); const contextAssemblyProps: ContextAwareCloudAssemblyProps = { services, context, @@ -85,7 +85,8 @@ export abstract class CloudAssemblySourceBuilder { */ public async fromCdkApp(app: string, props: CdkAppSourceProps = {}): Promise { const services: ToolkitServices = await this.toolkitServices(); - const context = new Context(); // @todo this definitely needs to read files + // @todo this definitely needs to read files from the CWD + const context = new Context({ bag: new Settings(props.context ?? {}) }); const contextAssemblyProps: ContextAwareCloudAssemblyProps = { services, context, diff --git a/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/source-builder.ts b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/source-builder.ts index 24f091846da81..9f87860ad30d8 100644 --- a/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/source-builder.ts +++ b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/source-builder.ts @@ -19,6 +19,8 @@ export type AssemblyBuilder = (props: AppProps) => Promise; */ export interface CdkAppSourceProps { /** + * Execute the application in this working directory. + * * @default - current working directory */ readonly workingDirectory?: string; @@ -39,6 +41,17 @@ export interface CdkAppSourceProps { */ readonly lookups?: boolean; + /** + * Context values for the application. + * + * Context can be read in the app from any construct using `node.getContext(key)`. + * + * @default - no context + */ + readonly context?: { + [key: string]: any; + }; + /** * Options that are passed through the context to a CDK app on synth */ diff --git a/packages/@aws-cdk/toolkit/test/_fixtures/external-context/app.js b/packages/@aws-cdk/toolkit/test/_fixtures/external-context/app.js new file mode 100644 index 0000000000000..fdc4cb2a166f5 --- /dev/null +++ b/packages/@aws-cdk/toolkit/test/_fixtures/external-context/app.js @@ -0,0 +1,9 @@ +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as core from 'aws-cdk-lib/core'; + +const app = new core.App(); +const stack = new core.Stack(app, 'Stack1'); +new s3.Bucket(stack, 'MyBucket', { + bucketName: app.node.tryGetContext('externally-provided-bucket-name') +}); +app.synth(); diff --git a/packages/@aws-cdk/toolkit/test/_fixtures/external-context/index.ts b/packages/@aws-cdk/toolkit/test/_fixtures/external-context/index.ts new file mode 100644 index 0000000000000..5f505989fcde6 --- /dev/null +++ b/packages/@aws-cdk/toolkit/test/_fixtures/external-context/index.ts @@ -0,0 +1,11 @@ +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as core from 'aws-cdk-lib/core'; + +export default async () => { + const app = new core.App(); + const stack = new core.Stack(app, 'Stack1'); + new s3.Bucket(stack, 'MyBucket', { + bucketName: app.node.tryGetContext('externally-provided-bucket-name'), + }); + return app.synth() as any; +}; diff --git a/packages/@aws-cdk/toolkit/test/_helpers/index.ts b/packages/@aws-cdk/toolkit/test/_helpers/index.ts index c619ee3c7919f..e854951701c69 100644 --- a/packages/@aws-cdk/toolkit/test/_helpers/index.ts +++ b/packages/@aws-cdk/toolkit/test/_helpers/index.ts @@ -9,7 +9,7 @@ function fixturePath(...parts: string[]): string { return path.normalize(path.join(__dirname, '..', '_fixtures', ...parts)); } -export async function appFixture(toolkit: Toolkit, name: string) { +export async function appFixture(toolkit: Toolkit, name: string, context?: { [key: string]: any }) { const appPath = fixturePath(name, 'app.js'); if (!fs.existsSync(appPath)) { throw new Error(`App Fixture ${name} does not exist in ${appPath}`); @@ -17,14 +17,16 @@ export async function appFixture(toolkit: Toolkit, name: string) { const app = `cat ${appPath} | node --input-type=module`; return toolkit.fromCdkApp(app, { outdir: determineOutputDirectory(), + context, }); } -export function builderFixture(toolkit: Toolkit, name: string) { +export function builderFixture(toolkit: Toolkit, name: string, context?: { [key: string]: any }) { // eslint-disable-next-line @typescript-eslint/no-require-imports const builder = require(path.join(__dirname, '..', '_fixtures', name)).default; return toolkit.fromAssemblyBuilder(builder, { outdir: determineOutputDirectory(), + context, }); } diff --git a/packages/@aws-cdk/toolkit/test/api/cloud-assembly/source-builder.test.ts b/packages/@aws-cdk/toolkit/test/api/cloud-assembly/source-builder.test.ts index 9e5ec588963c4..a69c4992591e2 100644 --- a/packages/@aws-cdk/toolkit/test/api/cloud-assembly/source-builder.test.ts +++ b/packages/@aws-cdk/toolkit/test/api/cloud-assembly/source-builder.test.ts @@ -12,32 +12,53 @@ describe('fromAssemblyBuilder', () => { test('defaults', async () => { // WHEN const cx = await builderFixture(toolkit, 'two-empty-stacks'); - const cached = await toolkit.synth(cx); - const assembly = await cached.produce(); + const assembly = await cx.produce(); // THEN expect(assembly.stacksRecursively.map(s => s.hierarchicalId)).toEqual(['Stack1', 'Stack2']); }); + + test('can provide context', async () => { + // WHEN + const cx = await builderFixture(toolkit, 'external-context', { + 'externally-provided-bucket-name': 'amzn-s3-demo-bucket', + }); + const assembly = await cx.produce(); + const stack = assembly.getStackByName('Stack1').template; + + // THEN + expect(JSON.stringify(stack)).toContain('amzn-s3-demo-bucket'); + }); }); -describe('fromAssemblyDirectory', () => { +describe('fromCdkApp', () => { test('defaults', async () => { // WHEN - const cx = await cdkOutFixture(toolkit, 'two-empty-stacks'); - const cached = await toolkit.synth(cx); - const assembly = await cached.produce(); + const cx = await appFixture(toolkit, 'two-empty-stacks'); + const assembly = await cx.produce(); // THEN expect(assembly.stacksRecursively.map(s => s.hierarchicalId)).toEqual(['Stack1', 'Stack2']); }); + + test('can provide context', async () => { + // WHEN + const cx = await appFixture(toolkit, 'external-context', { + 'externally-provided-bucket-name': 'amzn-s3-demo-bucket', + }); + const assembly = await cx.produce(); + const stack = assembly.getStackByName('Stack1').template; + + // THEN + expect(JSON.stringify(stack)).toContain('amzn-s3-demo-bucket'); + }); }); -describe('fromCdkApp', () => { +describe('fromAssemblyDirectory', () => { test('defaults', async () => { // WHEN - const cx = await appFixture(toolkit, 'two-empty-stacks'); - const cached = await toolkit.synth(cx); - const assembly = await cached.produce(); + const cx = await cdkOutFixture(toolkit, 'two-empty-stacks'); + const assembly = await cx.produce(); // THEN expect(assembly.stacksRecursively.map(s => s.hierarchicalId)).toEqual(['Stack1', 'Stack2']);