Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const cdk = require('aws-cdk-lib');

const app = new cdk.App();

const contextValue = app.node.tryGetContext('myContextParam');

if (!contextValue) {
throw new Error('Context parameter "myContextParam" is required');
}

const stack = new cdk.Stack(app, 'TestStack', {
description: `Stack created with context value: ${contextValue}`,
});

// Add a simple resource
new cdk.CfnOutput(stack, 'ContextValue', {
value: contextValue,
description: 'The context value passed via CLI',
});

app.synth();
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"app": "node app.js",
"context": {
"@aws-cdk/core:newStyleStackSynthesis": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { integTest, withAws, withSpecificCdkApp } from '../../../lib';

jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime

integTest(
'flags command works with CLI context parameters',
withAws(
withSpecificCdkApp('context-app', async (fixture) => {
await fixture.cdk(['bootstrap', '-c', 'myContextParam=testValue']);

const output = await fixture.cdk([
'flags',
'--unstable=flags',
'--set',
'--recommended',
'--all',
'-c', 'myContextParam=testValue',
'--yes',
]);

expect(output).toContain('Flag changes:');
}),
true,
),
);
2 changes: 1 addition & 1 deletion packages/aws-cdk/lib/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
unstableFeatures: configuration.settings.get(['unstable']),
});
const flagsData = await toolkit.flags(cloudExecutable);
const handler = new FlagCommandHandler(flagsData, ioHelper, args, toolkit);
const handler = new FlagCommandHandler(flagsData, ioHelper, args, toolkit, configuration.context.all);
return handler.processFlagsCommand();

