@aws/nx-plugin 1.0.0-rc.49 → 1.0.0-rc.50
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/LICENSE-THIRD-PARTY +436 -29
- package/migrations.json +17 -1
- package/package.json +2 -1
- package/src/migrations/latest/modernize-function-props-cast/migration.d.ts +6 -0
- package/src/migrations/latest/modernize-function-props-cast/migration.js +66 -0
- package/src/migrations/latest/modernize-function-props-cast/migration.js.map +1 -0
- package/src/migrations/latest/restrict-cors-to-custom-domains/migration.d.ts +6 -0
- package/src/migrations/latest/restrict-cors-to-custom-domains/migration.js +110 -0
- package/src/migrations/latest/restrict-cors-to-custom-domains/migration.js.map +1 -0
- package/src/migrations/latest/terraform-bootstrap-adopt-existing-bucket/migration.d.ts +6 -0
- package/src/migrations/latest/terraform-bootstrap-adopt-existing-bucket/migration.js +129 -0
- package/src/migrations/latest/terraform-bootstrap-adopt-existing-bucket/migration.js.map +1 -0
- package/src/py/agent/__snapshots__/generator.constructs.spec.ts.snap +1 -0
- package/src/py/fast-api/__snapshots__/generator.spec.ts.snap +25 -15
- package/src/py/mcp-server/__snapshots__/generator.spec.ts.snap +1 -0
- package/src/smithy/ts/api/__snapshots__/generator.spec.ts.snap +26 -16
- package/src/terraform/project/files/application/scripts/bootstrap.ts.template +40 -1
- package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +75 -45
- package/src/ts/agent/__snapshots__/generator.spec.ts.snap +1 -0
- package/src/ts/mcp-server/__snapshots__/generator.spec.ts.snap +1 -0
- package/src/ts/nx-migration/files/migration.ts.template +6 -0
- package/src/ts/rdb/__snapshots__/generator.spec.ts.snap +1 -0
- package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +43 -0
- package/src/ts/react-website/cognito-auth/__snapshots__/generator.spec.ts.snap +11 -14
- package/src/utils/__snapshots__/shared-constructs.spec.ts.snap +21 -0
- package/src/utils/api-constructs/files/cdk/app/apis/http/__apiNameKebabCase__.ts.template +12 -7
- package/src/utils/api-constructs/files/cdk/app/apis/rest/__apiNameKebabCase__.ts.template +13 -8
- package/src/utils/files/common/constructs/src/core/cloudfront.ts.template +14 -0
- package/src/utils/files/common/constructs/src/core/index.ts.template +1 -0
- package/src/utils/format.js +118 -240
- package/src/utils/format.js.map +1 -1
- package/src/utils/identity-constructs/files/cdk/core/user-identity.ts.template +7 -14
- package/src/utils/ruff.d.ts +59 -0
- package/src/utils/ruff.js +140 -0
- package/src/utils/ruff.js.map +1 -0
- package/src/utils/toml.d.ts +5 -0
- package/src/utils/toml.js +10 -0
- package/src/utils/toml.js.map +1 -1
- package/src/utils/warm-ruff-cache.d.ts +0 -8
- package/src/utils/warm-ruff-cache.js +0 -30
- package/src/utils/warm-ruff-cache.js.map +0 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/ import { visitNotIgnoredFiles } from "@nx/devkit";
|
|
5
|
+
import { addDestructuredImport, addStarExport, applyGritQL } from "../../../utils/ast.js";
|
|
6
|
+
import { formatFilesInSubtree } from "../../../utils/format.js";
|
|
7
|
+
import { isEsmWorkspace } from "../../../utils/module-format.js";
|
|
8
|
+
import { PACKAGES_DIR, SHARED_CONSTRUCTS_DIR } from "../../../utils/shared-constructs-constants.js";
|
|
9
|
+
/**
|
|
10
|
+
* Include CloudFront custom domain aliases in restrictCorsTo and UserIdentity callback URLs
|
|
11
|
+
*
|
|
12
|
+
* How to write a migration:
|
|
13
|
+
* - https://nx.dev/docs/kb/migration-generators
|
|
14
|
+
* - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject
|
|
15
|
+
*
|
|
16
|
+
* Guardrails:
|
|
17
|
+
* - Pattern-match before writing: skip files that have diverged from the shape
|
|
18
|
+
* your generators produce and report them via `nextSteps`, rather than
|
|
19
|
+
* clobbering the user's changes.
|
|
20
|
+
* - Idempotent: re-running must be a no-op.
|
|
21
|
+
* - Format what you write: finish with `formatFilesInSubtree` so the files your
|
|
22
|
+
* migration wrote are formatted correctly.
|
|
23
|
+
*/ const CORE_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core`;
|
|
24
|
+
const APIS_APP_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/app/apis`;
|
|
25
|
+
const CLOUDFRONT_CORE_FILE = `${CORE_DIR}/cloudfront.ts`;
|
|
26
|
+
const CORE_INDEX_FILE = `${CORE_DIR}/index.ts`;
|
|
27
|
+
const USER_IDENTITY_FILE = `${CORE_DIR}/user-identity.ts`;
|
|
28
|
+
const CLOUDFRONT_HELPER_CONTENT = `import { CfnDistribution, Distribution } from 'aws-cdk-lib/aws-cloudfront';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Finds the domain names associated with a CloudFront distribution.
|
|
32
|
+
*
|
|
33
|
+
* Includes the distribution's default \`*.cloudfront.net\` domain name plus any custom
|
|
34
|
+
* domain names (aliases) configured on it.
|
|
35
|
+
*/
|
|
36
|
+
export const findCloudFrontDomainNames = (
|
|
37
|
+
distribution: Distribution,
|
|
38
|
+
): string[] => {
|
|
39
|
+
const cfnDistribution = distribution.node.defaultChild as CfnDistribution;
|
|
40
|
+
const distributionConfig =
|
|
41
|
+
cfnDistribution.distributionConfig as CfnDistribution.DistributionConfigProperty;
|
|
42
|
+
return [distribution.domainName, ...(distributionConfig.aliases ?? [])];
|
|
43
|
+
};
|
|
44
|
+
`;
|
|
45
|
+
// Rewrites the `restrictCorsTo` body as produced by generators prior to this fix.
|
|
46
|
+
const RESTRICT_CORS_TO_GRITQL_PATTERN = "`origins.map(($o) => typeof $o === 'string' ? $o : 'cloudFrontDistribution' in $o ? $branch1 : $branch2)` where { $branch1 <: contains `distributionDomainName`, $branch2 <: contains `distributionDomainName` } => raw`origins.flatMap(($o) => typeof $o === 'string' ? [$o] : findCloudFrontDomainNames('cloudFrontDistribution' in $o ? $o.cloudFrontDistribution : $o).map((domain) => \\`https://${domain}\\`))`";
|
|
47
|
+
// Rewrites UserIdentity's callback/logout URL logic as produced by generators
|
|
48
|
+
// prior to this fix.
|
|
49
|
+
const USER_IDENTITY_CALLBACK_URLS_GRITQL_PATTERN = '`this.findCloudFrontDomainNames()` => `Stack.of(this).node.findAll().filter((child): child is Distribution => child instanceof Distribution).flatMap(findCloudFrontDomainNames)`';
|
|
50
|
+
export default async function migration(tree) {
|
|
51
|
+
const nextSteps = [];
|
|
52
|
+
if (!tree.exists(CORE_INDEX_FILE)) {
|
|
53
|
+
// No common/constructs shared library in this workspace - nothing to migrate.
|
|
54
|
+
return {
|
|
55
|
+
nextSteps
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const esm = isEsmWorkspace(tree);
|
|
59
|
+
const apiCloudFrontImportSpecifier = esm ? '../../core/cloudfront.js' : '../../core/cloudfront';
|
|
60
|
+
const coreCloudFrontImportSpecifier = esm ? './cloudfront.js' : './cloudfront';
|
|
61
|
+
if (!tree.exists(CLOUDFRONT_CORE_FILE)) {
|
|
62
|
+
tree.write(CLOUDFRONT_CORE_FILE, CLOUDFRONT_HELPER_CONTENT);
|
|
63
|
+
}
|
|
64
|
+
await addStarExport(tree, CORE_INDEX_FILE, './cloudfront.js');
|
|
65
|
+
const apiAppFiles = [];
|
|
66
|
+
visitNotIgnoredFiles(tree, APIS_APP_DIR, (filePath)=>{
|
|
67
|
+
apiAppFiles.push(filePath);
|
|
68
|
+
});
|
|
69
|
+
for (const filePath of apiAppFiles){
|
|
70
|
+
if (!filePath.endsWith('.ts') || filePath.endsWith('/index.ts')) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const rewrote = await applyGritQL(tree, filePath, RESTRICT_CORS_TO_GRITQL_PATTERN);
|
|
74
|
+
if (rewrote) {
|
|
75
|
+
await addDestructuredImport(tree, filePath, [
|
|
76
|
+
'findCloudFrontDomainNames'
|
|
77
|
+
], apiCloudFrontImportSpecifier);
|
|
78
|
+
nextSteps.push(`${filePath}: restrictCorsTo now includes CloudFront custom domain aliases automatically.`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const contents = tree.read(filePath, 'utf-8') ?? '';
|
|
82
|
+
if (!contents.includes('findCloudFrontDomainNames(')) {
|
|
83
|
+
nextSteps.push(`${filePath}: restrictCorsTo has diverged from the generated shape - left untouched. Manually apply the CloudFront custom domain fix (see findCloudFrontDomainNames in common/constructs/src/core/cloudfront.ts).`);
|
|
84
|
+
}
|
|
85
|
+
// Otherwise already migrated - silent skip.
|
|
86
|
+
}
|
|
87
|
+
if (tree.exists(USER_IDENTITY_FILE)) {
|
|
88
|
+
const rewrote = await applyGritQL(tree, USER_IDENTITY_FILE, USER_IDENTITY_CALLBACK_URLS_GRITQL_PATTERN);
|
|
89
|
+
if (rewrote) {
|
|
90
|
+
await applyGritQL(tree, USER_IDENTITY_FILE, "`import { CfnDistribution, Distribution } from 'aws-cdk-lib/aws-cloudfront'` => `import { Distribution } from 'aws-cdk-lib/aws-cloudfront';`");
|
|
91
|
+
await applyGritQL(tree, USER_IDENTITY_FILE, "or { `// Includes each distribution's default domain name plus any custom domain names (aliases) configured on it.` => ., `private findCloudFrontDomainNames = (): string[] => $body` => . }");
|
|
92
|
+
await addDestructuredImport(tree, USER_IDENTITY_FILE, [
|
|
93
|
+
'findCloudFrontDomainNames'
|
|
94
|
+
], coreCloudFrontImportSpecifier);
|
|
95
|
+
nextSteps.push(`${USER_IDENTITY_FILE}: now reuses the shared findCloudFrontDomainNames helper.`);
|
|
96
|
+
} else {
|
|
97
|
+
const contents = tree.read(USER_IDENTITY_FILE, 'utf-8') ?? '';
|
|
98
|
+
if (!contents.includes('.flatMap(findCloudFrontDomainNames)')) {
|
|
99
|
+
nextSteps.push(`${USER_IDENTITY_FILE}: the callback URL logic has diverged from the generated shape - left untouched. Manually apply the shared findCloudFrontDomainNames helper (see common/constructs/src/core/cloudfront.ts).`);
|
|
100
|
+
}
|
|
101
|
+
// Otherwise already migrated - silent skip.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
await formatFilesInSubtree(tree);
|
|
105
|
+
return {
|
|
106
|
+
nextSteps
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
//# sourceMappingURL=migration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/restrict-cors-to-custom-domains/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n type MigrationReturnObject,\n type Tree,\n visitNotIgnoredFiles,\n} from '@nx/devkit';\nimport {\n addDestructuredImport,\n addStarExport,\n applyGritQL,\n} from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport { isEsmWorkspace } from '../../../utils/module-format';\nimport {\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n} from '../../../utils/shared-constructs-constants';\n\n/**\n * Include CloudFront custom domain aliases in restrictCorsTo and UserIdentity callback URLs\n *\n * How to write a migration:\n * - https://nx.dev/docs/kb/migration-generators\n * - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject\n *\n * Guardrails:\n * - Pattern-match before writing: skip files that have diverged from the shape\n * your generators produce and report them via `nextSteps`, rather than\n * clobbering the user's changes.\n * - Idempotent: re-running must be a no-op.\n * - Format what you write: finish with `formatFilesInSubtree` so the files your\n * migration wrote are formatted correctly.\n */\n\nconst CORE_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core`;\nconst APIS_APP_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/app/apis`;\nconst CLOUDFRONT_CORE_FILE = `${CORE_DIR}/cloudfront.ts`;\nconst CORE_INDEX_FILE = `${CORE_DIR}/index.ts`;\nconst USER_IDENTITY_FILE = `${CORE_DIR}/user-identity.ts`;\n\nconst CLOUDFRONT_HELPER_CONTENT = `import { CfnDistribution, Distribution } from 'aws-cdk-lib/aws-cloudfront';\n\n/**\n * Finds the domain names associated with a CloudFront distribution.\n *\n * Includes the distribution's default \\`*.cloudfront.net\\` domain name plus any custom\n * domain names (aliases) configured on it.\n */\nexport const findCloudFrontDomainNames = (\n distribution: Distribution,\n): string[] => {\n const cfnDistribution = distribution.node.defaultChild as CfnDistribution;\n const distributionConfig =\n cfnDistribution.distributionConfig as CfnDistribution.DistributionConfigProperty;\n return [distribution.domainName, ...(distributionConfig.aliases ?? [])];\n};\n`;\n\n// Rewrites the `restrictCorsTo` body as produced by generators prior to this fix.\nconst RESTRICT_CORS_TO_GRITQL_PATTERN =\n \"`origins.map(($o) => typeof $o === 'string' ? $o : 'cloudFrontDistribution' in $o ? $branch1 : $branch2)` where { $branch1 <: contains `distributionDomainName`, $branch2 <: contains `distributionDomainName` } => raw`origins.flatMap(($o) => typeof $o === 'string' ? [$o] : findCloudFrontDomainNames('cloudFrontDistribution' in $o ? $o.cloudFrontDistribution : $o).map((domain) => \\\\`https://${domain}\\\\`))`\";\n\n// Rewrites UserIdentity's callback/logout URL logic as produced by generators\n// prior to this fix.\nconst USER_IDENTITY_CALLBACK_URLS_GRITQL_PATTERN =\n '`this.findCloudFrontDomainNames()` => `Stack.of(this).node.findAll().filter((child): child is Distribution => child instanceof Distribution).flatMap(findCloudFrontDomainNames)`';\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n if (!tree.exists(CORE_INDEX_FILE)) {\n // No common/constructs shared library in this workspace - nothing to migrate.\n return { nextSteps };\n }\n\n const esm = isEsmWorkspace(tree);\n const apiCloudFrontImportSpecifier = esm\n ? '../../core/cloudfront.js'\n : '../../core/cloudfront';\n const coreCloudFrontImportSpecifier = esm\n ? './cloudfront.js'\n : './cloudfront';\n\n if (!tree.exists(CLOUDFRONT_CORE_FILE)) {\n tree.write(CLOUDFRONT_CORE_FILE, CLOUDFRONT_HELPER_CONTENT);\n }\n await addStarExport(tree, CORE_INDEX_FILE, './cloudfront.js');\n\n const apiAppFiles: string[] = [];\n visitNotIgnoredFiles(tree, APIS_APP_DIR, (filePath) => {\n apiAppFiles.push(filePath);\n });\n\n for (const filePath of apiAppFiles) {\n if (!filePath.endsWith('.ts') || filePath.endsWith('/index.ts')) {\n continue;\n }\n const rewrote = await applyGritQL(\n tree,\n filePath,\n RESTRICT_CORS_TO_GRITQL_PATTERN,\n );\n if (rewrote) {\n await addDestructuredImport(\n tree,\n filePath,\n ['findCloudFrontDomainNames'],\n apiCloudFrontImportSpecifier,\n );\n nextSteps.push(\n `${filePath}: restrictCorsTo now includes CloudFront custom domain aliases automatically.`,\n );\n continue;\n }\n const contents = tree.read(filePath, 'utf-8') ?? '';\n if (!contents.includes('findCloudFrontDomainNames(')) {\n nextSteps.push(\n `${filePath}: restrictCorsTo has diverged from the generated shape - left untouched. Manually apply the CloudFront custom domain fix (see findCloudFrontDomainNames in common/constructs/src/core/cloudfront.ts).`,\n );\n }\n // Otherwise already migrated - silent skip.\n }\n\n if (tree.exists(USER_IDENTITY_FILE)) {\n const rewrote = await applyGritQL(\n tree,\n USER_IDENTITY_FILE,\n USER_IDENTITY_CALLBACK_URLS_GRITQL_PATTERN,\n );\n if (rewrote) {\n await applyGritQL(\n tree,\n USER_IDENTITY_FILE,\n \"`import { CfnDistribution, Distribution } from 'aws-cdk-lib/aws-cloudfront'` => `import { Distribution } from 'aws-cdk-lib/aws-cloudfront';`\",\n );\n await applyGritQL(\n tree,\n USER_IDENTITY_FILE,\n \"or { `// Includes each distribution's default domain name plus any custom domain names (aliases) configured on it.` => ., `private findCloudFrontDomainNames = (): string[] => $body` => . }\",\n );\n await addDestructuredImport(\n tree,\n USER_IDENTITY_FILE,\n ['findCloudFrontDomainNames'],\n coreCloudFrontImportSpecifier,\n );\n nextSteps.push(\n `${USER_IDENTITY_FILE}: now reuses the shared findCloudFrontDomainNames helper.`,\n );\n } else {\n const contents = tree.read(USER_IDENTITY_FILE, 'utf-8') ?? '';\n if (!contents.includes('.flatMap(findCloudFrontDomainNames)')) {\n nextSteps.push(\n `${USER_IDENTITY_FILE}: the callback URL logic has diverged from the generated shape - left untouched. Manually apply the shared findCloudFrontDomainNames helper (see common/constructs/src/core/cloudfront.ts).`,\n );\n }\n // Otherwise already migrated - silent skip.\n }\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["visitNotIgnoredFiles","addDestructuredImport","addStarExport","applyGritQL","formatFilesInSubtree","isEsmWorkspace","PACKAGES_DIR","SHARED_CONSTRUCTS_DIR","CORE_DIR","APIS_APP_DIR","CLOUDFRONT_CORE_FILE","CORE_INDEX_FILE","USER_IDENTITY_FILE","CLOUDFRONT_HELPER_CONTENT","RESTRICT_CORS_TO_GRITQL_PATTERN","USER_IDENTITY_CALLBACK_URLS_GRITQL_PATTERN","migration","tree","nextSteps","exists","esm","apiCloudFrontImportSpecifier","coreCloudFrontImportSpecifier","write","apiAppFiles","filePath","push","endsWith","rewrote","contents","read","includes"],"mappings":"AAAA;;;CAGC,GACD,SAGEA,oBAAoB,QACf,aAAa;AACpB,SACEC,qBAAqB,EACrBC,aAAa,EACbC,WAAW,QACN,wBAAqB;AAC5B,SAASC,oBAAoB,QAAQ,2BAAwB;AAC7D,SAASC,cAAc,QAAQ,kCAA+B;AAC9D,SACEC,YAAY,EACZC,qBAAqB,QAChB,gDAA6C;AAEpD;;;;;;;;;;;;;;CAcC,GAED,MAAMC,WAAW,GAAGF,aAAa,CAAC,EAAEC,sBAAsB,SAAS,CAAC;AACpE,MAAME,eAAe,GAAGH,aAAa,CAAC,EAAEC,sBAAsB,aAAa,CAAC;AAC5E,MAAMG,uBAAuB,GAAGF,SAAS,cAAc,CAAC;AACxD,MAAMG,kBAAkB,GAAGH,SAAS,SAAS,CAAC;AAC9C,MAAMI,qBAAqB,GAAGJ,SAAS,iBAAiB,CAAC;AAEzD,MAAMK,4BAA4B,CAAC;;;;;;;;;;;;;;;;AAgBnC,CAAC;AAED,kFAAkF;AAClF,MAAMC,kCACJ;AAEF,8EAA8E;AAC9E,qBAAqB;AACrB,MAAMC,6CACJ;AAEF,eAAe,eAAeC,UAC5BC,IAAU;IAEV,MAAMC,YAAsB,EAAE;IAE9B,IAAI,CAACD,KAAKE,MAAM,CAACR,kBAAkB;QACjC,8EAA8E;QAC9E,OAAO;YAAEO;QAAU;IACrB;IAEA,MAAME,MAAMf,eAAeY;IAC3B,MAAMI,+BAA+BD,MACjC,6BACA;IACJ,MAAME,gCAAgCF,MAClC,oBACA;IAEJ,IAAI,CAACH,KAAKE,MAAM,CAACT,uBAAuB;QACtCO,KAAKM,KAAK,CAACb,sBAAsBG;IACnC;IACA,MAAMX,cAAce,MAAMN,iBAAiB;IAE3C,MAAMa,cAAwB,EAAE;IAChCxB,qBAAqBiB,MAAMR,cAAc,CAACgB;QACxCD,YAAYE,IAAI,CAACD;IACnB;IAEA,KAAK,MAAMA,YAAYD,YAAa;QAClC,IAAI,CAACC,SAASE,QAAQ,CAAC,UAAUF,SAASE,QAAQ,CAAC,cAAc;YAC/D;QACF;QACA,MAAMC,UAAU,MAAMzB,YACpBc,MACAQ,UACAX;QAEF,IAAIc,SAAS;YACX,MAAM3B,sBACJgB,MACAQ,UACA;gBAAC;aAA4B,EAC7BJ;YAEFH,UAAUQ,IAAI,CACZ,GAAGD,SAAS,6EAA6E,CAAC;YAE5F;QACF;QACA,MAAMI,WAAWZ,KAAKa,IAAI,CAACL,UAAU,YAAY;QACjD,IAAI,CAACI,SAASE,QAAQ,CAAC,+BAA+B;YACpDb,UAAUQ,IAAI,CACZ,GAAGD,SAAS,qMAAqM,CAAC;QAEtN;IACA,4CAA4C;IAC9C;IAEA,IAAIR,KAAKE,MAAM,CAACP,qBAAqB;QACnC,MAAMgB,UAAU,MAAMzB,YACpBc,MACAL,oBACAG;QAEF,IAAIa,SAAS;YACX,MAAMzB,YACJc,MACAL,oBACA;YAEF,MAAMT,YACJc,MACAL,oBACA;YAEF,MAAMX,sBACJgB,MACAL,oBACA;gBAAC;aAA4B,EAC7BU;YAEFJ,UAAUQ,IAAI,CACZ,GAAGd,mBAAmB,yDAAyD,CAAC;QAEpF,OAAO;YACL,MAAMiB,WAAWZ,KAAKa,IAAI,CAAClB,oBAAoB,YAAY;YAC3D,IAAI,CAACiB,SAASE,QAAQ,CAAC,wCAAwC;gBAC7Db,UAAUQ,IAAI,CACZ,GAAGd,mBAAmB,2LAA2L,CAAC;YAEtN;QACA,4CAA4C;QAC9C;IACF;IAEA,MAAMR,qBAAqBa;IAE3B,OAAO;QAAEC;IAAU;AACrB"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { type MigrationReturnObject, type Tree } from '@nx/devkit';
|
|
6
|
+
export default function migration(tree: Tree): Promise<MigrationReturnObject>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/ import { getProjects, joinPathFragments } from "@nx/devkit";
|
|
5
|
+
import { TERRAFORM_PROJECT_GENERATOR_INFO } from "../../../terraform/project/generator.js";
|
|
6
|
+
import { applyGritQL, GRIT_INSERT_PLACEHOLDER, insertViaGritQL, matchGritQL } from "../../../utils/ast.js";
|
|
7
|
+
import { formatFilesInSubtree } from "../../../utils/format.js";
|
|
8
|
+
/**
|
|
9
|
+
* Adopt an existing Terraform state bucket in the vended bootstrap script when its state object is missing
|
|
10
|
+
*
|
|
11
|
+
* The state bucket stores its own tfstate inside itself, so if that object is
|
|
12
|
+
* deleted while the bucket survives, `bootstrap` reads the 404 as a first run
|
|
13
|
+
* and asks terraform to create a bucket that already exists — failing with
|
|
14
|
+
* `BucketAlreadyOwnedByYou` on every subsequent run, with no retry that clears
|
|
15
|
+
* it. The vended script now imports the existing bucket instead.
|
|
16
|
+
*
|
|
17
|
+
* Every edit is expressed as a GritQL rewrite so the script is matched on its
|
|
18
|
+
* AST rather than its formatting.
|
|
19
|
+
*
|
|
20
|
+
* How to write a migration:
|
|
21
|
+
* - https://nx.dev/docs/kb/migration-generators
|
|
22
|
+
* - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject
|
|
23
|
+
*
|
|
24
|
+
* Guardrails:
|
|
25
|
+
* - Pattern-match before writing: skip files that have diverged from the shape
|
|
26
|
+
* your generators produce and report them via `nextSteps`, or consider a
|
|
27
|
+
* hybrid migration, rather than clobbering the user's changes.
|
|
28
|
+
* - Idempotent: re-running must be a no-op.
|
|
29
|
+
* - Format what you write: finish with `formatFilesInSubtree` so the files your
|
|
30
|
+
* migration wrote are formatted correctly.
|
|
31
|
+
*/ // Guards the whole migration: present only once the import step exists.
|
|
32
|
+
const MIGRATED_PATTERN = '`bucketExists($_, $_)`';
|
|
33
|
+
// Every edit site, matched structurally so formatting and argument layout
|
|
34
|
+
// don't affect whether the script is recognised.
|
|
35
|
+
const STATE_FETCH_TRY_PATTERN = 'try_statement() as $try where { $try <: contains `GetObjectCommand` }';
|
|
36
|
+
const STATE_FLAG_SET_PATTERN = '`if (out.Body) { $body }`';
|
|
37
|
+
const HELPER_PATTERN = '`const main = async () => { $body }`';
|
|
38
|
+
const INIT_PATTERN = "`execFileSync('terraform', ['init'], $opts)`";
|
|
39
|
+
const S3_IMPORT_PATTERN = "`import { $before, GetObjectCommand, $after } from '@aws-sdk/client-s3'`";
|
|
40
|
+
const HEADER_COMMENT_PATTERN = 'comment() as $c where { $c <: includes "state back to S3." }';
|
|
41
|
+
// GritQL snippets are parsed as backtick-quoted patterns, so any backtick in
|
|
42
|
+
// the inserted source has to survive that layer escaped.
|
|
43
|
+
const BUCKET_EXISTS_HELPER = `// S3 answers 404 only when the name is genuinely free; 403 means it exists
|
|
44
|
+
// but is not readable with these credentials.
|
|
45
|
+
const bucketExists = async (s3: S3Client, bucket: string) => {
|
|
46
|
+
try {
|
|
47
|
+
await s3.send(new HeadBucketCommand({ Bucket: bucket }));
|
|
48
|
+
return true;
|
|
49
|
+
} catch (err: any) {
|
|
50
|
+
const status = err?.$metadata?.httpStatusCode;
|
|
51
|
+
if (status === 404) return false;
|
|
52
|
+
if (status === 403) return true;
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
};`;
|
|
56
|
+
const IMPORT_STEP = `// The bucket holds its own state, so losing the state object while the
|
|
57
|
+
// bucket survives would otherwise wedge bootstrap on a permanent
|
|
58
|
+
// \`BucketAlreadyOwnedByYou\`. Adopt the existing bucket instead.
|
|
59
|
+
if (!haveState && (await bucketExists(s3, bucket))) {
|
|
60
|
+
console.log(
|
|
61
|
+
\`State bucket \${bucket} already exists but its bootstrap state is missing — importing it.\`,
|
|
62
|
+
);
|
|
63
|
+
execFileSync(
|
|
64
|
+
'terraform',
|
|
65
|
+
[
|
|
66
|
+
'import',
|
|
67
|
+
\`-state=\${tfStatePath}\`,
|
|
68
|
+
\`-var=aws_region=\${region}\`,
|
|
69
|
+
'aws_s3_bucket.terraform_state',
|
|
70
|
+
bucket,
|
|
71
|
+
],
|
|
72
|
+
{ cwd: bootstrapDir, stdio: 'inherit' },
|
|
73
|
+
);
|
|
74
|
+
}`;
|
|
75
|
+
// Rewritten wholesale rather than appended to, since the summary sentence
|
|
76
|
+
// changes rather than gaining a clause.
|
|
77
|
+
const NEW_HEADER_COMMENT = `/**
|
|
78
|
+
* Bootstraps the remote Terraform state bucket.
|
|
79
|
+
*
|
|
80
|
+
* Equivalent to \\\`cdk bootstrap\\\` — resolves account + region from the AWS
|
|
81
|
+
* SDK credential chain, pulls any existing bootstrap tfstate from S3,
|
|
82
|
+
* runs \\\`terraform apply\\\` in the \\\`bootstrap\\\` dir, then pushes the new
|
|
83
|
+
* state back to S3. Adopts an already-existing state bucket when its
|
|
84
|
+
* state object is missing.
|
|
85
|
+
*/`;
|
|
86
|
+
const DIVERGED_NEXT_STEP = (filePath)=>`${filePath}: the bootstrap script has diverged from the generated shape - left untouched. Manually \`terraform import aws_s3_bucket.terraform_state <bucket>\` before \`terraform apply\` when the state bucket exists but its \`bootstrap.tfstate\` object does not, otherwise bootstrap fails with BucketAlreadyOwnedByYou.`;
|
|
87
|
+
export default async function migration(tree) {
|
|
88
|
+
const nextSteps = [];
|
|
89
|
+
// Terraform application projects are the only ones that vend bootstrap.ts.
|
|
90
|
+
const bootstrapScripts = [
|
|
91
|
+
...getProjects(tree).values()
|
|
92
|
+
].filter((project)=>project.metadata?.generator === TERRAFORM_PROJECT_GENERATOR_INFO.id).map((project)=>joinPathFragments(project.root, 'scripts/bootstrap.ts')).filter((filePath)=>tree.exists(filePath));
|
|
93
|
+
for (const filePath of bootstrapScripts){
|
|
94
|
+
if (await matchGritQL(tree, filePath, MIGRATED_PATTERN)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
// Confirm every edit site is present before writing any of them, so a
|
|
98
|
+
// script that only partly matches is left whole rather than half-edited.
|
|
99
|
+
const allSitesPresent = (await Promise.all([
|
|
100
|
+
STATE_FETCH_TRY_PATTERN,
|
|
101
|
+
STATE_FLAG_SET_PATTERN,
|
|
102
|
+
HELPER_PATTERN,
|
|
103
|
+
INIT_PATTERN,
|
|
104
|
+
S3_IMPORT_PATTERN,
|
|
105
|
+
HEADER_COMMENT_PATTERN
|
|
106
|
+
].map((pattern)=>matchGritQL(tree, filePath, pattern)))).every(Boolean);
|
|
107
|
+
if (!allSitesPresent) {
|
|
108
|
+
nextSteps.push(DIVERGED_NEXT_STEP(filePath));
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// `haveState` distinguishes "no remote state" from "no bucket", so the
|
|
112
|
+
// import only runs when terraform has no prior knowledge of the bucket.
|
|
113
|
+
await insertViaGritQL(tree, filePath, `${STATE_FETCH_TRY_PATTERN} => \`${GRIT_INSERT_PLACEHOLDER}\n $try\``, 'let haveState = false;');
|
|
114
|
+
await applyGritQL(tree, filePath, `${STATE_FLAG_SET_PATTERN} => \`if (out.Body) {\n $body\n haveState = true;\n }\``);
|
|
115
|
+
await insertViaGritQL(tree, filePath, `${HELPER_PATTERN} => \`${GRIT_INSERT_PLACEHOLDER}\n\nconst main = async () => { $body }\``, BUCKET_EXISTS_HELPER);
|
|
116
|
+
await insertViaGritQL(tree, filePath, `${INIT_PATTERN} => \`execFileSync('terraform', ['init'], $opts);\n\n ${GRIT_INSERT_PLACEHOLDER}\n\``, IMPORT_STEP);
|
|
117
|
+
// Placed in the generator's import position rather than appended, so a
|
|
118
|
+
// migrated workspace matches a freshly generated one.
|
|
119
|
+
await applyGritQL(tree, filePath, `${S3_IMPORT_PATTERN} => \`import { $before, GetObjectCommand, HeadBucketCommand, $after } from '@aws-sdk/client-s3'\``);
|
|
120
|
+
await applyGritQL(tree, filePath, `${HEADER_COMMENT_PATTERN} => \`${NEW_HEADER_COMMENT}\``);
|
|
121
|
+
nextSteps.push(`${filePath}: now adopts an existing Terraform state bucket when its bootstrap.tfstate object is missing.`);
|
|
122
|
+
}
|
|
123
|
+
await formatFilesInSubtree(tree);
|
|
124
|
+
return {
|
|
125
|
+
nextSteps
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
//# sourceMappingURL=migration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/terraform-bootstrap-adopt-existing-bucket/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n getProjects,\n joinPathFragments,\n type MigrationReturnObject,\n type Tree,\n} from '@nx/devkit';\nimport { TERRAFORM_PROJECT_GENERATOR_INFO } from '../../../terraform/project/generator';\nimport {\n applyGritQL,\n GRIT_INSERT_PLACEHOLDER,\n insertViaGritQL,\n matchGritQL,\n} from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\n\n/**\n * Adopt an existing Terraform state bucket in the vended bootstrap script when its state object is missing\n *\n * The state bucket stores its own tfstate inside itself, so if that object is\n * deleted while the bucket survives, `bootstrap` reads the 404 as a first run\n * and asks terraform to create a bucket that already exists — failing with\n * `BucketAlreadyOwnedByYou` on every subsequent run, with no retry that clears\n * it. The vended script now imports the existing bucket instead.\n *\n * Every edit is expressed as a GritQL rewrite so the script is matched on its\n * AST rather than its formatting.\n *\n * How to write a migration:\n * - https://nx.dev/docs/kb/migration-generators\n * - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject\n *\n * Guardrails:\n * - Pattern-match before writing: skip files that have diverged from the shape\n * your generators produce and report them via `nextSteps`, or consider a\n * hybrid migration, rather than clobbering the user's changes.\n * - Idempotent: re-running must be a no-op.\n * - Format what you write: finish with `formatFilesInSubtree` so the files your\n * migration wrote are formatted correctly.\n */\n\n// Guards the whole migration: present only once the import step exists.\nconst MIGRATED_PATTERN = '`bucketExists($_, $_)`';\n\n// Every edit site, matched structurally so formatting and argument layout\n// don't affect whether the script is recognised.\nconst STATE_FETCH_TRY_PATTERN =\n 'try_statement() as $try where { $try <: contains `GetObjectCommand` }';\nconst STATE_FLAG_SET_PATTERN = '`if (out.Body) { $body }`';\nconst HELPER_PATTERN = '`const main = async () => { $body }`';\nconst INIT_PATTERN = \"`execFileSync('terraform', ['init'], $opts)`\";\nconst S3_IMPORT_PATTERN =\n \"`import { $before, GetObjectCommand, $after } from '@aws-sdk/client-s3'`\";\nconst HEADER_COMMENT_PATTERN =\n 'comment() as $c where { $c <: includes \"state back to S3.\" }';\n\n// GritQL snippets are parsed as backtick-quoted patterns, so any backtick in\n// the inserted source has to survive that layer escaped.\nconst BUCKET_EXISTS_HELPER = `// S3 answers 404 only when the name is genuinely free; 403 means it exists\n// but is not readable with these credentials.\nconst bucketExists = async (s3: S3Client, bucket: string) => {\n try {\n await s3.send(new HeadBucketCommand({ Bucket: bucket }));\n return true;\n } catch (err: any) {\n const status = err?.$metadata?.httpStatusCode;\n if (status === 404) return false;\n if (status === 403) return true;\n throw err;\n }\n};`;\n\nconst IMPORT_STEP = `// The bucket holds its own state, so losing the state object while the\n // bucket survives would otherwise wedge bootstrap on a permanent\n // \\`BucketAlreadyOwnedByYou\\`. Adopt the existing bucket instead.\n if (!haveState && (await bucketExists(s3, bucket))) {\n console.log(\n \\`State bucket \\${bucket} already exists but its bootstrap state is missing — importing it.\\`,\n );\n execFileSync(\n 'terraform',\n [\n 'import',\n \\`-state=\\${tfStatePath}\\`,\n \\`-var=aws_region=\\${region}\\`,\n 'aws_s3_bucket.terraform_state',\n bucket,\n ],\n { cwd: bootstrapDir, stdio: 'inherit' },\n );\n }`;\n\n// Rewritten wholesale rather than appended to, since the summary sentence\n// changes rather than gaining a clause.\nconst NEW_HEADER_COMMENT = `/**\n * Bootstraps the remote Terraform state bucket.\n *\n * Equivalent to \\\\\\`cdk bootstrap\\\\\\` — resolves account + region from the AWS\n * SDK credential chain, pulls any existing bootstrap tfstate from S3,\n * runs \\\\\\`terraform apply\\\\\\` in the \\\\\\`bootstrap\\\\\\` dir, then pushes the new\n * state back to S3. Adopts an already-existing state bucket when its\n * state object is missing.\n */`;\n\nconst DIVERGED_NEXT_STEP = (filePath: string) =>\n `${filePath}: the bootstrap script has diverged from the generated shape - left untouched. Manually \\`terraform import aws_s3_bucket.terraform_state <bucket>\\` before \\`terraform apply\\` when the state bucket exists but its \\`bootstrap.tfstate\\` object does not, otherwise bootstrap fails with BucketAlreadyOwnedByYou.`;\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n // Terraform application projects are the only ones that vend bootstrap.ts.\n const bootstrapScripts = [...getProjects(tree).values()]\n .filter(\n (project) =>\n (project.metadata as any)?.generator ===\n TERRAFORM_PROJECT_GENERATOR_INFO.id,\n )\n .map((project) => joinPathFragments(project.root, 'scripts/bootstrap.ts'))\n .filter((filePath) => tree.exists(filePath));\n\n for (const filePath of bootstrapScripts) {\n if (await matchGritQL(tree, filePath, MIGRATED_PATTERN)) {\n // Already migrated - silent skip keeps re-runs a no-op.\n continue;\n }\n\n // Confirm every edit site is present before writing any of them, so a\n // script that only partly matches is left whole rather than half-edited.\n const allSitesPresent = (\n await Promise.all(\n [\n STATE_FETCH_TRY_PATTERN,\n STATE_FLAG_SET_PATTERN,\n HELPER_PATTERN,\n INIT_PATTERN,\n S3_IMPORT_PATTERN,\n HEADER_COMMENT_PATTERN,\n ].map((pattern) => matchGritQL(tree, filePath, pattern)),\n )\n ).every(Boolean);\n\n if (!allSitesPresent) {\n nextSteps.push(DIVERGED_NEXT_STEP(filePath));\n continue;\n }\n\n // `haveState` distinguishes \"no remote state\" from \"no bucket\", so the\n // import only runs when terraform has no prior knowledge of the bucket.\n await insertViaGritQL(\n tree,\n filePath,\n `${STATE_FETCH_TRY_PATTERN} => \\`${GRIT_INSERT_PLACEHOLDER}\\n $try\\``,\n 'let haveState = false;',\n );\n await applyGritQL(\n tree,\n filePath,\n `${STATE_FLAG_SET_PATTERN} => \\`if (out.Body) {\\n $body\\n haveState = true;\\n }\\``,\n );\n await insertViaGritQL(\n tree,\n filePath,\n `${HELPER_PATTERN} => \\`${GRIT_INSERT_PLACEHOLDER}\\n\\nconst main = async () => { $body }\\``,\n BUCKET_EXISTS_HELPER,\n );\n await insertViaGritQL(\n tree,\n filePath,\n `${INIT_PATTERN} => \\`execFileSync('terraform', ['init'], $opts);\\n\\n ${GRIT_INSERT_PLACEHOLDER}\\n\\``,\n IMPORT_STEP,\n );\n\n // Placed in the generator's import position rather than appended, so a\n // migrated workspace matches a freshly generated one.\n await applyGritQL(\n tree,\n filePath,\n `${S3_IMPORT_PATTERN} => \\`import { $before, GetObjectCommand, HeadBucketCommand, $after } from '@aws-sdk/client-s3'\\``,\n );\n\n await applyGritQL(\n tree,\n filePath,\n `${HEADER_COMMENT_PATTERN} => \\`${NEW_HEADER_COMMENT}\\``,\n );\n\n nextSteps.push(\n `${filePath}: now adopts an existing Terraform state bucket when its bootstrap.tfstate object is missing.`,\n );\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["getProjects","joinPathFragments","TERRAFORM_PROJECT_GENERATOR_INFO","applyGritQL","GRIT_INSERT_PLACEHOLDER","insertViaGritQL","matchGritQL","formatFilesInSubtree","MIGRATED_PATTERN","STATE_FETCH_TRY_PATTERN","STATE_FLAG_SET_PATTERN","HELPER_PATTERN","INIT_PATTERN","S3_IMPORT_PATTERN","HEADER_COMMENT_PATTERN","BUCKET_EXISTS_HELPER","IMPORT_STEP","NEW_HEADER_COMMENT","DIVERGED_NEXT_STEP","filePath","migration","tree","nextSteps","bootstrapScripts","values","filter","project","metadata","generator","id","map","root","exists","allSitesPresent","Promise","all","pattern","every","Boolean","push"],"mappings":"AAAA;;;CAGC,GACD,SACEA,WAAW,EACXC,iBAAiB,QAGZ,aAAa;AACpB,SAASC,gCAAgC,QAAQ,0CAAuC;AACxF,SACEC,WAAW,EACXC,uBAAuB,EACvBC,eAAe,EACfC,WAAW,QACN,wBAAqB;AAC5B,SAASC,oBAAoB,QAAQ,2BAAwB;AAE7D;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GAED,wEAAwE;AACxE,MAAMC,mBAAmB;AAEzB,0EAA0E;AAC1E,iDAAiD;AACjD,MAAMC,0BACJ;AACF,MAAMC,yBAAyB;AAC/B,MAAMC,iBAAiB;AACvB,MAAMC,eAAe;AACrB,MAAMC,oBACJ;AACF,MAAMC,yBACJ;AAEF,6EAA6E;AAC7E,yDAAyD;AACzD,MAAMC,uBAAuB,CAAC;;;;;;;;;;;;EAY5B,CAAC;AAEH,MAAMC,cAAc,CAAC;;;;;;;;;;;;;;;;;;GAkBlB,CAAC;AAEJ,0EAA0E;AAC1E,wCAAwC;AACxC,MAAMC,qBAAqB,CAAC;;;;;;;;GAQzB,CAAC;AAEJ,MAAMC,qBAAqB,CAACC,WAC1B,GAAGA,SAAS,kTAAkT,CAAC;AAEjU,eAAe,eAAeC,UAC5BC,IAAU;IAEV,MAAMC,YAAsB,EAAE;IAE9B,2EAA2E;IAC3E,MAAMC,mBAAmB;WAAIvB,YAAYqB,MAAMG,MAAM;KAAG,CACrDC,MAAM,CACL,CAACC,UACC,AAACA,QAAQC,QAAQ,EAAUC,cAC3B1B,iCAAiC2B,EAAE,EAEtCC,GAAG,CAAC,CAACJ,UAAYzB,kBAAkByB,QAAQK,IAAI,EAAE,yBACjDN,MAAM,CAAC,CAACN,WAAaE,KAAKW,MAAM,CAACb;IAEpC,KAAK,MAAMA,YAAYI,iBAAkB;QACvC,IAAI,MAAMjB,YAAYe,MAAMF,UAAUX,mBAAmB;YAEvD;QACF;QAEA,sEAAsE;QACtE,yEAAyE;QACzE,MAAMyB,kBAAkB,AACtB,CAAA,MAAMC,QAAQC,GAAG,CACf;YACE1B;YACAC;YACAC;YACAC;YACAC;YACAC;SACD,CAACgB,GAAG,CAAC,CAACM,UAAY9B,YAAYe,MAAMF,UAAUiB,UACjD,EACAC,KAAK,CAACC;QAER,IAAI,CAACL,iBAAiB;YACpBX,UAAUiB,IAAI,CAACrB,mBAAmBC;YAClC;QACF;QAEA,uEAAuE;QACvE,wEAAwE;QACxE,MAAMd,gBACJgB,MACAF,UACA,GAAGV,wBAAwB,MAAM,EAAEL,wBAAwB,UAAU,CAAC,EACtE;QAEF,MAAMD,YACJkB,MACAF,UACA,GAAGT,uBAAuB,oEAAoE,CAAC;QAEjG,MAAML,gBACJgB,MACAF,UACA,GAAGR,eAAe,MAAM,EAAEP,wBAAwB,wCAAwC,CAAC,EAC3FW;QAEF,MAAMV,gBACJgB,MACAF,UACA,GAAGP,aAAa,uDAAuD,EAAER,wBAAwB,IAAI,CAAC,EACtGY;QAGF,uEAAuE;QACvE,sDAAsD;QACtD,MAAMb,YACJkB,MACAF,UACA,GAAGN,kBAAkB,iGAAiG,CAAC;QAGzH,MAAMV,YACJkB,MACAF,UACA,GAAGL,uBAAuB,MAAM,EAAEG,mBAAmB,EAAE,CAAC;QAG1DK,UAAUiB,IAAI,CACZ,GAAGpB,SAAS,6FAA6F,CAAC;IAE9G;IAEA,MAAMZ,qBAAqBc;IAE3B,OAAO;QAAEC;IAAU;AACrB"}
|
|
@@ -171,6 +171,7 @@ exports[`py#agent generator > should match snapshot for BedrockAgentCoreRuntime
|
|
|
171
171
|
exports[`py#agent generator > should match snapshot for BedrockAgentCoreRuntime generated constructs files > core-index.ts 1`] = `
|
|
172
172
|
"export * from './app.js';
|
|
173
173
|
export * from './checkov.js';
|
|
174
|
+
export * from './cloudfront.js';
|
|
174
175
|
export * from './runtime-config.js';
|
|
175
176
|
export * from './workspace.js';
|
|
176
177
|
"
|
|
@@ -434,6 +434,7 @@ import {
|
|
|
434
434
|
HttpApiIntegration,
|
|
435
435
|
IntegrationBuilder,
|
|
436
436
|
} from '../../core/api/utils.js';
|
|
437
|
+
import { findCloudFrontDomainNames } from '../../core/cloudfront.js';
|
|
437
438
|
import { HttpApi } from '../../core/api/http-api.js';
|
|
438
439
|
import {
|
|
439
440
|
OPERATION_DETAILS,
|
|
@@ -474,7 +475,7 @@ export class TestApi<
|
|
|
474
475
|
return IntegrationBuilder.http({
|
|
475
476
|
pattern: 'isolated',
|
|
476
477
|
operations: OPERATION_DETAILS,
|
|
477
|
-
defaultIntegrationOptions:
|
|
478
|
+
defaultIntegrationOptions: {
|
|
478
479
|
runtime: Runtime.PYTHON_3_14,
|
|
479
480
|
handler: 'run.sh',
|
|
480
481
|
code: Code.fromAsset(
|
|
@@ -488,7 +489,7 @@ export class TestApi<
|
|
|
488
489
|
timeout: Duration.seconds(30),
|
|
489
490
|
tracing: Tracing.ACTIVE,
|
|
490
491
|
snapStart: SnapStartConf.ON_PUBLISHED_VERSIONS,
|
|
491
|
-
},
|
|
492
|
+
} as FunctionProps,
|
|
492
493
|
buildDefaultIntegration: (op, props: FunctionProps) => {
|
|
493
494
|
const handler = new Function(scope, \`TestApi\${op}Handler\`, props);
|
|
494
495
|
handler.addEnvironment(
|
|
@@ -549,6 +550,8 @@ export class TestApi<
|
|
|
549
550
|
* as the only permitted CORS origins
|
|
550
551
|
* in the API gateway. The CORS origins are not configured within the AWS Lambda
|
|
551
552
|
* integrations since the associated header is controlled by API Gateway v2.
|
|
553
|
+
* Any custom domain names (aliases) configured on a CloudFront distribution are
|
|
554
|
+
* included automatically alongside its default \`*.cloudfront.net\` domain.
|
|
552
555
|
*
|
|
553
556
|
* @param origins - The origin strings, CloudFront distributions, or objects containing a CloudFront distribution to grant CORS from
|
|
554
557
|
*/
|
|
@@ -559,12 +562,14 @@ export class TestApi<
|
|
|
559
562
|
| { cloudFrontDistribution: Distribution }
|
|
560
563
|
)[]
|
|
561
564
|
) {
|
|
562
|
-
const allowedOrigins = origins.
|
|
565
|
+
const allowedOrigins = origins.flatMap((origin) =>
|
|
563
566
|
typeof origin === 'string'
|
|
564
|
-
? origin
|
|
565
|
-
:
|
|
566
|
-
|
|
567
|
-
|
|
567
|
+
? [origin]
|
|
568
|
+
: findCloudFrontDomainNames(
|
|
569
|
+
'cloudFrontDistribution' in origin
|
|
570
|
+
? origin.cloudFrontDistribution
|
|
571
|
+
: origin,
|
|
572
|
+
).map((domain) => \`https://\${domain}\`),
|
|
568
573
|
);
|
|
569
574
|
|
|
570
575
|
const cfnApi = this.api.node.defaultChild;
|
|
@@ -1406,6 +1411,7 @@ import {
|
|
|
1406
1411
|
IntegrationBuilder,
|
|
1407
1412
|
RestApiIntegration,
|
|
1408
1413
|
} from '../../core/api/utils.js';
|
|
1414
|
+
import { findCloudFrontDomainNames } from '../../core/cloudfront.js';
|
|
1409
1415
|
import { AddCorsPreflightAspect, RestApi } from '../../core/api/rest-api.js';
|
|
1410
1416
|
import {
|
|
1411
1417
|
OPERATION_DETAILS,
|
|
@@ -1454,7 +1460,7 @@ export class TestApi<
|
|
|
1454
1460
|
return IntegrationBuilder.rest({
|
|
1455
1461
|
pattern: 'isolated',
|
|
1456
1462
|
operations: OPERATION_DETAILS,
|
|
1457
|
-
defaultIntegrationOptions:
|
|
1463
|
+
defaultIntegrationOptions: {
|
|
1458
1464
|
runtime: Runtime.PYTHON_3_14,
|
|
1459
1465
|
handler: 'run.sh',
|
|
1460
1466
|
code: Code.fromAsset(
|
|
@@ -1468,7 +1474,7 @@ export class TestApi<
|
|
|
1468
1474
|
timeout: Duration.seconds(30),
|
|
1469
1475
|
tracing: Tracing.ACTIVE,
|
|
1470
1476
|
snapStart: SnapStartConf.ON_PUBLISHED_VERSIONS,
|
|
1471
|
-
},
|
|
1477
|
+
} as FunctionProps,
|
|
1472
1478
|
buildDefaultIntegration: (op, props: FunctionProps) => {
|
|
1473
1479
|
const handler = new Function(scope, \`TestApi\${op}Handler\`, props);
|
|
1474
1480
|
handler.addEnvironment(
|
|
@@ -1532,7 +1538,9 @@ export class TestApi<
|
|
|
1532
1538
|
*
|
|
1533
1539
|
* Configures the provided CloudFront distribution domains or origin strings
|
|
1534
1540
|
* as the only permitted CORS origins in API Gateway preflight responses and the
|
|
1535
|
-
* AWS Lambda integrations.
|
|
1541
|
+
* AWS Lambda integrations. Any custom domain names (aliases) configured on a
|
|
1542
|
+
* CloudFront distribution are included automatically alongside its default
|
|
1543
|
+
* \`*.cloudfront.net\` domain.
|
|
1536
1544
|
*
|
|
1537
1545
|
* @param origins - The origin strings, CloudFront distributions, or objects containing a CloudFront distribution to grant CORS from
|
|
1538
1546
|
*/
|
|
@@ -1543,12 +1551,14 @@ export class TestApi<
|
|
|
1543
1551
|
| { cloudFrontDistribution: Distribution }
|
|
1544
1552
|
)[]
|
|
1545
1553
|
) {
|
|
1546
|
-
const allowedOrigins = origins.
|
|
1554
|
+
const allowedOrigins = origins.flatMap((origin) =>
|
|
1547
1555
|
typeof origin === 'string'
|
|
1548
|
-
? origin
|
|
1549
|
-
:
|
|
1550
|
-
|
|
1551
|
-
|
|
1556
|
+
? [origin]
|
|
1557
|
+
: findCloudFrontDomainNames(
|
|
1558
|
+
'cloudFrontDistribution' in origin
|
|
1559
|
+
? origin.cloudFrontDistribution
|
|
1560
|
+
: origin,
|
|
1561
|
+
).map((domain) => \`https://\${domain}\`),
|
|
1552
1562
|
);
|
|
1553
1563
|
|
|
1554
1564
|
this.allowedOrigins = allowedOrigins;
|
|
@@ -8,6 +8,7 @@ exports[`py#mcp-server generator > should match snapshot for BedrockAgentCoreRun
|
|
|
8
8
|
exports[`py#mcp-server generator > should match snapshot for BedrockAgentCoreRuntime generated constructs files > core-index.ts 1`] = `
|
|
9
9
|
"export * from './app.js';
|
|
10
10
|
export * from './checkov.js';
|
|
11
|
+
export * from './cloudfront.js';
|
|
11
12
|
export * from './runtime-config.js';
|
|
12
13
|
export * from './workspace.js';
|
|
13
14
|
"
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
IntegrationBuilder,
|
|
33
33
|
RestApiIntegration,
|
|
34
34
|
} from '../../core/api/utils.js';
|
|
35
|
+
import { findCloudFrontDomainNames } from '../../core/cloudfront.js';
|
|
35
36
|
import { AddCorsPreflightAspect, RestApi } from '../../core/api/rest-api.js';
|
|
36
37
|
import {
|
|
37
38
|
OPERATION_DETAILS,
|
|
@@ -86,7 +87,7 @@ export class TestApi<
|
|
|
86
87
|
return IntegrationBuilder.rest({
|
|
87
88
|
pattern: 'isolated',
|
|
88
89
|
operations: OPERATION_DETAILS,
|
|
89
|
-
defaultIntegrationOptions:
|
|
90
|
+
defaultIntegrationOptions: {
|
|
90
91
|
runtime: Runtime.NODEJS_LATEST,
|
|
91
92
|
handler: 'index.handler',
|
|
92
93
|
code: Code.fromAsset(
|
|
@@ -99,7 +100,7 @@ export class TestApi<
|
|
|
99
100
|
),
|
|
100
101
|
timeout: Duration.seconds(30),
|
|
101
102
|
tracing: Tracing.ACTIVE,
|
|
102
|
-
},
|
|
103
|
+
} as FunctionProps,
|
|
103
104
|
buildDefaultIntegration: (op, props: FunctionProps) => {
|
|
104
105
|
const handler = new Function(scope, \`TestApi\${op}Handler\`, props);
|
|
105
106
|
handler.addEnvironment(
|
|
@@ -156,7 +157,9 @@ export class TestApi<
|
|
|
156
157
|
*
|
|
157
158
|
* Configures the provided CloudFront distribution domains or origin strings
|
|
158
159
|
* as the only permitted CORS origins in API Gateway preflight responses and the
|
|
159
|
-
* AWS Lambda integrations.
|
|
160
|
+
* AWS Lambda integrations. Any custom domain names (aliases) configured on a
|
|
161
|
+
* CloudFront distribution are included automatically alongside its default
|
|
162
|
+
* \`*.cloudfront.net\` domain.
|
|
160
163
|
*
|
|
161
164
|
* @param origins - The origin strings, CloudFront distributions, or objects containing a CloudFront distribution to grant CORS from
|
|
162
165
|
*/
|
|
@@ -167,12 +170,14 @@ export class TestApi<
|
|
|
167
170
|
| { cloudFrontDistribution: Distribution }
|
|
168
171
|
)[]
|
|
169
172
|
) {
|
|
170
|
-
const allowedOrigins = origins.
|
|
173
|
+
const allowedOrigins = origins.flatMap((origin) =>
|
|
171
174
|
typeof origin === 'string'
|
|
172
|
-
? origin
|
|
173
|
-
:
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
? [origin]
|
|
176
|
+
: findCloudFrontDomainNames(
|
|
177
|
+
'cloudFrontDistribution' in origin
|
|
178
|
+
? origin.cloudFrontDistribution
|
|
179
|
+
: origin,
|
|
180
|
+
).map((domain) => \`https://\${domain}\`),
|
|
176
181
|
);
|
|
177
182
|
|
|
178
183
|
this.allowedOrigins = allowedOrigins;
|
|
@@ -221,6 +226,7 @@ import {
|
|
|
221
226
|
IntegrationBuilder,
|
|
222
227
|
RestApiIntegration,
|
|
223
228
|
} from '../../core/api/utils.js';
|
|
229
|
+
import { findCloudFrontDomainNames } from '../../core/cloudfront.js';
|
|
224
230
|
import { AddCorsPreflightAspect, RestApi } from '../../core/api/rest-api.js';
|
|
225
231
|
import {
|
|
226
232
|
OPERATION_DETAILS,
|
|
@@ -269,7 +275,7 @@ export class TestApi<
|
|
|
269
275
|
return IntegrationBuilder.rest({
|
|
270
276
|
pattern: 'isolated',
|
|
271
277
|
operations: OPERATION_DETAILS,
|
|
272
|
-
defaultIntegrationOptions:
|
|
278
|
+
defaultIntegrationOptions: {
|
|
273
279
|
runtime: Runtime.NODEJS_LATEST,
|
|
274
280
|
handler: 'index.handler',
|
|
275
281
|
code: Code.fromAsset(
|
|
@@ -282,7 +288,7 @@ export class TestApi<
|
|
|
282
288
|
),
|
|
283
289
|
timeout: Duration.seconds(30),
|
|
284
290
|
tracing: Tracing.ACTIVE,
|
|
285
|
-
},
|
|
291
|
+
} as FunctionProps,
|
|
286
292
|
buildDefaultIntegration: (op, props: FunctionProps) => {
|
|
287
293
|
const handler = new Function(scope, \`TestApi\${op}Handler\`, props);
|
|
288
294
|
handler.addEnvironment(
|
|
@@ -333,7 +339,9 @@ export class TestApi<
|
|
|
333
339
|
*
|
|
334
340
|
* Configures the provided CloudFront distribution domains or origin strings
|
|
335
341
|
* as the only permitted CORS origins in API Gateway preflight responses and the
|
|
336
|
-
* AWS Lambda integrations.
|
|
342
|
+
* AWS Lambda integrations. Any custom domain names (aliases) configured on a
|
|
343
|
+
* CloudFront distribution are included automatically alongside its default
|
|
344
|
+
* \`*.cloudfront.net\` domain.
|
|
337
345
|
*
|
|
338
346
|
* @param origins - The origin strings, CloudFront distributions, or objects containing a CloudFront distribution to grant CORS from
|
|
339
347
|
*/
|
|
@@ -344,12 +352,14 @@ export class TestApi<
|
|
|
344
352
|
| { cloudFrontDistribution: Distribution }
|
|
345
353
|
)[]
|
|
346
354
|
) {
|
|
347
|
-
const allowedOrigins = origins.
|
|
355
|
+
const allowedOrigins = origins.flatMap((origin) =>
|
|
348
356
|
typeof origin === 'string'
|
|
349
|
-
? origin
|
|
350
|
-
:
|
|
351
|
-
|
|
352
|
-
|
|
357
|
+
? [origin]
|
|
358
|
+
: findCloudFrontDomainNames(
|
|
359
|
+
'cloudFrontDistribution' in origin
|
|
360
|
+
? origin.cloudFrontDistribution
|
|
361
|
+
: origin,
|
|
362
|
+
).map((domain) => \`https://\${domain}\`),
|
|
353
363
|
);
|
|
354
364
|
|
|
355
365
|
this.allowedOrigins = allowedOrigins;
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Equivalent to `cdk bootstrap` — resolves account + region from the AWS
|
|
5
5
|
* SDK credential chain, pulls any existing bootstrap tfstate from S3,
|
|
6
6
|
* runs `terraform apply` in the `bootstrap` dir, then pushes the new
|
|
7
|
-
* state back to S3.
|
|
7
|
+
* state back to S3. Adopts an already-existing state bucket when its
|
|
8
|
+
* state object is missing.
|
|
8
9
|
*/
|
|
9
10
|
import { execFileSync } from 'node:child_process';
|
|
10
11
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
@@ -12,6 +13,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
12
13
|
import {
|
|
13
14
|
S3Client,
|
|
14
15
|
GetObjectCommand,
|
|
16
|
+
HeadBucketCommand,
|
|
15
17
|
PutObjectCommand,
|
|
16
18
|
} from '@aws-sdk/client-s3';
|
|
17
19
|
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
|
|
@@ -34,6 +36,20 @@ const tfStatePath = join(
|
|
|
34
36
|
'bootstrap.tfstate',
|
|
35
37
|
);
|
|
36
38
|
|
|
39
|
+
// S3 answers 404 only when the name is genuinely free; 403 means it exists
|
|
40
|
+
// but is not readable with these credentials.
|
|
41
|
+
const bucketExists = async (s3: S3Client, bucket: string) => {
|
|
42
|
+
try {
|
|
43
|
+
await s3.send(new HeadBucketCommand({ Bucket: bucket }));
|
|
44
|
+
return true;
|
|
45
|
+
} catch (err: any) {
|
|
46
|
+
const status = err?.$metadata?.httpStatusCode;
|
|
47
|
+
if (status === 404) return false;
|
|
48
|
+
if (status === 403) return true;
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
37
53
|
const main = async () => {
|
|
38
54
|
const { accountId, region } = await resolveAwsConfig();
|
|
39
55
|
const bucket = `${accountId}-tf-state-${region}`;
|
|
@@ -46,12 +62,14 @@ const main = async () => {
|
|
|
46
62
|
// Pull any existing bootstrap tfstate. First-time bootstrap has no
|
|
47
63
|
// remote state yet — fall through and let terraform apply create the
|
|
48
64
|
// bucket.
|
|
65
|
+
let haveState = false;
|
|
49
66
|
try {
|
|
50
67
|
const out = await s3.send(
|
|
51
68
|
new GetObjectCommand({ Bucket: bucket, Key: key }),
|
|
52
69
|
);
|
|
53
70
|
if (out.Body) {
|
|
54
71
|
writeFileSync(tfStatePath, await out.Body.transformToByteArray());
|
|
72
|
+
haveState = true;
|
|
55
73
|
}
|
|
56
74
|
} catch (err: any) {
|
|
57
75
|
const name = err?.name ?? '';
|
|
@@ -65,6 +83,27 @@ const main = async () => {
|
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
execFileSync('terraform', ['init'], { cwd: bootstrapDir, stdio: 'inherit' });
|
|
86
|
+
|
|
87
|
+
// The bucket holds its own state, so losing the state object while the
|
|
88
|
+
// bucket survives would otherwise wedge bootstrap on a permanent
|
|
89
|
+
// `BucketAlreadyOwnedByYou`. Adopt the existing bucket instead.
|
|
90
|
+
if (!haveState && (await bucketExists(s3, bucket))) {
|
|
91
|
+
console.log(
|
|
92
|
+
`State bucket ${bucket} already exists but its bootstrap state is missing — importing it.`,
|
|
93
|
+
);
|
|
94
|
+
execFileSync(
|
|
95
|
+
'terraform',
|
|
96
|
+
[
|
|
97
|
+
'import',
|
|
98
|
+
`-state=${tfStatePath}`,
|
|
99
|
+
`-var=aws_region=${region}`,
|
|
100
|
+
'aws_s3_bucket.terraform_state',
|
|
101
|
+
bucket,
|
|
102
|
+
],
|
|
103
|
+
{ cwd: bootstrapDir, stdio: 'inherit' },
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
68
107
|
execFileSync(
|
|
69
108
|
'terraform',
|
|
70
109
|
[
|