Skip to content
Merged
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
7 changes: 7 additions & 0 deletions packages/@aws-cdk/toolkit-lib/lib/actions/diff/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ export interface ChangeSetDiffOptions extends CloudFormationDiffOptions {
* @default false
*/
readonly importExistingResources?: boolean;

/**
* Name of the CloudFormation change set to create when computing the diff
*
* @default 'cdk-diff-change-set'
*/
readonly changeSetName?: string;
}

export interface LocalFileDiffOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ async function cfnDiff(
parameters: methodOptions.parameters ?? {},
failOnError: !(methodOptions.fallbackToTemplate ?? true),
importExistingResources: methodOptions.importExistingResources,
changeSetName: methodOptions.changeSetName,
uuid: randomUUID(),
}))?.changeSet : undefined;

Expand Down
10 changes: 8 additions & 2 deletions packages/@aws-cdk/toolkit-lib/lib/api/deployments/cfn-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ export type PrepareChangeSetOptions = {
resourcesToImport?: ResourcesToImport;
importExistingResources?: boolean;
includeNestedStacks?: boolean;
/**
* Name of the change set to create
*
* @default 'cdk-diff-change-set'
*/
changeSetName?: string;
/**
* Default behavior is to log AWS CloudFormation errors and move on. Set this property to true to instead
* fail on errors received by AWS CloudFormation.
Expand Down Expand Up @@ -82,7 +88,7 @@ export async function createDiffChangeSet(

return await createChangeSetAndCleanup(ioHelper, {
cfn,
changeSetName: 'cdk-diff-change-set',
changeSetName: options.changeSetName ?? 'cdk-diff-change-set',
stack: options.stack,
exists,
uuid: options.uuid,
Expand Down Expand Up @@ -279,7 +285,7 @@ async function createChangeSetAndCleanup(
*/
export async function createValidationChangeSet(
ioHelper: IoHelper,
options: Omit<PrepareChangeSetOptions, 'includeNestedStacks' | 'diagnoser' | 'sdkProvider'>,
options: Omit<PrepareChangeSetOptions, 'includeNestedStacks' | 'diagnoser' | 'sdkProvider' | 'changeSetName'>,
): Promise<ChangeSetReport> {
const { cfn, bodyParameter, exists, stackExistedBefore, executionRoleArn, diagnoser } = await prepareChangeSetEnv(ioHelper, options);
const changeSetName = `cdk-validate-${options.uuid}`;
Expand Down
26 changes: 26 additions & 0 deletions packages/@aws-cdk/toolkit-lib/test/actions/diff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,32 @@ describe('diff', () => {
}));
});

test('ChangeSet diff method uses the provided change set name', async () => {
// GIVEN - stack doesn't exist
jest.spyOn(deployments.Deployments.prototype, 'stackExists').mockResolvedValue(false);
mockCloudFormationClient.on(DescribeStacksCommand).resolves({ Stacks: [] });
mockSSMClient.on(GetParameterCommand).resolves({ Parameter: { Value: '99' } });
mockCloudFormationClient.on(CreateChangeSetCommand).resolves({ Id: 'arn:aws:cloudformation:us-east-1:123456789012:changeSet/my-custom-change-set' });
mockCloudFormationClient.on(DescribeChangeSetCommand).resolves({
Status: 'CREATE_COMPLETE',
Changes: [],
});

// WHEN
const cx = await cdkOutFixture(toolkit, 'stack-with-bucket');
await toolkit.diff(cx, {
stacks: { strategy: StackSelectionStrategy.ALL_STACKS },
method: DiffMethod.ChangeSet({ fallbackToTemplate: false, changeSetName: 'my-custom-change-set' }),
});

// THEN - the changeset was created with the custom name
const createCalls = mockCloudFormationClient.commandCalls(CreateChangeSetCommand);
expect(createCalls).toHaveLength(1);
expect(createCalls[0].args[0].input).toEqual(expect.objectContaining({
ChangeSetName: 'my-custom-change-set',
}));
});

