Skip to content

Commit

Permalink
feat(cli): directly deploy stacks in nested assemblies (#14379)
Browse files Browse the repository at this point in the history
This change allows users to address stacks located in nested assemblies, which is the common use case when dealing with pipelines. For example, suppose we have only two stacks, `stack1` and `stack1/foo/bar`, and the second one is in a sub-assembly. The table below shows the behavior of the CLI before and after:

| **Subcommand**               | **Pattern**    | **Matched stacks (before)** | **Matched stacks (after)** |
|------------------------------|----------------|-----------------------------|----------------------------|
| deploy, destroy, diff, synth | ε              | stack1                      | stack1                     |
| list                         | ε              | stack1                      | stack1, stack1/foo/bar     |
| metadata                     | ε              | ∅                           | ∅                          |
| _any_                        | *, --all       | stack1                      | stack1                     |
| _any_                        | **             | stack1                      | stack1, stack1/foo/bar     |
| _any_                        | stack1/**      | ∅                           | stack1/foo/bar             |
| _any_                        | stack1         | stack1                      | stack1                     |
| _any_                        | stack1/foo/bar | ∅                           | stack1/foo/bar             |

where:
ε = empty string (no parameters)
∅ = empty results/error

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
otaviomacedo authored Apr 29, 2021
1 parent 80d2324 commit 5a6fa7f
Show file tree
Hide file tree
Showing 21 changed files with 316 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ export interface ArtifactManifest {
* @default - no properties.
*/
readonly properties?: ArtifactProperties;

/**
* A string that represents this artifact. Should only be used in user interfaces.
*
* @default - no display name
*/
readonly displayName?: string;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@
"$ref": "#/definitions/NestedCloudAssemblyProperties"
}
]
},
"displayName": {
"description": "A string that represents this artifact. Should only be used in user interfaces. (Default - no display name)",
"type": "string"
}
},
"required": [
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":"9.0.0"}
{"version":"10.0.0"}
1 change: 1 addition & 0 deletions packages/@aws-cdk/core/lib/stack-synthesizers/_shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function addStackArtifactToAssembly(
properties,
dependencies: deps.length > 0 ? deps : undefined,
metadata: Object.keys(meta).length > 0 ? meta : undefined,
displayName: stack.node.path,
});
}

Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/core/test/synthesis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ nodeunitShim({
type: 'aws:cloudformation:stack',
environment: 'aws://unknown-account/unknown-region',
properties: { templateFile: 'one-stack.template.json' },
displayName: 'one-stack',
},
},
});
Expand Down
9 changes: 9 additions & 0 deletions packages/@aws-cdk/cx-api/lib/cloud-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,15 @@ export class CloudArtifact {

return messages;
}

/**
* An identifier that shows where this artifact is located in the tree
* of nested assemblies, based on their manifests. Defaults to the normal
* id. Should only be used in user interfaces.
*/
public get hierarchicalId(): string {
return this.manifest.displayName ?? this.id;
}
}

