Skip to content

Commit 64ace90

Browse files
jogoldmergify[bot]
authored andcommitted
fix(toolkit): do not deploy empty stacks (#3144)
* fix(toolkit): do not deploy empty stacks * use warning * destroy empty stack if it exists * change mode to ForReading * force destroy * optional roleArn * remove 'skipping deployment' when destroying * set exclusively to true * make deployesque * add integ test
1 parent e9eb183 commit 64ace90

File tree

5 files changed

+147
-37
lines changed

5 files changed

+147
-37
lines changed

packages/aws-cdk/bin/cdk.ts

Lines changed: 10 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import colors = require('colors/safe');
55
import path = require('path');
66
import yargs = require('yargs');
77

8-
import { bootstrapEnvironment, BootstrapEnvironmentProps, destroyStack, SDK } from '../lib';
8+
import { bootstrapEnvironment, BootstrapEnvironmentProps, SDK } from '../lib';
99
import { environmentsFromDescriptors, globEnvironmentsFromStacks } from '../lib/api/cxapp/environments';
1010
import { execProgram } from '../lib/api/cxapp/exec';
1111
import { AppStacks, DefaultSelection, ExtendedStackSelection } from '../lib/api/cxapp/stacks';
@@ -19,9 +19,6 @@ import { serializeStructure } from '../lib/serialize';
1919
import { Configuration, Settings } from '../lib/settings';
2020
import version = require('../lib/version');
2121

22-
// tslint:disable-next-line:no-var-requires
23-
const promptly = require('promptly');
24-
2522
// tslint:disable:no-shadowed-variable max-line-length
2623
async function parseCommandLineArguments() {
2724
const initTemplateLanuages = await availableInitLanguages;
@@ -201,11 +198,18 @@ async function initCommandLine() {
201198
requireApproval: configuration.settings.get(['requireApproval']),
202199
ci: args.ci,
203200
reuseAssets: args['build-exclude'],
204-
tags: configuration.settings.get(['tags'])
201+
tags: configuration.settings.get(['tags']),
202+
sdk: aws,
205203
});
206204

207205
case 'destroy':
208-
return await cliDestroy(args.STACKS, args.exclusively, args.force, args.roleArn);
206+
return await cli.destroy({
207+
stackNames: args.STACKS,
208+
exclusively: args.exclusively,
209+
force: args.force,
210+
roleArn: args.roleArn,
211+
sdk: aws,
212+
});
209213

210214
case 'synthesize':
211215
case 'synth':
@@ -332,35 +336,6 @@ async function initCommandLine() {
332336
return 0; // exit-code
333337
}
334338
335-
async function cliDestroy(stackNames: string[], exclusively: boolean, force: boolean, roleArn: string | undefined) {
336-
const stacks = await appStacks.selectStacks(stackNames, {
337-
extend: exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Downstream,
338-
defaultBehavior: DefaultSelection.OnlySingle
339-
});
340-
341-
// The stacks will have been ordered for deployment, so reverse them for deletion.
342-
stacks.reverse();
343-
344-
if (!force) {
345-
// tslint:disable-next-line:max-line-length
346-
const confirmed = await promptly.confirm(`Are you sure you want to delete: ${colors.blue(stacks.map(s => s.name).join(', '))} (y/n)?`);
347-
if (!confirmed) {
348-
return;
349-
}
350-
}
351-
352-
for (const stack of stacks) {
353-
success('%s: destroying...', colors.blue(stack.name));
354-
try {
355-
await destroyStack({ stack, sdk: aws, deployName: stack.name, roleArn });
356-
success('\n ✅ %s: destroyed', colors.blue(stack.name));
357-
} catch (e) {
358-
error('\n ❌ %s: destroy failed', colors.blue(stack.name), e);
359-
throw e;
360-
}
361-
}
362-
}
363-
364339
/**
365340
* Match a single stack from the list of available stacks
366341
*/

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

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import colors = require('colors/safe');
22
import fs = require('fs-extra');
33
import { format } from 'util';
4+
import { Mode } from './api/aws-auth/credentials';
45
import { AppStacks, DefaultSelection, ExtendedStackSelection, Tag } from "./api/cxapp/stacks";
6+
import { destroyStack } from './api/deploy-stack';
57
import { IDeploymentTarget } from './api/deployment-target';
8+
import { stackExists } from './api/util/cloudformation';
9+
import { ISDK } from './api/util/sdk';
610
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';
812
import { deserializeStructure } from './serialize';
913

1014
// tslint:disable-next-line:no-var-requires
@@ -90,6 +94,24 @@ export class CdkToolkit {
9094
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.`);
9195
}
9296

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+
93115
if (requireApproval !== RequireApproval.Never) {
94116
const currentTemplate = await this.provisioner.readCurrentTemplate(stack);
95117
if (printSecurityDiff(currentTemplate, stack, requireApproval)) {
@@ -152,6 +174,36 @@ export class CdkToolkit {
152174
}
153175
}
154176
}
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+
}
155207
}
156208

157209
export interface DiffOptions {
@@ -244,4 +296,41 @@ export interface DeployOptions {
244296
* Tags to pass to CloudFormation for deployment
245297
*/
246298
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
247336
}

packages/aws-cdk/test/integ/cli/app/app.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,16 @@ class ImportVpcStack extends cdk.Stack {
124124
}
125125
}
126126

127+
class ConditionalResourceStack extends cdk.Stack {
128+
constructor(parent, id, props) {
129+
super(parent, id, props);
130+
131+
if (!process.env.NO_RESOURCE) {
132+
new iam.User(this, 'User');
133+
}
134+
}
135+
}
136+
127137
const stackPrefix = process.env.STACK_NAME_PREFIX || 'cdk-toolkit-integration';
128138

129139
const app = new cdk.App();
@@ -154,4 +164,6 @@ if (process.env.ENABLE_VPC_TESTING) { // Gating so we don't do context fetching
154164
new ImportVpcStack(app, `${stackPrefix}-import-vpc`, { env });
155165
}
156166

167+
new ConditionalResourceStack(app, `${stackPrefix}-conditional-resource`)
168+
157169
app.synth();
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
scriptdir=$(cd $(dirname $0) && pwd)
4+
source ${scriptdir}/common.bash
5+
# ----------------------------------------------------------
6+
7+
setup
8+
9+
# Deploy without resource
10+
NO_RESOURCE="TRUE" cdk deploy ${STACK_NAME_PREFIX}-conditional-resource
11+
12+
# Verify that deploy has been skipped
13+
deployed=1
14+
aws cloudformation describe-stacks --stack-name ${STACK_NAME_PREFIX}-conditional-resource > /dev/null 2>&1 || deployed=0
15+
16+
if [ $deployed -ne 0 ]; then
17+
fail 'Stack has been deployed'
18+
fi
19+
20+
# Deploy the stack with resources
21+
cdk deploy ${STACK_NAME_PREFIX}-conditional-resource
22+
23+
# Now, deploy the stack without resources
24+
NO_RESOURCE="TRUE" cdk deploy ${STACK_NAME_PREFIX}-conditional-resource
25+
26+
# Verify that the stack has been destroyed
27+
destroyed=0
28+
aws cloudformation describe-stacks --stack-name ${STACK_NAME_PREFIX}-conditional-resource > /dev/null 2>&1 || destroyed=1
29+
30+
if [ $destroyed -ne 1 ]; then
31+
fail 'Stack has not been destroyed'
32+
fi
33+

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import nodeunit = require('nodeunit');
33
import { AppStacks, Tag } from '../lib/api/cxapp/stacks';
44
import { DeployStackResult } from '../lib/api/deploy-stack';
55
import { DeployStackOptions, IDeploymentTarget, Template } from '../lib/api/deployment-target';
6+
import { SDK } from '../lib/api/util/sdk';
67
import { CdkToolkit } from '../lib/cdk-toolkit';
78

89
export = nodeunit.testCase({
@@ -19,7 +20,7 @@ export = nodeunit.testCase({
1920
});
2021

2122
// WHEN
22-
toolkit.deploy({ stackNames: ['Test-Stack-A', 'Test-Stack-B'] });
23+
toolkit.deploy({ stackNames: ['Test-Stack-A', 'Test-Stack-B'], sdk: new SDK() });
2324

2425
// THEN
2526
test.done();

0 commit comments

Comments
 (0)