Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): option to ignore no stacks #28387

Merged
merged 10 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
}));

msambol marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -382,6 +382,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 @@ -198,6 +198,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 @@ -175,7 +175,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 @@ -305,6 +306,7 @@ export class CdkToolkit {
hotswap: options.hotswap,
extraUserAgent: options.extraUserAgent,
assetParallelism: options.assetParallelism,
ignoreNoStacks: options.ignoreNoStacks,
});

const message = result.noOp
Expand Down Expand Up @@ -479,7 +481,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 @@ -729,11 +731,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 @@ -1134,6 +1138,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 @@ -583,6 +584,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
Loading