@aws/nx-plugin 1.0.0-rc.53 → 1.0.0-rc.54

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 CHANGED
@@ -21,6 +21,11 @@
21
21
  "version": "1.0.0-rc.52",
22
22
  "description": "Count the EC2MetaDataSSRF_QUERYARGUMENTS WAF rule on the UserIdentity Web ACL so sign-in from a local dev server is not blocked",
23
23
  "implementation": "./src/migrations/latest/user-identity-waf-allow-localhost-callback/migration"
24
+ },
25
+ "latest-order-access-log-delivery-after-bucket-policy": {
26
+ "version": "1.0.0-rc.54",
27
+ "description": "Order the S3 server access log delivery source after the bucket policy to avoid a 409 from concurrent bucket configuration writes",
28
+ "implementation": "./src/migrations/latest/order-access-log-delivery-after-bucket-policy/migration"
24
29
  }
25
30
  }
26
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aws/nx-plugin",
3
- "version": "1.0.0-rc.53",
3
+ "version": "1.0.0-rc.54",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/awslabs/nx-plugin-for-aws.git",
@@ -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, Tree } from '@nx/devkit';
6
+ export default function migration(tree: Tree): Promise<MigrationReturnObject>;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */ import { addDestructuredImport, applyGritQL, captureGritQL, matchGritQL } from "../../../utils/ast.js";
5
+ import { formatFilesInSubtree } from "../../../utils/format.js";
6
+ import { PACKAGES_DIR, SHARED_CONSTRUCTS_DIR } from "../../../utils/shared-constructs-constants.js";
7
+ /**
8
+ * Order the S3 server access log delivery source after the bucket policy.
9
+ *
10
+ * S3 rejects concurrent configuration writes against the same bucket with a 409
11
+ * (OperationAborted). The generated StaticWebsite construct left the delivery
12
+ * source and the bucket policy unordered, so CloudFormation could submit both at
13
+ * once and fail the stack.
14
+ */ const STATIC_WEBSITE_FILE = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core/static-website.ts`;
15
+ const BUCKET_ARN_SUFFIX = '.bucketArn';
16
+ /**
17
+ * The bucket whose policy the delivery source must be ordered after is the one
18
+ * the delivery source itself targets, so read it off `resourceArn` rather than
19
+ * assuming the generated parameter name. Returns undefined unless exactly one
20
+ * simple identifier is found, so a diverged helper is left alone.
21
+ */ const findDeliverySourceBucket = async (tree, filePath)=>{
22
+ const captured = await captureGritQL(tree, filePath, '`resourceArn: $bucket.bucketArn`');
23
+ if (!captured) return undefined;
24
+ const bucket = captured.slice(captured.indexOf(':') + 1).replace(BUCKET_ARN_SUFFIX, '').trim();
25
+ // Only a plain identifier can be cast and dereferenced safely in the
26
+ // statement this migration writes.
27
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(bucket) ? bucket : undefined;
28
+ };
29
+ /**
30
+ * Confirm the bucket the delivery source targets is declared as a parameter of
31
+ * the enclosing helper, so the statement this migration writes references a
32
+ * variable that is actually in scope.
33
+ */ const isBucketDeclaredParameter = async (tree, filePath, bucket)=>await matchGritQL(tree, filePath, `\`private deliverAccessLogsToCloudWatch($params) { $_ }\` where {
34
+ $params <: contains \`${bucket}: $bucketType\`,
35
+ $bucketType <: or { \`IBucket\`, \`Bucket\` }
36
+ }`);
37
+ // Inserts the bucket policy dependency between the delivery source and the
38
+ // delivery destination, matching the shape generators produced prior to this
39
+ // fix. Anchored on the destination declaration so the source's own (multi-line,
40
+ // Lazy-valued) arguments don't need to be matched.
41
+ const addDependencyPattern = (bucket)=>`\`const $dest: CfnDeliveryDestination = new CfnDeliveryDestination($destArgs)\` as $decl where {
42
+ $dest <: \`destination\`
43
+ } => \`const bucketPolicy = (${bucket} as Bucket).policy;
44
+ if (bucketPolicy) {
45
+ source.node.addDependency(bucketPolicy);
46
+ }
47
+ $decl\``;
48
+ export default async function migration(tree) {
49
+ const nextSteps = [];
50
+ if (!tree.exists(STATIC_WEBSITE_FILE)) {
51
+ // No vended StaticWebsite construct in this workspace - nothing to migrate.
52
+ return {
53
+ nextSteps
54
+ };
55
+ }
56
+ const contents = tree.read(STATIC_WEBSITE_FILE, 'utf-8') ?? '';
57
+ if (contents.includes('source.node.addDependency(bucketPolicy)')) {
58
+ // Already migrated.
59
+ return {
60
+ nextSteps
61
+ };
62
+ }
63
+ const divergedMessage = `${STATIC_WEBSITE_FILE}: deliverAccessLogsToCloudWatch has diverged from the generated shape - left untouched. Manually order the S3 server access log delivery source after the bucket policy of the bucket it targets (\`source.node.addDependency(bucketPolicy)\`), avoiding a 409 (OperationAborted) from concurrent bucket configuration writes.`;
64
+ // The rewrite is anchored on the delivery destination but references the
65
+ // `source` variable, so only apply it when the delivery source still has the
66
+ // generated shape.
67
+ const hasGeneratedSource = await matchGritQL(tree, STATIC_WEBSITE_FILE, '`const source: CfnDeliverySource = new CfnDeliverySource($_)`');
68
+ const bucket = hasGeneratedSource ? await findDeliverySourceBucket(tree, STATIC_WEBSITE_FILE) : undefined;
69
+ if (!bucket || !await isBucketDeclaredParameter(tree, STATIC_WEBSITE_FILE, bucket)) {
70
+ nextSteps.push(divergedMessage);
71
+ return {
72
+ nextSteps
73
+ };
74
+ }
75
+ const rewrote = await applyGritQL(tree, STATIC_WEBSITE_FILE, addDependencyPattern(bucket));
76
+ if (!rewrote) {
77
+ nextSteps.push(divergedMessage);
78
+ return {
79
+ nextSteps
80
+ };
81
+ }
82
+ // The inserted statement casts to the concrete Bucket to reach its policy,
83
+ // which the helper's own `IBucket` parameter type does not expose.
84
+ await addDestructuredImport(tree, STATIC_WEBSITE_FILE, [
85
+ 'Bucket'
86
+ ], 'aws-cdk-lib/aws-s3');
87
+ nextSteps.push(`${STATIC_WEBSITE_FILE}: the S3 server access log delivery source is now created after the policy of the bucket it targets, avoiding a 409 (OperationAborted) from concurrent bucket configuration writes.`);
88
+ await formatFilesInSubtree(tree);
89
+ return {
90
+ nextSteps
91
+ };
92
+ }
93
+
94
+ //# sourceMappingURL=migration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/order-access-log-delivery-after-bucket-policy/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { MigrationReturnObject, Tree } from '@nx/devkit';\nimport {\n addDestructuredImport,\n applyGritQL,\n captureGritQL,\n matchGritQL,\n} from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport {\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n} from '../../../utils/shared-constructs-constants';\n\n/**\n * Order the S3 server access log delivery source after the bucket policy.\n *\n * S3 rejects concurrent configuration writes against the same bucket with a 409\n * (OperationAborted). The generated StaticWebsite construct left the delivery\n * source and the bucket policy unordered, so CloudFormation could submit both at\n * once and fail the stack.\n */\n\nconst STATIC_WEBSITE_FILE = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core/static-website.ts`;\n\nconst BUCKET_ARN_SUFFIX = '.bucketArn';\n\n/**\n * The bucket whose policy the delivery source must be ordered after is the one\n * the delivery source itself targets, so read it off `resourceArn` rather than\n * assuming the generated parameter name. Returns undefined unless exactly one\n * simple identifier is found, so a diverged helper is left alone.\n */\nconst findDeliverySourceBucket = async (\n tree: Tree,\n filePath: string,\n): Promise<string | undefined> => {\n const captured = await captureGritQL(\n tree,\n filePath,\n '`resourceArn: $bucket.bucketArn`',\n );\n if (!captured) return undefined;\n\n const bucket = captured\n .slice(captured.indexOf(':') + 1)\n .replace(BUCKET_ARN_SUFFIX, '')\n .trim();\n\n // Only a plain identifier can be cast and dereferenced safely in the\n // statement this migration writes.\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(bucket) ? bucket : undefined;\n};\n\n/**\n * Confirm the bucket the delivery source targets is declared as a parameter of\n * the enclosing helper, so the statement this migration writes references a\n * variable that is actually in scope.\n */\nconst isBucketDeclaredParameter = async (\n tree: Tree,\n filePath: string,\n bucket: string,\n): Promise<boolean> =>\n await matchGritQL(\n tree,\n filePath,\n `\\`private deliverAccessLogsToCloudWatch($params) { $_ }\\` where {\n $params <: contains \\`${bucket}: $bucketType\\`,\n $bucketType <: or { \\`IBucket\\`, \\`Bucket\\` }\n }`,\n );\n\n// Inserts the bucket policy dependency between the delivery source and the\n// delivery destination, matching the shape generators produced prior to this\n// fix. Anchored on the destination declaration so the source's own (multi-line,\n// Lazy-valued) arguments don't need to be matched.\nconst addDependencyPattern = (bucket: string) =>\n `\\`const $dest: CfnDeliveryDestination = new CfnDeliveryDestination($destArgs)\\` as $decl where {\n $dest <: \\`destination\\`\n} => \\`const bucketPolicy = (${bucket} as Bucket).policy;\n if (bucketPolicy) {\n source.node.addDependency(bucketPolicy);\n }\n $decl\\``;\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n if (!tree.exists(STATIC_WEBSITE_FILE)) {\n // No vended StaticWebsite construct in this workspace - nothing to migrate.\n return { nextSteps };\n }\n\n const contents = tree.read(STATIC_WEBSITE_FILE, 'utf-8') ?? '';\n if (contents.includes('source.node.addDependency(bucketPolicy)')) {\n // Already migrated.\n return { nextSteps };\n }\n\n const divergedMessage = `${STATIC_WEBSITE_FILE}: deliverAccessLogsToCloudWatch has diverged from the generated shape - left untouched. Manually order the S3 server access log delivery source after the bucket policy of the bucket it targets (\\`source.node.addDependency(bucketPolicy)\\`), avoiding a 409 (OperationAborted) from concurrent bucket configuration writes.`;\n\n // The rewrite is anchored on the delivery destination but references the\n // `source` variable, so only apply it when the delivery source still has the\n // generated shape.\n const hasGeneratedSource = await matchGritQL(\n tree,\n STATIC_WEBSITE_FILE,\n '`const source: CfnDeliverySource = new CfnDeliverySource($_)`',\n );\n\n const bucket = hasGeneratedSource\n ? await findDeliverySourceBucket(tree, STATIC_WEBSITE_FILE)\n : undefined;\n\n if (\n !bucket ||\n !(await isBucketDeclaredParameter(tree, STATIC_WEBSITE_FILE, bucket))\n ) {\n nextSteps.push(divergedMessage);\n return { nextSteps };\n }\n\n const rewrote = await applyGritQL(\n tree,\n STATIC_WEBSITE_FILE,\n addDependencyPattern(bucket),\n );\n\n if (!rewrote) {\n nextSteps.push(divergedMessage);\n return { nextSteps };\n }\n\n // The inserted statement casts to the concrete Bucket to reach its policy,\n // which the helper's own `IBucket` parameter type does not expose.\n await addDestructuredImport(\n tree,\n STATIC_WEBSITE_FILE,\n ['Bucket'],\n 'aws-cdk-lib/aws-s3',\n );\n\n nextSteps.push(\n `${STATIC_WEBSITE_FILE}: the S3 server access log delivery source is now created after the policy of the bucket it targets, avoiding a 409 (OperationAborted) from concurrent bucket configuration writes.`,\n );\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["addDestructuredImport","applyGritQL","captureGritQL","matchGritQL","formatFilesInSubtree","PACKAGES_DIR","SHARED_CONSTRUCTS_DIR","STATIC_WEBSITE_FILE","BUCKET_ARN_SUFFIX","findDeliverySourceBucket","tree","filePath","captured","undefined","bucket","slice","indexOf","replace","trim","test","isBucketDeclaredParameter","addDependencyPattern","migration","nextSteps","exists","contents","read","includes","divergedMessage","hasGeneratedSource","push","rewrote"],"mappings":"AAAA;;;CAGC,GAED,SACEA,qBAAqB,EACrBC,WAAW,EACXC,aAAa,EACbC,WAAW,QACN,wBAAqB;AAC5B,SAASC,oBAAoB,QAAQ,2BAAwB;AAC7D,SACEC,YAAY,EACZC,qBAAqB,QAChB,gDAA6C;AAEpD;;;;;;;CAOC,GAED,MAAMC,sBAAsB,GAAGF,aAAa,CAAC,EAAEC,sBAAsB,2BAA2B,CAAC;AAEjG,MAAME,oBAAoB;AAE1B;;;;;CAKC,GACD,MAAMC,2BAA2B,OAC/BC,MACAC;IAEA,MAAMC,WAAW,MAAMV,cACrBQ,MACAC,UACA;IAEF,IAAI,CAACC,UAAU,OAAOC;IAEtB,MAAMC,SAASF,SACZG,KAAK,CAACH,SAASI,OAAO,CAAC,OAAO,GAC9BC,OAAO,CAACT,mBAAmB,IAC3BU,IAAI;IAEP,qEAAqE;IACrE,mCAAmC;IACnC,OAAO,6BAA6BC,IAAI,CAACL,UAAUA,SAASD;AAC9D;AAEA;;;;CAIC,GACD,MAAMO,4BAA4B,OAChCV,MACAC,UACAG,SAEA,MAAMX,YACJO,MACAC,UACA,CAAC;4BACuB,EAAEG,OAAO;;KAEhC,CAAC;AAGN,2EAA2E;AAC3E,6EAA6E;AAC7E,gFAAgF;AAChF,mDAAmD;AACnD,MAAMO,uBAAuB,CAACP,SAC5B,CAAC;;6BAE0B,EAAEA,OAAO;;;;WAI3B,CAAC;AAEZ,eAAe,eAAeQ,UAC5BZ,IAAU;IAEV,MAAMa,YAAsB,EAAE;IAE9B,IAAI,CAACb,KAAKc,MAAM,CAACjB,sBAAsB;QACrC,4EAA4E;QAC5E,OAAO;YAAEgB;QAAU;IACrB;IAEA,MAAME,WAAWf,KAAKgB,IAAI,CAACnB,qBAAqB,YAAY;IAC5D,IAAIkB,SAASE,QAAQ,CAAC,4CAA4C;QAChE,oBAAoB;QACpB,OAAO;YAAEJ;QAAU;IACrB;IAEA,MAAMK,kBAAkB,GAAGrB,oBAAoB,8TAA8T,CAAC;IAE9W,yEAAyE;IACzE,6EAA6E;IAC7E,mBAAmB;IACnB,MAAMsB,qBAAqB,MAAM1B,YAC/BO,MACAH,qBACA;IAGF,MAAMO,SAASe,qBACX,MAAMpB,yBAAyBC,MAAMH,uBACrCM;IAEJ,IACE,CAACC,UACD,CAAE,MAAMM,0BAA0BV,MAAMH,qBAAqBO,SAC7D;QACAS,UAAUO,IAAI,CAACF;QACf,OAAO;YAAEL;QAAU;IACrB;IAEA,MAAMQ,UAAU,MAAM9B,YACpBS,MACAH,qBACAc,qBAAqBP;IAGvB,IAAI,CAACiB,SAAS;QACZR,UAAUO,IAAI,CAACF;QACf,OAAO;YAAEL;QAAU;IACrB;IAEA,2EAA2E;IAC3E,mEAAmE;IACnE,MAAMvB,sBACJU,MACAH,qBACA;QAAC;KAAS,EACV;IAGFgB,UAAUO,IAAI,CACZ,GAAGvB,oBAAoB,mLAAmL,CAAC;IAG7M,MAAMH,qBAAqBM;IAE3B,OAAO;QAAEa;IAAU;AACrB"}
@@ -1940,6 +1940,10 @@ export class StaticWebsite extends Construct {
1940
1940
  resourceArn: bucket.bucketArn,
1941
1941
  },
