Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): change set name is now a constant, and --no-execute will always produce one (even if empty) #12683

15 changes: 15 additions & 0 deletions packages/aws-cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,21 @@ When `cdk deploy` is executed, deployment events will include the complete histo

The `progress` key can also be specified as a user setting (`~/.cdk.json`)

#### Externally Executable CloudFormation Change Sets

For more control over when stack changes are deployed, the CDK can generate a CloudFormation change set but not execute it.
A name can also be given to the change set to make it easier to later execute.

```console
$ cdk deploy --no-execute --change-set-name MyChangeSetName
```

Additionally, a change set can always be generated, even when it would be empty (no changes to deploy). Empty change sets cannot be executed, but some tools always require a change set, such as AWS CodePipeline's CloudFormation action.

```console
$ cdk deploy --no-execute --force --retain-empty-change-set --change-set-name MyChangeSetName
```

### `cdk destroy`

Deletes a stack from it's environment. This will cause the resources in the stack to be destroyed (unless they were
Expand Down
4 changes: 4 additions & 0 deletions packages/aws-cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ async function parseCommandLineArguments() {
// @deprecated(v2) -- tags are part of the Cloud Assembly and tags specified here will be overwritten on the next deployment
.option('tags', { type: 'array', alias: 't', desc: 'Tags to add to the stack (KEY=VALUE), overrides tags from Cloud Assembly (deprecated)', nargs: 1, requiresArg: true })
.option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true })
.option('change-set-name', { type: 'string', desc: 'Name of the CloudFormation change set to create' } )
.option('retain-empty-change-set', { type: 'boolean', desc: 'Retain empty CloudFormation change sets instead of deleting them', default: false })
.option('force', { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false })
.option('parameters', { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', nargs: 1, requiresArg: true, default: {} })
.option('outputs-file', { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true })
Expand Down Expand Up @@ -313,6 +315,8 @@ async function initCommandLine() {
reuseAssets: args['build-exclude'],
tags: configuration.settings.get(['tags']),
execute: args.execute,
changeSetName: args.changeSetName,
retainEmptyChangeSet: args.retainEmptyChangeSet,
force: args.force,
parameters: parameterMap,
usePreviousParameters: args['previous-parameters'],
Expand Down
13 changes: 13 additions & 0 deletions packages/aws-cdk/lib/api/cloudformation-deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@ export interface DeployStackOptions {
*/
execute?: boolean;

/**
* Optional name to use for the CloudFormation change set.
* If not provided, a name will be generated automatically.
*/
changeSetName?: string;

/**
* Optionally retain empty CloudFormation change sets instead of deleting them.
*/
retainEmptyChangeSet?: boolean;

/**
* Force deployment, even if the deployed template is identical to the one we are about to deploy.
* @default false deployment will be skipped if the template is identical
Expand Down Expand Up @@ -173,6 +184,8 @@ export class CloudFormationDeployments {
toolkitInfo,
tags: options.tags,
execute: options.execute,
changeSetName: options.changeSetName,
retainEmptyChangeSet: options.retainEmptyChangeSet,
force: options.force,
parameters: options.parameters,
usePreviousParameters: options.usePreviousParameters,
Expand Down
17 changes: 15 additions & 2 deletions packages/aws-cdk/lib/api/deploy-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,17 @@ export interface DeployStackOptions {
*/
execute?: boolean;

/**
* Optional name to use for the CloudFormation change set.
* If not provided, a name will be generated automatically.
*/
changeSetName?: string;

/**
* Optionally retain empty CloudFormation change sets instead of deleting them.
*/
retainEmptyChangeSet?: boolean;

/**
* The collection of extra parameters
* (in addition to those used for assets)
Expand Down Expand Up @@ -228,7 +239,7 @@ export async function deployStack(options: DeployStackOptions): Promise<DeploySt

await publishAssets(legacyAssets.toManifest(stackArtifact.assembly.directory), options.sdkProvider, stackEnv);

const changeSetName = `CDK-${executionId}`;
const changeSetName = options.changeSetName || `CDK-${executionId}`;
const update = cloudFormationStack.exists && cloudFormationStack.stackStatus.name !== 'REVIEW_IN_PROGRESS';

debug(`Attempting to create ChangeSet ${changeSetName} to ${update ? 'update' : 'create'} stack ${deployName}`);
Expand Down Expand Up @@ -262,7 +273,9 @@ export async function deployStack(options: DeployStackOptions): Promise<DeploySt

if (changeSetHasNoChanges(changeSetDescription)) {
debug('No changes are to be performed on %s.', deployName);
await cfn.deleteChangeSet({ StackName: deployName, ChangeSetName: changeSetName }).promise();
if (!options.retainEmptyChangeSet) {
await cfn.deleteChangeSet({ StackName: deployName, ChangeSetName: changeSetName }).promise();
}
return { noOp: true, outputs: cloudFormationStack.outputs, stackArn: changeSet.StackId!, stackArtifact };
}

Expand Down
13 changes: 13 additions & 0 deletions packages/aws-cdk/lib/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ export class CdkToolkit {
notificationArns: options.notificationArns,
tags,
execute: options.execute,
changeSetName: options.changeSetName,
retainEmptyChangeSet: options.retainEmptyChangeSet,
force: options.force,
parameters: Object.assign({}, parameterMap['*'], parameterMap[stack.stackName]),
usePreviousParameters: options.usePreviousParameters,
Expand Down Expand Up @@ -554,6 +556,17 @@ export interface DeployOptions {
*/
execute?: boolean;

/**
* Optional name to use for the CloudFormation change set.
* If not provided, a name will be generated automatically.
*/
changeSetName?: string;

/**
* Optionally retain empty CloudFormation change sets instead of deleting them.
*/
retainEmptyChangeSet?: boolean;

/**
* Always deploy, even if templates are identical.
* @default false
Expand Down
47 changes: 43 additions & 4 deletions packages/aws-cdk/test/integ/cli/cli.integtest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,19 +139,58 @@ integTest('nested stack with parameters', withDefaultFixture(async (fixture) =>
expect(response.StackResources?.length).toEqual(1);
}));

integTest('deploy without execute', withDefaultFixture(async (fixture) => {
integTest('deploy without execute a named change set', withDefaultFixture(async (fixture) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is piggy-backing off the existing integration test for the --no-execute-flag by also testing the use of a custom name for the change set. I think it's a safe change because it's not changing anything originally being tested, and at code level specifying a name shouldn't really change the previous behavior.

const changeSetName = 'custom-change-set-name';
const stackArn = await fixture.cdkDeploy('test-2', {
options: ['--no-execute'],
options: ['--no-execute', '--change-set-name', changeSetName],
captureStderr: false,
});
// verify that we only deployed a single stack (there's a single ARN in the output)
expect(stackArn.split('\n').length).toEqual(1);

const response = await fixture.aws.cloudFormation('describeStacks', {
//verify the stack was created but resource deployment is pending review
const stackResponse = await fixture.aws.cloudFormation('describeStacks', {
StackName: stackArn,
});
expect(stackResponse.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS');

//verify a change set was created with the provided name
const changeSetResponse = await fixture.aws.cloudFormation('listChangeSets', {
StackName: stackArn,
});

const changeSets = changeSetResponse.Summaries || [];
expect(changeSets.length).toEqual(1);
expect(changeSets[0].ChangeSetName).toEqual(changeSetName);
expect(changeSets[0].Status).toEqual('CREATE_COMPLETE');
}));

integTest('deploy without execute a named empty change set ', withDefaultFixture(async (fixture) => {
const changeSetName = 'custom-change-set-name';

//deploy a stack
let stackArn = await fixture.cdkDeploy('test-2', {
captureStderr: false,
});

// verify that we only deployed a single stack (there's a single ARN in the output)
expect(stackArn.split('\n').length).toEqual(1);

//deploy again without executing the change set
stackArn = await fixture.cdkDeploy('test-2', {
options: ['--no-execute', '--force', '--retain-empty-change-set', '--change-set-name', changeSetName],
captureStderr: false,
});

//fetch the change set and verify it is empty (and not deleted) and has the given name
const changeSetResponse = await fixture.aws.cloudFormation('listChangeSets', {
StackName: stackArn,
});

expect(response.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS');
const changeSets = changeSetResponse.Summaries || [];
expect(changeSets.length).toEqual(1);
expect(changeSets[0].ChangeSetName).toEqual(changeSetName);
expect(changeSets[0].StatusReason).toMatch(/^The submitted information didn't contain changes/);
}));

integTest('security related changes without a CLI are expected to fail', withDefaultFixture(async (fixture) => {
Expand Down