-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathstack.ts
286 lines (258 loc) · 9.37 KB
/
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
280
281
282
283
284
285
286
import type { CfnElement, StackProps } from "aws-cdk-lib";
import { Annotations, App, Aspects, CfnParameter, LegacyStackSynthesizer, Stack, Tags } from "aws-cdk-lib";
import { Template } from "aws-cdk-lib/assertions";
import type { IConstruct } from "constructs";
import gitUrlParse from "git-url-parse";
import { AwsBackupTag } from "../../aspects/aws-backup";
import { CfnIncludeReporter } from "../../aspects/cfn-include-reporter";
import { CfnParameterReporter } from "../../aspects/cfn-parameter-reporter";
import { Metadata } from "../../aspects/metadata";
import { ContextKeys, MetadataKeys, TrackingTag } from "../../constants";
import { gitRemoteOriginUrl } from "../../utils/git";
import type { StackStageIdentity } from "./identity";
import type { GuStaticLogicalId } from "./migrating";
export interface GuStackProps extends Omit<StackProps, "stackName"> {
/**
* The Guardian stack being used (as defined in your riff-raff.yaml).
* This will be applied as a tag to all of your resources.
*/
stack: string;
stage: string;
/**
* Optional name of the app. If defined, all resources will have an App tag.
*/
app?: string;
/**
* The AWS CloudFormation stack name (as shown in the AWS CloudFormation UI).
* @defaultValue the `GU_CFN_STACK_NAME` environment variable
*/
cloudFormationStackName?: string;
/**
* Set this to true to stop the GuStack from tagging all of your AWS resources.
* This should only be turned on as part of an initial migration from CloudFormation.
*/
withoutTags?: boolean;
/**
* Set to disable CDK metadata. Only for internal use (for disabling for some
* snapshot tests). We rely on tracking data to prioritise future work so
* please do not override this.
*/
withoutMetadata?: boolean;
/**
* Set to enable all resources in the stack for backup provided by https://github.com/guardian/aws-backup.
*
* @default false - backups are not enabled
*
* @see https://github.com/guardian/aws-backup
*/
withBackup?: boolean;
}
/**
* GuStack provides the `stack` and `stage` parameters to a template.
* It also takes the `app` in the constructor.
*
* GuStack will add the Stack, Stage and App tags to all resources.
*
* GuStack also adds the tag `X-Gu-CDK-Version`.
* This tag allows us to measure adoption of this library.
* It's value is the version of guardian/cdk being used, as defined in `package.json`.
* As a result, the change sets between version numbers will be fairly noisy,
* as all resources receive a tag update.
* It is recommended to upgrade the version of @guardian/cdk being used in two steps:
* 1. Bump the library, apply the tag updates
* 2. Make any other stack changes
*
* Typical usage is to extend GuStack:
*
* ```typescript
* class MyStack extends GuStack {
* constructor(scope: App, id: string, props: GuStackProps) {
* super(scope, id, props)
* }
*
* // add resources here
* }
* ```
*/
export class GuStack extends Stack implements StackStageIdentity {
private readonly _stack: string;
private readonly _stage: string;
private readonly _app?: string;
private readonly _repositoryName?: string;
get stage(): string {
return this._stage;
}
get stack(): string {
return this._stack;
}
get app(): string | undefined {
return this._app;
}
/**
* The repository name, if it can be determined from the context or the git remote origin url.
* If it cannot be determined from either of these sources, it will be `undefined`.
*/
get repositoryName(): string | undefined {
return this._repositoryName;
}
/**
* A helper function to add a tag to all resources in a stack.
*
* Note: tags will be listed in alphabetical order during synthesis.
*
* @param key the tag name
* @param value the value of the tag
* @param applyToLaunchedInstances whether or not to apply the tag to instances launched in an ASG.
* @protected
*/
protected addTag(key: string, value: string, applyToLaunchedInstances: boolean = true): void {
// TODO add validation for `key` and `value`
// see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html
Tags.of(this).add(key, value, { applyToLaunchedInstances });
}
/**
* Returns all the parameters on this stack.
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html
*/
get parameters(): Record<string, CfnParameter> {
return this.node
.findAll()
.filter((construct) => construct instanceof CfnParameter)
.reduce((acc, param) => ({ ...acc, [param.node.id]: param as CfnParameter }), {});
}
// eslint-disable-next-line custom-rules/valid-constructors -- GuStack is the exception as it must take an App
constructor(scope: App, id: string, props: GuStackProps) {
const {
cloudFormationStackName = process.env.GU_CFN_STACK_NAME,
stack,
stage,
withoutTags,
withBackup = false,
} = props;
super(scope, id, {
...props,
stackName: cloudFormationStackName,
// TODO Use `DefaultStackSynthesizer` or create own synthesizer?
// see https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html#bootstrapping-custom-synth
synthesizer: new LegacyStackSynthesizer(),
});
this._stack = stack;
this._stage = stage.toUpperCase();
this._repositoryName = this.tryGetRepositoryTag();
if (!withoutTags) {
this.addTag(TrackingTag.Key, TrackingTag.Value);
this.addTag("Stack", this.stack);
this.addTag("Stage", this.stage);
if (this.app) {
this.addTag("App", this.app);
}
if (this.repositoryName) {
this.addTag(MetadataKeys.REPOSITORY_NAME, this.repositoryName);
}
}
if (!props.withoutMetadata) {
Aspects.of(this).add(new Metadata(this));
}
Aspects.of(this).add(new CfnIncludeReporter());
Aspects.of(this).add(new CfnParameterReporter());
if (withBackup) {
Aspects.of(this).add(new AwsBackupTag());
}
}
/**
* Returns the repository name.
* The value is retrieved in the following order:
* 1. From the context
* 2. From git config
*
* @private
*/
private tryGetRepositoryTag(): string | undefined {
try {
const urlFromContext = this.node.tryGetContext(ContextKeys.REPOSITORY_URL) as string | undefined;
const repositoryUrl: string = urlFromContext ?? gitRemoteOriginUrl();
return gitUrlParse(repositoryUrl).full_name;
} catch {
console.info(
`Unable to find git repository name. Set the ${ContextKeys.REPOSITORY_URL} context value or configure a git remote`,
);
return undefined;
}
}
/**
* Override the auto-generated logical ID for a resource, in the generated CloudFormation template, with a static one.
*
* Of particular use when migrating a JSON/YAML CloudFormation template into GuCDK.
* It's generally advised to retain the logical ID for stateful resources, such as databases or buckets.
*
* Let's say we have a YAML template:
*
* ```yaml
* AWSTemplateFormatVersion: '2010-09-09'
* Resources:
* UsesTable:
* Type: AWS::DynamoDB::Table
* Properties:
* TableName: !Sub 'users-${stage}'
* ```
*
* When moving to GuCDK we'll have this:
*
* class MyStack extends GuStack {
* constructor(app: App, id: string, props: GuStackProps) {
* super(app, id, props);
*
* const { stage } = this;
*
* new Table(this, "UsersTable", {
* name: `users-${stage}`
* });
* }
* }
*
* During synthesis, CDK auto-generates logical IDs, so we'll have a stack with a DynamoDB table named 'UsersTable<SOME GUID>', NOT `UsersTable`.
* That is, the `UsersTable` table will be deleted.
*
* In order to retain the original ID from the YAML template, we will do:
*
* class MyStack extends GuStack {
* constructor(app: App, id: string, props: GuStackProps) {
* super(app, id, props);
*
* const { stage } = this;
*
* const table = new Table(this, "UsersTable", {
* name: `users-${stage}`
* });
*
* this.overrideLogicalId(table, { logicalId: "UsersTable", reason: "Retaining a stateful resource from the YAML template" });
* }
* }
*
* @param construct The (stateful) resource to retain the logical ID of.
* @param logicalId The logical ID of the resource (as defined in the JSON/YAML template.
* @param reason A small explanation to keep the logical ID. Mainly used to help future developers.
*/
public overrideLogicalId(construct: IConstruct, { logicalId, reason }: GuStaticLogicalId): void {
const {
node: { id, defaultChild },
} = construct;
(defaultChild as CfnElement).overrideLogicalId(logicalId);
Annotations.of(construct).addInfo(`Setting logical ID for ${id} to ${logicalId}. Reason: ${reason}`);
}
}
/**
* A GuStack but designed for Stack Set instances.
*
* In a stack set application, `GuStackForStackSetInstance` is used to represent the infrastructure to provision in target AWS accounts.
*/
export class GuStackForStackSetInstance extends GuStack {
// eslint-disable-next-line custom-rules/valid-constructors -- GuStackForStackSet should have a unique `App`
constructor(id: string, props: GuStackProps) {
super(new App(), id, props);
}
get cfnJson(): string {
return JSON.stringify(Template.fromStack(this).toJSON(), null, 2);
}
}