Skip to content

Commit 2c0570a

Browse files
authored
fix(cli): flags commands do not forward context parameters to synth (#1005)
Fixes #918 --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent 7d091fc commit 2c0570a

File tree

8 files changed

+246
-9
lines changed

8 files changed

+246
-9
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const cdk = require('aws-cdk-lib');
2+
3+
const app = new cdk.App();
4+
5+
const contextValue = app.node.tryGetContext('myContextParam');
6+
7+
if (!contextValue) {
8+
throw new Error('Context parameter "myContextParam" is required');
9+
}
10+
11+
const stack = new cdk.Stack(app, 'TestStack', {
12+
description: `Stack created with context value: ${contextValue}`,
13+
});
14+
15+
// Add a simple resource
16+
new cdk.CfnOutput(stack, 'ContextValue', {
17+
value: contextValue,
18+
description: 'The context value passed via CLI',
19+
});
20+
21+
app.synth();
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"app": "node app.js",
3+
"context": {
4+
"@aws-cdk/core:newStyleStackSynthesis": true
5+
}
6+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { integTest, withAws, withSpecificCdkApp } from '../../../lib';
2+
3+
jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime
4+
5+
integTest(
6+
'flags command works with CLI context parameters',
7+
withAws(
8+
withSpecificCdkApp('context-app', async (fixture) => {
9+
await fixture.cdk(['bootstrap', '-c', 'myContextParam=testValue']);
10+
11+
const output = await fixture.cdk([
12+
'flags',
13+
'--unstable=flags',
14+
'--set',
15+
'--recommended',
16+
'--all',
17+
'-c', 'myContextParam=testValue',
18+
'--yes',
19+
]);
20+
21+
expect(output).toContain('Flag changes:');
22+
}),
23+
true,
24+
),
25+
);

packages/aws-cdk/lib/cli/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
498498
unstableFeatures: configuration.settings.get(['unstable']),
499499
});
500500
const flagsData = await toolkit.flags(cloudExecutable);
501-
const handler = new FlagCommandHandler(flagsData, ioHelper, args, toolkit);
501+
const handler = new FlagCommandHandler(flagsData, ioHelper, args, toolkit, configuration.context.all);
502502
return handler.processFlagsCommand();
503503

504504
case 'synthesize':

