Skip to content

Commit 37c79b9

Browse files
authored
feat(cli): option to ignore no stacks (#28387)
I'm new to development on this package—any feedback regarding testing is appreciated. Closes #28371. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 783f610 commit 37c79b9

File tree

9 files changed

+112
-5
lines changed

9 files changed

+112
-5
lines changed

packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js

+3
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,9 @@ switch (stackSet) {
492492
stage.synth({ validateOnSynthesis: true });
493493
break;
494494

495+
case 'stage-with-no-stacks':
496+
break;
497+
495498
default:
496499
throw new Error(`Unrecognized INTEG_STACK_SET: '${stackSet}'`);
497500
}

packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts

+19
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,25 @@ integTest('deploy stack without resource', withDefaultFixture(async (fixture) =>
780780
.rejects.toThrow('conditional-resource does not exist');
781781
}));
782782

783+
integTest('deploy no stacks with --ignore-no-stacks', withDefaultFixture(async (fixture) => {
784+
// empty array for stack names
785+
await fixture.cdkDeploy([], {
786+
options: ['--ignore-no-stacks'],
787+
modEnv: {
788+
INTEG_STACK_SET: 'stage-with-no-stacks',
789+
},
790+
});
791+
}));
792+
793+
integTest('deploy no stacks error', withDefaultFixture(async (fixture) => {
794+
// empty array for stack names
795+
await expect(fixture.cdkDeploy([], {
796+
modEnv: {
797+
INTEG_STACK_SET: 'stage-with-no-stacks',
798+
},
799+
})).rejects.toThrow('exited with error');
800+
}));
801+
783802
integTest('IAM diff', withDefaultFixture(async (fixture) => {
784803
const output = await fixture.cdk(['diff', fixture.fullStackName('iam-test')]);
785804

packages/aws-cdk/README.md

+14
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,20 @@ $ cdk deploy --method=prepare-change-set --change-set-name MyChangeSetName
386386
For more control over when stack changes are deployed, the CDK can generate a
387387
CloudFormation change set but not execute it.
388388

389+
#### Ignore No Stacks
390+
391+
You may have an app with multiple environments, e.g., dev and prod. When starting
392+
development, your prod app may not have any resources or the resources are commented
393+
out. In this scenario, you will receive an error message stating that the app has no
394+
stacks.
395+
396+
To bypass this error messages, you can pass the `--ignore-no-stacks` flag to the
397+
`deploy` command:
398+
399+
```console
400+
$ cdk deploy --ignore-no-stacks
401+
```
402+
389403
#### Hotswap deployments for faster development
390404

391405
You can pass the `--hotswap` flag to the `deploy` command:

packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,16 @@ export interface SelectStacksOptions {
3838
extend?: ExtendedStackSelection;
3939

4040
/**
41-
* The behavior if if no selectors are privided.
41+
* The behavior if if no selectors are provided.
4242
*/
4343
defaultBehavior: DefaultSelection;
44+
45+
/**
46+
* Whether to deploy if the app contains no stacks.
47+
*
48+
* @default false
49+
*/
50+
ignoreNoStacks?: boolean;
4451
}
4552

4653
/**
@@ -100,6 +107,9 @@ export class CloudAssembly {
100107
const patterns = sanitizePatterns(selector.patterns);
101108

102109
if (stacks.length === 0) {
110+
if (options.ignoreNoStacks) {
111+
return new StackCollection(this, []);
112+
}
103113
throw new Error('This app contains no stacks');
104114
}
105115

packages/aws-cdk/lib/api/deployments.ts

+7
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,13 @@ export interface DeployStackOptions {
199199
* @default true To remain backward compatible.
200200
*/
201201
readonly assetParallelism?: boolean;
202+
203+
/**
204+
* Whether to deploy if the app contains no stacks.
205+
*
206+
* @default false
207+
*/
208+
ignoreNoStacks?: boolean;
202209
}
203210

204211
interface AssetOptions {

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

+14-3
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,8 @@ export class CdkToolkit {
200200
}
201201

202202
const startSynthTime = new Date().getTime();
203-
const stackCollection = await this.selectStacksForDeploy(options.selector, options.exclusively, options.cacheCloudAssembly);
203+
const stackCollection = await this.selectStacksForDeploy(options.selector, options.exclusively,
204+
options.cacheCloudAssembly, options.ignoreNoStacks);
204205
const elapsedSynthTime = new Date().getTime() - startSynthTime;
205206
print('\n✨ Synthesis time: %ss\n', formatTime(elapsedSynthTime));
206207

@@ -317,6 +318,7 @@ export class CdkToolkit {
317318
hotswap: options.hotswap,
318319
extraUserAgent: options.extraUserAgent,
319320
assetParallelism: options.assetParallelism,
321+
ignoreNoStacks: options.ignoreNoStacks,
320322
});
321323

322324
const message = result.noOp
@@ -491,7 +493,7 @@ export class CdkToolkit {
491493
}
492494

493495
public async import(options: ImportOptions) {
494-
const stacks = await this.selectStacksForDeploy(options.selector, true, true);
496+
const stacks = await this.selectStacksForDeploy(options.selector, true, true, false);
495497

496498
if (stacks.stackCount > 1) {
497499
throw new Error(`Stack selection is ambiguous, please choose a specific stack for import [${stacks.stackArtifacts.map(x => x.id).join(', ')}]`);
@@ -741,11 +743,13 @@ export class CdkToolkit {
741743
return stacks;
742744
}
743745

744-
private async selectStacksForDeploy(selector: StackSelector, exclusively?: boolean, cacheCloudAssembly?: boolean): Promise<StackCollection> {
746+
private async selectStacksForDeploy(selector: StackSelector, exclusively?: boolean,
747+
cacheCloudAssembly?: boolean, ignoreNoStacks?: boolean): Promise<StackCollection> {
745748
const assembly = await this.assembly(cacheCloudAssembly);
746749
const stacks = await assembly.selectStacks(selector, {
747750
extend: exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Upstream,
748751
defaultBehavior: DefaultSelection.OnlySingle,
752+
ignoreNoStacks,
749753
});
750754

751755
this.validateStacksSelected(stacks, selector.patterns);
@@ -1159,6 +1163,13 @@ export interface DeployOptions extends CfnDeployOptions, WatchOptions {
11591163
* @default AssetBuildTime.ALL_BEFORE_DEPLOY
11601164
*/
11611165
readonly assetBuildTime?: AssetBuildTime;
1166+
1167+
/**
1168+
* Whether to deploy if the app contains no stacks.
1169+
*
1170+
* @default false
1171+
*/
1172+
readonly ignoreNoStacks?: boolean;
11621173
}
11631174

11641175
export interface ImportOptions extends CfnDeployOptions {

packages/aws-cdk/lib/cli.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ async function parseCommandLineArguments(args: string[]) {
172172
})
173173
.option('concurrency', { type: 'number', desc: 'Maximum number of simultaneous deployments (dependency permitting) to execute.', default: 1, requiresArg: true })
174174
.option('asset-parallelism', { type: 'boolean', desc: 'Whether to build/publish assets in parallel' })
175-
.option('asset-prebuild', { type: 'boolean', desc: 'Whether to build all assets before deploying the first stack (useful for failing Docker builds)', default: true }),
175+
.option('asset-prebuild', { type: 'boolean', desc: 'Whether to build all assets before deploying the first stack (useful for failing Docker builds)', default: true })
176+
.option('ignore-no-stacks', { type: 'boolean', desc: 'Whether to deploy if the app contains no stacks', default: false }),
176177
)
177178
.command('import [STACK]', 'Import existing resource(s) into the given STACK', (yargs: Argv) => yargs
178179
.option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true })
@@ -585,6 +586,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
585586
concurrency: args.concurrency,
586587
assetParallelism: configuration.settings.get(['assetParallelism']),
587588
assetBuildTime: configuration.settings.get(['assetPrebuild']) ? AssetBuildTime.ALL_BEFORE_DEPLOY : AssetBuildTime.JUST_IN_TIME,
589+
ignoreNoStacks: args.ignoreNoStacks,
588590
});
589591

590592
case 'import':

packages/aws-cdk/lib/settings.ts

+1
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ export class Settings {
292292
notices: argv.notices,
293293
assetParallelism: argv['asset-parallelism'],
294294
assetPrebuild: argv['asset-prebuild'],
295+
ignoreNoStacks: argv['ignore-no-stacks'],
295296
});
296297
}
297298

