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): directly deploy stacks in nested assemblies #14379

Merged
merged 7 commits into from Apr 29, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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
@@ -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
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
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
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
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
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');
});
@@ -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"
}
}
}
@@ -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"
}
}
}
}
@@ -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"
}
}
}
}
@@ -0,0 +1,7 @@
{
"Resources": {
"MyBucket": {
"Type": "AWS::S3::Bucket"
}
}
}
@@ -0,0 +1,7 @@
{
"Resources": {
"MyBucket": {
"Type": "AWS::S3::Bucket"
}
}
}
27 changes: 27 additions & 0 deletions packages/@aws-cdk/cx-api/test/stack-artifact.test.ts
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
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
28 changes: 19 additions & 9 deletions packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts
Expand Up @@ -16,7 +16,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 +77,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 +105,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 +114,9 @@ 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 id = stack.hierarchicalId;
if (minimatch(id, pattern) && !selectedStacks.has(id)) {
selectedStacks.set(id, stack);
found = true;
}
}
Expand All @@ -127,7 +137,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 +292,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
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