Skip to content

Commit

Permalink
fix(toolkit): 'cdk deploy' support updates to Outputs (#2029)
Browse files Browse the repository at this point in the history
If only stack Outputs are changed, CloudFormation generates a ChangeSet
that is executable but has 0 changes.

Before, we looked at the amount of changes to say there was nothing to
do, but now we look at the actual change set status to determine
whether it's an empty change set or not.

The effect is that we can now deploy updates even if only Outputs
changed. This becomes very important when the only thing changed
to a stack is an Output got added because a cross-stack reference
was taken by a downstream stack.

Fixes #778.
  • Loading branch information
rix0rrr committed Mar 18, 2019
1 parent 130e2d0 commit 23509ae
Show file tree
Hide file tree
Showing 6 changed files with 354 additions and 35 deletions.
9 changes: 5 additions & 4 deletions packages/aws-cdk/lib/api/deploy-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { debug, error, print } from '../logging';
import { toYAML } from '../serialize';
import { Mode } from './aws-auth/credentials';
import { ToolkitInfo } from './toolkit-info';
import { describeStack, stackExists, stackFailedCreating, waitForChangeSet, waitForStack } from './util/cloudformation';
import { changeSetHasNoChanges, describeStack, stackExists, stackFailedCreating, waitForChangeSet, waitForStack } from './util/cloudformation';
import { StackActivityMonitor } from './util/cloudformation/stack-activity-monitor';
import { StackStatus } from './util/cloudformation/stack-status';
import { SDK } from './util/sdk';
Expand Down Expand Up @@ -77,16 +77,17 @@ export async function deployStack(options: DeployStackOptions): Promise<DeploySt
}).promise();
debug('Initiated creation of changeset: %s; waiting for it to finish creating...', changeSet.Id);
const changeSetDescription = await waitForChangeSet(cfn, deployName, changeSetName);
if (!changeSetDescription || !changeSetDescription.Changes || changeSetDescription.Changes.length === 0) {
debug('No changes are to be performed on %s, assuming success.', deployName);

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

debug('Initiating execution of changeset %s on stack %s', changeSetName, deployName);
await cfn.executeChangeSet({ StackName: deployName, ChangeSetName: changeSetName }).promise();
// tslint:disable-next-line:max-line-length
const monitor = options.quiet ? undefined : new StackActivityMonitor(cfn, deployName, options.stack, changeSetDescription.Changes.length).start();
const monitor = options.quiet ? undefined : new StackActivityMonitor(cfn, deployName, options.stack, (changeSetDescription.Changes || []).length).start();
debug('Execution of changeset %s on stack %s has started; waiting for the update to complete...', changeSetName, deployName);
await waitForStack(cfn, deployName);
if (monitor) { await monitor.stop(); }
Expand Down
35 changes: 28 additions & 7 deletions packages/aws-cdk/lib/api/util/cloudformation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,32 +86,53 @@ async function waitFor<T>(valueProvider: () => Promise<T | null | undefined>, ti
/**
* Waits for a ChangeSet to be available for triggering a StackUpdate.
*
* Will return a changeset that is either ready to be executed or has no changes.
* Will throw in other cases.
*
* @param cfn a CloudFormation client
* @param stackName the name of the Stack that the ChangeSet belongs to
* @param changeSetName the name of the ChangeSet
*
* @returns the CloudFormation description of the ChangeSet
*/
// tslint:disable-next-line:max-line-length
export async function waitForChangeSet(cfn: CloudFormation, stackName: string, changeSetName: string): Promise<CloudFormation.DescribeChangeSetOutput | undefined> {
export async function waitForChangeSet(cfn: CloudFormation, stackName: string, changeSetName: string): Promise<CloudFormation.DescribeChangeSetOutput> {
debug('Waiting for changeset %s on stack %s to finish creating...', changeSetName, stackName);
return waitFor(async () => {
const ret = await waitFor(async () => {
const description = await describeChangeSet(cfn, stackName, changeSetName);
// The following doesn't use a switch because tsc will not allow fall-through, UNLESS it is allows
// EVERYWHERE that uses this library directly or indirectly, which is undesirable.
if (description.Status === 'CREATE_PENDING' || description.Status === 'CREATE_IN_PROGRESS') {
debug('Changeset %s on stack %s is still creating', changeSetName, stackName);
return undefined;
} else if (description.Status === 'CREATE_COMPLETE') {
}

if (description.Status === 'CREATE_COMPLETE' || changeSetHasNoChanges(description)) {
return description;
} else if (description.Status === 'FAILED') {
if (description.StatusReason && description.StatusReason.startsWith('The submitted information didn\'t contain changes.')) {
return description;
}
}

// tslint:disable-next-line:max-line-length
throw new Error(`Failed to create ChangeSet ${changeSetName} on ${stackName}: ${description.Status || 'NO_STATUS'}, ${description.StatusReason || 'no reason provided'}`);
});

if (!ret) {
throw new Error('Change set took too long to be created; aborting');
}

return ret;
}

/**
* Return true if the given change set has no changes
*
* This must be determined from the status, not the 'Changes' array on the
* object; the latter can be empty because no resources were changed, but if
* there are changes to Outputs, the change set can still be executed.
*/
export function changeSetHasNoChanges(description: CloudFormation.DescribeChangeSetOutput) {
return description.Status === 'FAILED'
&& description.StatusReason
&& description.StatusReason.startsWith('The submitted information didn\'t contain changes.');
}

/**
Expand Down
Loading

0 comments on commit 23509ae

Please sign in to comment.