case 'synthesize':
Expand Down
10 changes: 8 additions & 2 deletions packages/aws-cdk/lib/commands/flags/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ export class FlagCommandHandler {
private readonly ioHelper: IoHelper;

/** Main component that sets up all flag operation components */
constructor(flagData: FeatureFlag[], ioHelper: IoHelper, options: FlagOperationsParams, toolkit: Toolkit) {
constructor(
flagData: FeatureFlag[],
ioHelper: IoHelper,
options: FlagOperationsParams,
toolkit: Toolkit,
cliContextValues: Record<string, any> = {},
) {
this.flags = flagData.filter(flag => !OBSOLETE_FLAGS.includes(flag.name));
this.options = { ...options, concurrency: options.concurrency ?? 4 };
this.ioHelper = ioHelper;

const validator = new FlagValidator(ioHelper);
const flagOperations = new FlagOperations(this.flags, toolkit, ioHelper);
const flagOperations = new FlagOperations(this.flags, toolkit, ioHelper, cliContextValues);
const interactiveHandler = new InteractiveHandler(this.flags, flagOperations);

this.router = new FlagOperationRouter(validator, interactiveHandler, flagOperations);
Expand Down
10 changes: 6 additions & 4 deletions packages/aws-cdk/lib/commands/flags/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export class FlagOperations {
private readonly flags: FeatureFlag[],
private readonly toolkit: Toolkit,
private readonly ioHelper: IoHelper,
private readonly cliContextValues: Record<string, any> = {},
) {
this.app = '';
this.baseContextValues = {};
Expand Down Expand Up @@ -159,11 +160,12 @@ export class FlagOperations {
/** Initializes the safety check by reading context and synthesizing baseline templates */
private async initializeSafetyCheck(): Promise<void> {
const baseContext = new CdkAppMultiContext(process.cwd());
this.baseContextValues = await baseContext.read();
this.baseContextValues = { ...await baseContext.read(), ...this.cliContextValues };

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

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

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

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

Expand Down
76 changes: 76 additions & 0 deletions packages/aws-cdk/test/cli/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Toolkit } from '@aws-cdk/toolkit-lib';
import { Notices } from '../../lib/api/notices';
import * as cdkToolkitModule from '../../lib/cli/cdk-toolkit';
import { exec } from '../../lib/cli/cli';
Expand Down Expand Up @@ -65,6 +66,8 @@ jest.mock('../../lib/cli/parse-command-line-arguments', () => ({
_: ['deploy'],
parameters: [],
};
} else if (args.includes('flags')) {
result = { ...result, _: ['flags'] };
}

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

// Mock FlagCommandHandler to capture constructor calls
const mockFlagCommandHandlerConstructor = jest.fn();
const mockProcessFlagsCommand = jest.fn().mockResolvedValue(undefined);

jest.mock('../../lib/commands/flags/flags', () => {
return {
FlagCommandHandler: jest.fn().mockImplementation((...args) => {
mockFlagCommandHandlerConstructor(...args);
return {
processFlagsCommand: mockProcessFlagsCommand,
};
}),
};
});

describe('exec verbose flag tests', () => {
beforeEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -513,3 +531,61 @@ describe('--yes', () => {
execSpy.mockRestore();
});
});

describe('flags command tests', () => {
let mockConfig: any;
let flagsSpy: jest.SpyInstance;

beforeEach(() => {
jest.clearAllMocks();
mockFlagCommandHandlerConstructor.mockClear();
mockProcessFlagsCommand.mockClear();

flagsSpy = jest.spyOn(Toolkit.prototype, 'flags').mockResolvedValue([]);

mockConfig = {
loadConfigFiles: jest.fn().mockResolvedValue(undefined),
settings: {
get: jest.fn().mockImplementation((key: string[]) => {
if (key[0] === 'unstable') return ['flags'];
return undefined;
}),
},
context: {
all: {
myContextParam: 'testValue',
},
get: jest.fn().mockReturnValue([]),
},
};

Configuration.fromArgsAndFiles = jest.fn().mockResolvedValue(mockConfig);
});

afterEach(() => {
flagsSpy.mockRestore();
});

test('passes CLI context to FlagCommandHandler', async () => {
// WHEN
await exec([
'flags',
'--unstable=flags',
'--set',
'--recommended',
'--all',
'-c', 'myContextParam=testValue',
'--yes',
]);

// THEN
expect(mockFlagCommandHandlerConstructor).toHaveBeenCalledWith(
expect.anything(), // flagsData
expect.anything(), // ioHelper
expect.anything(), // args
expect.anything(), // toolkit
mockConfig.context.all, // cliContextValues
);
expect(mockProcessFlagsCommand).toHaveBeenCalled();
});
});
105 changes: 103 additions & 2 deletions packages/aws-cdk/test/commands/flag-operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,101 @@ describe('interactive prompts lead to the correct function calls', () => {
});
});

describe('CLI context parameters', () => {
beforeEach(() => {
setupMockToolkitForPrototyping(mockToolkit);
jest.clearAllMocks();
});

test('CLI context values are merged with file context during prototyping', async () => {
const cdkJsonPath = await createCdkJsonFile({
'@aws-cdk/core:existingFlag': true,
});

setupMockToolkitForPrototyping(mockToolkit);

const requestResponseSpy = jest.spyOn(ioHelper, 'requestResponse');
requestResponseSpy.mockResolvedValue(false);

const cliContextValues = {
foo: 'bar',
myContextParam: 'myValue',
};

const options: FlagsOptions = {
FLAGNAME: ['@aws-cdk/core:testFlag'],
set: true,
value: 'true',
};

const flagOperations = new FlagCommandHandler(mockFlagsData, ioHelper, options, mockToolkit, cliContextValues);
await flagOperations.processFlagsCommand();

// Get the first call's context store and verify it contains merged context
// fromCdkApp(app, { contextStore: ..., outdir: ... }) was called
const firstCallArgs = mockToolkit.fromCdkApp.mock.calls[0]; // Get first call arguments
const contextStore = firstCallArgs[1]?.contextStore; // Extract contextStore from second argument (options object)
expect(contextStore).toBeDefined();

// contextStore is defined as we've verified above
const contextData = await contextStore!.read();

expect(contextData).toEqual({
'@aws-cdk/core:existingFlag': true,
'@aws-cdk/core:testFlag': true,
'foo': 'bar',
'myContextParam': 'myValue',
});

await cleanupCdkJsonFile(cdkJsonPath);
requestResponseSpy.mockRestore();
});

test('CLI context values are passed to synthesis during safe flag checking', async () => {
const cdkJsonPath = await createCdkJsonFile({
'@aws-cdk/core:existingFlag': true,
});

mockToolkit.diff.mockResolvedValue({
TestStack: { differenceCount: 0 } as any,
});

const requestResponseSpy = jest.spyOn(ioHelper, 'requestResponse');
requestResponseSpy.mockResolvedValue(false);

const cliContextValues = {
foo: 'bar',
myContextParam: 'myValue',
};

const options: FlagsOptions = {
safe: true,
concurrency: 4,
};

const flagOperations = new FlagCommandHandler(mockFlagsData, ioHelper, options, mockToolkit, cliContextValues);
await flagOperations.processFlagsCommand();

// Get the first call's context store and verify it contains merged context
// fromCdkApp(app, { contextStore: ..., outdir: ... }) was called
const firstCallArgs = mockToolkit.fromCdkApp.mock.calls[0]; // Get first call arguments
const contextStore = firstCallArgs[1]?.contextStore; // Extract contextStore from second argument (options object)
expect(contextStore).toBeDefined();

// contextStore is defined as we've verified above
const contextData = await contextStore!.read();

expect(contextData).toEqual({
'@aws-cdk/core:existingFlag': true,
'foo': 'bar',
'myContextParam': 'myValue',
});

await cleanupCdkJsonFile(cdkJsonPath);
requestResponseSpy.mockRestore();
});
});

describe('setSafeFlags', () => {
beforeEach(() => {
setupMockToolkitForPrototyping(mockToolkit);
Expand Down Expand Up @@ -1390,7 +1485,13 @@ async function displayFlags(params: FlagOperationsParams): Promise<void> {
await f.displayFlags(params);
}

async function handleFlags(flagData: FeatureFlag[], _ioHelper: IoHelper, options: FlagsOptions, toolkit: Toolkit) {
const f = new FlagCommandHandler(flagData, _ioHelper, options, toolkit);
async function handleFlags(
flagData: FeatureFlag[],
_ioHelper: IoHelper,
options: FlagsOptions,
toolkit: Toolkit,
cliContextValues: Record<string, any> = {},
) {
const f = new FlagCommandHandler(flagData, _ioHelper, options, toolkit, cliContextValues);
await f.processFlagsCommand();
}
Loading