Skip to content

Commit 6a9cbc2

Browse files
authored
chore(toolkit): rollback implementation and tests (#33074)
introduces rollback to the programatic toolkit, plus tests ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 4713bdd commit 6a9cbc2

File tree

3 files changed

+107
-5
lines changed

3 files changed

+107
-5
lines changed

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

+36-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as chalk from 'chalk';
44
import * as chokidar from 'chokidar';
55
import * as fs from 'fs-extra';
66
import { ToolkitServices } from './private';
7+
import { formatErrorMessage } from '../../../../aws-cdk/lib/util/error';
78
import { AssetBuildTime, DeployOptions, RequireApproval } from '../actions/deploy';
89
import { buildParameterMap, removePublishedAssets } from '../actions/deploy/private';
910
import { DestroyOptions } from '../actions/destroy';
@@ -349,7 +350,7 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab
349350
stack,
350351
deployName: stack.stackName,
351352
roleArn: options.roleArn,
352-
toolkitStackName: options.toolkitStackName,
353+
toolkitStackName: this.toolkitStackName,
353354
reuseAssets: options.reuseAssets,
354355
notificationArns,
355356
tags,
@@ -616,16 +617,46 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab
616617
const ioHost = withAction(this.ioHost, 'rollback');
617618
const timer = Timer.start();
618619
const assembly = await this.assemblyFromSource(cx);
619-
const stacks = await assembly.selectStacksV2(options.stacks);
620+
const stacks = assembly.selectStacksV2(options.stacks);
620621
await this.validateStacksMetadata(stacks, ioHost);
621622
const synthTime = timer.end();
622623
await ioHost.notify(info(`\n✨ Synthesis time: ${synthTime.asSec}s\n`, 'CDK_TOOLKIT_I5001', {
623624
time: synthTime.asMs,
624625
}));
625626

626-
// temporary
627-
// eslint-disable-next-line @cdklabs/no-throw-default-error
628-
throw new Error('Not implemented yet');
627+
if (stacks.stackCount === 0) {
628+
await ioHost.notify(error('No stacks selected'));
629+
return;
630+
}
631+
632+
let anyRollbackable = false;
633+
634+
for (const stack of stacks.stackArtifacts) {
635+
await ioHost.notify(info(`Rolling back ${chalk.bold(stack.displayName)}`));
636+
const startRollbackTime = Timer.start();
637+
const deployments = await this.deploymentsForAction('rollback');
638+
try {
639+
const result = await deployments.rollbackStack({
640+
stack,
641+
roleArn: options.roleArn,
642+
toolkitStackName: this.toolkitStackName,
643+
force: options.orphanFailedResources,
644+
validateBootstrapStackVersion: options.validateBootstrapStackVersion,
645+
orphanLogicalIds: options.orphanLogicalIds,
646+
});
647+
if (!result.notInRollbackableState) {
648+
anyRollbackable = true;
649+
}
650+
const elapsedRollbackTime = startRollbackTime.end();
651+
await ioHost.notify(info(`\n✨ Rollback time: ${elapsedRollbackTime.asSec}s\n`));
652+
} catch (e: any) {
653+
await ioHost.notify(error(`\n ❌ ${chalk.bold(stack.displayName)} failed: ${formatErrorMessage(e)}`));
654+
throw new ToolkitError('Rollback failed (use --force to orphan failing resources)');
655+
}
656+
}
657+
if (!anyRollbackable) {
658+
throw new ToolkitError('No stacks were in a state that could be rolled back');
659+
}
629660
}
630661

631662
/**

packages/@aws-cdk/toolkit/test/actions/deploy.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ describe('deploy', () => {
172172
await toolkit.deploy(cx);
173173

174174
// THEN
175+
// We called rollback
176+
expect(toolkit.rollback).toHaveBeenCalledTimes(1);
175177
successfulDeployment();
176178
});
177179

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { StackSelectionStrategy } from '../../lib';
2+
import { Toolkit } from '../../lib/toolkit';
3+
import { builderFixture, TestIoHost } from '../_helpers';
4+
5+
const ioHost = new TestIoHost();
6+
const toolkit = new Toolkit({ ioHost });
7+
8+
let mockRollbackStack = jest.fn().mockResolvedValue({
9+
notInRollbackableState: false,
10+
success: true,
11+
});
12+
13+
jest.mock('../../lib/api/aws-cdk', () => {
14+
return {
15+
...jest.requireActual('../../lib/api/aws-cdk'),
16+
Deployments: jest.fn().mockImplementation(() => ({
17+
rollbackStack: mockRollbackStack,
18+
})),
19+
};
20+
});
21+
22+
beforeEach(() => {
23+
ioHost.notifySpy.mockClear();
24+
ioHost.requestSpy.mockClear();
25+
jest.clearAllMocks();
26+
});
27+
28+
describe('rollback', () => {
29+
test('successful rollback', async () => {
30+
// WHEN
31+
const cx = await builderFixture(toolkit, 'two-empty-stacks');
32+
await toolkit.rollback(cx, { stacks: { strategy: StackSelectionStrategy.ALL_STACKS } });
33+
34+
// THEN
35+
successfulRollback();
36+
});
37+
38+
test('rollback not in rollbackable state', async () => {
39+
// GIVEN
40+
mockRollbackStack.mockImplementation(() => ({
41+
notInRollbackableState: true,
42+
success: false,
43+
}));
44+
// WHEN
45+
const cx = await builderFixture(toolkit, 'two-empty-stacks');
46+
await expect(async () => toolkit.rollback(cx, {
47+
stacks: { strategy: StackSelectionStrategy.ALL_STACKS },
48+
})).rejects.toThrow(/No stacks were in a state that could be rolled back/);
49+
});
50+
51+
test('rollback not in rollbackable state', async () => {
52+
// GIVEN
53+
mockRollbackStack.mockRejectedValue({});
54+
55+
// WHEN
56+
const cx = await builderFixture(toolkit, 'two-empty-stacks');
57+
await expect(async () => toolkit.rollback(cx, {
58+
stacks: { strategy: StackSelectionStrategy.ALL_STACKS },
59+
})).rejects.toThrow(/Rollback failed/);
60+
});
61+
});
62+
63+
function successfulRollback() {
64+
expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
65+
action: 'rollback',
66+
level: 'info',
67+
message: expect.stringContaining('Rollback time:'),
68+
}));
69+
}

0 commit comments

Comments
 (0)