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(cloudformation): nested stacks #2821

Merged
merged 105 commits into from
Oct 3, 2019
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
105 commits
Select commit Hold shift + click to select a range
276811d
WIP
Jul 17, 2019
5fac5f4
bidi cross references work
Jul 21, 2019
7e1cc30
two directions work
Jul 21, 2019
c89048b
Merge remote-tracking branch 'origin/master' into benisrae/nested-stacks
Jul 21, 2019
d40b8b7
Merge remote-tracking branch 'origin/master' into benisrae/nested-stacks
Jul 22, 2019
2f85de4
Progression
Jul 23, 2019
b1355c4
update expectations
Jul 23, 2019
c7799ca
update more expectations
Jul 23, 2019
b2ed25b
support assets (and as a result nested nested)
Jul 24, 2019
06d60f4
Merge remote-tracking branch 'origin/master' into benisrae/nested-stacks
Jul 24, 2019
d2901bc
Merge remote-tracking branch 'origin/master' into benisrae/nested-stacks
Jul 29, 2019
e297701
Merge remote-tracking branch 'origin/master' into benisrae/nested-stacks
Sep 16, 2019
66ff21d
Merge branch 'master' into benisrae/nested-stacks
Sep 16, 2019
f6a0880
chore: fix CodeBuild project name in mergify configuration (#4088)
RomainMuller Sep 16, 2019
b729a17
chore: fix deprecation message (#4083)
CaerusKaru Sep 16, 2019
77c4adf
fixed gitignore to include jest file (#4079)
rhboyd Sep 16, 2019
af48c61
feat(iam): support NotPrincipal in policy statements (#4077)
rmorris1218 Sep 16, 2019
9ca38c6
Change casing of secretsManager module reference (#4074)
Sep 16, 2019
c8abd35
feat(route53): Domain redirect pattern (#3946)
hoegertn Sep 16, 2019
929f0c1
chore: correctly perform auto-merging of @dependabot PRs (#4095)
RomainMuller Sep 16, 2019
7f805c5
chore(deps-dev): bump @types/node from 8.10.53 to 8.10.54 (#4035)
dependabot-preview[bot] Sep 16, 2019
00458cb
chore(deps): bump aws-sdk from 2.527.0 to 2.528.0 (#4047)
dependabot-preview[bot] Sep 16, 2019
cdc6882
chore(mergify): unroll team into members (#4097)
rix0rrr Sep 16, 2019
444348d
chore: fix regex in @dependabot auto-merging (#4098)
RomainMuller Sep 16, 2019
359045b
chore(deps): bump ts-jest from 24.0.2 to 24.1.0 (#4049)
dependabot-preview[bot] Sep 16, 2019
d8a7366
fix(ec2): improve errors around subnet selection (#4089)
rix0rrr Sep 16, 2019
3d3b1a8
chore(deps-dev): bump nock from 10.0.6 to 11.3.4 (#4050)
dependabot-preview[bot] Sep 16, 2019
f20d714
chore: relax pattern matching for codebuild PR builds (#4099)
Sep 16, 2019
2d2c9d5
chore(deps): bump aws-sdk from 2.528.0 to 2.529.0 (#4103)
dependabot-preview[bot] Sep 16, 2019
797720a
chore: improvement to issue template (#4086)
Sep 17, 2019
2eb9b8a
feat(toolkit): conditionally emit AWS::CDK::Metadata resource (#3692)
RomainMuller Sep 17, 2019
cf8002e
feat: upgrade CloudFormation resource specification to v6.1.0 (#4112)
shivlaks Sep 17, 2019
0ffe2fa
chore: configure iconUrl for dotnet packages (#4094)
RomainMuller Sep 17, 2019
392b41f
fix(cloudfront): actually default 'compress' to true (#3359)
Sep 18, 2019
1130add
fix(ec2): fix subnet selection on looked-up VPCs (#4090)
rix0rrr Sep 18, 2019
123489e
chore: mention s3-deployment in s3 README (#4124)
rix0rrr Sep 18, 2019
57e2e59
chore: update deprecated `lerna publish` usage (#4123)
Sep 18, 2019
8616d31
chore(deps): bump aws-sdk from 2.529.0 to 2.530.0 (#4117)
dependabot-preview[bot] Sep 18, 2019
ec64994
fix(elbv2): fix cross-stack use of ALB (#4111)
parisholley Sep 18, 2019
2655d92
chore: fix s3-deployment package typo (#4126)
Sep 18, 2019
8760359
fix(route53): remove `http://` from bucket target (#4070)
Sep 18, 2019
484d2ea
feat: update baseline requirement for node engine to 10.3.0 (#4135)
RomainMuller Sep 18, 2019
0d1342d
chore: update jsii to v0.17.0 (#4146)
shivlaks Sep 18, 2019
e02c242
feat(codepipeline): handle non-CFN cross-region actions (#3777)
skinny85 Sep 18, 2019
7b1a709
chore(deps): bump aws-sdk from 2.530.0 to 2.531.0 (#4136)
dependabot-preview[bot] Sep 19, 2019
2f38e0f
feat(ecs): allow load balancing to any container and port of service …
iamhopaul123 Sep 19, 2019
be89679
fix(elbv2): unhealthyHostCount metric case fix (#4133)
Sep 19, 2019
ca0ed5d
fix(elbv2): allow multiple certificates on ALB listener (#4116)
MrArnoldPalmer Sep 19, 2019
2ce3ab0
chore: upgrade NuGet options to 256x256 icon (#4158)
RomainMuller Sep 19, 2019
598681b
chore: remove @types/nock dependency (#4164)
RomainMuller Sep 19, 2019
306e0bc
feat(s3-deployment): allow multiple Sources for single Deployment (#4…
CaerusKaru Sep 19, 2019
4efe37f
v1.9.0 (#4170)
shivlaks Sep 20, 2019
95fe8e1
feat(core): context lookup errors are reported to CX app (#3772)
rix0rrr Sep 20, 2019
a16f7b3
chore: correct Nuget Gallery icon URL (#4174)
RomainMuller Sep 20, 2019
6ed9184
fix(ecr-assets): docker build targets (#4185)
themizzi Sep 22, 2019
ad351f8
feat(ecr-assets): Support .dockerignore (faster Docker builds) (#4104)
parisholley Sep 22, 2019
e4ba3a1
chore: minor grammar updates (#4175)
robertd Sep 22, 2019
f1d2b03
fix(s3): missing http on website url (#4189)
hoegertn Sep 22, 2019
3d8ecb3
fix(apigateway): proxy method options are not duplicated to root (#4192)
Sep 22, 2019
8bb987c
fix(ssm): AWS::EC2::Image::Id parameter type (#4161)
Sep 23, 2019
0ed899c
chore(deps): bump colors from 1.3.3 to 1.4.0 (#4195)
dependabot-preview[bot] Sep 23, 2019
13bb9ee
chore(deps): bump aws-sdk from 2.531.0 to 2.533.0 (#4181)
dependabot-preview[bot] Sep 23, 2019
7e967ab
feat(eks): retrieve ami with ssm (#4156)
Sep 23, 2019
73c6f9c
feat(eks): upgrade latest kubertenes version to 1.14 (#4157)
Sep 23, 2019
efce508
chore: stop trying to auto-merge dependabot PR's (#4202)
RomainMuller Sep 23, 2019
ef31838
chore(deps-dev): bump nock from 11.3.4 to 11.3.5 (#4177)
dependabot-preview[bot] Sep 23, 2019
0797f19
Merge remote-tracking branch 'origin/master' into benisrae/nested-stacks
Sep 23, 2019
070c723
fix test: templateFileName renamed to templateFile
Sep 23, 2019
b65f518
Merge branch 'master' into benisrae/nested-stacks
Sep 23, 2019
d0762ee
fix asset tests
Sep 24, 2019
9aabe35
fix lambda tests
Sep 24, 2019
bc6ced8
make displayName optional
Sep 24, 2019
c1803ef
make assert.synthesizeWithNested internal
Sep 25, 2019
404d386
intermediate
Sep 25, 2019
7399a90
move nested addFileAsset logic to core
Sep 25, 2019
7944eed
get rid of references
Sep 25, 2019
3270b71
refactoring
Sep 25, 2019
f07b01c
fix build
Sep 25, 2019
16cf71c
support docker assets
Sep 25, 2019
4ad4266
fix cyclic dependency by moving ecr integ test to aws-ecr-assets
Sep 26, 2019
ad04031
Merge remote-tracking branch 'origin/master' into benisrae/nested-stacks
Sep 26, 2019
efca435
ignore cfn synth errors during prepare
Sep 26, 2019
1d88659
Merge remote-tracking branch 'origin/master' into benisrae/nested-stacks
Oct 2, 2019
a18a037
catch CfnSynthesisErrors properly during prepare
Oct 2, 2019
2d96862
fix lambda tests
Oct 2, 2019
9bfa531
update expectations
Oct 2, 2019
2b850e9
update tests
Oct 2, 2019
10f816b
fix some expectations
Oct 2, 2019
8927125
update expectations
Oct 2, 2019
bb7ceef
make cfn-reference private
Oct 2, 2019
fc905b8
fix more expectations
Oct 2, 2019
ccd7b57
update decdk snapshot
Oct 2, 2019
49f9a16
more cleanups
Oct 2, 2019
dc4a113
make nested stacks experimental
Oct 2, 2019
ae73522
add readme
Oct 2, 2019
aa2099b
Merge branch 'master' into benisrae/nested-stacks
Oct 2, 2019
7192a4d
update expectations
Oct 2, 2019
3c55d17
allow customization of the source hash for file assets
Oct 3, 2019
b2a2df9
update integration test expectations
Oct 3, 2019
833ab52
update expectations
Oct 3, 2019
9cc84df
use custom source hash
Oct 3, 2019
7fa6c6f
Merge remote-tracking branch 'origin/master' into benisrae/nested-stacks
Oct 3, 2019
0bc0519
update more expectations
Oct 3, 2019
54cd6ac
exclude breaking changes related to references
Oct 3, 2019
31d03d2
Merge branch 'master' into benisrae/nested-stacks
mergify[bot] Oct 3, 2019
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
2 changes: 1 addition & 1 deletion packages/@aws-cdk/assert/lib/expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import { SynthUtils } from './synth-utils';

export function expect(stack: api.CloudFormationStackArtifact | cdk.Stack, skipValidation = false): StackInspector {
// if this is already a synthesized stack, then just inspect it.
const artifact = stack instanceof api.CloudFormationStackArtifact ? stack : SynthUtils.synthesize(stack, { skipValidation });
const artifact = stack instanceof api.CloudFormationStackArtifact ? stack : SynthUtils.synthesizeWithNested(stack, { skipValidation });
return new StackInspector(artifact);
}
13 changes: 11 additions & 2 deletions packages/@aws-cdk/assert/lib/inspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,20 @@ export abstract class Inspector {
}

export class StackInspector extends Inspector {
constructor(public readonly stack: api.CloudFormationStackArtifact) {

private readonly template: { [key: string]: any };

constructor(public readonly stack: api.CloudFormationStackArtifact | object) {
super();

this.template = stack instanceof api.CloudFormationStackArtifact ? stack.template : stack;
}

public at(path: string | string[]): StackPathInspector {
if (!(this.stack instanceof api.CloudFormationStackArtifact)) {
throw new Error(`Cannot use "expect(stack).at(path)" for a raw template, only CloudFormationStackArtifact`);
}

const strPath = typeof path === 'string' ? path : path.join('/');
return new StackPathInspector(this.stack, strPath);
}
Expand All @@ -41,7 +50,7 @@ export class StackInspector extends Inspector {
}

public get value(): { [key: string]: any } {
return this.stack.template;
return this.template;
}
}

Expand Down
37 changes: 32 additions & 5 deletions packages/@aws-cdk/assert/lib/synth-utils.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,56 @@
import { ConstructNode, Stack, SynthesisOptions } from '@aws-cdk/core';
import { App, ConstructNode, Stack, SynthesisOptions } from '@aws-cdk/core';
import cxapi = require('@aws-cdk/cx-api');
import fs = require('fs');
import path = require('path');

export class SynthUtils {
public static synthesize(stack: Stack, options: SynthesisOptions = { }): cxapi.CloudFormationStackArtifact {
// always synthesize against the root (be it an App or whatever) so all artifacts will be included
const root = stack.node.root;

// if the root is an app, invoke "synth" to avoid double synthesis
const assembly = root instanceof App ? root.synth() : ConstructNode.synth(root.node, options);

return assembly.getStack(stack.stackName);
}

/**
* Synthesizes the stack and returns a `CloudFormationStackArtifact` which can be inspected.
* Supports nested stacks as well as normal stacks.
*/
public static synthesize(stack: Stack, options: SynthesisOptions = { }): cxapi.CloudFormationStackArtifact {
public static synthesizeWithNested(stack: Stack, options: SynthesisOptions = { }): cxapi.CloudFormationStackArtifact | object {
eladb marked this conversation as resolved.
Show resolved Hide resolved
// always synthesize against the root (be it an App or whatever) so all artifacts will be included
const root = stack.node.root;
const assembly = ConstructNode.synth(root.node, options);

// if the root is an app, invoke "synth" to avoid double synthesis
const assembly = root instanceof App ? root.synth() : ConstructNode.synth(root.node, options);

// if this is a nested stack (it has a parent), then just read the template as a string
if (stack.parentStack) {
return JSON.parse(fs.readFileSync(path.join(assembly.directory, stack.templateFileName)).toString('utf-8'));
}

return assembly.getStack(stack.stackName);
}

/**
* Synthesizes the stack and returns the resulting CloudFormation template.
*/
public static toCloudFormation(stack: Stack, options: SynthesisOptions = { }): any {
return this.synthesize(stack, options).template;
const synth = this.synthesizeWithNested(stack, options);
if (synth instanceof cxapi.CloudFormationStackArtifact) {
return synth.template;
} else {
return synth;
}
}

/**
* @returns Returns a subset of the synthesized CloudFormation template (only specific resource types).
*/
public static subset(stack: Stack, options: SubsetOptions): any {
const template = SynthUtils.synthesize(stack).template;
const synth = this.synthesize(stack);
const template = synth instanceof cxapi.CloudFormationStackArtifact ? synth.template : synth;
eladb marked this conversation as resolved.
Show resolved Hide resolved
if (template.Resources) {
for (const [key, resource] of Object.entries(template.Resources)) {
if (options.resourceTypes && !options.resourceTypes.includes((resource as any).Type)) {
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-cloudformation/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './cloud-formation-capabilities';
export * from './custom-resource';
export * from './nested-stack';

// AWS::CloudFormation CloudFormation Resources:
export * from './cloudformation.generated';
263 changes: 263 additions & 0 deletions packages/@aws-cdk/aws-cloudformation/lib/nested-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import s3_assets = require('@aws-cdk/aws-s3-assets');
import sns = require('@aws-cdk/aws-sns');
import {
Aws, CfnOutput, CfnParameter, Construct, Duration, Fn, IConstruct,
IResolvable, IResolveContext, Lazy, Reference, Stack, Token } from '@aws-cdk/core';
import cxapi = require('@aws-cdk/cx-api');
import { CfnStack } from './cloudformation.generated';

const NESTED_STACK_SYMBOL = Symbol.for('@aws-cdk/aws-cloudformation.NestedStack');

export interface NestedStackProps {

/**
* The set value pairs that represent the parameters passed to CloudFormation
* when this nested stack is created. Each parameter has a name corresponding
* to a parameter defined in the embedded template and a value representing
* the value that you want to set for the parameter.
*
* @default - no parameters are passed to the nested stack
eladb marked this conversation as resolved.
Show resolved Hide resolved
*/
readonly parameters?: { [key: string]: string };

/**
* The length of time that CloudFormation waits for the nested stack to reach
* the CREATE_COMPLETE state.
*
* When CloudFormation detects that the nested stack has reached the
* CREATE_COMPLETE state, it marks the nested stack resource as
* CREATE_COMPLETE in the parent stack and resumes creating the parent stack.
* If the timeout period expires before the nested stack reaches
* CREATE_COMPLETE, CloudFormation marks the nested stack as failed and rolls
* back both the nested stack and parent stack.
*
* @default - no timeout
*/
readonly timeout?: Duration;

/**
* The Simple Notification Service (SNS) topics to publish stack related
* events.
*
* @default - notifications are not sent for this stack.
*/
readonly notifications?: sns.ITopic[];
}

/**
* A nested CloudFormation stack.
*
* This means that it must have a non-nested Stack as an ancestor, into which an
* `AWS::CloudFormation::Stack` resource will be synthesized into the parent
* stack.
*
* Furthermore, this stack will not be treated as an independent deployment
* artifact (won't be listed in "cdk list" or deployable through "cdk deploy"),
* but rather only synthesized as a template and uploaded as an asset to S3.
*
* Cross references of resource attributes between the parent stack and the
* nested stack will automatically be translated to stack parameters and
* outputs.
*/
export class NestedStack extends Stack {

/**
* Checks if `x` is an object of type `NestedStack`.
*/
public static isNestedStack(x: any): x is NestedStack {
return x != null && typeof(x) === 'object' && NESTED_STACK_SYMBOL in x;
}

public readonly templateFileName: string;

/**
* The stack this stack is nested in.
*/
public readonly parentStack?: Stack;

/**
* An attribute that represents the name of the nested stack.
*
* If this is referenced from the parent stack, it will return a token that parses the name from the stack ID.
* If this is referenced from the context of the nested stack, it will return `{ "Ref": "AWS::StackName" }`
*
* @example mystack-mynestedstack-sggfrhxhum7w
* @attribute
*/
public readonly stackName: string;

private readonly parameters: { [name: string]: string };
private readonly resource: CfnStack;

constructor(scope: Construct, id: string, props: NestedStackProps = { }) {
const parentStack = findParentStack(scope);

super(scope, id, {
env: {
account: parentStack.account,
region: parentStack.region
},
});

this.parentStack = parentStack;

const parentScope = new Construct(scope, id + '.NestedStack');

Object.defineProperty(this, NESTED_STACK_SYMBOL, { value: true });

// this is the file name of the synthesized template file within the cloud assembly
this.templateFileName = `${this.node.uniqueId}.nested.template.json`;

const asset = new s3_assets.SynthesizedAsset(parentScope, 'Asset', {
packaging: s3_assets.AssetPackaging.FILE,
assemblyPath: this.templateFileName,
sourceHash: this.node.uniqueId
eladb marked this conversation as resolved.
Show resolved Hide resolved
});

this.parameters = props.parameters || {};

this.resource = new CfnStack(parentScope, `${id}.NestedStackResource`, {
templateUrl: asset.s3Url,
parameters: Lazy.anyValue({ produce: () => Object.keys(this.parameters).length > 0 ? this.parameters : undefined }),
notificationArns: props.notifications ? props.notifications.map(n => n.topicArn) : undefined,
timeoutInMinutes: props.timeout ? props.timeout.toMinutes() : undefined,
});

this.stackName = Token.asString({
resolve: (context: IResolveContext) => {
const stack = Stack.of(context.scope);
if (stack === this) {
return Aws.STACK_NAME;
} else {
// resource.ref returns the stack ID, so we need to split by "/" and select the 2nd component, which is the stack name:
// arn:aws:cloudformation:us-east-2:123456789012:stack/mystack-mynestedstack-sggfrhxhum7w/f449b250-b969-11e0-a185-5081d0136786
eladb marked this conversation as resolved.
Show resolved Hide resolved
return Fn.select(1, Fn.split('/', this.resource.ref));
}
}
});
}

/**
* If a file asset is added to the nested stack, we also need to add it to the
* parent and wire the parameters.
*
* @param asset
*/
public addFileAsset(asset: cxapi.FileAssetMetadataEntry) {
eladb marked this conversation as resolved.
Show resolved Hide resolved
const parent = this.parentStack!;

const proxyParameter = (type: string, logicalId: string) => {
const p = new CfnParameter(parent, `${this.node.uniqueId}.${asset.id}.${type}`, {
type: 'String',
description: `Proxy for asset parameter "${asset.id}.${type}" within the nested stack "${this.node.path}"`
});

this.parameters[logicalId] = p.valueAsString;
return p.logicalId;
};

parent.addFileAsset({
...asset,
s3BucketParameter: proxyParameter('bucket', asset.s3BucketParameter),
s3KeyParameter: proxyParameter('key', asset.s3KeyParameter),
artifactHashParameter: proxyParameter('hash', asset.artifactHashParameter)
});
}

/**
* An attribute that represents the ID of the stack.
*
* If this is referenced from the parent stack, it will return `{ "Ref": "LogicalIdOfNestedStackResource" }`.
*
* If this is referenced from the context of the nested stack, it will return `{ "Ref": "AWS::StackId" }`
*
* @example arn:aws:cloudformation:us-east-2:123456789012:stack/mystack-mynestedstack-sggfrhxhum7w/f449b250-b969-11e0-a185-5081d0136786
* @attribute
*/
public get stackId(): string {
return Token.asString({
resolve: (context: IResolveContext) => {
const stack = Stack.of(context.scope);
if (stack === this) {
return Aws.STACK_ID;
} else {
return this.resource.ref;
}
}
});
}

protected createCrossReference(source: IConstruct, reference: Reference): IResolvable {
eladb marked this conversation as resolved.
Show resolved Hide resolved
const consumingStack = Stack.of(source);
const producingStack = Stack.of(reference.target);

// the nested stack references a resource from the parent stack:
// we pass it through a as a cloudformation parameter
if (producingStack === consumingStack.parentStack) {
const paramId = `reference-to-${reference.target.node.uniqueId}.${reference.displayName}`;
let param = this.node.tryFindChild(paramId) as CfnParameter;
if (!param) {
param = new CfnParameter(this, paramId, { type: 'String' });
this.parameters[param.logicalId] = Token.asString(reference);
}

return param.value;
}

// parent stack references a resource from the nested stack:
// we output it from the nested stack and use "Fn::GetAtt" as the reference value
if (producingStack === this && producingStack.parentStack === consumingStack) {
return this.getCreateOutputForReference(reference);
}

// sibling nested stacks (same parent):
// output from one and pass as parameter to the other
if (producingStack.parentStack && producingStack.parentStack === consumingStack.parentStack) {
const outputValue = this.getCreateOutputForReference(reference);
return (consumingStack as any).createCrossReference(source, outputValue);
}

// nested stack references a value from some other non-nested stack:
// normal export/import, with dependency between the parents
if (consumingStack.parentStack && consumingStack.parentStack !== producingStack) {
return super.createCrossReference(source, reference);
}

// some non-nested stack (that is not the parent) references a resource inside the nested stack:
// we output the value and let our parent export it
if (!consumingStack.parentStack && producingStack.parentStack && producingStack.parentStack !== consumingStack) {
const outputValue = this.getCreateOutputForReference(reference);
return (producingStack.parentStack as any).createCrossReference(source, outputValue);
}

throw new Error('unexpected');

return super.createCrossReference(source, reference);
}

private getCreateOutputForReference(reference: Reference) {
const outputId = `${reference.target.node.uniqueId}${reference.displayName}`;
let output = this.node.tryFindChild(outputId) as CfnOutput;
if (!output) {
output = new CfnOutput(this, outputId, { value: Token.asString(reference) });
}

return this.resource.getAtt(`Outputs.${outputId}`);
}
}

/**
* Validates the scope for a nested stack. Nested stacks must be defined within the scope of another `Stack`.
*/
function findParentStack(scope: Construct): Stack {
if (!scope) {
throw new Error(`Nested stacks cannot be defined as a root construct`);
}

const parentStack = scope.node.scopes.reverse().find(p => Stack.isStack(p));
if (!parentStack) {
throw new Error(`Nested stacks must be defined within scope of another non-nested stack`);
}

return parentStack as Stack;
}
Loading