Skip to content

Commit

Permalink
chore(toolkit): pass context to cx builders (#32996)
Browse files Browse the repository at this point in the history
### Issue #32994

Closes #32994

### Reason for this change

Previously it was not possible to provide external context.

### Description of changes

Cloud Assembly Source Builder now optionally take a Context object that is provided to the source when the assembly is produced.

### Describe any new or updated permissions being added

n/a

### Description of how you validated changes

Unit tests

### Checklist
- [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md)

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
mrgrain authored Jan 17, 2025
1 parent d16482f commit ebe9580
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ export class ContextAwareCloudAssembly implements ICloudAssemblySource {
* Produce a Cloud Assembly, i.e. a set of stacks
*/
public async produce(): Promise<cxapi.CloudAssembly> {
// 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<string> | undefined;
Expand All @@ -82,14 +82,20 @@ 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,
this.props.services.sdkProvider,
);

// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export async function assemblyFromDirectory(assemblyDir: string, ioHost: ActionA
throw err;
}
}

function synthOptsDefaults(synthOpts: AppSynthOptions = {}): Settings {
return new Settings({
debug: false,
Expand All @@ -179,4 +180,3 @@ function synthOptsDefaults(synthOpts: AppSynthOptions = {}): Settings {
...synthOpts,
}, true);
}

Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -28,7 +28,7 @@ export abstract class CloudAssemblySourceBuilder {
props: CdkAppSourceProps = {},
): Promise<ICloudAssemblySource> {
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,
Expand Down Expand Up @@ -85,7 +85,8 @@ export abstract class CloudAssemblySourceBuilder {
*/
public async fromCdkApp(app: string, props: CdkAppSourceProps = {}): Promise<ICloudAssemblySource> {
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,
Expand Down
13 changes: 13 additions & 0 deletions packages/@aws-cdk/toolkit/lib/api/cloud-assembly/source-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export type AssemblyBuilder = (props: AppProps) => Promise<cxapi.CloudAssembly>;
*/
export interface CdkAppSourceProps {
/**
* Execute the application in this working directory.
*
* @default - current working directory
*/
readonly workingDirectory?: string;
Expand All @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
11 changes: 11 additions & 0 deletions packages/@aws-cdk/toolkit/test/_fixtures/external-context/index.ts
Original file line number Diff line number Diff line change
@@ -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;
};
6 changes: 4 additions & 2 deletions packages/@aws-cdk/toolkit/test/_helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,24 @@ 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}`);
}
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,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
Expand Down

0 comments on commit ebe9580

Please sign in to comment.