// needs to be defined at the end to avoid a cyclic dependency
Expand Down
24 changes: 23 additions & 1 deletion packages/@aws-cdk/cx-api/lib/cloud-assembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ export class CloudAssembly {
* @returns a `CloudFormationStackArtifact` object.
*/
public getStackArtifact(artifactId: string): CloudFormationStackArtifact {
const artifact = this.tryGetArtifact(artifactId);
const artifact = this.tryGetArtifactRecursively(artifactId);

if (!artifact) {
throw new Error(`Unable to find artifact with id "${artifactId}"`);
}
Expand All @@ -120,6 +121,27 @@ export class CloudAssembly {
return artifact;
}

private tryGetArtifactRecursively(artifactId: string): CloudArtifact | undefined {
return this.stacksRecursively.find(a => a.id === artifactId);
}

/**
* Returns all the stacks, including the ones in nested assemblies
*/
public get stacksRecursively(): CloudFormationStackArtifact[] {
function search(stackArtifacts: CloudFormationStackArtifact[], assemblies: CloudAssembly[]): CloudFormationStackArtifact[] {
if (assemblies.length === 0) {
return stackArtifacts;
}

const [head, ...tail] = assemblies;
const nestedAssemblies = head.nestedAssemblies.map(asm => asm.nestedAssembly);
return search(stackArtifacts.concat(head.stacks), tail.concat(nestedAssemblies));
};

return search([], [this]);
}

/**
* Returns a nested assembly artifact.
*
Expand Down
11 changes: 11 additions & 0 deletions packages/@aws-cdk/cx-api/test/cloud-assembly.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,14 @@ test('can read assembly with asset manifest', () => {
expect(assembly.stacks).toHaveLength(1);
expect(assembly.artifacts).toHaveLength(2);
});

test('getStackArtifact retrieves a stack by artifact id from a nested assembly', () => {
const assembly = new CloudAssembly(path.join(FIXTURES, 'nested-assemblies'));

expect(assembly.getStackArtifact('topLevelStack').stackName).toEqual('topLevelStack');
expect(assembly.getStackArtifact('stack1').stackName).toEqual('first-stack');
expect(assembly.getStackArtifact('stack2').stackName).toEqual('second-stack');
expect(assembly.getStackArtifact('topLevelStack').id).toEqual('topLevelStack');
expect(assembly.getStackArtifact('stack1').id).toEqual('stack1');
expect(assembly.getStackArtifact('stack2').id).toEqual('stack2');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"version": "0.0.0",
"artifacts": {
"subassembly": {
"type": "cdk:cloud-assembly",
"properties": {
"directoryName": "subassembly",
"displayName": "subassembly"
}
},
"topLevelStack": {
"type": "aws:cloudformation:stack",
"environment": "aws://111111111111/us-east-1",
"properties": {
"templateFile": "topLevelStack.template.json",
"stackName": "topLevelStack"
},
"displayName": "topLevelStack"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"version": "0.0.0",
"artifacts": {
"subsubassembly": {
"type": "cdk:cloud-assembly",
"properties": {
"directoryName": "subsubassembly",
"displayName": "subsubassembly"
}
},
"stack1": {
"type": "aws:cloudformation:stack",
"environment": "aws://37736633/us-region-1",
"properties": {
"templateFile": "template.json",
"stackName": "first-stack"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "0.0.0",
"artifacts": {
"stack2": {
"type": "aws:cloudformation:stack",
"environment": "aws://37736633/us-region-1",
"properties": {
"templateFile": "template.2.json",
"stackName": "second-stack"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"Resources": {
"MyBucket": {
"Type": "AWS::S3::Bucket"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"Resources": {
"MyBucket": {
"Type": "AWS::S3::Bucket"
}
}
}
Empty file.
27 changes: 27 additions & 0 deletions packages/@aws-cdk/cx-api/test/stack-artifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,30 @@ test('read tags from stack metadata', () => {
// THEN
expect(assembly.getStackByName('Stack').tags).toEqual({ foo: 'bar' });
});

test('user friendly id is the assembly display name', () => {
// GIVEN
builder.addArtifact('Stack', {
...stackBase,
displayName: 'some/path/to/the/stack',
});

// WHEN
const assembly = builder.buildAssembly();

// THEN
expect(assembly.getStackByName('Stack').hierarchicalId).toEqual('some/path/to/the/stack');
});

test('user friendly id is the id itself if no display name is given', () => {
// GIVEN
builder.addArtifact('Stack', {
...stackBase,
});

// WHEN
const assembly = builder.buildAssembly();

// THEN
expect(assembly.getStackByName('Stack').hierarchicalId).toEqual('Stack');
});
4 changes: 2 additions & 2 deletions packages/aws-cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,9 @@ and always deploy the stack.

You can have multiple stacks in a cdk app. An example can be found in [how to create multiple stacks](https://docs.aws.amazon.com/cdk/latest/guide/stack_how_to_create_multiple_stacks.html).

In order to deploy them, you can list the stacks you want to deploy.
In order to deploy them, you can list the stacks you want to deploy. If your application contains pipeline stacks, the `cdk list` command will show stack names as paths, showing where they are in the pipeline hierarchy (e.g., `PipelineStack`, `PipelineStack/Prod`, `PipelineStack/Prod/MyService` etc).

If you want to deploy all of them, you can use the flag `--all` or the wildcard `*` to deploy all stacks in an app.
If you want to deploy all of them, you can use the flag `--all` or the wildcard `*` to deploy all stacks in an app. Please note that, if you have a hierarchy of stacks as described above, `--all` and `*` will only match the stacks on the top level. If you want to match all the stacks in the hierarchy, use `**`. You can also combine these patterns. For example, if you want to deploy all stacks in the `Prod` stage, you can use `cdk deploy PipelineStack/Prod/**`.

#### Parameters

Expand Down
35 changes: 26 additions & 9 deletions packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import * as cxapi from '@aws-cdk/cx-api';
import * as colors from 'colors/safe';
import * as minimatch from 'minimatch';
import * as semver from 'semver';
import { error, print, warning } from '../../logging';
import { versionNumber } from '../../version';

export enum DefaultSelection {
/**
Expand All @@ -16,7 +18,13 @@ export enum DefaultSelection {
OnlySingle = 'single',

/**
* If no selectors are provided, returns all stacks in the app.
* Returns all stacks in the main (top level) assembly only.
*/
MainAssembly = 'main',

/**
* If no selectors are provided, returns all stacks in the app,
* including stacks inside nested assemblies.
*/
AllStacks = 'all',
}
Expand Down Expand Up @@ -71,20 +79,23 @@ export class CloudAssembly {
selectors = selectors.filter(s => s != null); // filter null/undefined
selectors = [...new Set(selectors)]; // make them unique

const stacks = this.assembly.stacks;
const stacks = this.assembly.stacksRecursively;
if (stacks.length === 0) {
throw new Error('This app contains no stacks');
}

if (selectors.length === 0) {
const topLevelStacks = this.assembly.stacks;
switch (options.defaultBehavior) {
case DefaultSelection.MainAssembly:
return new StackCollection(this, topLevelStacks);
case DefaultSelection.AllStacks:
return new StackCollection(this, stacks);
case DefaultSelection.None:
return new StackCollection(this, []);
case DefaultSelection.OnlySingle:
if (stacks.length === 1) {
return new StackCollection(this, stacks);
if (topLevelStacks.length === 1) {
return new StackCollection(this, topLevelStacks);
} else {
throw new Error('Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`\n' +
`Stacks: ${stacks.map(x => x.id).join(' ')}`);
Expand All @@ -96,7 +107,7 @@ export class CloudAssembly {

const allStacks = new Map<string, cxapi.CloudFormationStackArtifact>();
for (const stack of stacks) {
allStacks.set(stack.id, stack);
allStacks.set(stack.hierarchicalId, stack);
}

// For every selector argument, pick stacks from the list.
Expand All @@ -105,8 +116,14 @@ export class CloudAssembly {
let found = false;

for (const stack of stacks) {
if (minimatch(stack.id, pattern) && !selectedStacks.has(stack.id)) {
selectedStacks.set(stack.id, stack);
const hierarchicalId = stack.hierarchicalId;
if (minimatch(hierarchicalId, pattern) && !selectedStacks.has(hierarchicalId)) {
selectedStacks.set(hierarchicalId, stack);
found = true;
} else if (minimatch(stack.id, pattern) && !selectedStacks.has(hierarchicalId) && semver.major(versionNumber()) < 2) {
warning('Selecting stack by identifier "%s". This identifier is deprecated and will be removed in v2. Please use "%s" instead.', colors.bold(stack.id), colors.bold(stack.hierarchicalId));
warning('Run "cdk ls" to see a list of all stack identifiers');
selectedStacks.set(hierarchicalId, stack);
found = true;
}
}
Expand All @@ -127,7 +144,7 @@ export class CloudAssembly {
}

// Filter original array because it is in the right order
const selectedList = stacks.filter(s => selectedStacks.has(s.id));
const selectedList = stacks.filter(s => selectedStacks.has(s.hierarchicalId));

return new StackCollection(this, selectedList);
}
Expand Down Expand Up @@ -282,7 +299,7 @@ function includeUpstreamStacks(

for (const stack of selectedStacks.values()) {
// Select an additional stack if it's not selected yet and a dependency of a selected stack (and exists, obviously)
for (const dependencyId of stack.dependencies.map(x => x.id)) {
for (const dependencyId of stack.dependencies.map(x => x.manifest.displayName ?? x.id)) {
if (!selectedStacks.has(dependencyId) && allStacks.has(dependencyId)) {
added.push(dependencyId);
selectedStacks.set(dependencyId, allStacks.get(dependencyId)!);
Expand Down
6 changes: 3 additions & 3 deletions packages/aws-cdk/lib/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ export class CdkToolkit {

if (!options.force) {
// eslint-disable-next-line max-len
const confirmed = await promptly.confirm(`Are you sure you want to delete: ${colors.blue(stacks.stackArtifacts.map(s => s.id).join(', '))} (y/n)?`);
const confirmed = await promptly.confirm(`Are you sure you want to delete: ${colors.blue(stacks.stackArtifacts.map(s => s.hierarchicalId).join(', '))} (y/n)?`);
if (!confirmed) {
return;
}
Expand Down Expand Up @@ -281,7 +281,7 @@ export class CdkToolkit {

// just print stack IDs
for (const stack of stacks.stackArtifacts) {
data(stack.id);
data(stack.hierarchicalId);
}

return 0; // exit-code
Expand Down Expand Up @@ -396,7 +396,7 @@ export class CdkToolkit {
const assembly = await this.assembly();
const stacks = await assembly.selectStacks(stackNames, {
extend: exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Upstream,
defaultBehavior: DefaultSelection.AllStacks,
defaultBehavior: DefaultSelection.MainAssembly,
});

await this.validateStacks(stacks);
Expand Down
Loading

0 comments on commit 5a6fa7f

Please sign in to comment.