test('ChangeSet diff deletes stack created in REVIEW_IN_PROGRESS for new stacks', async () => {
// GIVEN - stack doesn't exist
jest.spyOn(deployments.Deployments.prototype, 'stackExists').mockResolvedValue(false);
Expand Down
10 changes: 10 additions & 0 deletions packages/aws-cdk/lib/cli/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,7 @@ export class CdkToolkit {
parameters: Object.assign({}, parameterMap['*'], parameterMap[stack.stackName]),
resourcesToImport,
importExistingResources: options.importExistingResources,
changeSetName: options.changeSetName,
failOnError: options.method === 'change-set',
});
}
Expand Down Expand Up @@ -1645,6 +1646,15 @@ export interface DiffOptions {
*/
readonly method?: 'auto' | 'change-set' | 'template';

/**
* Name of the CloudFormation change set to create when computing the diff
*
* Only used if the method is not 'template'.
*
* @default 'cdk-diff-change-set'
*/
readonly changeSetName?: string;

/**
* Whether or not the change set imports resources that already exist.
*
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/lib/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ export async function makeConfig(): Promise<CliConfig> {
'processed': { type: 'boolean', desc: 'Whether to compare against the template with Transforms already processed', default: false },
'quiet': { type: 'boolean', alias: 'q', desc: 'Do not print stack name and default message when there is no diff to stdout', default: false },
'change-set': { type: 'boolean', alias: 'changeset', desc: 'Whether to create a change set to analyze resource replacements. In this mode, diff will use the deploy role instead of the lookup role.', default: true, deprecated: 'use --method instead' },
'change-set-name': { type: 'string', desc: 'Name of the CloudFormation change set to create (only if method is not template)' },
'method': {
alias: 'm',
type: 'string',
Expand Down
4 changes: 4 additions & 0 deletions packages/aws-cdk/lib/cli/cli-type-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,10 @@
"default": true,
"deprecated": "use --method instead"
},
"change-set-name": {
"type": "string",
"desc": "Name of the CloudFormation change set to create (only if method is not template)"
},
"method": {
"alias": "m",
"type": "string",
Expand Down
9 changes: 8 additions & 1 deletion packages/aws-cdk/lib/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,12 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
case 'diff':
ioHost.currentAction = 'diff';
const enableDiffNoFail = isFeatureEnabled(configuration, cxapi.ENABLE_DIFF_NO_FAIL_CONTEXT);
const diffMethod = determineDiffMethod(args);
if (diffMethod === 'template') {
rejectIncompatibleOptions(args, '--method=template', {
changeSetName: '--change-set-name',
});
}
return cli.diff({
stackNames: args.STACKS,
exclusively: args.exclusively,
Expand All @@ -356,7 +362,8 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
fail: args.fail != null ? args.fail : !enableDiffNoFail,
compareAgainstProcessedTemplate: args.processed,
quiet: args.quiet,
method: determineDiffMethod(args),
method: diffMethod,
changeSetName: args.changeSetName,
toolkitStackName: toolkitStackName,
importExistingResources: args.importExistingResources,
includeMoves: args['include-moves'],
Expand Down
2 changes: 2 additions & 0 deletions packages/aws-cdk/lib/cli/convert-to-user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ export function convertYargsToUserInput(args: any): UserInput {
processed: args.processed,
quiet: args.quiet,
changeSet: args.changeSet,
changeSetName: args.changeSetName,
method: args.method,
importExistingResources: args.importExistingResources,
includeMoves: args.includeMoves,
Expand Down Expand Up @@ -559,6 +560,7 @@ export function convertConfigToUserInput(config: any): UserInput {
processed: config.diff?.processed,
quiet: config.diff?.quiet,
changeSet: config.diff?.changeSet,
changeSetName: config.diff?.changeSetName,
method: config.diff?.method,
importExistingResources: config.diff?.importExistingResources,
includeMoves: config.diff?.includeMoves,
Expand Down
5 changes: 5 additions & 0 deletions packages/aws-cdk/lib/cli/parse-command-line-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,11 @@ export function parseCommandLineArguments(args: Array<string>): any {
desc: 'Whether to create a change set to analyze resource replacements. In this mode, diff will use the deploy role instead of the lookup role.',
deprecated: 'use --method instead',
})
.option('change-set-name', {
default: undefined,
type: 'string',
desc: 'Name of the CloudFormation change set to create (only if method is not template)',
})
.option('method', {
default: 'auto',
alias: 'm',
Expand Down
7 changes: 7 additions & 0 deletions packages/aws-cdk/lib/cli/user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1493,6 +1493,13 @@ export interface DiffOptions {
*/
readonly changeSet?: boolean;

/**
* Name of the CloudFormation change set to create (only if method is not template)
*
* @default - undefined
*/
readonly changeSetName?: string;

/**
* How to compute the diff. "auto" attempts to create a change set and falls back to template-only on failure. "change-set" creates a change set and fails if it cannot be created. Both use the deploy role instead of the lookup role. "template" compares templates directly and uses the lookup role.
*
Expand Down
46 changes: 20 additions & 26 deletions packages/aws-cdk/test/cli/diff-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { exec } from '../../lib/cli/cli';

// Prevent actual toolkit operations
let diffSpy: jest.SpyInstance;

beforeEach(() => {
diffSpy = jest.spyOn(cdkToolkitModule.CdkToolkit.prototype, 'diff').mockResolvedValue(0);
});
Expand All @@ -11,39 +12,32 @@ afterEach(() => {
jest.restoreAllMocks();
});

describe('diff --method option', () => {
test('defaults to method=auto', async () => {
await exec(['diff', '--app', 'echo']);
expect(diffSpy).toHaveBeenCalledWith(expect.objectContaining({ method: 'auto' }));
});
describe('diff --change-set-name', () => {
test('passes through to CdkToolkit.diff', async () => {
await exec(['diff', '--app', 'echo', '--change-set-name=MyCS', 'MyStack']);

test('--method=change-set', async () => {
await exec(['diff', '--app', 'echo', '--method=change-set']);
expect(diffSpy).toHaveBeenCalledWith(expect.objectContaining({ method: 'change-set' }));
expect(diffSpy).toHaveBeenCalledWith(expect.objectContaining({
changeSetName: 'MyCS',
}));
});

test('--method=template', async () => {
await exec(['diff', '--app', 'echo', '--method=template']);
expect(diffSpy).toHaveBeenCalledWith(expect.objectContaining({ method: 'template' }));
});

test('deprecated --no-change-set maps to method=template', async () => {
await exec(['diff', '--app', 'echo', '--no-change-set']);
expect(diffSpy).toHaveBeenCalledWith(expect.objectContaining({ method: 'template' }));
});
test('defaults to undefined', async () => {
await exec(['diff', '--app', 'echo', 'MyStack']);

test('deprecated --change-set maps to method=auto', async () => {
await exec(['diff', '--app', 'echo', '--change-set']);
expect(diffSpy).toHaveBeenCalledWith(expect.objectContaining({ method: 'auto' }));
expect(diffSpy).toHaveBeenCalledWith(expect.objectContaining({
changeSetName: undefined,
}));
});

test('--method takes precedence over deprecated --no-change-set', async () => {
await exec(['diff', '--app', 'echo', '--method=change-set', '--no-change-set']);
expect(diffSpy).toHaveBeenCalledWith(expect.objectContaining({ method: 'change-set' }));
test('cannot be used with --method=template', async () => {
await expect(
exec(['diff', '--app', 'echo', '--method=template', '--change-set-name=MyCS', 'MyStack']),
).rejects.toThrow('--change-set-name cannot be used with --method=template');
});

test('--template implies method=template', async () => {
await exec(['diff', '--app', 'echo', '--template=/tmp/template.json']);
expect(diffSpy).toHaveBeenCalledWith(expect.objectContaining({ method: 'template' }));
test('cannot be used with --no-change-set', async () => {
await expect(
exec(['diff', '--app', 'echo', '--no-change-set', '--change-set-name=MyCS', 'MyStack']),
).rejects.toThrow('--change-set-name cannot be used with --method=template');
});
});
33 changes: 33 additions & 0 deletions packages/aws-cdk/test/commands/diff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,39 @@ Resources
expect(exitCode).toBe(0);
});

test('passes the change set name to createDiffChangeSet', async () => {
createDiffChangeSet = jest.spyOn(cfnApi, 'createDiffChangeSet').mockImplementationOnce(async () => {
return {
changeSet: {
$metadata: {},
Changes: [
{
ResourceChange: {
Action: 'Add',
LogicalResourceId: 'MyGlobalTable',
},
},
],
},
diagnosis: Diagnosis.noProblem(),
};
});

// WHEN
const exitCode = await toolkit.diff({
stackNames: ['A'],
method: 'auto',
changeSetName: 'my-custom-change-set',
});

// THEN
expect(createDiffChangeSet).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ changeSetName: 'my-custom-change-set' }),
);
expect(exitCode).toBe(0);
});

test('when invoked with local template path', async () => {
const templatePath = 'oldTemplate.json';
const oldTemplate = {
Expand Down
Loading