Skip to content

Commit 559d676

Browse files
authored
fix(cli): warns about missing --no-rollback flag that is present (#32309)
The logic on `rollback` and `!rollback` was inverted in a couple of places, causing the check to be the wrong way around and making reasoning about these options harder. Revert the logic and do a more comprehensive test suite around these options. Also remove a stray `debug()` statement that was left in from a previous PR, and show the stack status in the error message. Closes #32295. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 512cf95 commit 559d676

File tree

5 files changed

+42
-29
lines changed

5 files changed

+42
-29
lines changed

packages/aws-cdk/lib/api/aws-auth/sdk.ts

-1
Original file line numberDiff line numberDiff line change
@@ -951,7 +951,6 @@ export class SDK {
951951
});
952952
const command = new GetCallerIdentityCommand({});
953953
const result = await client.send(command);
954-
debug(result.Account!, result.Arn, result.UserId);
955954
const accountId = result.Account;
956955
const partition = result.Arn!.split(':')[1];
957956
if (!accountId) {

packages/aws-cdk/lib/api/deploy-stack.ts

+9-8
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { StringWithoutPlaceholders } from './util/placeholders';
3737
export type DeployStackResult =
3838
| SuccessfulDeployStackResult
3939
| NeedRollbackFirstDeployStackResult
40-
| ReplacementRequiresNoRollbackStackResult
40+
| ReplacementRequiresRollbackStackResult
4141
;
4242

4343
/** Successfully deployed a stack */
@@ -52,11 +52,12 @@ export interface SuccessfulDeployStackResult {
5252
export interface NeedRollbackFirstDeployStackResult {
5353
readonly type: 'failpaused-need-rollback-first';
5454
readonly reason: 'not-norollback' | 'replacement';
55+
readonly status: string;
5556
}
5657

57-
/** The upcoming change has a replacement, which requires deploying without --no-rollback */
58-
export interface ReplacementRequiresNoRollbackStackResult {
59-
readonly type: 'replacement-requires-norollback';
58+
/** The upcoming change has a replacement, which requires deploying with --rollback */
59+
export interface ReplacementRequiresRollbackStackResult {
60+
readonly type: 'replacement-requires-rollback';
6061
}
6162

6263
export function assertIsSuccessfulDeployStackResult(x: DeployStackResult): asserts x is SuccessfulDeployStackResult {
@@ -512,13 +513,13 @@ class FullCloudFormationDeployment {
512513
const isPausedFailState = this.cloudFormationStack.stackStatus.isRollbackable;
513514
const rollback = this.options.rollback ?? true;
514515
if (isPausedFailState && replacement) {
515-
return { type: 'failpaused-need-rollback-first', reason: 'replacement' };
516+
return { type: 'failpaused-need-rollback-first', reason: 'replacement', status: this.cloudFormationStack.stackStatus.name };
516517
}
517-
if (isPausedFailState && !rollback) {
518-
return { type: 'failpaused-need-rollback-first', reason: 'not-norollback' };
518+
if (isPausedFailState && rollback) {
519+
return { type: 'failpaused-need-rollback-first', reason: 'not-norollback', status: this.cloudFormationStack.stackStatus.name };
519520
}
520521
if (!rollback && replacement) {
521-
return { type: 'replacement-requires-norollback' };
522+
return { type: 'replacement-requires-rollback' };
522523
}
523524

524525
return this.executeChangeSet(changeSetDescription);

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -436,8 +436,8 @@ export class CdkToolkit {
436436

437437
case 'failpaused-need-rollback-first': {
438438
const motivation = r.reason === 'replacement'
439-
? 'Stack is in a paused fail state and change includes a replacement which cannot be deployed with "--no-rollback"'
440-
: 'Stack is in a paused fail state and command line arguments do not include "--no-rollback"';
439+
? `Stack is in a paused fail state (${r.status}) and change includes a replacement which cannot be deployed with "--no-rollback"`
440+
: `Stack is in a paused fail state (${r.status}) and command line arguments do not include "--no-rollback"`;
441441

442442
if (options.force) {
443443
warning(`${motivation}. Rolling back first (--force).`);
@@ -461,7 +461,7 @@ export class CdkToolkit {
461461
break;
462462
}
463463

464-
case 'replacement-requires-norollback': {
464+
case 'replacement-requires-rollback': {
465465
const motivation = 'Change includes a replacement which cannot be deployed with "--no-rollback"';
466466

467467
if (options.force) {

packages/aws-cdk/test/api/deploy-stack.test.ts

+25-12
Original file line numberDiff line numberDiff line change
@@ -1127,30 +1127,41 @@ describe('disable rollback', () => {
11271127
});
11281128

11291129
test.each([
1130-
[StackStatus.UPDATE_FAILED, 'failpaused-need-rollback-first'],
1131-
[StackStatus.CREATE_COMPLETE, 'replacement-requires-norollback'],
1132-
])('no-rollback and replacement is disadvised: %p -> %p', async (stackStatus, expectedType) => {
1130+
// From a failed state, a --no-rollback is possible as long as there is not a replacement
1131+
[StackStatus.UPDATE_FAILED, 'no-rollback', 'no-replacement', 'did-deploy-stack'],
1132+
[StackStatus.UPDATE_FAILED, 'no-rollback', 'replacement', 'failpaused-need-rollback-first'],
1133+
// Any combination of UPDATE_FAILED & rollback always requires a rollback first
1134+
[StackStatus.UPDATE_FAILED, 'rollback', 'replacement', 'failpaused-need-rollback-first'],
1135+
[StackStatus.UPDATE_FAILED, 'rollback', 'no-replacement', 'failpaused-need-rollback-first'],
1136+
// From a stable state, any deployment containing a replacement requires a regular deployment (--rollback)
1137+
[StackStatus.UPDATE_COMPLETE, 'no-rollback', 'replacement', 'replacement-requires-rollback'],
1138+
] satisfies Array<[StackStatus, 'rollback' | 'no-rollback', 'replacement' | 'no-replacement', string]>)
1139+
('no-rollback and replacement is disadvised: %s %s %s -> %s', async (stackStatus, rollback, replacement, expectedType) => {
11331140
// GIVEN
11341141
givenTemplateIs(FAKE_STACK.template);
11351142
givenStackExists({
1136-
NotificationARNs: ['arn:aws:sns:bermuda-triangle-1337:123456789012:TestTopic'],
1143+
// First call
11371144
StackStatus: stackStatus,
1145+
}, {
1146+
// Later calls
1147+
StackStatus: 'UPDATE_COMPLETE',
11381148
});
1139-
givenChangeSetContainsReplacement();
1149+
givenChangeSetContainsReplacement(replacement === 'replacement');
11401150

11411151
// WHEN
11421152
const result = await deployStack({
11431153
...standardDeployStackArguments(),
11441154
stack: FAKE_STACK,
1145-
rollback: false,
1155+
rollback: rollback === 'rollback',
1156+
force: true, // Bypass 'canSkipDeploy'
11461157
});
11471158

11481159
// THEN
11491160
expect(result.type).toEqual(expectedType);
11501161
});
11511162

11521163
test('assertIsSuccessfulDeployStackResult does what it says', () => {
1153-
expect(() => assertIsSuccessfulDeployStackResult({ type: 'replacement-requires-norollback' })).toThrow();
1164+
expect(() => assertIsSuccessfulDeployStackResult({ type: 'replacement-requires-rollback' })).toThrow();
11541165
});
11551166
/**
11561167
* Set up the mocks so that it looks like the stack exists to start with
@@ -1162,12 +1173,14 @@ function givenStackExists(...overrides: Array<Partial<Stack>>) {
11621173
overrides = [{}];
11631174
}
11641175

1176+
let handler = mockCloudFormationClient.on(DescribeStacksCommand);
1177+
11651178
for (const override of overrides.slice(0, overrides.length - 1)) {
1166-
mockCloudFormationClient.on(DescribeStacksCommand).resolvesOnce({
1179+
handler = handler.resolvesOnce({
11671180
Stacks: [{ ...baseResponse, ...override }],
11681181
});
11691182
}
1170-
mockCloudFormationClient.on(DescribeStacksCommand).resolves({
1183+
handler.resolves({
11711184
Stacks: [{ ...baseResponse, ...overrides[overrides.length - 1] }],
11721185
});
11731186
}
@@ -1178,10 +1191,10 @@ function givenTemplateIs(template: any) {
11781191
});
11791192
}
11801193

1181-
function givenChangeSetContainsReplacement() {
1194+
function givenChangeSetContainsReplacement(replacement: boolean) {
11821195
mockCloudFormationClient.on(DescribeChangeSetCommand).resolves({
11831196
Status: 'CREATE_COMPLETE',
1184-
Changes: [
1197+
Changes: replacement ? [
11851198
{
11861199
Type: 'Resource',
11871200
ResourceChange: {
@@ -1205,6 +1218,6 @@ function givenChangeSetContainsReplacement() {
12051218
],
12061219
},
12071220
},
1208-
],
1221+
] : [],
12091222
});
12101223
}

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -1464,11 +1464,11 @@ describe('synth', () => {
14641464
});
14651465

14661466
test.each([
1467-
[{ type: 'failpaused-need-rollback-first', reason: 'replacement' }, false],
1468-
[{ type: 'failpaused-need-rollback-first', reason: 'replacement' }, true],
1469-
[{ type: 'failpaused-need-rollback-first', reason: 'not-norollback' }, false],
1470-
[{ type: 'replacement-requires-norollback' }, false],
1471-
[{ type: 'replacement-requires-norollback' }, true],
1467+
[{ type: 'failpaused-need-rollback-first', reason: 'replacement', status: 'OOPS' }, false],
1468+
[{ type: 'failpaused-need-rollback-first', reason: 'replacement', status: 'OOPS' }, true],
1469+
[{ type: 'failpaused-need-rollback-first', reason: 'not-norollback', status: 'OOPS' }, false],
1470+
[{ type: 'replacement-requires-rollback' }, false],
1471+
[{ type: 'replacement-requires-rollback' }, true],
14721472
] satisfies Array<[DeployStackResult, boolean]>)('no-rollback deployment that cant proceed will be called with rollback on retry: %p (using force: %p)', async (firstResult, useForce) => {
14731473
cloudExecutable = new MockCloudExecutable({
14741474
stacks: [

0 commit comments

Comments
 (0)