Skip to content

Commit

Permalink
feat(cli): option to ignore no stacks (#28387)
Browse files Browse the repository at this point in the history
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*
  • Loading branch information
msambol authored Jan 10, 2024
1 parent 783f610 commit 37c79b9
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,9 @@ switch (stackSet) {
stage.synth({ validateOnSynthesis: true });
break;

case 'stage-with-no-stacks':
break;

default:
throw new Error(`Unrecognized INTEG_STACK_SET: '${stackSet}'`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,25 @@ integTest('deploy stack without resource', withDefaultFixture(async (fixture) =>
.rejects.toThrow('conditional-resource does not exist');
}));

integTest('deploy no stacks with --ignore-no-stacks', withDefaultFixture(async (fixture) => {
// empty array for stack names
await fixture.cdkDeploy([], {
options: ['--ignore-no-stacks'],
modEnv: {
INTEG_STACK_SET: 'stage-with-no-stacks',
},
});
}));

integTest('deploy no stacks error', withDefaultFixture(async (fixture) => {
// empty array for stack names
await expect(fixture.cdkDeploy([], {
modEnv: {
INTEG_STACK_SET: 'stage-with-no-stacks',
},
})).rejects.toThrow('exited with error');
}));

integTest('IAM diff', withDefaultFixture(async (fixture) => {
const output = await fixture.cdk(['diff', fixture.fullStackName('iam-test')]);

Expand Down
14 changes: 14 additions & 0 deletions packages/aws-cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,20 @@ $ cdk deploy --method=prepare-change-set --change-set-name MyChangeSetName
For more control over when stack changes are deployed, the CDK can generate a
CloudFormation change set but not execute it.

#### Ignore No Stacks

You may have an app with multiple environments, e.g., dev and prod. When starting
development, your prod app may not have any resources or the resources are commented
out. In this scenario, you will receive an error message stating that the app has no
stacks.

To bypass this error messages, you can pass the `--ignore-no-stacks` flag to the
`deploy` command:

```console
$ cdk deploy --ignore-no-stacks
```

#### Hotswap deployments for faster development

You can pass the `--hotswap` flag to the `deploy` command:
Expand Down
12 changes: 11 additions & 1 deletion packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,16 @@ export interface SelectStacksOptions {
extend?: ExtendedStackSelection;

/**
* The behavior if if no selectors are privided.
* The behavior if if no selectors are provided.
*/
defaultBehavior: DefaultSelection;

/**
* Whether to deploy if the app contains no stacks.
*
* @default false
*/
ignoreNoStacks?: boolean;
}

/**
Expand Down Expand Up @@ -100,6 +107,9 @@ export class CloudAssembly {
const patterns = sanitizePatterns(selector.patterns);

if (stacks.length === 0) {
if (options.ignoreNoStacks) {
return new StackCollection(this, []);
}
throw new Error('This app contains no stacks');
}

Expand Down
7 changes: 7 additions & 0 deletions packages/aws-cdk/lib/api/deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,13 @@ export interface DeployStackOptions {
* @default true To remain backward compatible.
*/
readonly assetParallelism?: boolean;

/**
* Whether to deploy if the app contains no stacks.
*
* @default false
*/
ignoreNoStacks?: boolean;
}

interface AssetOptions {
Expand Down
17 changes: 14 additions & 3 deletions packages/aws-cdk/lib/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@ export class CdkToolkit {
}

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

Expand Down Expand Up @@ -317,6 +318,7 @@ export class CdkToolkit {
hotswap: options.hotswap,
extraUserAgent: options.extraUserAgent,
assetParallelism: options.assetParallelism,
ignoreNoStacks: options.ignoreNoStacks,
});

const message = result.noOp
Expand Down Expand Up @@ -491,7 +493,7 @@ export class CdkToolkit {
}

public async import(options: ImportOptions) {
const stacks = await this.selectStacksForDeploy(options.selector, true, true);
const stacks = await this.selectStacksForDeploy(options.selector, true, true, false);

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

private async selectStacksForDeploy(selector: StackSelector, exclusively?: boolean, cacheCloudAssembly?: boolean): Promise<StackCollection> {
private async selectStacksForDeploy(selector: StackSelector, exclusively?: boolean,
cacheCloudAssembly?: boolean, ignoreNoStacks?: boolean): Promise<StackCollection> {
const assembly = await this.assembly(cacheCloudAssembly);
const stacks = await assembly.selectStacks(selector, {
extend: exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Upstream,
defaultBehavior: DefaultSelection.OnlySingle,
ignoreNoStacks,
});

this.validateStacksSelected(stacks, selector.patterns);
Expand Down Expand Up @@ -1159,6 +1163,13 @@ export interface DeployOptions extends CfnDeployOptions, WatchOptions {
* @default AssetBuildTime.ALL_BEFORE_DEPLOY
*/
readonly assetBuildTime?: AssetBuildTime;

/**
* Whether to deploy if the app contains no stacks.
*
* @default false
*/
readonly ignoreNoStacks?: boolean;
}

export interface ImportOptions extends CfnDeployOptions {
Expand Down
4 changes: 3 additions & 1 deletion packages/aws-cdk/lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ async function parseCommandLineArguments(args: string[]) {
})
.option('concurrency', { type: 'number', desc: 'Maximum number of simultaneous deployments (dependency permitting) to execute.', default: 1, requiresArg: true })
.option('asset-parallelism', { type: 'boolean', desc: 'Whether to build/publish assets in parallel' })
.option('asset-prebuild', { type: 'boolean', desc: 'Whether to build all assets before deploying the first stack (useful for failing Docker builds)', default: true }),
.option('asset-prebuild', { type: 'boolean', desc: 'Whether to build all assets before deploying the first stack (useful for failing Docker builds)', default: true })
.option('ignore-no-stacks', { type: 'boolean', desc: 'Whether to deploy if the app contains no stacks', default: false }),
)
.command('import [STACK]', 'Import existing resource(s) into the given STACK', (yargs: Argv) => yargs
.option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true })
Expand Down Expand Up @@ -585,6 +586,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
concurrency: args.concurrency,
assetParallelism: configuration.settings.get(['assetParallelism']),
assetBuildTime: configuration.settings.get(['assetPrebuild']) ? AssetBuildTime.ALL_BEFORE_DEPLOY : AssetBuildTime.JUST_IN_TIME,
ignoreNoStacks: args.ignoreNoStacks,
});

case 'import':
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ export class Settings {
notices: argv.notices,
assetParallelism: argv['asset-parallelism'],
assetPrebuild: argv['asset-prebuild'],
ignoreNoStacks: argv['ignore-no-stacks'],
});
}

Expand Down
40 changes: 40 additions & 0 deletions packages/aws-cdk/test/api/cloud-assembly.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,38 @@ test('select behavior with nested assemblies: repeat', async() => {
expect(x.stackCount).toBe(2);
});

test('select behavior with no stacks and ignore stacks option', async() => {
// GIVEN
const cxasm = await testCloudAssemblyNoStacks();

// WHEN
const x = await cxasm.selectStacks({ patterns: [] }, {
defaultBehavior: DefaultSelection.AllStacks,
ignoreNoStacks: true,
});

// THEN
expect(x.stackCount).toBe(0);
});

test('select behavior with no stacks and no ignore stacks option', async() => {
// GIVEN
const cxasm = await testCloudAssemblyNoStacks();

// WHEN & THEN
await expect(cxasm.selectStacks({ patterns: [] }, { defaultBehavior: DefaultSelection.AllStacks, ignoreNoStacks: false }))
.rejects.toThrow('This app contains no stacks');
});

test('select behavior with no stacks and default ignore stacks options (false)', async() => {
// GIVEN
const cxasm = await testCloudAssemblyNoStacks();

// WHEN & THEN
await expect(cxasm.selectStacks({ patterns: [] }, { defaultBehavior: DefaultSelection.AllStacks }))
.rejects.toThrow('This app contains no stacks');
});

async function testCloudAssembly({ env }: { env?: string, versionReporting?: boolean } = {}) {
const cloudExec = new MockCloudExecutable({
stacks: [{
Expand All @@ -182,6 +214,14 @@ async function testCloudAssembly({ env }: { env?: string, versionReporting?: boo
return cloudExec.synthesize();
}

async function testCloudAssemblyNoStacks() {
const cloudExec = new MockCloudExecutable({
stacks: [],
});

return cloudExec.synthesize();
}

async function testNestedCloudAssembly({ env }: { env?: string, versionReporting?: boolean } = {}) {
const cloudExec = new MockCloudExecutable({
stacks: [{
Expand Down

0 comments on commit 37c79b9

Please sign in to comment.