packages/aws-cdk/test/api/cloud-assembly.test.ts

+40
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,38 @@ test('select behavior with nested assemblies: repeat', async() => {
156156
expect(x.stackCount).toBe(2);
157157
});
158158

159+
test('select behavior with no stacks and ignore stacks option', async() => {
160+
// GIVEN
161+
const cxasm = await testCloudAssemblyNoStacks();
162+
163+
// WHEN
164+
const x = await cxasm.selectStacks({ patterns: [] }, {
165+
defaultBehavior: DefaultSelection.AllStacks,
166+
ignoreNoStacks: true,
167+
});
168+
169+
// THEN
170+
expect(x.stackCount).toBe(0);
171+
});
172+
173+
test('select behavior with no stacks and no ignore stacks option', async() => {
174+
// GIVEN
175+
const cxasm = await testCloudAssemblyNoStacks();
176+
177+
// WHEN & THEN
178+
await expect(cxasm.selectStacks({ patterns: [] }, { defaultBehavior: DefaultSelection.AllStacks, ignoreNoStacks: false }))
179+
.rejects.toThrow('This app contains no stacks');
180+
});
181+
182+
test('select behavior with no stacks and default ignore stacks options (false)', async() => {
183+
// GIVEN
184+
const cxasm = await testCloudAssemblyNoStacks();
185+
186+
// WHEN & THEN
187+
await expect(cxasm.selectStacks({ patterns: [] }, { defaultBehavior: DefaultSelection.AllStacks }))
188+
.rejects.toThrow('This app contains no stacks');
189+
});
190+
159191
async function testCloudAssembly({ env }: { env?: string, versionReporting?: boolean } = {}) {
160192
const cloudExec = new MockCloudExecutable({
161193
stacks: [{
@@ -182,6 +214,14 @@ async function testCloudAssembly({ env }: { env?: string, versionReporting?: boo
182214
return cloudExec.synthesize();
183215
}
184216

217+
async function testCloudAssemblyNoStacks() {
218+
const cloudExec = new MockCloudExecutable({
219+
stacks: [],
220+
});
221+
222+
return cloudExec.synthesize();
223+
}
224+
185225
async function testNestedCloudAssembly({ env }: { env?: string, versionReporting?: boolean } = {}) {
186226
const cloudExec = new MockCloudExecutable({
187227
stacks: [{

0 commit comments

Comments
 (0)