|
1 | 1 | import colors = require('colors/safe');
|
2 | 2 | import fs = require('fs-extra');
|
3 | 3 | import { format } from 'util';
|
| 4 | +import { Mode } from './api/aws-auth/credentials'; |
4 | 5 | import { AppStacks, DefaultSelection, ExtendedStackSelection, Tag } from "./api/cxapp/stacks";
|
| 6 | +import { destroyStack } from './api/deploy-stack'; |
5 | 7 | import { IDeploymentTarget } from './api/deployment-target';
|
| 8 | +import { stackExists } from './api/util/cloudformation'; |
| 9 | +import { ISDK } from './api/util/sdk'; |
6 | 10 | import { printSecurityDiff, printStackDiff, RequireApproval } from './diff';
|
7 |
| -import { data, error, highlight, print, success } from './logging'; |
| 11 | +import { data, error, highlight, print, success, warning } from './logging'; |
8 | 12 | import { deserializeStructure } from './serialize';
|
9 | 13 |
|
10 | 14 | // tslint:disable-next-line:no-var-requires
|
@@ -90,6 +94,24 @@ export class CdkToolkit {
|
90 | 94 | throw new Error(`Stack ${stack.name} does not define an environment, and AWS credentials could not be obtained from standard locations or no region was configured.`);
|
91 | 95 | }
|
92 | 96 |
|
| 97 | + if (Object.keys(stack.template.Resources || {}).length === 0) { // The generated stack has no resources |
| 98 | + const cfn = await options.sdk.cloudFormation(stack.environment.account, stack.environment.region, Mode.ForReading); |
| 99 | + if (!await stackExists(cfn, stack.name)) { |
| 100 | + warning('%s: stack has no resources, skipping deployment.', colors.bold(stack.name)); |
| 101 | + } else { |
| 102 | + warning('%s: stack has no resources, deleting existing stack.', colors.bold(stack.name)); |
| 103 | + await this.destroy({ |
| 104 | + stackNames: [stack.name], |
| 105 | + exclusively: true, |
| 106 | + force: true, |
| 107 | + roleArn: options.roleArn, |
| 108 | + sdk: options.sdk, |
| 109 | + fromDeploy: true, |
| 110 | + }); |
| 111 | + } |
| 112 | + continue; |
| 113 | + } |
| 114 | + |
93 | 115 | if (requireApproval !== RequireApproval.Never) {
|
94 | 116 | const currentTemplate = await this.provisioner.readCurrentTemplate(stack);
|
95 | 117 | if (printSecurityDiff(currentTemplate, stack, requireApproval)) {
|
@@ -152,6 +174,36 @@ export class CdkToolkit {
|
152 | 174 | }
|
153 | 175 | }
|
154 | 176 | }
|
| 177 | + |
| 178 | + public async destroy(options: DestroyOptions) { |
| 179 | + const stacks = await this.appStacks.selectStacks(options.stackNames, { |
| 180 | + extend: options.exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Downstream, |
| 181 | + defaultBehavior: DefaultSelection.OnlySingle |
| 182 | + }); |
| 183 | + |
| 184 | + // The stacks will have been ordered for deployment, so reverse them for deletion. |
| 185 | + stacks.reverse(); |
| 186 | + |
| 187 | + if (!options.force) { |
| 188 | + // tslint:disable-next-line:max-line-length |
| 189 | + const confirmed = await promptly.confirm(`Are you sure you want to delete: ${colors.blue(stacks.map(s => s.name).join(', '))} (y/n)?`); |
| 190 | + if (!confirmed) { |
| 191 | + return; |
| 192 | + } |
| 193 | + } |
| 194 | + |
| 195 | + const action = options.fromDeploy ? 'deploy' : 'destroy'; |
| 196 | + for (const stack of stacks) { |
| 197 | + success('%s: destroying...', colors.blue(stack.name)); |
| 198 | + try { |
| 199 | + await destroyStack({ stack, sdk: options.sdk, deployName: stack.name, roleArn: options.roleArn }); |
| 200 | + success(`\n ✅ %s: ${action}ed`, colors.blue(stack.name)); |
| 201 | + } catch (e) { |
| 202 | + error(`\n ❌ %s: ${action} failed`, colors.blue(stack.name), e); |
| 203 | + throw e; |
| 204 | + } |
| 205 | + } |
| 206 | + } |
155 | 207 | }
|
156 | 208 |
|
157 | 209 | export interface DiffOptions {
|
@@ -244,4 +296,41 @@ export interface DeployOptions {
|
244 | 296 | * Tags to pass to CloudFormation for deployment
|
245 | 297 | */
|
246 | 298 | tags?: Tag[];
|
| 299 | + |
| 300 | + /** |
| 301 | + * AWS SDK |
| 302 | + */ |
| 303 | + sdk: ISDK; |
| 304 | +} |
| 305 | + |
| 306 | +export interface DestroyOptions { |
| 307 | + /** |
| 308 | + * The names of the stacks to delete |
| 309 | + */ |
| 310 | + stackNames: string[]; |
| 311 | + |
| 312 | + /** |
| 313 | + * Whether to exclude stacks that depend on the stacks to be deleted |
| 314 | + */ |
| 315 | + exclusively: boolean; |
| 316 | + |
| 317 | + /** |
| 318 | + * Whether to skip prompting for confirmation |
| 319 | + */ |
| 320 | + force: boolean; |
| 321 | + |
| 322 | + /** |
| 323 | + * The arn of the IAM role to use |
| 324 | + */ |
| 325 | + roleArn?: string; |
| 326 | + |
| 327 | + /** |
| 328 | + * AWS SDK |
| 329 | + */ |
| 330 | + sdk: ISDK; |
| 331 | + |
| 332 | + /** |
| 333 | + * Whether the destroy request came from a deploy. |
| 334 | + */ |
| 335 | + fromDeploy?: boolean |
247 | 336 | }
|
0 commit comments