Skip to content

Commit 46bf6fd

Browse files
committed
feat(cli): add --no-lookups flag to disable context lookups
Context lookups are supposed to be performed on developer desktops, and committed to `cdk.context.json`. If you don't, your CI build might try to perform a lookup and fail with an unclear error message about permissions, or worse: appear to work properly but leave you with a nondeterministic build. Introduce a CLI flag called `--no-lookups` that throws an appropriately descriptive error message if you forgot to perform context lookups before committing. This now also makes it possible to write an integration test for PR #11461.
1 parent 4768c44 commit 46bf6fd

File tree

8 files changed

+135
-39
lines changed

8 files changed

+135
-39
lines changed

packages/aws-cdk/CONTRIBUTING.md

+3
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ than one test will run at a time in that region.
5858
If `AWS_REGIONS` is not set, all tests will sequentially run in the one
5959
region set in `AWS_REGION`.
6060

61+
Run with `env INTEG_NO_CLEAN=1` to forego cleaning up the temporary directory,
62+
in order to be able to debug 'cdk synth' output.
63+
6164
### CLI integration tests
6265

6366
CLI tests will exercise a number of common CLI scenarios, and deploy actual

packages/aws-cdk/bin/cdk.ts

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ async function parseCommandLineArguments() {
4646
.option('plugin', { type: 'array', alias: 'p', desc: 'Name or path of a node package that extend the CDK features. Can be specified multiple times', nargs: 1 })
4747
.option('trace', { type: 'boolean', desc: 'Print trace for stack warnings' })
4848
.option('strict', { type: 'boolean', desc: 'Do not construct stacks with warnings' })
49+
.option('lookups', { type: 'boolean', desc: 'Perform context lookups (synthesis fails if this is disabled and context lookups need to be performed)', default: true })
4950
.option('ignore-errors', { type: 'boolean', default: false, desc: 'Ignores synthesis errors, which will likely produce an invalid output' })
5051
.option('json', { type: 'boolean', alias: 'j', desc: 'Use JSON output instead of YAML when templates are printed to STDOUT', default: false })
5152
.option('verbose', { type: 'boolean', alias: 'v', desc: 'Show debug logs (specify multiple times to increase verbosity)', default: false })

packages/aws-cdk/lib/api/cxapp/cloud-executable.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,16 @@ export class CloudExecutable {
6666
while (true) {
6767
const assembly = await this.props.synthesizer(this.props.sdkProvider, this.props.configuration);
6868

69-
if (assembly.manifest.missing) {
69+
if (assembly.manifest.missing && assembly.manifest.missing.length > 0) {
7070
const missingKeys = missingContextKeys(assembly.manifest.missing);
7171

72+
if (!this.canLookup) {
73+
throw new Error(
74+
'Context lookups have been disabled. '
75+
+ 'Make sure all necessary context is already in \'cdk.context.json\' by running \'cdk synth\' on a machine with sufficient AWS credentials and committing the result. '
76+
+ `Missing context keys: '${Array.from(missingKeys)}'`);
77+
}
78+
7279
let tryLookup = true;
7380
if (previouslyMissingKeys && setsEqual(missingKeys, previouslyMissingKeys)) {
7481
debug('Not making progress trying to resolve environmental context. Giving up.');
@@ -162,6 +169,10 @@ export class CloudExecutable {
162169
await fs.writeFile(stack.templateFullPath, JSON.stringify(stack.template, undefined, 2), { encoding: 'utf-8' });
163170
}
164171
}
172+
173+
private get canLookup() {
174+
return !!(this.props.configuration.settings.get(['lookups']) ?? true);
175+
}
165176
}
166177

167178
/**

packages/aws-cdk/lib/settings.ts

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export type Arguments = {
4343
readonly _: [Command, ...string[]];
4444
readonly exclusively?: boolean;
4545
readonly STACKS?: string[];
46+
readonly lookups?: boolean;
4647
readonly [name: string]: unknown;
4748
};
4849

@@ -245,6 +246,7 @@ export class Settings {
245246
output: argv.output,
246247
progress: argv.progress,
247248
bundlingStacks,
249+
lookups: argv.lookups,
248250
});
249251
}
250252

packages/aws-cdk/test/api/cloud-executable.test.ts

+19
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,25 @@ test('stop executing if context providers are not making progress', async () =>
7373
// THEN: the test finishes normally});
7474
});
7575

76+
test('fails if lookups are disabled and missing context is synthesized', async () => {
77+
// GIVEN
78+
const cloudExecutable = new MockCloudExecutable({
79+
stacks: [{
80+
stackName: 'thestack',
81+
template: { resource: 'noerrorresource' },
82+
}],
83+
// Always return the same missing keys, synthesis should still finish.
84+
missing: [
85+
{ key: 'abcdef', props: { account: '1324', region: 'us-east-1' }, provider: cxschema.ContextProvider.AVAILABILITY_ZONE_PROVIDER },
86+
],
87+
});
88+
cloudExecutable.configuration.settings.set(['lookups'], false);
89+
90+
// WHEN
91+
await expect(cloudExecutable.synthesize()).rejects.toThrow(/Context lookups have been disabled/);
92+
});
93+
94+
7695
async function testCloudExecutable({ env, versionReporting = true }: { env?: string, versionReporting?: boolean } = {}) {
7796
const cloudExec = new MockCloudExecutable({
7897
stacks: [{

packages/aws-cdk/test/integ/cli/app/app.js

+76-37
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ class YourStack extends cdk.Stack {
4444
}
4545
}
4646

47+
class StackUsingContext extends cdk.Stack {
48+
constructor(parent, id, props) {
49+
super(parent, id, props);
50+
new core.CfnResource(this, 'Handle', {
51+
type: 'AWS::CloudFormation::WaitConditionHandle'
52+
});
53+
54+
new core.CfnOutput(this, 'Output', {
55+
value: this.availabilityZones,
56+
});
57+
}
58+
}
59+
4760
class ParameterStack extends cdk.Stack {
4861
constructor(parent, id, props) {
4962
super(parent, id, props);
@@ -247,53 +260,79 @@ class SomeStage extends cdk.Stage {
247260
}
248261
}
249262

263+
class StageUsingContext extends cdk.Stage {
264+
constructor(parent, id, props) {
265+
super(parent, id, props);
266+
267+
new StackUsingContext(this, 'StackInStage');
268+
}
269+
}
270+
250271
const app = new cdk.App();
251272

252273
const defaultEnv = {
253274
account: process.env.CDK_DEFAULT_ACCOUNT,
254275
region: process.env.CDK_DEFAULT_REGION
255276
};
256277

257-
// Deploy all does a wildcard ${stackPrefix}-test-*
258-
new MyStack(app, `${stackPrefix}-test-1`, { env: defaultEnv });
259-
new YourStack(app, `${stackPrefix}-test-2`);
260-
// Deploy wildcard with parameters does ${stackPrefix}-param-test-*
261-
new ParameterStack(app, `${stackPrefix}-param-test-1`);
262-
new OtherParameterStack(app, `${stackPrefix}-param-test-2`);
263-
// Deploy stack with multiple parameters
264-
new MultiParameterStack(app, `${stackPrefix}-param-test-3`);
265-
// Deploy stack with outputs does ${stackPrefix}-outputs-test-*
266-
new OutputsStack(app, `${stackPrefix}-outputs-test-1`);
267-
new AnotherOutputsStack(app, `${stackPrefix}-outputs-test-2`);
268-
// Not included in wildcard
269-
new IamStack(app, `${stackPrefix}-iam-test`, { env: defaultEnv });
270-
const providing = new ProvidingStack(app, `${stackPrefix}-order-providing`);
271-
new ConsumingStack(app, `${stackPrefix}-order-consuming`, { providingStack: providing });
272-
273-
new MissingSSMParameterStack(app, `${stackPrefix}-missing-ssm-parameter`, { env: defaultEnv });
274-
275-
new LambdaStack(app, `${stackPrefix}-lambda`);
276-
new DockerStack(app, `${stackPrefix}-docker`);
277-
new DockerStackWithCustomFile(app, `${stackPrefix}-docker-with-custom-file`);
278-
new FailedStack(app, `${stackPrefix}-failed`)
279-
280-
if (process.env.ENABLE_VPC_TESTING) { // Gating so we don't do context fetching unless that's what we are here for
281-
const env = { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION };
282-
if (process.env.ENABLE_VPC_TESTING === 'DEFINE')
283-
new DefineVpcStack(app, `${stackPrefix}-define-vpc`, { env });
284-
if (process.env.ENABLE_VPC_TESTING === 'IMPORT')
285-
new ImportVpcStack(app, `${stackPrefix}-import-vpc`, { env });
286-
}
278+
// Sometimes we don't want to synthesize all stacks because it will impact the results
279+
const stackSet = process.env.INTEG_STACK_SET || 'default';
280+
281+
switch (stackSet) {
282+
case 'default':
283+
// Deploy all does a wildcard ${stackPrefix}-test-*
284+
new MyStack(app, `${stackPrefix}-test-1`, { env: defaultEnv });
285+
new YourStack(app, `${stackPrefix}-test-2`);
286+
// Deploy wildcard with parameters does ${stackPrefix}-param-test-*
287+
new ParameterStack(app, `${stackPrefix}-param-test-1`);
288+
new OtherParameterStack(app, `${stackPrefix}-param-test-2`);
289+
// Deploy stack with multiple parameters
290+
new MultiParameterStack(app, `${stackPrefix}-param-test-3`);
291+
// Deploy stack with outputs does ${stackPrefix}-outputs-test-*
292+
new OutputsStack(app, `${stackPrefix}-outputs-test-1`);
293+
new AnotherOutputsStack(app, `${stackPrefix}-outputs-test-2`);
294+
// Not included in wildcard
295+
new IamStack(app, `${stackPrefix}-iam-test`, { env: defaultEnv });
296+
const providing = new ProvidingStack(app, `${stackPrefix}-order-providing`);
297+
new ConsumingStack(app, `${stackPrefix}-order-consuming`, { providingStack: providing });
298+
299+
new MissingSSMParameterStack(app, `${stackPrefix}-missing-ssm-parameter`, { env: defaultEnv });
300+
301+
new LambdaStack(app, `${stackPrefix}-lambda`);
302+
new DockerStack(app, `${stackPrefix}-docker`);
303+
new DockerStackWithCustomFile(app, `${stackPrefix}-docker-with-custom-file`);
304+
new FailedStack(app, `${stackPrefix}-failed`)
305+
306+
if (process.env.ENABLE_VPC_TESTING) { // Gating so we don't do context fetching unless that's what we are here for
307+
const env = { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION };
308+
if (process.env.ENABLE_VPC_TESTING === 'DEFINE')
309+
new DefineVpcStack(app, `${stackPrefix}-define-vpc`, { env });
310+
if (process.env.ENABLE_VPC_TESTING === 'IMPORT')
311+
new ImportVpcStack(app, `${stackPrefix}-import-vpc`, { env });
312+
}
287313

288-
new ConditionalResourceStack(app, `${stackPrefix}-conditional-resource`)
314+
new ConditionalResourceStack(app, `${stackPrefix}-conditional-resource`)
289315

290-
new StackWithNestedStack(app, `${stackPrefix}-with-nested-stack`);
291-
new StackWithNestedStackUsingParameters(app, `${stackPrefix}-with-nested-stack-using-parameters`);
316+
new StackWithNestedStack(app, `${stackPrefix}-with-nested-stack`);
317+
new StackWithNestedStackUsingParameters(app, `${stackPrefix}-with-nested-stack-using-parameters`);
292318

293-
new YourStack(app, `${stackPrefix}-termination-protection`, {
294-
terminationProtection: process.env.TERMINATION_PROTECTION !== 'FALSE' ? true : false,
295-
});
319+
new YourStack(app, `${stackPrefix}-termination-protection`, {
320+
terminationProtection: process.env.TERMINATION_PROTECTION !== 'FALSE' ? true : false,
321+
});
322+
323+
new SomeStage(app, `${stackPrefix}-stage`);
324+
break;
296325

297-
new SomeStage(app, `${stackPrefix}-stage`);
326+
case 'stage-using-context':
327+
// Needs a dummy stack at the top level because the CLI will fail otherwise
328+
new YourStack(app, `${stackPrefix}-toplevel`, { env: defaultEnv });
329+
new StageUsingContext(app, `${stackPrefix}-stage-using-context`, {
330+
env: defaultEnv,
331+
});
332+
break;
333+
334+
default:
335+
throw new Error(`Unrecognized INTEG_STACK_SET: '${stackSet}'`);
336+
}
298337

299338
app.synth();

packages/aws-cdk/test/integ/cli/cdk-helpers.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,11 @@ export function withCdkApp<A extends TestContext & AwsContext>(block: (context:
8383
success = false;
8484
throw e;
8585
} finally {
86-
await fixture.dispose(success);
86+
if (process.env.INTEG_NO_CLEAN) {
87+
process.stderr.write(`Left test directory in '${integTestDir}' ($INTEG_NO_CLEAN)\n`);
88+
} else {
89+
await fixture.dispose(success);
90+
}
8791
}
8892
};
8993
}
@@ -177,6 +181,13 @@ export class TestFixture {
177181
...this.fullStackName(stackNames)], options);
178182
}
179183

184+
public async cdkSynth(options: CdkCliOptions = {}) {
185+
return this.cdk([
186+
'synth',
187+
...(options.options ?? [])
188+
], options);
189+
}
190+
180191
public async cdkDestroy(stackNames: string | string[], options: CdkCliOptions = {}) {
181192
stackNames = typeof stackNames === 'string' ? [stackNames] : stackNames;
182193

packages/aws-cdk/test/integ/cli/cli.integtest.ts

+10
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,16 @@ integTest('context setting', withDefaultFixture(async (fixture) => {
9393
}
9494
}));
9595

96+
integTest('context in stage propagates to top', withDefaultFixture(async (fixture) => {
97+
await fixture.cdkSynth({
98+
// This will make it error to prove that the context bubbles up, and also that we can fail on command
99+
options: ['--no-lookups'],
100+
modEnv: {
101+
INTEG_STACK_SET: 'stage-using-context',
102+
},
103+
});
104+
}));
105+
96106
integTest('deploy', withDefaultFixture(async (fixture) => {
97107
const stackArn = await fixture.cdkDeploy('test-2', { captureStderr: false });
98108

0 commit comments

Comments
 (0)