Skip to content

Commit 2c7999c

Browse files
rix0rrrgithub-actions
and
github-actions
authored
feat(toolkit): add a return type for toolkit.destroy() (#318)
`toolkit.destroy()` now returns information about the stacks it deployed. Closes aws/aws-cdk#33190 --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Signed-off-by: github-actions <[email protected]> Co-authored-by: github-actions <[email protected]>
1 parent d21e66e commit 2c7999c

File tree

6 files changed

+211
-27
lines changed

6 files changed

+211
-27
lines changed

packages/@aws-cdk/tmp-toolkit-helpers/src/api/cloudformation/stack-helpers.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export class CloudFormationStack {
9494
}
9595

9696
/**
97-
* The stack's ID
97+
* The stack's ID (which is the same as its ARN)
9898
*
9999
* Throws if the stack doesn't exist.
100100
*/

packages/@aws-cdk/tmp-toolkit-helpers/src/api/deployments/deploy-stack.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -663,13 +663,23 @@ export interface DestroyStackOptions {
663663
deployName?: string;
664664
}
665665

666-
export async function destroyStack(options: DestroyStackOptions, ioHelper: IoHelper) {
666+
export interface DestroyStackResult {
667+
/**
668+
* The ARN of the stack that was destroyed, if any.
669+
*
670+
* If the stack didn't exist to begin with, the operation will succeed
671+
* but this value will be undefined.
672+
*/
673+
readonly stackArn?: string;
674+
}
675+
676+
export async function destroyStack(options: DestroyStackOptions, ioHelper: IoHelper): Promise<DestroyStackResult> {
667677
const deployName = options.deployName || options.stack.stackName;
668678
const cfn = options.sdk.cloudFormation();
669679

670680
const currentStack = await CloudFormationStack.lookup(cfn, deployName);
671681
if (!currentStack.exists) {
672-
return;
682+
return {};
673683
}
674684
const monitor = new StackActivityMonitor({
675685
cfn,
@@ -685,6 +695,8 @@ export async function destroyStack(options: DestroyStackOptions, ioHelper: IoHel
685695
if (destroyedStack && destroyedStack.stackStatus.name !== 'DELETE_COMPLETE') {
686696
throw new ToolkitError(`Failed to destroy ${deployName}: ${destroyedStack.stackStatus}`);
687697
}
698+
699+
return { stackArn: currentStack.stackId };
688700
} catch (e: any) {
689701
throw new ToolkitError(suffixWithErrors(formatErrorMessage(e), monitor.errors));
690702
} finally {

packages/@aws-cdk/tmp-toolkit-helpers/src/api/deployments/deployments.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,10 @@ export interface RollbackStackOptions {
229229
readonly validateBootstrapStackVersion?: boolean;
230230
}
231231

232-
export interface RollbackStackResult {
233-
readonly notInRollbackableState?: boolean;
234-
readonly success?: boolean;
235-
}
232+
export type RollbackStackResult = { readonly stackArn: string } & (
233+
| { readonly notInRollbackableState: true }
234+
| { readonly success: true; notInRollbackableState?: undefined }
235+
);
236236

237237
interface AssetOptions {
238238
/**
@@ -467,14 +467,15 @@ export class Deployments {
467467
// We loop in case of `--force` and the stack ends up in `CONTINUE_UPDATE_ROLLBACK`.
468468
let maxLoops = 10;
469469
while (maxLoops--) {
470-
let cloudFormationStack = await CloudFormationStack.lookup(cfn, deployName);
470+
const cloudFormationStack = await CloudFormationStack.lookup(cfn, deployName);
471+
const stackArn = cloudFormationStack.stackId;
471472

472473
const executionRoleArn = await env.replacePlaceholders(options.roleArn ?? options.stack.cloudFormationExecutionRoleArn);
473474

474475
switch (cloudFormationStack.stackStatus.rollbackChoice) {
475476
case RollbackChoice.NONE:
476477
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_WARN.msg(`Stack ${deployName} does not need a rollback: ${cloudFormationStack.stackStatus}`));
477-
return { notInRollbackableState: true };
478+
return { stackArn: cloudFormationStack.stackId, notInRollbackableState: true };
478479

479480
case RollbackChoice.START_ROLLBACK:
480481
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`Initiating rollback of stack ${deployName}`));
@@ -516,7 +517,7 @@ export class Deployments {
516517
await this.ioHelper.notify(IO.DEFAULT_TOOLKIT_WARN.msg(
517518
`Stack ${deployName} failed creation and rollback. This state cannot be rolled back. You can recreate this stack by running 'cdk deploy'.`,
518519
));
519-
return { notInRollbackableState: true };
520+
return { stackArn, notInRollbackableState: true };
520521

521522
default:
522523
throw new ToolkitError(`Unexpected rollback choice: ${cloudFormationStack.stackStatus.rollbackChoice}`);
@@ -552,7 +553,7 @@ export class Deployments {
552553
}
553554

554555
if (finalStackState.stackStatus.isRollbackSuccess || !stackErrorMessage) {
555-
return { success: true };
556+
return { stackArn, success: true };
556557
}
557558

558559
// Either we need to ignore some resources to continue the rollback, or something went wrong
@@ -570,7 +571,7 @@ export class Deployments {
570571
);
571572
}
572573

573-
public async destroyStack(options: DestroyStackOptions): Promise<void> {
574+
public async destroyStack(options: DestroyStackOptions) {
574575
const env = await this.envs.accessStackForMutableStackOperations(options.stack);
575576
const executionRoleArn = await env.replacePlaceholders(options.roleArn ?? options.stack.cloudFormationExecutionRoleArn);
576577

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

+42-8
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as fs from 'fs-extra';
66
import { NonInteractiveIoHost } from './non-interactive-io-host';
77
import type { ToolkitServices } from './private';
88
import { assemblyFromSource } from './private';
9-
import type { DeployResult } from './types';
9+
import type { DeployResult, DestroyResult, RollbackResult } from './types';
1010
import type { BootstrapEnvironments, BootstrapOptions, BootstrapResult, EnvironmentBootstrapResult } from '../actions/bootstrap';
1111
import { BootstrapSource } from '../actions/bootstrap';
1212
import { AssetBuildTime, type DeployOptions } from '../actions/deploy';
@@ -796,7 +796,7 @@ export class Toolkit extends CloudAssemblySourceBuilder {
796796
*
797797
* Rolls back the selected stacks.
798798
*/
799-
public async rollback(cx: ICloudAssemblySource, options: RollbackOptions): Promise<void> {
799+
public async rollback(cx: ICloudAssemblySource, options: RollbackOptions): Promise<RollbackResult> {
800800
const ioHelper = asIoHelper(this.ioHost, 'rollback');
801801
const assembly = await assemblyFromSource(ioHelper, cx);
802802
return this._rollback(assembly, 'rollback', options);
@@ -805,16 +805,20 @@ export class Toolkit extends CloudAssemblySourceBuilder {
805805
/**
806806
* Helper to allow rollback being called as part of the deploy or watch action.
807807
*/
808-
private async _rollback(assembly: StackAssembly, action: 'rollback' | 'deploy' | 'watch', options: RollbackOptions): Promise<void> {
808+
private async _rollback(assembly: StackAssembly, action: 'rollback' | 'deploy' | 'watch', options: RollbackOptions): Promise<RollbackResult> {
809809
const ioHelper = asIoHelper(this.ioHost, action);
810810
const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: options.stacks });
811811
const stacks = await assembly.selectStacksV2(options.stacks);
812812
await this.validateStacksMetadata(stacks, ioHelper);
813813
await synthSpan.end();
814814

815+
const ret: RollbackResult = {
816+
stacks: [],
817+
};
818+
815819
if (stacks.stackCount === 0) {
816820
await ioHelper.notify(IO.CDK_TOOLKIT_E6001.msg('No stacks selected'));
817-
return;
821+
return ret;
818822
}
819823

820824
let anyRollbackable = false;
@@ -839,6 +843,16 @@ export class Toolkit extends CloudAssemblySourceBuilder {
839843
anyRollbackable = true;
840844
}
841845
await rollbackSpan.end();
846+
847+
ret.stacks.push({
848+
environment: {
849+
account: stack.environment.account,
850+
region: stack.environment.region,
851+
},
852+
stackName: stack.stackName,
853+
stackArn: stackResult.stackArn,
854+
result: stackResult.notInRollbackableState ? 'already-stable' : 'rolled-back',
855+
});
842856
} catch (e: any) {
843857
await ioHelper.notify(IO.CDK_TOOLKIT_E6900.msg(`\n ❌ ${chalk.bold(stack.displayName)} failed: ${formatErrorMessage(e)}`, { error: e }));
844858
throw new ToolkitError('Rollback failed (use --force to orphan failing resources)');
@@ -847,14 +861,16 @@ export class Toolkit extends CloudAssemblySourceBuilder {
847861
if (!anyRollbackable) {
848862
throw new ToolkitError('No stacks were in a state that could be rolled back');
849863
}
864+
865+
return ret;
850866
}
851867

852868
/**
853869
* Destroy Action
854870
*
855871
* Destroys the selected Stacks.
856872
*/
857-
public async destroy(cx: ICloudAssemblySource, options: DestroyOptions): Promise<void> {
873+
public async destroy(cx: ICloudAssemblySource, options: DestroyOptions): Promise<DestroyResult> {
858874
const ioHelper = asIoHelper(this.ioHost, 'destroy');
859875
const assembly = await assemblyFromSource(ioHelper, cx);
860876
return this._destroy(assembly, 'destroy', options);
@@ -863,18 +879,23 @@ export class Toolkit extends CloudAssemblySourceBuilder {
863879
/**
864880
* Helper to allow destroy being called as part of the deploy action.
865881
*/
866-
private async _destroy(assembly: StackAssembly, action: 'deploy' | 'destroy', options: DestroyOptions): Promise<void> {
882+
private async _destroy(assembly: StackAssembly, action: 'deploy' | 'destroy', options: DestroyOptions): Promise<DestroyResult> {
867883
const ioHelper = asIoHelper(this.ioHost, action);
868884
const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: options.stacks });
869885
// The stacks will have been ordered for deployment, so reverse them for deletion.
870886
const stacks = (await assembly.selectStacksV2(options.stacks)).reversed();
871887
await synthSpan.end();
872888

889+
const ret: DestroyResult = {
890+
stacks: [],
891+
};
892+
873893
const motivation = 'Destroying stacks is an irreversible action';
874894
const question = `Are you sure you want to delete: ${chalk.red(stacks.hierarchicalIds.join(', '))}`;
875895
const confirmed = await ioHelper.requestResponse(IO.CDK_TOOLKIT_I7010.req(question, { motivation }));
876896
if (!confirmed) {
877-
return ioHelper.notify(IO.CDK_TOOLKIT_E7010.msg('Aborted by user'));
897+
await ioHelper.notify(IO.CDK_TOOLKIT_E7010.msg('Aborted by user'));
898+
return ret;
878899
}
879900

880901
const destroySpan = await ioHelper.span(SPAN.DESTROY_ACTION).begin({
@@ -890,18 +911,31 @@ export class Toolkit extends CloudAssemblySourceBuilder {
890911
stack,
891912
});
892913
const deployments = await this.deploymentsForAction(action);
893-
await deployments.destroyStack({
914+
const result = await deployments.destroyStack({
894915
stack,
895916
deployName: stack.stackName,
896917
roleArn: options.roleArn,
897918
});
919+
920+
ret.stacks.push({
921+
environment: {
922+
account: stack.environment.account,
923+
region: stack.environment.region,
924+
},
925+
stackName: stack.stackName,
926+
stackArn: result.stackArn,
927+
stackExisted: result.stackArn !== undefined,
928+
});
929+
898930
await ioHelper.notify(IO.CDK_TOOLKIT_I7900.msg(chalk.green(`\n ✅ ${chalk.blue(stack.displayName)}: ${action}ed`), stack));
899931
await singleDestroySpan.end();
900932
} catch (e: any) {
901933
await ioHelper.notify(IO.CDK_TOOLKIT_E7900.msg(`\n ❌ ${chalk.blue(stack.displayName)}: ${action} failed ${e}`, { error: e }));
902934
throw e;
903935
}
904936
}
937+
938+
return ret;
905939
} finally {
906940
await destroySpan.end();
907941
}

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

+119-6
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,35 @@ export interface DeployResult {
99
}
1010

1111
/**
12-
* Information about a deployed stack
12+
* Properties that describe a physically deployed stack
1313
*/
14-
export interface DeployedStack {
14+
export interface PhysicalStack<Arn extends 'arnRequired' | 'arnOptional' = 'arnRequired'> {
1515
/**
16-
* The name of the deployed stack
16+
* The name of the stack
1717
*
1818
* A stack name is unique inside its environment, but not unique globally.
1919
*/
2020
readonly stackName: string;
2121

2222
/**
23-
* The environment where the stack was deployed
23+
* The environment of the stack
2424
*
2525
* This environment is always concrete, because even though the CDK app's
2626
* stack may be region-agnostic, in order to be deployed it will have to have
2727
* been specialized.
2828
*/
2929
readonly environment: Environment;
3030

31+
/**
32+
* The ARN of the stack
33+
*/
34+
readonly stackArn: Arn extends 'arnOptional' ? string | undefined : string;
35+
}
36+
37+
/**
38+
* Information about a deployed stack
39+
*/
40+
export interface DeployedStack extends PhysicalStack {
3141
/**
3242
* Hierarchical identifier
3343
*
@@ -39,9 +49,49 @@ export interface DeployedStack {
3949
readonly hierarchicalId: string;
4050

4151
/**
42-
* The ARN of the deployed stack
52+
* The outputs of the deployed CloudFormation stack
53+
*/
54+
readonly outputs: { [key: string]: string };
55+
}
56+
57+
/**
58+
* An environment, which is an (account, region) pair
59+
*/
60+
export interface Environment {
61+
/**
62+
* The account number
63+
*/
64+
readonly account: string;
65+
66+
/**
67+
* The region number
68+
*/
69+
readonly region: string;
70+
}
71+
72+
/**
73+
* Result interface for toolkit.deploy operation
74+
*/
75+
export interface DeployResult {
76+
/**
77+
* List of stacks deployed by this operation
78+
*/
79+
readonly stacks: DeployedStack[];
80+
}
81+
82+
/**
83+
* Information about a deployed stack
84+
*/
85+
export interface DeployedStack extends PhysicalStack {
86+
/**
87+
* Hierarchical identifier
88+
*
89+
* This uniquely identifies the stack inside the CDK app.
90+
*
91+
* In practice this will be the stack's construct path, but unfortunately the
92+
* Cloud Assembly contract doesn't require or guarantee that.
4393
*/
44-
readonly stackArn: string;
94+
readonly hierarchicalId: string;
4595

4696
/**
4797
* The outputs of the deployed CloudFormation stack
@@ -63,3 +113,66 @@ export interface Environment {
63113
*/
64114
readonly region: string;
65115
}
116+
117+
/**
118+
* Result interface for toolkit.destroy operation
119+
*/
120+
export interface DestroyResult {
121+
/**
122+
* List of stacks destroyed by this operation
123+
*/
124+
readonly stacks: DestroyedStack[];
125+
}
126+
127+
/**
128+
* A stack targeted by a destroy operation
129+
*/
130+
export interface DestroyedStack extends PhysicalStack<'arnOptional'> {
131+
/**
132+
* Whether the stack existed to begin with
133+
*
134+
* If `!stackExisted`, the stack didn't exist, wasn't deleted, and `stackArn`
135+
* will be `undefined`.
136+
*/
137+
readonly stackExisted: boolean;
138+
}
139+
140+
/**
141+
* Result interface for toolkit.rollback operation
142+
*/
143+
export interface RollbackResult {
144+
/**
145+
* List of stacks rolled back by this operation
146+
*/
147+
readonly stacks: RolledBackStack[];
148+
}
149+
150+
/**
151+
* A stack targeted by a rollback operation
152+
*/
153+
export interface RolledBackStack extends PhysicalStack {
154+
/**
155+
* What operation we did for this stack
156+
*
157+
* Either: we did roll it back, or we didn't need to roll it back because
158+
* it was already stable.
159+
*/
160+
readonly result: StackRollbackResult;
161+
}
162+
163+
/**
164+
* An environment, which is an (account, region) pair
165+
*/
166+
export interface Environment {
167+
/**
168+
* The account number
169+
*/
170+
readonly account: string;
171+
172+
/**
173+
* The region number
174+
*/
175+
readonly region: string;
176+
}
177+
178+
export type StackRollbackResult = 'rolled-back' | 'already-stable';

0 commit comments

Comments
 (0)