@aws/nx-plugin 1.0.0-rc.76 → 1.0.0-rc.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/migrations.json +16 -1
- package/package.json +1 -1
- package/src/migrations/latest/dynamodb-configurable-encryption/__snapshots__/migration.spec.ts.snap +429 -0
- package/src/migrations/latest/dynamodb-configurable-encryption/metadata.json +3 -0
- package/src/migrations/latest/dynamodb-configurable-encryption/migration.d.ts +6 -0
- package/src/migrations/latest/dynamodb-configurable-encryption/migration.js +325 -0
- package/src/migrations/latest/dynamodb-configurable-encryption/migration.js.map +1 -0
- package/src/migrations/latest/static-website-configurable-waf-encryption/metadata.json +3 -0
- package/src/migrations/latest/static-website-configurable-waf-encryption/migration.d.ts +6 -0
- package/src/migrations/latest/static-website-configurable-waf-encryption/migration.js +418 -0
- package/src/migrations/latest/static-website-configurable-waf-encryption/migration.js.map +1 -0
- package/src/migrations/latest/user-identity-configurable-mfa/metadata.json +3 -0
- package/src/migrations/latest/user-identity-configurable-mfa/migration.d.ts +6 -0
- package/src/migrations/latest/user-identity-configurable-mfa/migration.js +362 -0
- package/src/migrations/latest/user-identity-configurable-mfa/migration.js.map +1 -0
- package/src/py/dynamodb/__snapshots__/generator.spec.ts.snap +90 -9
- package/src/ts/dynamodb/__snapshots__/generator.spec.ts.snap +90 -9
- package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +259 -48
- package/src/ts/react-website/cognito-auth/__snapshots__/generator.spec.ts.snap +37 -7
- package/src/ts/react-website/cognito-auth/__snapshots__/generator.terraform.spec.ts.snap +26 -0
- package/src/utils/dynamodb-constructs/files/cdk/core/dynamodb.ts.template +23 -3
- package/src/utils/dynamodb-constructs/files/terraform/app/dynamodb/__nameKebabCase__/__nameKebabCase__.tf.template +27 -1
- package/src/utils/dynamodb-constructs/files/terraform/core/dynamodb/dynamodb.tf.template +39 -5
- package/src/utils/identity-constructs/files/cdk/core/user-identity.ts.template +41 -7
- package/src/utils/identity-constructs/files/terraform/core/user-identity/identity/identity.tf.template +48 -13
- package/src/utils/identity-constructs/files/terraform/core/user-identity/main.tf.template +26 -0
- package/src/utils/website-constructs/files/cdk/app/static-websites/__websiteNameKebabCase__.ts.template +11 -2
- package/src/utils/website-constructs/files/cdk/core/static-website.ts.template +47 -10
- package/src/utils/website-constructs/files/terraform/app/static-websites/__websiteNameKebabCase__/__websiteNameKebabCase__.tf.template +55 -0
- package/src/utils/website-constructs/files/terraform/core/static-website/static-website.tf.template +57 -15
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/ import { joinPathFragments } from "@nx/devkit";
|
|
5
|
+
import { addDestructuredImport, applyGritQL, captureGritQLVariable, GRIT_INSERT_PLACEHOLDER, insertViaGritQL, matchGritQL } from "../../../utils/ast.js";
|
|
6
|
+
import { formatFilesInSubtree } from "../../../utils/format.js";
|
|
7
|
+
import { PACKAGES_DIR, SHARED_CONSTRUCTS_DIR, SHARED_TERRAFORM_DIR } from "../../../utils/shared-constructs-constants.js";
|
|
8
|
+
/**
|
|
9
|
+
* Bring existing workspaces up to the generators' current StaticWebsite
|
|
10
|
+
* configurability:
|
|
11
|
+
*
|
|
12
|
+
* - The vended `StaticWebsite` CDK construct gains `enableWaf`, `encryption`,
|
|
13
|
+
* `encryptionKey` and `enableKeyRotation` props. The WAF Web ACL is now
|
|
14
|
+
* optional, the KMS key used to encrypt the website and distribution log
|
|
15
|
+
* buckets is now overridable (or skippable in favour of another
|
|
16
|
+
* `BucketEncryption`), and the auto-created key's rotation is configurable.
|
|
17
|
+
* - Each vended per-website app construct (`app/static-websites/*.ts`) gains
|
|
18
|
+
* an optional `props` constructor parameter so these can be configured per
|
|
19
|
+
* app, rather than only by hand-editing the vended construct.
|
|
20
|
+
* - The vended Terraform static-website core module gains the equivalent
|
|
21
|
+
* `enable_waf`, `encryption`, `kms_key_arn`, `create_kms_key` and
|
|
22
|
+
* `enable_key_rotation` variables. The CloudFront distribution's
|
|
23
|
+
* `lifecycle.replace_triggered_by` on the WAF ACL is also dropped - it
|
|
24
|
+
* can't resolve once the ACL has a count of 0 and no prior state to
|
|
25
|
+
* reference, which broke `terraform plan` on any greenfield deployment
|
|
26
|
+
* with `enable_waf = false`.
|
|
27
|
+
* - Each vended per-website Terraform app module (`app/static-websites/<name>/<name>.tf`)
|
|
28
|
+
* gains pass-through `custom_domain_names`, `acm_certificate_arn`,
|
|
29
|
+
* `enable_waf`, `encryption`, `kms_key_arn`, `create_kms_key` and
|
|
30
|
+
* `enable_key_rotation` variables forwarded to the core module call,
|
|
31
|
+
* matching the pass-through convention every other app-wraps-core
|
|
32
|
+
* Terraform module in this plugin already follows (`rdb`, `agent-core`,
|
|
33
|
+
* `dcr-proxies`, the REST/HTTP API).
|
|
34
|
+
*
|
|
35
|
+
* These files are generated with `KeepExisting`, so without this an upgraded
|
|
36
|
+
* workspace has generators that support this configuration but vended files
|
|
37
|
+
* that don't. Diverged files are left untouched and reported via `nextSteps`.
|
|
38
|
+
*/ const CDK_STATIC_WEBSITE_FILE = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core/static-website.ts`;
|
|
39
|
+
const CDK_STATIC_WEBSITES_APP_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/app/static-websites`;
|
|
40
|
+
const TERRAFORM_STATIC_WEBSITE_FILE = `${PACKAGES_DIR}/${SHARED_TERRAFORM_DIR}/src/core/static-website/static-website.tf`;
|
|
41
|
+
const TERRAFORM_STATIC_WEBSITES_APP_DIR = `${PACKAGES_DIR}/${SHARED_TERRAFORM_DIR}/src/app/static-websites`;
|
|
42
|
+
const CDK_DIVERGED_MESSAGE = `${CDK_STATIC_WEBSITE_FILE}: has diverged from the generated shape - left untouched. To pick up the enableWaf, encryption, encryptionKey and enableKeyRotation props, manually port them from the vended core/static-website.ts template (see the ts#react-website generator's static-website construct).`;
|
|
43
|
+
const TERRAFORM_DIVERGED_MESSAGE = `${TERRAFORM_STATIC_WEBSITE_FILE}: has diverged from the generated shape - left untouched. To pick up the enable_waf, encryption, kms_key_arn, create_kms_key and enable_key_rotation variables, manually port them from the vended static-website.tf template (see the ts#react-website generator's static-website module).`;
|
|
44
|
+
const terraformAppDivergedMessage = (filePath)=>`${filePath}: has diverged from the generated shape - left untouched. To make custom_domain_names, acm_certificate_arn, enable_waf, encryption, kms_key_arn, create_kms_key and enable_key_rotation configurable from your root Terraform configuration, add pass-through variables here and forward them to the static_website module call (see the ts#react-website generator's static-websites app template).`;
|
|
45
|
+
// The doc comments below carry backticked markdown, which must stay out of
|
|
46
|
+
// the GritQL pattern that inserts this text (see insertViaGritQL). It's routed
|
|
47
|
+
// in as plain text via the placeholder instead.
|
|
48
|
+
const STATIC_WEBSITE_PROPS_TEXT = `/**
|
|
49
|
+
* Whether to protect the CloudFront distribution with an AWS WAF Web ACL.
|
|
50
|
+
*
|
|
51
|
+
* @default true
|
|
52
|
+
*/
|
|
53
|
+
readonly enableWaf?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Server-side encryption for the website and distribution log buckets.
|
|
56
|
+
*
|
|
57
|
+
* @default BucketEncryption.KMS
|
|
58
|
+
*/
|
|
59
|
+
readonly encryption?: BucketEncryption;
|
|
60
|
+
/**
|
|
61
|
+
* KMS key used to encrypt the website and distribution log buckets. Only used when \`encryption\` is
|
|
62
|
+
* \`BucketEncryption.KMS\`. When not provided, a new key is created. Note that a key imported via
|
|
63
|
+
* \`Key.fromKeyArn\` must already grant the CloudWatch Logs, S3 and CloudFront service principals the
|
|
64
|
+
* necessary permissions in its own key policy - \`addToResourcePolicy\` is a no-op on an imported key,
|
|
65
|
+
* so this construct cannot grant them on your behalf.
|
|
66
|
+
*/
|
|
67
|
+
readonly encryptionKey?: IKey;
|
|
68
|
+
/**
|
|
69
|
+
* Whether the automatically created KMS key has rotation enabled. Only applies when \`encryption\` is
|
|
70
|
+
* \`BucketEncryption.KMS\` and no \`encryptionKey\` is supplied.
|
|
71
|
+
*
|
|
72
|
+
* @default true
|
|
73
|
+
*/
|
|
74
|
+
readonly enableKeyRotation?: boolean`;
|
|
75
|
+
/**
|
|
76
|
+
* Surface enableWaf/encryption/encryptionKey/enableKeyRotation on the vended
|
|
77
|
+
* StaticWebsite CDK construct.
|
|
78
|
+
*/ const migrateCdkConstruct = async (tree, nextSteps)=>{
|
|
79
|
+
if (!tree.exists(CDK_STATIC_WEBSITE_FILE)) {
|
|
80
|
+
return; // This workspace has no CDK static website construct.
|
|
81
|
+
}
|
|
82
|
+
if (await matchGritQL(tree, CDK_STATIC_WEBSITE_FILE, '`readonly enableWaf?: boolean`')) {
|
|
83
|
+
return; // Already migrated.
|
|
84
|
+
}
|
|
85
|
+
const CERTIFICATE_FIELD = '`readonly certificate?: ICertificate`';
|
|
86
|
+
const DESTRUCTURE_OLD = '`{ websiteFilePath, websiteName, domainNames, certificate }: StaticWebsiteProps`';
|
|
87
|
+
const KEY_CREATION_OLD = "`const websiteKey = new Key(this, 'WebsiteKey', { enableKeyRotation: true })`";
|
|
88
|
+
const ADD_TO_RESOURCE_POLICY_OLD = '`websiteKey.addToResourcePolicy($args)`';
|
|
89
|
+
const WEBSITE_BUCKET_ENCRYPTION_OLD = "`encryption: BucketEncryption.KMS` as $prop where { $prop <: within `new Bucket($_, 'WebsiteBucket', $_)` }";
|
|
90
|
+
const DISTRIBUTION_LOGS_BUCKET_ENCRYPTION_OLD = "`encryption: BucketEncryption.KMS` as $prop where { $prop <: within `new Bucket($_, 'DistributionLogBucket', $_)` }";
|
|
91
|
+
const WAF_STACK_OLD = "`const wafStack = new CloudfrontWebAcl(this, 'waf')`";
|
|
92
|
+
const WEB_ACL_ID_OLD = '`webAclId: wafStack.wafArn`';
|
|
93
|
+
const anchors = [
|
|
94
|
+
CERTIFICATE_FIELD,
|
|
95
|
+
DESTRUCTURE_OLD,
|
|
96
|
+
KEY_CREATION_OLD,
|
|
97
|
+
ADD_TO_RESOURCE_POLICY_OLD,
|
|
98
|
+
WEBSITE_BUCKET_ENCRYPTION_OLD,
|
|
99
|
+
DISTRIBUTION_LOGS_BUCKET_ENCRYPTION_OLD,
|
|
100
|
+
WAF_STACK_OLD,
|
|
101
|
+
WEB_ACL_ID_OLD
|
|
102
|
+
];
|
|
103
|
+
const allPresent = (await Promise.all(anchors.map((pattern)=>matchGritQL(tree, CDK_STATIC_WEBSITE_FILE, pattern)))).every(Boolean);
|
|
104
|
+
if (!allPresent) {
|
|
105
|
+
nextSteps.push(CDK_DIVERGED_MESSAGE);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
await addDestructuredImport(tree, CDK_STATIC_WEBSITE_FILE, [
|
|
109
|
+
'IKey'
|
|
110
|
+
], 'aws-cdk-lib/aws-kms');
|
|
111
|
+
await insertViaGritQL(tree, CDK_STATIC_WEBSITE_FILE, `${CERTIFICATE_FIELD} as $field => \`$field;
|
|
112
|
+
${GRIT_INSERT_PLACEHOLDER}\``, STATIC_WEBSITE_PROPS_TEXT);
|
|
113
|
+
await applyGritQL(tree, CDK_STATIC_WEBSITE_FILE, `${DESTRUCTURE_OLD} => \`{
|
|
114
|
+
websiteFilePath,
|
|
115
|
+
websiteName,
|
|
116
|
+
domainNames,
|
|
117
|
+
certificate,
|
|
118
|
+
enableWaf = true,
|
|
119
|
+
encryption = BucketEncryption.KMS,
|
|
120
|
+
encryptionKey,
|
|
121
|
+
enableKeyRotation = true,
|
|
122
|
+
}: StaticWebsiteProps\``);
|
|
123
|
+
await applyGritQL(tree, CDK_STATIC_WEBSITE_FILE, `${KEY_CREATION_OLD} => \`const websiteKey: IKey | undefined =
|
|
124
|
+
encryption === BucketEncryption.KMS
|
|
125
|
+
? (encryptionKey ?? new Key(this, 'WebsiteKey', { enableKeyRotation }))
|
|
126
|
+
: undefined;\``);
|
|
127
|
+
await applyGritQL(tree, CDK_STATIC_WEBSITE_FILE, `${ADD_TO_RESOURCE_POLICY_OLD} => \`websiteKey?.addToResourcePolicy($args)\``);
|
|
128
|
+
await applyGritQL(tree, CDK_STATIC_WEBSITE_FILE, `${WEBSITE_BUCKET_ENCRYPTION_OLD} => \`encryption\``);
|
|
129
|
+
await applyGritQL(tree, CDK_STATIC_WEBSITE_FILE, `${DISTRIBUTION_LOGS_BUCKET_ENCRYPTION_OLD} => \`encryption\``);
|
|
130
|
+
await applyGritQL(tree, CDK_STATIC_WEBSITE_FILE, `${WAF_STACK_OLD} => \`const wafStack = enableWaf ? new CloudfrontWebAcl(this, 'waf') : undefined;\``);
|
|
131
|
+
await applyGritQL(tree, CDK_STATIC_WEBSITE_FILE, `${WEB_ACL_ID_OLD} => \`webAclId: wafStack?.wafArn\``);
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Surface the same props on each vended per-website app construct, so they
|
|
135
|
+
* can be configured per app rather than only by editing the shared construct.
|
|
136
|
+
*/ const migrateCdkAppConstructs = async (tree, nextSteps)=>{
|
|
137
|
+
if (!tree.exists(CDK_STATIC_WEBSITES_APP_DIR)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
for (const fileName of tree.children(CDK_STATIC_WEBSITES_APP_DIR)){
|
|
141
|
+
if (fileName === 'index.ts' || !fileName.endsWith('.ts')) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const filePath = joinPathFragments(CDK_STATIC_WEBSITES_APP_DIR, fileName);
|
|
145
|
+
const contents = tree.read(filePath, 'utf-8') ?? '';
|
|
146
|
+
if (contents.includes('StaticWebsiteProps')) {
|
|
147
|
+
continue; // Already migrated.
|
|
148
|
+
}
|
|
149
|
+
const name = await captureGritQLVariable(tree, filePath, '`export class $name extends StaticWebsite { $_ }`', 'name');
|
|
150
|
+
if (!name) {
|
|
151
|
+
continue; // Not a StaticWebsite subclass, not ours to touch.
|
|
152
|
+
}
|
|
153
|
+
const importPattern = '`import { StaticWebsite } from $path`';
|
|
154
|
+
const ctorPattern = '`constructor(scope: Construct, id: string) { $body }`';
|
|
155
|
+
// Captures the whole property list rather than anchoring on websiteName
|
|
156
|
+
// being followed by exactly one more property ($rest binds a single
|
|
157
|
+
// trailing node, not a variadic list). This must still match when a
|
|
158
|
+
// workspace already customised the super() call with e.g. domainNames
|
|
159
|
+
// and certificate (the pre-existing documented customization point).
|
|
160
|
+
const superPattern = `\`super(scope, id, { $props })\` as $call where {
|
|
161
|
+
$props <: contains \`websiteName: '${name}'\`
|
|
162
|
+
}`;
|
|
163
|
+
const ready = (await Promise.all([
|
|
164
|
+
importPattern,
|
|
165
|
+
ctorPattern,
|
|
166
|
+
superPattern
|
|
167
|
+
].map((pattern)=>matchGritQL(tree, filePath, pattern)))).every(Boolean);
|
|
168
|
+
if (!ready) {
|
|
169
|
+
nextSteps.push(`${filePath}: has diverged from the generated shape - left untouched. To make enableWaf, encryption, encryptionKey and enableKeyRotation configurable here, add an optional \`props?: ${name}Props\` constructor parameter (\`Omit<StaticWebsiteProps, 'websiteName' | 'websiteFilePath'>\`) and spread it into the super() call (see the ts#react-website generator's static-websites app template).`);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
await applyGritQL(tree, filePath, `${importPattern} => \`import { StaticWebsite, StaticWebsiteProps } from $path\``);
|
|
173
|
+
await insertViaGritQL(tree, filePath, `\`export class ${name} extends StaticWebsite { $body }\` as $cls => \`${GRIT_INSERT_PLACEHOLDER}
|
|
174
|
+
|
|
175
|
+
$cls\``, `export type ${name}Props = Omit<\n StaticWebsiteProps,\n 'websiteName' | 'websiteFilePath'\n>;`);
|
|
176
|
+
await applyGritQL(tree, filePath, `${ctorPattern} => \`constructor(scope: Construct, id: string, props?: ${name}Props) { $body }\``);
|
|
177
|
+
await applyGritQL(tree, filePath, `${superPattern} => \`super(scope, id, { ...props, $props })\``);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
// Terraform (HCL) patterns are built from plain strings rather than JS
|
|
181
|
+
// template literals, since the HCL text itself is full of `${...}`
|
|
182
|
+
// interpolations that would otherwise be parsed as JS interpolation.
|
|
183
|
+
const hcl = (pattern)=>'language hcl\n' + pattern;
|
|
184
|
+
const withinResource = (linePattern, resourceType, resourceLabel)=>hcl('`' + linePattern + '` as $line where {\n' + ' $line <: within `resource "' + resourceType + '" "' + resourceLabel + '" { $_ }`\n' + '}');
|
|
185
|
+
const NEW_VARIABLES_TEXT = [
|
|
186
|
+
'variable "enable_waf" {',
|
|
187
|
+
' description = "Whether to protect the CloudFront distribution with an AWS WAF Web ACL."',
|
|
188
|
+
' type = bool',
|
|
189
|
+
' default = true',
|
|
190
|
+
'}',
|
|
191
|
+
'',
|
|
192
|
+
'variable "encryption" {',
|
|
193
|
+
' description = "Server-side encryption for the website and distribution log buckets. One of KMS or S3_MANAGED."',
|
|
194
|
+
' type = string',
|
|
195
|
+
' default = "KMS"',
|
|
196
|
+
'',
|
|
197
|
+
' validation {',
|
|
198
|
+
' condition = contains(["KMS", "S3_MANAGED"], var.encryption)',
|
|
199
|
+
' error_message = "encryption must be one of KMS or S3_MANAGED."',
|
|
200
|
+
' }',
|
|
201
|
+
'}',
|
|
202
|
+
'',
|
|
203
|
+
'variable "kms_key_arn" {',
|
|
204
|
+
' description = "ARN of an existing KMS key used to encrypt the website and distribution log buckets when encryption is KMS. When not provided and create_kms_key is true, a new key is created. Note that a customer-supplied key must already grant the CloudWatch Logs, S3 and CloudFront service principals the necessary permissions in its own key policy."',
|
|
205
|
+
' type = string',
|
|
206
|
+
' default = null',
|
|
207
|
+
'}',
|
|
208
|
+
'',
|
|
209
|
+
'variable "create_kms_key" {',
|
|
210
|
+
' description = "Whether to create a KMS key for the website. Only applies when encryption is KMS. Set to false when supplying kms_key_arn."',
|
|
211
|
+
' type = bool',
|
|
212
|
+
' default = true',
|
|
213
|
+
'}',
|
|
214
|
+
'',
|
|
215
|
+
'variable "enable_key_rotation" {',
|
|
216
|
+
' description = "Whether the automatically created KMS key has rotation enabled. Only applies when encryption is KMS and create_kms_key is true."',
|
|
217
|
+
' type = bool',
|
|
218
|
+
' default = true',
|
|
219
|
+
'}'
|
|
220
|
+
].join('\n');
|
|
221
|
+
const NEW_LOCALS_TEXT = [
|
|
222
|
+
'',
|
|
223
|
+
' create_website_key = var.encryption == "KMS" && var.create_kms_key',
|
|
224
|
+
' website_kms_key_arn = (',
|
|
225
|
+
' var.encryption != "KMS" ? null :',
|
|
226
|
+
' local.create_website_key ? aws_kms_key.website_key[0].arn :',
|
|
227
|
+
' var.kms_key_arn',
|
|
228
|
+
' )'
|
|
229
|
+
].join('\n');
|
|
230
|
+
/**
|
|
231
|
+
* Surface the same configuration on the vended Terraform static-website
|
|
232
|
+
* module.
|
|
233
|
+
*/ const migrateTerraformModule = async (tree, nextSteps)=>{
|
|
234
|
+
if (!tree.exists(TERRAFORM_STATIC_WEBSITE_FILE)) {
|
|
235
|
+
return; // This workspace has no Terraform static website module.
|
|
236
|
+
}
|
|
237
|
+
if (await matchGritQL(tree, TERRAFORM_STATIC_WEBSITE_FILE, hcl('`variable "enable_waf" { $_ }`'))) {
|
|
238
|
+
return; // Already migrated.
|
|
239
|
+
}
|
|
240
|
+
const ACM_CERT_VARIABLE = hcl('`variable "acm_certificate_arn" { $_ }`');
|
|
241
|
+
const ACCESS_LOGS_LOCAL = hcl('`access_logs_name_prefix = substr(lower(var.website_name), 0, 16)`');
|
|
242
|
+
const KMS_KEY_BLOCK = hcl('`resource "aws_kms_key" "website_key" { $_ }`');
|
|
243
|
+
const KMS_KEY_ROTATION_LINE = withinResource('enable_key_rotation = true', 'aws_kms_key', 'website_key');
|
|
244
|
+
const KMS_ALIAS_BLOCK = hcl('`resource "aws_kms_alias" "website_key_alias" { $_ }`');
|
|
245
|
+
const KMS_ALIAS_TARGET_LINE = hcl('`target_key_id = aws_kms_key.website_key.key_id`');
|
|
246
|
+
const LOG_GROUP_KMS_LINE = hcl('`kms_key_id = aws_kms_key.website_key.arn`');
|
|
247
|
+
const WEBSITE_ENCRYPTION_KEY_LINE = withinResource('kms_master_key_id = aws_kms_key.website_key.arn', 'aws_s3_bucket_server_side_encryption_configuration', 'website_encryption');
|
|
248
|
+
const WEBSITE_ENCRYPTION_ALGO_LINE = withinResource('sse_algorithm = "aws:kms"', 'aws_s3_bucket_server_side_encryption_configuration', 'website_encryption');
|
|
249
|
+
const DISTRIBUTION_LOGS_ENCRYPTION_KEY_LINE = withinResource('kms_master_key_id = aws_kms_key.website_key.arn', 'aws_s3_bucket_server_side_encryption_configuration', 'distribution_logs_encryption');
|
|
250
|
+
const DISTRIBUTION_LOGS_ENCRYPTION_ALGO_LINE = withinResource('sse_algorithm = "aws:kms"', 'aws_s3_bucket_server_side_encryption_configuration', 'distribution_logs_encryption');
|
|
251
|
+
const WAF_BLOCK = hcl('`resource "aws_wafv2_web_acl" "cloudfront_waf" { $_ }`');
|
|
252
|
+
const WEB_ACL_ID_LINE = hcl('`web_acl_id = aws_wafv2_web_acl.cloudfront_waf.arn`');
|
|
253
|
+
const WAF_OUTPUT_VALUE_LINE = hcl('`value = aws_wafv2_web_acl.cloudfront_waf.arn` as $line where {\n' + ' $line <: within `output "waf_web_acl_arn" { $_ }`\n' + '}');
|
|
254
|
+
// Forces distribution replacement whenever the WAF ACL changes. Once the
|
|
255
|
+
// ACL gains `count`, this can't be resolved on a greenfield apply where
|
|
256
|
+
// count is 0 and there's no prior state to reference - see
|
|
257
|
+
// https://github.com/awslabs/nx-plugin-for-aws/pull/1107. It's also
|
|
258
|
+
// redundant: web_acl_id already references the ACL ARN directly, so a
|
|
259
|
+
// replaced ACL still drives a plain distribution update.
|
|
260
|
+
const LIFECYCLE_REPLACE_TRIGGERED_BY_BLOCK = hcl('`lifecycle {\n replace_triggered_by = [\n aws_wafv2_web_acl.cloudfront_waf\n ]\n }`');
|
|
261
|
+
const anchors = [
|
|
262
|
+
ACM_CERT_VARIABLE,
|
|
263
|
+
ACCESS_LOGS_LOCAL,
|
|
264
|
+
KMS_KEY_BLOCK,
|
|
265
|
+
KMS_KEY_ROTATION_LINE,
|
|
266
|
+
KMS_ALIAS_BLOCK,
|
|
267
|
+
KMS_ALIAS_TARGET_LINE,
|
|
268
|
+
LOG_GROUP_KMS_LINE,
|
|
269
|
+
WEBSITE_ENCRYPTION_KEY_LINE,
|
|
270
|
+
WEBSITE_ENCRYPTION_ALGO_LINE,
|
|
271
|
+
DISTRIBUTION_LOGS_ENCRYPTION_KEY_LINE,
|
|
272
|
+
DISTRIBUTION_LOGS_ENCRYPTION_ALGO_LINE,
|
|
273
|
+
WAF_BLOCK,
|
|
274
|
+
WEB_ACL_ID_LINE,
|
|
275
|
+
WAF_OUTPUT_VALUE_LINE,
|
|
276
|
+
LIFECYCLE_REPLACE_TRIGGERED_BY_BLOCK
|
|
277
|
+
];
|
|
278
|
+
const allPresent = (await Promise.all(anchors.map((pattern)=>matchGritQL(tree, TERRAFORM_STATIC_WEBSITE_FILE, pattern)))).every(Boolean);
|
|
279
|
+
if (!allPresent) {
|
|
280
|
+
nextSteps.push(TERRAFORM_DIVERGED_MESSAGE);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
// 1. New variables, after acm_certificate_arn. Routed through the
|
|
284
|
+
// placeholder since the variable descriptions contain double-quoted
|
|
285
|
+
// HCL string values.
|
|
286
|
+
await insertViaGritQL(tree, TERRAFORM_STATIC_WEBSITE_FILE, hcl('`variable "acm_certificate_arn" { $body }` as $var => `$var\n\n' + GRIT_INSERT_PLACEHOLDER + '`'), NEW_VARIABLES_TEXT);
|
|
287
|
+
// 2. New locals, after access_logs_name_prefix.
|
|
288
|
+
await insertViaGritQL(tree, TERRAFORM_STATIC_WEBSITE_FILE, hcl('`access_logs_name_prefix = substr(lower(var.website_name), 0, 16)` as $line => `$line\n' + GRIT_INSERT_PLACEHOLDER + '`'), NEW_LOCALS_TEXT);
|
|
289
|
+
// 3. Make the KMS key and alias conditional on encryption/kms_key_arn.
|
|
290
|
+
await applyGritQL(tree, TERRAFORM_STATIC_WEBSITE_FILE, hcl('`resource "aws_kms_key" "website_key" { $body }` => `resource "aws_kms_key" "website_key" {\n' + ' count = local.create_website_key ? 1 : 0\n\n' + ' $body\n' + '}`'));
|
|
291
|
+
await applyGritQL(tree, TERRAFORM_STATIC_WEBSITE_FILE, KMS_KEY_ROTATION_LINE + ' => `enable_key_rotation = var.enable_key_rotation`');
|
|
292
|
+
await applyGritQL(tree, TERRAFORM_STATIC_WEBSITE_FILE, hcl('`resource "aws_kms_alias" "website_key_alias" { $body }` => `resource "aws_kms_alias" "website_key_alias" {\n' + ' count = local.create_website_key ? 1 : 0\n\n' + ' $body\n' + '}`'));
|
|
293
|
+
await applyGritQL(tree, TERRAFORM_STATIC_WEBSITE_FILE, KMS_ALIAS_TARGET_LINE + ' => `target_key_id = aws_kms_key.website_key[0].key_id`');
|
|
294
|
+
// 4. Resolve the key ARN through the new local everywhere it's consumed.
|
|
295
|
+
await applyGritQL(tree, TERRAFORM_STATIC_WEBSITE_FILE, LOG_GROUP_KMS_LINE + ' => `kms_key_id = local.website_kms_key_arn`');
|
|
296
|
+
await applyGritQL(tree, TERRAFORM_STATIC_WEBSITE_FILE, WEBSITE_ENCRYPTION_KEY_LINE + ' => `kms_master_key_id = var.encryption == "KMS" ? local.website_kms_key_arn : null`');
|
|
297
|
+
await applyGritQL(tree, TERRAFORM_STATIC_WEBSITE_FILE, WEBSITE_ENCRYPTION_ALGO_LINE + ' => `sse_algorithm = var.encryption == "KMS" ? "aws:kms" : "AES256"`');
|
|
298
|
+
await applyGritQL(tree, TERRAFORM_STATIC_WEBSITE_FILE, DISTRIBUTION_LOGS_ENCRYPTION_KEY_LINE + ' => `kms_master_key_id = var.encryption == "KMS" ? local.website_kms_key_arn : null`');
|
|
299
|
+
await applyGritQL(tree, TERRAFORM_STATIC_WEBSITE_FILE, DISTRIBUTION_LOGS_ENCRYPTION_ALGO_LINE + ' => `sse_algorithm = var.encryption == "KMS" ? "aws:kms" : "AES256"`');
|
|
300
|
+
// 5. Make the WAF Web ACL conditional on enable_waf.
|
|
301
|
+
await applyGritQL(tree, TERRAFORM_STATIC_WEBSITE_FILE, hcl('`resource "aws_wafv2_web_acl" "cloudfront_waf" { $body }` => `resource "aws_wafv2_web_acl" "cloudfront_waf" {\n' + ' count = var.enable_waf ? 1 : 0\n\n' + ' $body\n' + '}`'));
|
|
302
|
+
await applyGritQL(tree, TERRAFORM_STATIC_WEBSITE_FILE, WEB_ACL_ID_LINE + ' => `web_acl_id = var.enable_waf ? aws_wafv2_web_acl.cloudfront_waf[0].arn : null`');
|
|
303
|
+
await applyGritQL(tree, TERRAFORM_STATIC_WEBSITE_FILE, WAF_OUTPUT_VALUE_LINE + ' => `value = var.enable_waf ? aws_wafv2_web_acl.cloudfront_waf[0].arn : null`');
|
|
304
|
+
// 6. Drop the lifecycle block that can no longer resolve once the WAF ACL
|
|
305
|
+
// has a count of 0 and no prior state to reference.
|
|
306
|
+
await applyGritQL(tree, TERRAFORM_STATIC_WEBSITE_FILE, LIFECYCLE_REPLACE_TRIGGERED_BY_BLOCK + ' => ``');
|
|
307
|
+
};
|
|
308
|
+
const NEW_APP_MODULE_VARIABLES_TEXT = [
|
|
309
|
+
'variable "custom_domain_names" {',
|
|
310
|
+
' description = "Custom domain names (aliases) for the CloudFront distribution. Requires acm_certificate_arn."',
|
|
311
|
+
' type = list(string)',
|
|
312
|
+
' default = []',
|
|
313
|
+
'}',
|
|
314
|
+
'',
|
|
315
|
+
'variable "acm_certificate_arn" {',
|
|
316
|
+
' description = "ARN of an ACM certificate (in us-east-1) for the custom domain names. When set, viewers are required to use TLS 1.2 or later."',
|
|
317
|
+
' type = string',
|
|
318
|
+
' default = null',
|
|
319
|
+
'}',
|
|
320
|
+
'',
|
|
321
|
+
'variable "enable_waf" {',
|
|
322
|
+
' description = "Whether to protect the CloudFront distribution with an AWS WAF Web ACL."',
|
|
323
|
+
' type = bool',
|
|
324
|
+
' default = true',
|
|
325
|
+
'}',
|
|
326
|
+
'',
|
|
327
|
+
'variable "encryption" {',
|
|
328
|
+
' description = "Server-side encryption for the website and distribution log buckets. One of KMS or S3_MANAGED."',
|
|
329
|
+
' type = string',
|
|
330
|
+
' default = "KMS"',
|
|
331
|
+
'',
|
|
332
|
+
' validation {',
|
|
333
|
+
' condition = contains(["KMS", "S3_MANAGED"], var.encryption)',
|
|
334
|
+
' error_message = "encryption must be one of KMS or S3_MANAGED."',
|
|
335
|
+
' }',
|
|
336
|
+
'}',
|
|
337
|
+
'',
|
|
338
|
+
'variable "kms_key_arn" {',
|
|
339
|
+
' description = "ARN of an existing KMS key used to encrypt the website and distribution log buckets when encryption is KMS. When not provided and create_kms_key is true, a new key is created. Note that a customer-supplied key must already grant the CloudWatch Logs, S3 and CloudFront service principals the necessary permissions in its own key policy."',
|
|
340
|
+
' type = string',
|
|
341
|
+
' default = null',
|
|
342
|
+
'}',
|
|
343
|
+
'',
|
|
344
|
+
'variable "create_kms_key" {',
|
|
345
|
+
' description = "Whether to create a KMS key for the website. Only applies when encryption is KMS. Set to false when supplying kms_key_arn."',
|
|
346
|
+
' type = bool',
|
|
347
|
+
' default = true',
|
|
348
|
+
'}',
|
|
349
|
+
'',
|
|
350
|
+
'variable "enable_key_rotation" {',
|
|
351
|
+
' description = "Whether the automatically created KMS key has rotation enabled. Only applies when encryption is KMS and create_kms_key is true."',
|
|
352
|
+
' type = bool',
|
|
353
|
+
' default = true',
|
|
354
|
+
'}'
|
|
355
|
+
].join('\n');
|
|
356
|
+
const NEW_APP_MODULE_FORWARD_TEXT = [
|
|
357
|
+
'custom_domain_names = var.custom_domain_names',
|
|
358
|
+
'acm_certificate_arn = var.acm_certificate_arn',
|
|
359
|
+
'enable_waf = var.enable_waf',
|
|
360
|
+
'encryption = var.encryption',
|
|
361
|
+
'kms_key_arn = var.kms_key_arn',
|
|
362
|
+
'create_kms_key = var.create_kms_key',
|
|
363
|
+
'enable_key_rotation = var.enable_key_rotation'
|
|
364
|
+
].join('\n ');
|
|
365
|
+
/**
|
|
366
|
+
* Surface the same configuration as pass-through variables on each vended
|
|
367
|
+
* per-website Terraform app module, matching the pass-through convention
|
|
368
|
+
* every other app-wraps-core Terraform module already follows.
|
|
369
|
+
*/ const migrateTerraformAppModules = async (tree, nextSteps)=>{
|
|
370
|
+
if (!tree.exists(TERRAFORM_STATIC_WEBSITES_APP_DIR)) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
for (const dirName of tree.children(TERRAFORM_STATIC_WEBSITES_APP_DIR)){
|
|
374
|
+
const filePath = joinPathFragments(TERRAFORM_STATIC_WEBSITES_APP_DIR, dirName, `${dirName}.tf`);
|
|
375
|
+
if (!tree.exists(filePath)) {
|
|
376
|
+
continue; // Not a website app module directory.
|
|
377
|
+
}
|
|
378
|
+
if (await matchGritQL(tree, filePath, hcl('`variable "enable_waf" { $_ }`'))) {
|
|
379
|
+
continue; // Already migrated.
|
|
380
|
+
}
|
|
381
|
+
const TERRAFORM_BLOCK = hcl('`terraform { $_ }`');
|
|
382
|
+
const MODULE_BLOCK = hcl('`module "static_website" { $_ }`');
|
|
383
|
+
const FILE_PATH_LINE = hcl('`website_file_path = $val` as $line where {\n' + ' $line <: within `module "static_website" { $_ }`\n' + '}');
|
|
384
|
+
// custom_domain_names/acm_certificate_arn predate this migration as a
|
|
385
|
+
// documented customization point (hand-editing the module block). If
|
|
386
|
+
// either is already present as a literal argument, blindly forwarding
|
|
387
|
+
// our own `= var.x` line would produce a duplicate argument, which is
|
|
388
|
+
// invalid HCL. Treat that as diverged instead of silently corrupting the file.
|
|
389
|
+
const hasExistingCustomDomainArg = async (name)=>matchGritQL(tree, filePath, hcl(`\`${name} = $_\` as $line where {\n` + ' $line <: within `module "static_website" { $_ }`\n' + '}'));
|
|
390
|
+
const ready = (await Promise.all([
|
|
391
|
+
TERRAFORM_BLOCK,
|
|
392
|
+
MODULE_BLOCK,
|
|
393
|
+
FILE_PATH_LINE
|
|
394
|
+
].map((pattern)=>matchGritQL(tree, filePath, pattern)))).every(Boolean) && !await hasExistingCustomDomainArg('custom_domain_names') && !await hasExistingCustomDomainArg('acm_certificate_arn');
|
|
395
|
+
if (!ready) {
|
|
396
|
+
nextSteps.push(terraformAppDivergedMessage(filePath));
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
// Inserted right after the terraform/required_providers block (and
|
|
400
|
+
// before the "Static website module" comment), matching where the
|
|
401
|
+
// vended template puts these variables.
|
|
402
|
+
await insertViaGritQL(tree, filePath, hcl('`terraform { $body }` as $tf') + ` => \`$tf\n\n${GRIT_INSERT_PLACEHOLDER}\``, NEW_APP_MODULE_VARIABLES_TEXT);
|
|
403
|
+
await insertViaGritQL(tree, filePath, FILE_PATH_LINE + ` => \`$line\n\n ${GRIT_INSERT_PLACEHOLDER}\``, NEW_APP_MODULE_FORWARD_TEXT);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
export default async function migration(tree) {
|
|
407
|
+
const nextSteps = [];
|
|
408
|
+
await migrateCdkConstruct(tree, nextSteps);
|
|
409
|
+
await migrateCdkAppConstructs(tree, nextSteps);
|
|
410
|
+
await migrateTerraformModule(tree, nextSteps);
|
|
411
|
+
await migrateTerraformAppModules(tree, nextSteps);
|
|
412
|
+
await formatFilesInSubtree(tree);
|
|
413
|
+
return {
|
|
414
|
+
nextSteps
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
//# sourceMappingURL=migration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/static-website-configurable-waf-encryption/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n joinPathFragments,\n type MigrationReturnObject,\n type Tree,\n} from '@nx/devkit';\nimport {\n addDestructuredImport,\n applyGritQL,\n captureGritQLVariable,\n GRIT_INSERT_PLACEHOLDER,\n insertViaGritQL,\n matchGritQL,\n} from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport {\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n SHARED_TERRAFORM_DIR,\n} from '../../../utils/shared-constructs-constants';\n\n/**\n * Bring existing workspaces up to the generators' current StaticWebsite\n * configurability:\n *\n * - The vended `StaticWebsite` CDK construct gains `enableWaf`, `encryption`,\n * `encryptionKey` and `enableKeyRotation` props. The WAF Web ACL is now\n * optional, the KMS key used to encrypt the website and distribution log\n * buckets is now overridable (or skippable in favour of another\n * `BucketEncryption`), and the auto-created key's rotation is configurable.\n * - Each vended per-website app construct (`app/static-websites/*.ts`) gains\n * an optional `props` constructor parameter so these can be configured per\n * app, rather than only by hand-editing the vended construct.\n * - The vended Terraform static-website core module gains the equivalent\n * `enable_waf`, `encryption`, `kms_key_arn`, `create_kms_key` and\n * `enable_key_rotation` variables. The CloudFront distribution's\n * `lifecycle.replace_triggered_by` on the WAF ACL is also dropped - it\n * can't resolve once the ACL has a count of 0 and no prior state to\n * reference, which broke `terraform plan` on any greenfield deployment\n * with `enable_waf = false`.\n * - Each vended per-website Terraform app module (`app/static-websites/<name>/<name>.tf`)\n * gains pass-through `custom_domain_names`, `acm_certificate_arn`,\n * `enable_waf`, `encryption`, `kms_key_arn`, `create_kms_key` and\n * `enable_key_rotation` variables forwarded to the core module call,\n * matching the pass-through convention every other app-wraps-core\n * Terraform module in this plugin already follows (`rdb`, `agent-core`,\n * `dcr-proxies`, the REST/HTTP API).\n *\n * These files are generated with `KeepExisting`, so without this an upgraded\n * workspace has generators that support this configuration but vended files\n * that don't. Diverged files are left untouched and reported via `nextSteps`.\n */\n\nconst CDK_STATIC_WEBSITE_FILE = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core/static-website.ts`;\nconst CDK_STATIC_WEBSITES_APP_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/app/static-websites`;\nconst TERRAFORM_STATIC_WEBSITE_FILE = `${PACKAGES_DIR}/${SHARED_TERRAFORM_DIR}/src/core/static-website/static-website.tf`;\nconst TERRAFORM_STATIC_WEBSITES_APP_DIR = `${PACKAGES_DIR}/${SHARED_TERRAFORM_DIR}/src/app/static-websites`;\n\nconst CDK_DIVERGED_MESSAGE = `${CDK_STATIC_WEBSITE_FILE}: has diverged from the generated shape - left untouched. To pick up the enableWaf, encryption, encryptionKey and enableKeyRotation props, manually port them from the vended core/static-website.ts template (see the ts#react-website generator's static-website construct).`;\n\nconst TERRAFORM_DIVERGED_MESSAGE = `${TERRAFORM_STATIC_WEBSITE_FILE}: has diverged from the generated shape - left untouched. To pick up the enable_waf, encryption, kms_key_arn, create_kms_key and enable_key_rotation variables, manually port them from the vended static-website.tf template (see the ts#react-website generator's static-website module).`;\n\nconst terraformAppDivergedMessage = (filePath: string) =>\n `${filePath}: has diverged from the generated shape - left untouched. To make custom_domain_names, acm_certificate_arn, enable_waf, encryption, kms_key_arn, create_kms_key and enable_key_rotation configurable from your root Terraform configuration, add pass-through variables here and forward them to the static_website module call (see the ts#react-website generator's static-websites app template).`;\n\n// The doc comments below carry backticked markdown, which must stay out of\n// the GritQL pattern that inserts this text (see insertViaGritQL). It's routed\n// in as plain text via the placeholder instead.\nconst STATIC_WEBSITE_PROPS_TEXT = `/**\n * Whether to protect the CloudFront distribution with an AWS WAF Web ACL.\n *\n * @default true\n */\n readonly enableWaf?: boolean;\n /**\n * Server-side encryption for the website and distribution log buckets.\n *\n * @default BucketEncryption.KMS\n */\n readonly encryption?: BucketEncryption;\n /**\n * KMS key used to encrypt the website and distribution log buckets. Only used when \\`encryption\\` is\n * \\`BucketEncryption.KMS\\`. When not provided, a new key is created. Note that a key imported via\n * \\`Key.fromKeyArn\\` must already grant the CloudWatch Logs, S3 and CloudFront service principals the\n * necessary permissions in its own key policy - \\`addToResourcePolicy\\` is a no-op on an imported key,\n * so this construct cannot grant them on your behalf.\n */\n readonly encryptionKey?: IKey;\n /**\n * Whether the automatically created KMS key has rotation enabled. Only applies when \\`encryption\\` is\n * \\`BucketEncryption.KMS\\` and no \\`encryptionKey\\` is supplied.\n *\n * @default true\n */\n readonly enableKeyRotation?: boolean`;\n\n/**\n * Surface enableWaf/encryption/encryptionKey/enableKeyRotation on the vended\n * StaticWebsite CDK construct.\n */\nconst migrateCdkConstruct = async (\n tree: Tree,\n nextSteps: string[],\n): Promise<void> => {\n if (!tree.exists(CDK_STATIC_WEBSITE_FILE)) {\n return; // This workspace has no CDK static website construct.\n }\n\n if (\n await matchGritQL(\n tree,\n CDK_STATIC_WEBSITE_FILE,\n '`readonly enableWaf?: boolean`',\n )\n ) {\n return; // Already migrated.\n }\n\n const CERTIFICATE_FIELD = '`readonly certificate?: ICertificate`';\n const DESTRUCTURE_OLD =\n '`{ websiteFilePath, websiteName, domainNames, certificate }: StaticWebsiteProps`';\n const KEY_CREATION_OLD =\n \"`const websiteKey = new Key(this, 'WebsiteKey', { enableKeyRotation: true })`\";\n const ADD_TO_RESOURCE_POLICY_OLD = '`websiteKey.addToResourcePolicy($args)`';\n const WEBSITE_BUCKET_ENCRYPTION_OLD =\n \"`encryption: BucketEncryption.KMS` as $prop where { $prop <: within `new Bucket($_, 'WebsiteBucket', $_)` }\";\n const DISTRIBUTION_LOGS_BUCKET_ENCRYPTION_OLD =\n \"`encryption: BucketEncryption.KMS` as $prop where { $prop <: within `new Bucket($_, 'DistributionLogBucket', $_)` }\";\n const WAF_STACK_OLD = \"`const wafStack = new CloudfrontWebAcl(this, 'waf')`\";\n const WEB_ACL_ID_OLD = '`webAclId: wafStack.wafArn`';\n\n const anchors = [\n CERTIFICATE_FIELD,\n DESTRUCTURE_OLD,\n KEY_CREATION_OLD,\n ADD_TO_RESOURCE_POLICY_OLD,\n WEBSITE_BUCKET_ENCRYPTION_OLD,\n DISTRIBUTION_LOGS_BUCKET_ENCRYPTION_OLD,\n WAF_STACK_OLD,\n WEB_ACL_ID_OLD,\n ];\n\n const allPresent = (\n await Promise.all(\n anchors.map((pattern) =>\n matchGritQL(tree, CDK_STATIC_WEBSITE_FILE, pattern),\n ),\n )\n ).every(Boolean);\n\n if (!allPresent) {\n nextSteps.push(CDK_DIVERGED_MESSAGE);\n return;\n }\n\n await addDestructuredImport(\n tree,\n CDK_STATIC_WEBSITE_FILE,\n ['IKey'],\n 'aws-cdk-lib/aws-kms',\n );\n\n await insertViaGritQL(\n tree,\n CDK_STATIC_WEBSITE_FILE,\n `${CERTIFICATE_FIELD} as $field => \\`$field;\n ${GRIT_INSERT_PLACEHOLDER}\\``,\n STATIC_WEBSITE_PROPS_TEXT,\n );\n\n await applyGritQL(\n tree,\n CDK_STATIC_WEBSITE_FILE,\n `${DESTRUCTURE_OLD} => \\`{\n websiteFilePath,\n websiteName,\n domainNames,\n certificate,\n enableWaf = true,\n encryption = BucketEncryption.KMS,\n encryptionKey,\n enableKeyRotation = true,\n }: StaticWebsiteProps\\``,\n );\n\n await applyGritQL(\n tree,\n CDK_STATIC_WEBSITE_FILE,\n `${KEY_CREATION_OLD} => \\`const websiteKey: IKey | undefined =\n encryption === BucketEncryption.KMS\n ? (encryptionKey ?? new Key(this, 'WebsiteKey', { enableKeyRotation }))\n : undefined;\\``,\n );\n\n await applyGritQL(\n tree,\n CDK_STATIC_WEBSITE_FILE,\n `${ADD_TO_RESOURCE_POLICY_OLD} => \\`websiteKey?.addToResourcePolicy($args)\\``,\n );\n\n await applyGritQL(\n tree,\n CDK_STATIC_WEBSITE_FILE,\n `${WEBSITE_BUCKET_ENCRYPTION_OLD} => \\`encryption\\``,\n );\n\n await applyGritQL(\n tree,\n CDK_STATIC_WEBSITE_FILE,\n `${DISTRIBUTION_LOGS_BUCKET_ENCRYPTION_OLD} => \\`encryption\\``,\n );\n\n await applyGritQL(\n tree,\n CDK_STATIC_WEBSITE_FILE,\n `${WAF_STACK_OLD} => \\`const wafStack = enableWaf ? new CloudfrontWebAcl(this, 'waf') : undefined;\\``,\n );\n\n await applyGritQL(\n tree,\n CDK_STATIC_WEBSITE_FILE,\n `${WEB_ACL_ID_OLD} => \\`webAclId: wafStack?.wafArn\\``,\n );\n};\n\n/**\n * Surface the same props on each vended per-website app construct, so they\n * can be configured per app rather than only by editing the shared construct.\n */\nconst migrateCdkAppConstructs = async (\n tree: Tree,\n nextSteps: string[],\n): Promise<void> => {\n if (!tree.exists(CDK_STATIC_WEBSITES_APP_DIR)) {\n return;\n }\n\n for (const fileName of tree.children(CDK_STATIC_WEBSITES_APP_DIR)) {\n if (fileName === 'index.ts' || !fileName.endsWith('.ts')) {\n continue;\n }\n const filePath = joinPathFragments(CDK_STATIC_WEBSITES_APP_DIR, fileName);\n\n const contents = tree.read(filePath, 'utf-8') ?? '';\n if (contents.includes('StaticWebsiteProps')) {\n continue; // Already migrated.\n }\n\n const name = await captureGritQLVariable(\n tree,\n filePath,\n '`export class $name extends StaticWebsite { $_ }`',\n 'name',\n );\n if (!name) {\n continue; // Not a StaticWebsite subclass, not ours to touch.\n }\n\n const importPattern = '`import { StaticWebsite } from $path`';\n const ctorPattern = '`constructor(scope: Construct, id: string) { $body }`';\n // Captures the whole property list rather than anchoring on websiteName\n // being followed by exactly one more property ($rest binds a single\n // trailing node, not a variadic list). This must still match when a\n // workspace already customised the super() call with e.g. domainNames\n // and certificate (the pre-existing documented customization point).\n const superPattern = `\\`super(scope, id, { $props })\\` as $call where {\n $props <: contains \\`websiteName: '${name}'\\`\n}`;\n\n const ready = (\n await Promise.all(\n [importPattern, ctorPattern, superPattern].map((pattern) =>\n matchGritQL(tree, filePath, pattern),\n ),\n )\n ).every(Boolean);\n\n if (!ready) {\n nextSteps.push(\n `${filePath}: has diverged from the generated shape - left untouched. To make enableWaf, encryption, encryptionKey and enableKeyRotation configurable here, add an optional \\`props?: ${name}Props\\` constructor parameter (\\`Omit<StaticWebsiteProps, 'websiteName' | 'websiteFilePath'>\\`) and spread it into the super() call (see the ts#react-website generator's static-websites app template).`,\n );\n continue;\n }\n\n await applyGritQL(\n tree,\n filePath,\n `${importPattern} => \\`import { StaticWebsite, StaticWebsiteProps } from $path\\``,\n );\n\n await insertViaGritQL(\n tree,\n filePath,\n `\\`export class ${name} extends StaticWebsite { $body }\\` as $cls => \\`${GRIT_INSERT_PLACEHOLDER}\n\n$cls\\``,\n `export type ${name}Props = Omit<\\n StaticWebsiteProps,\\n 'websiteName' | 'websiteFilePath'\\n>;`,\n );\n\n await applyGritQL(\n tree,\n filePath,\n `${ctorPattern} => \\`constructor(scope: Construct, id: string, props?: ${name}Props) { $body }\\``,\n );\n\n await applyGritQL(\n tree,\n filePath,\n `${superPattern} => \\`super(scope, id, { ...props, $props })\\``,\n );\n }\n};\n\n// Terraform (HCL) patterns are built from plain strings rather than JS\n// template literals, since the HCL text itself is full of `${...}`\n// interpolations that would otherwise be parsed as JS interpolation.\nconst hcl = (pattern: string) => 'language hcl\\n' + pattern;\n\nconst withinResource = (\n linePattern: string,\n resourceType: string,\n resourceLabel: string,\n) =>\n hcl(\n '`' +\n linePattern +\n '` as $line where {\\n' +\n ' $line <: within `resource \"' +\n resourceType +\n '\" \"' +\n resourceLabel +\n '\" { $_ }`\\n' +\n '}',\n );\n\nconst NEW_VARIABLES_TEXT = [\n 'variable \"enable_waf\" {',\n ' description = \"Whether to protect the CloudFront distribution with an AWS WAF Web ACL.\"',\n ' type = bool',\n ' default = true',\n '}',\n '',\n 'variable \"encryption\" {',\n ' description = \"Server-side encryption for the website and distribution log buckets. One of KMS or S3_MANAGED.\"',\n ' type = string',\n ' default = \"KMS\"',\n '',\n ' validation {',\n ' condition = contains([\"KMS\", \"S3_MANAGED\"], var.encryption)',\n ' error_message = \"encryption must be one of KMS or S3_MANAGED.\"',\n ' }',\n '}',\n '',\n 'variable \"kms_key_arn\" {',\n ' description = \"ARN of an existing KMS key used to encrypt the website and distribution log buckets when encryption is KMS. When not provided and create_kms_key is true, a new key is created. Note that a customer-supplied key must already grant the CloudWatch Logs, S3 and CloudFront service principals the necessary permissions in its own key policy.\"',\n ' type = string',\n ' default = null',\n '}',\n '',\n 'variable \"create_kms_key\" {',\n ' description = \"Whether to create a KMS key for the website. Only applies when encryption is KMS. Set to false when supplying kms_key_arn.\"',\n ' type = bool',\n ' default = true',\n '}',\n '',\n 'variable \"enable_key_rotation\" {',\n ' description = \"Whether the automatically created KMS key has rotation enabled. Only applies when encryption is KMS and create_kms_key is true.\"',\n ' type = bool',\n ' default = true',\n '}',\n].join('\\n');\n\nconst NEW_LOCALS_TEXT = [\n '',\n ' create_website_key = var.encryption == \"KMS\" && var.create_kms_key',\n ' website_kms_key_arn = (',\n ' var.encryption != \"KMS\" ? null :',\n ' local.create_website_key ? aws_kms_key.website_key[0].arn :',\n ' var.kms_key_arn',\n ' )',\n].join('\\n');\n\n/**\n * Surface the same configuration on the vended Terraform static-website\n * module.\n */\nconst migrateTerraformModule = async (\n tree: Tree,\n nextSteps: string[],\n): Promise<void> => {\n if (!tree.exists(TERRAFORM_STATIC_WEBSITE_FILE)) {\n return; // This workspace has no Terraform static website module.\n }\n\n if (\n await matchGritQL(\n tree,\n TERRAFORM_STATIC_WEBSITE_FILE,\n hcl('`variable \"enable_waf\" { $_ }`'),\n )\n ) {\n return; // Already migrated.\n }\n\n const ACM_CERT_VARIABLE = hcl('`variable \"acm_certificate_arn\" { $_ }`');\n const ACCESS_LOGS_LOCAL = hcl(\n '`access_logs_name_prefix = substr(lower(var.website_name), 0, 16)`',\n );\n const KMS_KEY_BLOCK = hcl('`resource \"aws_kms_key\" \"website_key\" { $_ }`');\n const KMS_KEY_ROTATION_LINE = withinResource(\n 'enable_key_rotation = true',\n 'aws_kms_key',\n 'website_key',\n );\n const KMS_ALIAS_BLOCK = hcl(\n '`resource \"aws_kms_alias\" \"website_key_alias\" { $_ }`',\n );\n const KMS_ALIAS_TARGET_LINE = hcl(\n '`target_key_id = aws_kms_key.website_key.key_id`',\n );\n const LOG_GROUP_KMS_LINE = hcl('`kms_key_id = aws_kms_key.website_key.arn`');\n const WEBSITE_ENCRYPTION_KEY_LINE = withinResource(\n 'kms_master_key_id = aws_kms_key.website_key.arn',\n 'aws_s3_bucket_server_side_encryption_configuration',\n 'website_encryption',\n );\n const WEBSITE_ENCRYPTION_ALGO_LINE = withinResource(\n 'sse_algorithm = \"aws:kms\"',\n 'aws_s3_bucket_server_side_encryption_configuration',\n 'website_encryption',\n );\n const DISTRIBUTION_LOGS_ENCRYPTION_KEY_LINE = withinResource(\n 'kms_master_key_id = aws_kms_key.website_key.arn',\n 'aws_s3_bucket_server_side_encryption_configuration',\n 'distribution_logs_encryption',\n );\n const DISTRIBUTION_LOGS_ENCRYPTION_ALGO_LINE = withinResource(\n 'sse_algorithm = \"aws:kms\"',\n 'aws_s3_bucket_server_side_encryption_configuration',\n 'distribution_logs_encryption',\n );\n const WAF_BLOCK = hcl(\n '`resource \"aws_wafv2_web_acl\" \"cloudfront_waf\" { $_ }`',\n );\n const WEB_ACL_ID_LINE = hcl(\n '`web_acl_id = aws_wafv2_web_acl.cloudfront_waf.arn`',\n );\n const WAF_OUTPUT_VALUE_LINE = hcl(\n '`value = aws_wafv2_web_acl.cloudfront_waf.arn` as $line where {\\n' +\n ' $line <: within `output \"waf_web_acl_arn\" { $_ }`\\n' +\n '}',\n );\n // Forces distribution replacement whenever the WAF ACL changes. Once the\n // ACL gains `count`, this can't be resolved on a greenfield apply where\n // count is 0 and there's no prior state to reference - see\n // https://github.com/awslabs/nx-plugin-for-aws/pull/1107. It's also\n // redundant: web_acl_id already references the ACL ARN directly, so a\n // replaced ACL still drives a plain distribution update.\n const LIFECYCLE_REPLACE_TRIGGERED_BY_BLOCK = hcl(\n '`lifecycle {\\n replace_triggered_by = [\\n aws_wafv2_web_acl.cloudfront_waf\\n ]\\n }`',\n );\n\n const anchors = [\n ACM_CERT_VARIABLE,\n ACCESS_LOGS_LOCAL,\n KMS_KEY_BLOCK,\n KMS_KEY_ROTATION_LINE,\n KMS_ALIAS_BLOCK,\n KMS_ALIAS_TARGET_LINE,\n LOG_GROUP_KMS_LINE,\n WEBSITE_ENCRYPTION_KEY_LINE,\n WEBSITE_ENCRYPTION_ALGO_LINE,\n DISTRIBUTION_LOGS_ENCRYPTION_KEY_LINE,\n DISTRIBUTION_LOGS_ENCRYPTION_ALGO_LINE,\n WAF_BLOCK,\n WEB_ACL_ID_LINE,\n WAF_OUTPUT_VALUE_LINE,\n LIFECYCLE_REPLACE_TRIGGERED_BY_BLOCK,\n ];\n\n const allPresent = (\n await Promise.all(\n anchors.map((pattern) =>\n matchGritQL(tree, TERRAFORM_STATIC_WEBSITE_FILE, pattern),\n ),\n )\n ).every(Boolean);\n\n if (!allPresent) {\n nextSteps.push(TERRAFORM_DIVERGED_MESSAGE);\n return;\n }\n\n // 1. New variables, after acm_certificate_arn. Routed through the\n // placeholder since the variable descriptions contain double-quoted\n // HCL string values.\n await insertViaGritQL(\n tree,\n TERRAFORM_STATIC_WEBSITE_FILE,\n hcl(\n '`variable \"acm_certificate_arn\" { $body }` as $var => `$var\\n\\n' +\n GRIT_INSERT_PLACEHOLDER +\n '`',\n ),\n NEW_VARIABLES_TEXT,\n );\n\n // 2. New locals, after access_logs_name_prefix.\n await insertViaGritQL(\n tree,\n TERRAFORM_STATIC_WEBSITE_FILE,\n hcl(\n '`access_logs_name_prefix = substr(lower(var.website_name), 0, 16)` as $line => `$line\\n' +\n GRIT_INSERT_PLACEHOLDER +\n '`',\n ),\n NEW_LOCALS_TEXT,\n );\n\n // 3. Make the KMS key and alias conditional on encryption/kms_key_arn.\n await applyGritQL(\n tree,\n TERRAFORM_STATIC_WEBSITE_FILE,\n hcl(\n '`resource \"aws_kms_key\" \"website_key\" { $body }` => `resource \"aws_kms_key\" \"website_key\" {\\n' +\n ' count = local.create_website_key ? 1 : 0\\n\\n' +\n ' $body\\n' +\n '}`',\n ),\n );\n await applyGritQL(\n tree,\n TERRAFORM_STATIC_WEBSITE_FILE,\n KMS_KEY_ROTATION_LINE +\n ' => `enable_key_rotation = var.enable_key_rotation`',\n );\n await applyGritQL(\n tree,\n TERRAFORM_STATIC_WEBSITE_FILE,\n hcl(\n '`resource \"aws_kms_alias\" \"website_key_alias\" { $body }` => `resource \"aws_kms_alias\" \"website_key_alias\" {\\n' +\n ' count = local.create_website_key ? 1 : 0\\n\\n' +\n ' $body\\n' +\n '}`',\n ),\n );\n await applyGritQL(\n tree,\n TERRAFORM_STATIC_WEBSITE_FILE,\n KMS_ALIAS_TARGET_LINE +\n ' => `target_key_id = aws_kms_key.website_key[0].key_id`',\n );\n\n // 4. Resolve the key ARN through the new local everywhere it's consumed.\n await applyGritQL(\n tree,\n TERRAFORM_STATIC_WEBSITE_FILE,\n LOG_GROUP_KMS_LINE + ' => `kms_key_id = local.website_kms_key_arn`',\n );\n await applyGritQL(\n tree,\n TERRAFORM_STATIC_WEBSITE_FILE,\n WEBSITE_ENCRYPTION_KEY_LINE +\n ' => `kms_master_key_id = var.encryption == \"KMS\" ? local.website_kms_key_arn : null`',\n );\n await applyGritQL(\n tree,\n TERRAFORM_STATIC_WEBSITE_FILE,\n WEBSITE_ENCRYPTION_ALGO_LINE +\n ' => `sse_algorithm = var.encryption == \"KMS\" ? \"aws:kms\" : \"AES256\"`',\n );\n await applyGritQL(\n tree,\n TERRAFORM_STATIC_WEBSITE_FILE,\n DISTRIBUTION_LOGS_ENCRYPTION_KEY_LINE +\n ' => `kms_master_key_id = var.encryption == \"KMS\" ? local.website_kms_key_arn : null`',\n );\n await applyGritQL(\n tree,\n TERRAFORM_STATIC_WEBSITE_FILE,\n DISTRIBUTION_LOGS_ENCRYPTION_ALGO_LINE +\n ' => `sse_algorithm = var.encryption == \"KMS\" ? \"aws:kms\" : \"AES256\"`',\n );\n\n // 5. Make the WAF Web ACL conditional on enable_waf.\n await applyGritQL(\n tree,\n TERRAFORM_STATIC_WEBSITE_FILE,\n hcl(\n '`resource \"aws_wafv2_web_acl\" \"cloudfront_waf\" { $body }` => `resource \"aws_wafv2_web_acl\" \"cloudfront_waf\" {\\n' +\n ' count = var.enable_waf ? 1 : 0\\n\\n' +\n ' $body\\n' +\n '}`',\n ),\n );\n await applyGritQL(\n tree,\n TERRAFORM_STATIC_WEBSITE_FILE,\n WEB_ACL_ID_LINE +\n ' => `web_acl_id = var.enable_waf ? aws_wafv2_web_acl.cloudfront_waf[0].arn : null`',\n );\n await applyGritQL(\n tree,\n TERRAFORM_STATIC_WEBSITE_FILE,\n WAF_OUTPUT_VALUE_LINE +\n ' => `value = var.enable_waf ? aws_wafv2_web_acl.cloudfront_waf[0].arn : null`',\n );\n\n // 6. Drop the lifecycle block that can no longer resolve once the WAF ACL\n // has a count of 0 and no prior state to reference.\n await applyGritQL(\n tree,\n TERRAFORM_STATIC_WEBSITE_FILE,\n LIFECYCLE_REPLACE_TRIGGERED_BY_BLOCK + ' => ``',\n );\n};\n\nconst NEW_APP_MODULE_VARIABLES_TEXT = [\n 'variable \"custom_domain_names\" {',\n ' description = \"Custom domain names (aliases) for the CloudFront distribution. Requires acm_certificate_arn.\"',\n ' type = list(string)',\n ' default = []',\n '}',\n '',\n 'variable \"acm_certificate_arn\" {',\n ' description = \"ARN of an ACM certificate (in us-east-1) for the custom domain names. When set, viewers are required to use TLS 1.2 or later.\"',\n ' type = string',\n ' default = null',\n '}',\n '',\n 'variable \"enable_waf\" {',\n ' description = \"Whether to protect the CloudFront distribution with an AWS WAF Web ACL.\"',\n ' type = bool',\n ' default = true',\n '}',\n '',\n 'variable \"encryption\" {',\n ' description = \"Server-side encryption for the website and distribution log buckets. One of KMS or S3_MANAGED.\"',\n ' type = string',\n ' default = \"KMS\"',\n '',\n ' validation {',\n ' condition = contains([\"KMS\", \"S3_MANAGED\"], var.encryption)',\n ' error_message = \"encryption must be one of KMS or S3_MANAGED.\"',\n ' }',\n '}',\n '',\n 'variable \"kms_key_arn\" {',\n ' description = \"ARN of an existing KMS key used to encrypt the website and distribution log buckets when encryption is KMS. When not provided and create_kms_key is true, a new key is created. Note that a customer-supplied key must already grant the CloudWatch Logs, S3 and CloudFront service principals the necessary permissions in its own key policy.\"',\n ' type = string',\n ' default = null',\n '}',\n '',\n 'variable \"create_kms_key\" {',\n ' description = \"Whether to create a KMS key for the website. Only applies when encryption is KMS. Set to false when supplying kms_key_arn.\"',\n ' type = bool',\n ' default = true',\n '}',\n '',\n 'variable \"enable_key_rotation\" {',\n ' description = \"Whether the automatically created KMS key has rotation enabled. Only applies when encryption is KMS and create_kms_key is true.\"',\n ' type = bool',\n ' default = true',\n '}',\n].join('\\n');\n\nconst NEW_APP_MODULE_FORWARD_TEXT = [\n 'custom_domain_names = var.custom_domain_names',\n 'acm_certificate_arn = var.acm_certificate_arn',\n 'enable_waf = var.enable_waf',\n 'encryption = var.encryption',\n 'kms_key_arn = var.kms_key_arn',\n 'create_kms_key = var.create_kms_key',\n 'enable_key_rotation = var.enable_key_rotation',\n].join('\\n ');\n\n/**\n * Surface the same configuration as pass-through variables on each vended\n * per-website Terraform app module, matching the pass-through convention\n * every other app-wraps-core Terraform module already follows.\n */\nconst migrateTerraformAppModules = async (\n tree: Tree,\n nextSteps: string[],\n): Promise<void> => {\n if (!tree.exists(TERRAFORM_STATIC_WEBSITES_APP_DIR)) {\n return;\n }\n\n for (const dirName of tree.children(TERRAFORM_STATIC_WEBSITES_APP_DIR)) {\n const filePath = joinPathFragments(\n TERRAFORM_STATIC_WEBSITES_APP_DIR,\n dirName,\n `${dirName}.tf`,\n );\n if (!tree.exists(filePath)) {\n continue; // Not a website app module directory.\n }\n\n if (\n await matchGritQL(tree, filePath, hcl('`variable \"enable_waf\" { $_ }`'))\n ) {\n continue; // Already migrated.\n }\n\n const TERRAFORM_BLOCK = hcl('`terraform { $_ }`');\n const MODULE_BLOCK = hcl('`module \"static_website\" { $_ }`');\n const FILE_PATH_LINE = hcl(\n '`website_file_path = $val` as $line where {\\n' +\n ' $line <: within `module \"static_website\" { $_ }`\\n' +\n '}',\n );\n // custom_domain_names/acm_certificate_arn predate this migration as a\n // documented customization point (hand-editing the module block). If\n // either is already present as a literal argument, blindly forwarding\n // our own `= var.x` line would produce a duplicate argument, which is\n // invalid HCL. Treat that as diverged instead of silently corrupting the file.\n const hasExistingCustomDomainArg = async (name: string) =>\n matchGritQL(\n tree,\n filePath,\n hcl(\n `\\`${name} = $_\\` as $line where {\\n` +\n ' $line <: within `module \"static_website\" { $_ }`\\n' +\n '}',\n ),\n );\n\n const ready =\n (\n await Promise.all(\n [TERRAFORM_BLOCK, MODULE_BLOCK, FILE_PATH_LINE].map((pattern) =>\n matchGritQL(tree, filePath, pattern),\n ),\n )\n ).every(Boolean) &&\n !(await hasExistingCustomDomainArg('custom_domain_names')) &&\n !(await hasExistingCustomDomainArg('acm_certificate_arn'));\n\n if (!ready) {\n nextSteps.push(terraformAppDivergedMessage(filePath));\n continue;\n }\n\n // Inserted right after the terraform/required_providers block (and\n // before the \"Static website module\" comment), matching where the\n // vended template puts these variables.\n await insertViaGritQL(\n tree,\n filePath,\n hcl('`terraform { $body }` as $tf') +\n ` => \\`$tf\\n\\n${GRIT_INSERT_PLACEHOLDER}\\``,\n NEW_APP_MODULE_VARIABLES_TEXT,\n );\n\n await insertViaGritQL(\n tree,\n filePath,\n FILE_PATH_LINE + ` => \\`$line\\n\\n ${GRIT_INSERT_PLACEHOLDER}\\``,\n NEW_APP_MODULE_FORWARD_TEXT,\n );\n }\n};\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n await migrateCdkConstruct(tree, nextSteps);\n await migrateCdkAppConstructs(tree, nextSteps);\n await migrateTerraformModule(tree, nextSteps);\n await migrateTerraformAppModules(tree, nextSteps);\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["joinPathFragments","addDestructuredImport","applyGritQL","captureGritQLVariable","GRIT_INSERT_PLACEHOLDER","insertViaGritQL","matchGritQL","formatFilesInSubtree","PACKAGES_DIR","SHARED_CONSTRUCTS_DIR","SHARED_TERRAFORM_DIR","CDK_STATIC_WEBSITE_FILE","CDK_STATIC_WEBSITES_APP_DIR","TERRAFORM_STATIC_WEBSITE_FILE","TERRAFORM_STATIC_WEBSITES_APP_DIR","CDK_DIVERGED_MESSAGE","TERRAFORM_DIVERGED_MESSAGE","terraformAppDivergedMessage","filePath","STATIC_WEBSITE_PROPS_TEXT","migrateCdkConstruct","tree","nextSteps","exists","CERTIFICATE_FIELD","DESTRUCTURE_OLD","KEY_CREATION_OLD","ADD_TO_RESOURCE_POLICY_OLD","WEBSITE_BUCKET_ENCRYPTION_OLD","DISTRIBUTION_LOGS_BUCKET_ENCRYPTION_OLD","WAF_STACK_OLD","WEB_ACL_ID_OLD","anchors","allPresent","Promise","all","map","pattern","every","Boolean","push","migrateCdkAppConstructs","fileName","children","endsWith","contents","read","includes","name","importPattern","ctorPattern","superPattern","ready","hcl","withinResource","linePattern","resourceType","resourceLabel","NEW_VARIABLES_TEXT","join","NEW_LOCALS_TEXT","migrateTerraformModule","ACM_CERT_VARIABLE","ACCESS_LOGS_LOCAL","KMS_KEY_BLOCK","KMS_KEY_ROTATION_LINE","KMS_ALIAS_BLOCK","KMS_ALIAS_TARGET_LINE","LOG_GROUP_KMS_LINE","WEBSITE_ENCRYPTION_KEY_LINE","WEBSITE_ENCRYPTION_ALGO_LINE","DISTRIBUTION_LOGS_ENCRYPTION_KEY_LINE","DISTRIBUTION_LOGS_ENCRYPTION_ALGO_LINE","WAF_BLOCK","WEB_ACL_ID_LINE","WAF_OUTPUT_VALUE_LINE","LIFECYCLE_REPLACE_TRIGGERED_BY_BLOCK","NEW_APP_MODULE_VARIABLES_TEXT","NEW_APP_MODULE_FORWARD_TEXT","migrateTerraformAppModules","dirName","TERRAFORM_BLOCK","MODULE_BLOCK","FILE_PATH_LINE","hasExistingCustomDomainArg","migration"],"mappings":"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"}
|