1942
1942
  );
1943
+ const bucketPolicy = (bucket as Bucket).policy;
1944
+ if (bucketPolicy) {
1945
+ source.node.addDependency(bucketPolicy);
1946
+ }
1943
1947
  const destination: CfnDeliveryDestination = new CfnDeliveryDestination(
1944
1948
  this,
1945
1949
  \`\${id}AccessLogsDestination\`,
@@ -3750,6 +3754,10 @@ export class StaticWebsite extends Construct {
3750
3754
  resourceArn: bucket.bucketArn,
3751
3755
  },
3752
3756
  );
3757
+ const bucketPolicy = (bucket as Bucket).policy;
3758
+ if (bucketPolicy) {
3759
+ source.node.addDependency(bucketPolicy);
3760
+ }
3753
3761
  const destination: CfnDeliveryDestination = new CfnDeliveryDestination(
3754
3762
  this,
3755
3763
  \`\${id}AccessLogsDestination\`,
@@ -5420,6 +5428,10 @@ export class StaticWebsite extends Construct {
5420
5428
  resourceArn: bucket.bucketArn,
5421
5429
  },
5422
5430
  );
5431
+ const bucketPolicy = (bucket as Bucket).policy;
5432
+ if (bucketPolicy) {
5433
+ source.node.addDependency(bucketPolicy);
5434
+ }
5423
5435
  const destination: CfnDeliveryDestination = new CfnDeliveryDestination(
5424
5436
  this,
5425
5437
  \`\${id}AccessLogsDestination\`,
@@ -324,6 +324,10 @@ export class StaticWebsite extends Construct {
324
324
  resourceArn: bucket.bucketArn,
325
325
  },
326
326
  );
327
+ const bucketPolicy = (bucket as Bucket).policy;
328
+ if (bucketPolicy) {
329
+ source.node.addDependency(bucketPolicy);
330
+ }
327
331
  const destination: CfnDeliveryDestination = new CfnDeliveryDestination(
328
332
  this,
329
333
  `${id}AccessLogsDestination`,