packages/aws-cdk/lib/commands/flags/flags.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,19 @@ export class FlagCommandHandler {
1414
private readonly ioHelper: IoHelper;
1515

1616
/** Main component that sets up all flag operation components */
17-
constructor(flagData: FeatureFlag[], ioHelper: IoHelper, options: FlagOperationsParams, toolkit: Toolkit) {
17+
constructor(
18+
flagData: FeatureFlag[],
19+
ioHelper: IoHelper,
20+
options: FlagOperationsParams,
21+
toolkit: Toolkit,
22+
cliContextValues: Record<string, any> = {},
23+
) {
1824
this.flags = flagData.filter(flag => !OBSOLETE_FLAGS.includes(flag.name));
1925
this.options = { ...options, concurrency: options.concurrency ?? 4 };
2026
this.ioHelper = ioHelper;
2127

2228
const validator = new FlagValidator(ioHelper);
23-
const flagOperations = new FlagOperations(this.flags, toolkit, ioHelper);
29+
const flagOperations = new FlagOperations(this.flags, toolkit, ioHelper, cliContextValues);
2430
const interactiveHandler = new InteractiveHandler(this.flags, flagOperations);
2531

2632
this.router = new FlagOperationRouter(validator, interactiveHandler, flagOperations);

packages/aws-cdk/lib/commands/flags/operations.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export class FlagOperations {
4848
private readonly flags: FeatureFlag[],
4949
private readonly toolkit: Toolkit,
5050
private readonly ioHelper: IoHelper,
51+
private readonly cliContextValues: Record<string, any> = {},
5152
) {
5253
this.app = '';
5354
this.baseContextValues = {};
@@ -159,11 +160,12 @@ export class FlagOperations {
159160
/** Initializes the safety check by reading context and synthesizing baseline templates */
160161
private async initializeSafetyCheck(): Promise<void> {
161162
const baseContext = new CdkAppMultiContext(process.cwd());
162-
this.baseContextValues = await baseContext.read();
163+
this.baseContextValues = { ...await baseContext.read(), ...this.cliContextValues };
163164

164165
this.baselineTempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-baseline-'));
166+
const mergedContext = new MemoryContext(this.baseContextValues);
165167
const baseSource = await this.toolkit.fromCdkApp(this.app, {
166-
contextStore: baseContext,
168+
contextStore: mergedContext,
167169
outdir: this.baselineTempDir,
168170
});
169171

@@ -270,14 +272,14 @@ export class FlagOperations {
270272
/** Prototypes flag changes by synthesizing templates and showing diffs to the user */
271273
private async prototypeChanges(flagNames: string[], params: FlagOperationsParams): Promise<boolean> {
272274
const baseContext = new CdkAppMultiContext(process.cwd());
273-
const baseContextValues = await baseContext.read();
275+
const baseContextValues = { ...await baseContext.read(), ...this.cliContextValues };
274276
const memoryContext = new MemoryContext(baseContextValues);
275277

276278
const cdkJson = await JSON.parse(await fs.readFile(path.join(process.cwd(), 'cdk.json'), 'utf-8'));
277279
const app = cdkJson.app;
278280

279281
const source = await this.toolkit.fromCdkApp(app, {
280-
contextStore: baseContext,
282+
contextStore: memoryContext,
281283
outdir: fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-original-')),
282284
});
283285

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

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Toolkit } from '@aws-cdk/toolkit-lib';
12
import { Notices } from '../../lib/api/notices';
23
import * as cdkToolkitModule from '../../lib/cli/cdk-toolkit';
34
import { exec } from '../../lib/cli/cli';
@@ -65,6 +66,8 @@ jest.mock('../../lib/cli/parse-command-line-arguments', () => ({
6566
_: ['deploy'],
6667
parameters: [],
6768
};
69+
} else if (args.includes('flags')) {
70+
result = { ...result, _: ['flags'] };
6871
}
6972

7073
// Handle notices flags
@@ -93,6 +96,21 @@ jest.mock('../../lib/cli/parse-command-line-arguments', () => ({
9396
}),
9497
}));
9598

99+
// Mock FlagCommandHandler to capture constructor calls
100+
const mockFlagCommandHandlerConstructor = jest.fn();
101+
const mockProcessFlagsCommand = jest.fn().mockResolvedValue(undefined);
102+
103+
jest.mock('../../lib/commands/flags/flags', () => {
104+
return {
105+
FlagCommandHandler: jest.fn().mockImplementation((...args) => {
106+
mockFlagCommandHandlerConstructor(...args);
107+
return {
108+
processFlagsCommand: mockProcessFlagsCommand,
109+
};
110+
}),
111+
};
112+
});
113+
96114
describe('exec verbose flag tests', () => {
97115
beforeEach(() => {
98116
jest.clearAllMocks();
@@ -513,3 +531,61 @@ describe('--yes', () => {
513531
execSpy.mockRestore();
514532
});
515533
});
534+
535+
describe('flags command tests', () => {
536+
let mockConfig: any;
537+
let flagsSpy: jest.SpyInstance;
538+
539+
beforeEach(() => {
540+
jest.clearAllMocks();
541+
mockFlagCommandHandlerConstructor.mockClear();
542+
mockProcessFlagsCommand.mockClear();
543+
544+
flagsSpy = jest.spyOn(Toolkit.prototype, 'flags').mockResolvedValue([]);
545+
546+
mockConfig = {
547+
loadConfigFiles: jest.fn().mockResolvedValue(undefined),
548+
settings: {
549+
get: jest.fn().mockImplementation((key: string[]) => {
550+
if (key[0] === 'unstable') return ['flags'];
551+
return undefined;
552+
}),
553+
},
554+
context: {
555+
all: {
556+
myContextParam: 'testValue',
557+
},
558+
get: jest.fn().mockReturnValue([]),
559+
},
560+
};
561+
562+
Configuration.fromArgsAndFiles = jest.fn().mockResolvedValue(mockConfig);
563+
});
564+
565+
afterEach(() => {
566+
flagsSpy.mockRestore();
567+
});
568+
569+
test('passes CLI context to FlagCommandHandler', async () => {
570+
// WHEN
571+
await exec([
572+
'flags',
573+
'--unstable=flags',
574+
'--set',
575+
'--recommended',
576+
'--all',
577+
'-c', 'myContextParam=testValue',
578+
'--yes',
579+
]);
580+
581+
// THEN
582+
expect(mockFlagCommandHandlerConstructor).toHaveBeenCalledWith(
583+
expect.anything(), // flagsData
584+
expect.anything(), // ioHelper
585+
expect.anything(), // args
586+
expect.anything(), // toolkit
587+
mockConfig.context.all, // cliContextValues
588+
);
589+
expect(mockProcessFlagsCommand).toHaveBeenCalled();
590+
});
591+
});

packages/aws-cdk/test/commands/flag-operations.test.ts

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,6 +1197,101 @@ describe('interactive prompts lead to the correct function calls', () => {
11971197
});
11981198
});
11991199

1200+
describe('CLI context parameters', () => {
1201+
beforeEach(() => {
1202+
setupMockToolkitForPrototyping(mockToolkit);
1203+
jest.clearAllMocks();
1204+
});
1205+
1206+
test('CLI context values are merged with file context during prototyping', async () => {
1207+
const cdkJsonPath = await createCdkJsonFile({
1208+
'@aws-cdk/core:existingFlag': true,
1209+
});
1210+
1211+
setupMockToolkitForPrototyping(mockToolkit);
1212+
1213+
const requestResponseSpy = jest.spyOn(ioHelper, 'requestResponse');
1214+
requestResponseSpy.mockResolvedValue(false);
1215+
1216+
const cliContextValues = {
1217+
foo: 'bar',
1218+
myContextParam: 'myValue',
1219+
};
1220+
1221+
const options: FlagsOptions = {
1222+
FLAGNAME: ['@aws-cdk/core:testFlag'],
1223+
set: true,
1224+
value: 'true',
1225+
};
1226+
1227+
const flagOperations = new FlagCommandHandler(mockFlagsData, ioHelper, options, mockToolkit, cliContextValues);
1228+
await flagOperations.processFlagsCommand();
1229+
1230+
// Get the first call's context store and verify it contains merged context
1231+
// fromCdkApp(app, { contextStore: ..., outdir: ... }) was called
1232+
const firstCallArgs = mockToolkit.fromCdkApp.mock.calls[0]; // Get first call arguments
1233+
const contextStore = firstCallArgs[1]?.contextStore; // Extract contextStore from second argument (options object)
1234+
expect(contextStore).toBeDefined();
1235+
1236+
// contextStore is defined as we've verified above
1237+
const contextData = await contextStore!.read();
1238+
1239+
expect(contextData).toEqual({
1240+
'@aws-cdk/core:existingFlag': true,
1241+
'@aws-cdk/core:testFlag': true,
1242+
'foo': 'bar',
1243+
'myContextParam': 'myValue',
1244+
});
1245+
1246+
await cleanupCdkJsonFile(cdkJsonPath);
1247+
requestResponseSpy.mockRestore();
1248+
});
1249+
1250+
test('CLI context values are passed to synthesis during safe flag checking', async () => {
1251+
const cdkJsonPath = await createCdkJsonFile({
1252+
'@aws-cdk/core:existingFlag': true,
1253+
});
1254+
1255+
mockToolkit.diff.mockResolvedValue({
1256+
TestStack: { differenceCount: 0 } as any,
1257+
});
1258+
1259+
const requestResponseSpy = jest.spyOn(ioHelper, 'requestResponse');
1260+
requestResponseSpy.mockResolvedValue(false);
1261+
1262+
const cliContextValues = {
1263+
foo: 'bar',
1264+
myContextParam: 'myValue',
1265+
};
1266+
1267+
const options: FlagsOptions = {
1268+
safe: true,
1269+
concurrency: 4,
1270+
};
1271+
1272+
const flagOperations = new FlagCommandHandler(mockFlagsData, ioHelper, options, mockToolkit, cliContextValues);
1273+
await flagOperations.processFlagsCommand();
1274+
1275+
// Get the first call's context store and verify it contains merged context
1276+
// fromCdkApp(app, { contextStore: ..., outdir: ... }) was called
1277+
const firstCallArgs = mockToolkit.fromCdkApp.mock.calls[0]; // Get first call arguments
1278+
const contextStore = firstCallArgs[1]?.contextStore; // Extract contextStore from second argument (options object)
1279+
expect(contextStore).toBeDefined();
1280+
1281+
// contextStore is defined as we've verified above
1282+
const contextData = await contextStore!.read();
1283+
1284+
expect(contextData).toEqual({
1285+
'@aws-cdk/core:existingFlag': true,
1286+
'foo': 'bar',
1287+
'myContextParam': 'myValue',
1288+
});
1289+
1290+
await cleanupCdkJsonFile(cdkJsonPath);
1291+
requestResponseSpy.mockRestore();
1292+
});
1293+
});
1294+
12001295
describe('setSafeFlags', () => {
12011296
beforeEach(() => {
12021297
setupMockToolkitForPrototyping(mockToolkit);
@@ -1390,7 +1485,13 @@ async function displayFlags(params: FlagOperationsParams): Promise<void> {
13901485
await f.displayFlags(params);
13911486
}
13921487

1393-
async function handleFlags(flagData: FeatureFlag[], _ioHelper: IoHelper, options: FlagsOptions, toolkit: Toolkit) {
1394-
const f = new FlagCommandHandler(flagData, _ioHelper, options, toolkit);
1488+
async function handleFlags(
1489+
flagData: FeatureFlag[],
1490+
_ioHelper: IoHelper,
1491+
options: FlagsOptions,
1492+
toolkit: Toolkit,
1493+
cliContextValues: Record<string, any> = {},
1494+
) {
1495+
const f = new FlagCommandHandler(flagData, _ioHelper, options, toolkit, cliContextValues);
13951496
await f.processFlagsCommand();
13961497
}

0 commit comments

Comments
 (0)