Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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 @@ -993,6 +993,10 @@ switch (stackSet) {
case 'stage-with-no-stacks':
break;

case 'stage-only':
new SomeStage(app, `${stackPrefix}-stage`);
break;

default:
throw new Error(`Unrecognized INTEG_STACK_SET: '${stackSet}'`);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation';
import { integTest, withDefaultFixture } from '../../../lib';

integTest('cdk destroy can destroy stacks in stage-only configuration with --all option', withDefaultFixture(async (fixture) => {
const integStackSet = 'stage-only';

await fixture.cdkDeploy([], {
options: ['--all'],
modEnv: {
INTEG_STACK_SET: integStackSet,
},
});

const stackName = `${fixture.fullStackName('stage')}-StackInStage`;
const stack = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: stackName }));
expect(stack.Stacks?.length ?? 0).toEqual(1);

await fixture.cdkDestroy([], {
options: ['--all'],
modEnv: {
INTEG_STACK_SET: integStackSet,
},
});

await expect(fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: stackName })))
.rejects.toThrow(/does not exist/);
}));
3 changes: 3 additions & 0 deletions packages/aws-cdk/lib/cxapp/cloud-assembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ export class CloudAssembly extends BaseStackAssembly {
): Promise<StackCollection> {
if (topLevelStacks.length > 0) {
return this.extendStacks(topLevelStacks, stacks, extend);
} else if (stacks.length > 0) {
// Fallback to all stacks if no top-level stacks (e.g., app including stages only in top-level assembly)
return this.extendStacks(stacks, stacks, extend);
} else {
throw new ToolkitError('No stack found in the main cloud assembly. Use "list" to print manifest');
}
Expand Down
56 changes: 48 additions & 8 deletions packages/aws-cdk/test/cli/cdk-toolkit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,26 @@ function defaultToolkitSetup() {
});
}

// only stacks within stages (no top-level stacks)
async function stageOnlyToolkitSetup() {
const stageOnlyExecutable = await MockCloudExecutable.create({
stacks: [],
nestedAssemblies: [{
stacks: [MockStack.MOCK_STACK_C],
}],
});

return new CdkToolkit({
ioHost,
cloudExecutable: stageOnlyExecutable,
configuration: stageOnlyExecutable.configuration,
sdkProvider: stageOnlyExecutable.sdkProvider,
deployments: new FakeCloudFormation({
'Test-Stack-C': { Baz: 'Zinga!' },
}),
});
}

const mockSdk = new MockSdk();

describe('bootstrap', () => {
Expand Down Expand Up @@ -284,6 +304,17 @@ describe('deploy', () => {
});
});

test('deploy all stacks in stage-only configuration with --all option', async () => {
// GIVEN
const toolkit = await stageOnlyToolkitSetup();

// WHEN & THEN
await expect(toolkit.deploy({
selector: { patterns: [], allTopLevel: true },
deploymentMethod: { method: 'change-set' },
})).resolves.not.toThrow();
});

test('uses display names to reference assets', async () => {
// GIVEN
cloudExecutable = await MockCloudExecutable.create({
Expand Down Expand Up @@ -1080,14 +1111,23 @@ describe('destroy', () => {
test('destroy correct stack', async () => {
const toolkit = defaultToolkitSetup();

expect(() => {
return toolkit.destroy({
selector: { patterns: ['Test-Stack-A/Test-Stack-C'] },
exclusively: true,
force: true,
fromDeploy: true,
});
}).resolves;
await expect(toolkit.destroy({
selector: { patterns: ['Test-Stack-A/Test-Stack-C'] },
exclusively: true,
force: true,
fromDeploy: true,
})).resolves.not.toThrow();
});

test('destroy all stacks in stage-only configuration with --all option', async () => {
const toolkit = await stageOnlyToolkitSetup();

await expect(toolkit.destroy({
selector: { patterns: [], allTopLevel: true },
exclusively: true,
force: true,
fromDeploy: true,
})).resolves.not.toThrow();
});
});

Expand Down
38 changes: 38 additions & 0 deletions packages/aws-cdk/test/cxapp/cloud-assembly.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ test('select all top level stacks in the presence of nested assemblies', async (
expect(x.stackIds).toContain('withouterrors');
});

test('select all stacks when only nested assemblies exist (Stage-only apps)', async () => {
// GIVEN
const cxasm = await testStageOnlyCloudAssembly();

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

// THEN
expect(x.stackCount).toBe(2);
expect(x.stackIds).toContain('stack1');
expect(x.stackIds).toContain('stack2');
});

test('select stacks by glob pattern', async () => {
// GIVEN
const cxasm = await testCloudAssembly();
Expand Down Expand Up @@ -337,3 +350,28 @@ async function testNestedCloudAssembly({ env }: { env?: string; versionReporting
const asm = await cloudExec.synthesize();
return cliAssemblyWithForcedVersion(asm, '30.0.0');
}

async function testStageOnlyCloudAssembly({ env }: { env?: string; versionReporting?: boolean } = {}) {
const cloudExec = await MockCloudExecutable.create({
stacks: [], // No top-level stacks
nestedAssemblies: [{
stacks: [{
stackName: 'stack1',
displayName: 'stage1/stack1',
env,
template: { resource: 'resource1' },
}],
},
{
stacks: [{
stackName: 'stack2',
displayName: 'stage2/stack2',
env,
template: { resource: 'resource2' },
}],
}],
});

const asm = await cloudExec.synthesize();
return cliAssemblyWithForcedVersion(asm, '30.0.0');
}
Loading