-
Notifications
You must be signed in to change notification settings - Fork 4k
/
Copy pathnested-stack.ts
279 lines (244 loc) · 10.6 KB
/
nested-stack.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
import * as crypto from 'crypto';
import { Construct, Node } from 'constructs';
import { FileAssetPackaging } from './assets';
import { Fn } from './cfn-fn';
import { Aws } from './cfn-pseudo';
import { CfnResource } from './cfn-resource';
import { CfnStack } from './cloudformation.generated';
import { Duration } from './duration';
import { Lazy } from './lazy';
import { Names } from './names';
import { RemovalPolicy } from './removal-policy';
import { IResolveContext } from './resolvable';
import { Stack } from './stack';
import { NestedStackSynthesizer } from './stack-synthesizers';
import { Token } from './token';
import * as cxapi from '../../cx-api';
const NESTED_STACK_SYMBOL = Symbol.for('@aws-cdk/core.NestedStack');
/**
* Initialization props for the `NestedStack` construct.
*
*/
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.
*
* The nested stack construct will automatically synthesize parameters in order
* to bind references from the parent stack(s) into the nested stack.
*
* @default - no user-defined parameters are passed to the nested stack
*/
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 notificationArns?: string[];
/**
* Policy to apply when the nested stack is removed
*
* The default is `Destroy`, because all Removal Policies of resources inside the
* Nested Stack should already have been set correctly. You normally should
* not need to set this value.
*
* @default RemovalPolicy.DESTROY
*/
readonly removalPolicy?: RemovalPolicy;
/**
* A description of the stack.
*
* @default - No description.
*/
readonly description?: string;
}
/**
* A CloudFormation nested stack.
*
* When you apply template changes to update a top-level stack, CloudFormation
* updates the top-level stack and initiates an update to its nested stacks.
* CloudFormation updates the resources of modified nested stacks, but does not
* update the resources of unmodified nested stacks.
*
* 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 templateFile: string;
public readonly nestedStackResource?: CfnResource;
private readonly parameters: { [name: string]: string };
private readonly resource: CfnStack;
private readonly _contextualStackId: string;
private readonly _contextualStackName: string;
private _templateUrl?: string;
private _parentStack: Stack;
constructor(scope: Construct, id: string, props: NestedStackProps = { }) {
const parentStack = findParentStack(scope);
super(scope, id, {
env: { account: parentStack.account, region: parentStack.region },
synthesizer: new NestedStackSynthesizer(parentStack.synthesizer),
description: props.description,
crossRegionReferences: parentStack._crossRegionReferences,
});
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.templateFile = `${Names.uniqueId(this)}.nested.template.json`;
this.parameters = props.parameters || {};
this.resource = new CfnStack(parentScope, `${id}.NestedStackResource`, {
// This value cannot be cached since it changes during the synthesis phase
templateUrl: Lazy.uncachedString({ produce: () => this._templateUrl || '<unresolved>' }),
parameters: Lazy.any({ produce: () => Object.keys(this.parameters).length > 0 ? this.parameters : undefined }),
notificationArns: props.notificationArns,
timeoutInMinutes: props.timeout ? props.timeout.toMinutes() : undefined,
});
this.resource.applyRemovalPolicy(props.removalPolicy ?? RemovalPolicy.DESTROY);
this.nestedStackResource = this.resource;
this.node.defaultChild = this.resource;
// context-aware stack name: if resolved from within this stack, return AWS::StackName
// if resolved from the outer stack, use the { Ref } of the AWS::CloudFormation::Stack resource
// which resolves the ARN of the stack. We need to extract the stack name, which is the second
// component after splitting by "/"
this._contextualStackName = this.contextualAttribute(Aws.STACK_NAME, Fn.select(1, Fn.split('/', this.resource.ref)));
this._contextualStackId = this.contextualAttribute(Aws.STACK_ID, this.resource.ref);
}
/**
* An attribute that represents the name of the nested stack.
*
* This is a context aware attribute:
* - 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 value: `mystack-mynestedstack-sggfrhxhum7w`
* @attribute
*/
public get stackName() {
return this._contextualStackName;
}
/**
* An attribute that represents the ID of the stack.
*
* This is a context aware attribute:
* - 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 value: `arn:aws:cloudformation:us-east-2:123456789012:stack/mystack-mynestedstack-sggfrhxhum7w/f449b250-b969-11e0-a185-5081d0136786`
* @attribute
*/
public get stackId() {
return this._contextualStackId;
}
/**
* Assign a value to one of the nested stack parameters.
* @param name The parameter name (ID)
* @param value The value to assign
*/
public setParameter(name: string, value: string) {
this.parameters[name] = value;
}
/**
* Defines an asset at the parent stack which represents the template of this
* nested stack.
*
* This private API is used by `App.prepare()` within a loop that rectifies
* references every time an asset is added. This is because (at the moment)
* assets are addressed using CloudFormation parameters.
*
* @returns `true` if a new asset was added or `false` if an asset was
* previously added. When this returns `true`, App will do another reference
* rectification cycle.
*
* @internal
*/
public _prepareTemplateAsset() {
if (this._templateUrl) {
return false;
}
// When adding tags to nested stack, the tags need to be added to all the resources in
// nested stack, which is handled by the `tags` property, But to tag the
// tags have to be added in the parent stack CfnStack resource. The CfnStack resource created
// by this class don't share the same TagManager as that of the one exposed by the `tag` property of the
// class, all the tags need to be copied to the CfnStack resource before synthesizing the resource.
// See https://github.com/aws/aws-cdk/pull/19128
Object.entries(this.tags.tagValues()).forEach(([key, value]) => {
this.resource.tags.setTag(key, value);
});
const cfn = JSON.stringify(this._toCloudFormation());
const templateHash = crypto.createHash('sha256').update(cfn).digest('hex');
const templateLocation = this._parentStack.synthesizer.addFileAsset({
packaging: FileAssetPackaging.FILE,
sourceHash: templateHash,
fileName: this.templateFile,
});
this.addResourceMetadata(this.resource, 'TemplateURL');
// if bucketName/objectKey are cfn parameters from a stack other than the parent stack, they will
// be resolved as cross-stack references like any other (see "multi" tests).
this._templateUrl = `https://s3.${this._parentStack.region}.${this._parentStack.urlSuffix}/${templateLocation.bucketName}/${templateLocation.objectKey}`;
return true;
}
private contextualAttribute(innerValue: string, outerValue: string) {
return Token.asString({
resolve: (context: IResolveContext) => {
if (Stack.of(context.scope) === this) {
return innerValue;
} else {
return outerValue;
}
},
});
}
private addResourceMetadata(resource: CfnResource, resourceProperty: string) {
if (!this.node.tryGetContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT)) {
return; // not enabled
}
// tell tools such as SAM CLI that the "TemplateURL" property of this resource
// points to the nested stack template for local emulation
resource.cfnOptions.metadata = resource.cfnOptions.metadata || { };
resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_PATH_KEY] = this.templateFile;
resource.cfnOptions.metadata[cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY] = resourceProperty;
}
}
/**
* 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 = Node.of(scope).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;
}