@intentius/chant-lexicon-aws 0.52.0 → 0.52.2
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/dist/api/read-client.d.ts +12 -0
- package/dist/api/read-client.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +4 -4
- package/dist/lint/post-synth/waw016.d.ts +4 -0
- package/dist/lint/post-synth/waw016.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/meta.json +7 -7
- package/dist/okf/rules/WAW016.md +1 -0
- package/dist/okf/types/Url.md +4 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/waw016.ts +31 -3
- package/dist/spec/parse.d.ts +16 -0
- package/dist/spec/parse.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/api/read-client.ts +27 -4
- package/src/generated/lexicon-aws.json +7 -7
- package/src/index.ts +2 -0
- package/src/lifecycle-integration.test.ts +161 -17
- package/src/lint/post-synth/waw016.test.ts +150 -0
- package/src/lint/post-synth/waw016.ts +31 -3
- package/src/plugin.ts +52 -32
- package/src/spec/parse.test.ts +80 -0
- package/src/spec/parse.ts +85 -12
|
@@ -155,6 +155,18 @@ export interface StackResource {
|
|
|
155
155
|
}
|
|
156
156
|
/** `DescribeStackResources` for one stack, mapped off the Query XML. */
|
|
157
157
|
export declare function describeStackResources(stackName: string, options?: AwsReadClientOptions): Promise<StackResource[]>;
|
|
158
|
+
/** One stack's `DescribeStacks` answer: its outputs and its own tags. */
|
|
159
|
+
export interface StackDetail {
|
|
160
|
+
outputs: Record<string, string>;
|
|
161
|
+
/** The stack's own tags, as a flat map — where the ownership marker lives (#1222). */
|
|
162
|
+
tags: Record<string, string>;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* `DescribeStacks` for one stack, outputs and tags off the same response. The
|
|
166
|
+
* two ride together because they come from one call: reading the ownership
|
|
167
|
+
* marker on the observation path (#1998) costs no extra round trip.
|
|
168
|
+
*/
|
|
169
|
+
export declare function describeStackDetail(stackName: string, options?: AwsReadClientOptions): Promise<StackDetail>;
|
|
158
170
|
/** `DescribeStacks` outputs for one stack, as `key → value`. */
|
|
159
171
|
export declare function describeStackOutputs(stackName: string, options?: AwsReadClientOptions): Promise<Record<string, string>>;
|
|
160
172
|
/** One live resource, as Cloud Control describes it. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read-client.d.ts","sourceRoot":"","sources":["../../src/api/read-client.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"read-client.d.ts","sourceRoot":"","sources":["../../src/api/read-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,OAAO,EAAmC,KAAK,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEpF,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAM1F,sFAAsF;AACtF,MAAM,MAAM,WAAW,GAAG,CACxB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACvD,MAAM,CAAC,EAAE,WAAW,KACjB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAO/C,2EAA2E;AAC3E,qBAAa,YAAa,SAAQ,KAAK;IAGnC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,gGAAgG;IAChG,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM;gBAHtB,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM;IACvB,gGAAgG;IACvF,IAAI,CAAC,EAAE,MAAM,YAAA;CAKzB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,kGAAkG;IAClG,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,iFAAiF;IACjF,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,uEAAuE;IACvE,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAWD,0FAA0F;AAC1F,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GACpD,MAAM,GAAG,SAAS,CAEpB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,GAAG,oBAAoB,CAGzG;AAED,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,SAAiB,GAAG,MAAM,CAE9F;AAqCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,oBAAoB,GAC5B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAexB;AAID;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOlE;AAYD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAItF;AAED,0FAA0F;AAC1F,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAIrF;AAED;;;;GAIG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,MAAM,CAAC,CA4BjB;AAED,kEAAkE;AAClE,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wEAAwE;AACxE,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,aAAa,EAAE,CAAC,CAS1B;AAED,yEAAyE;AACzE,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,sFAAsF;IACtF,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,WAAW,CAAC,CAWtB;AAED,gEAAgE;AAChE,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAEjC;AAID,wDAAwD;AACxD,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAoDD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,OAAO,GAAG,uBAAuB,GAAG,IAAI,CAQrF;AAWD,8DAA8D;AAC9D,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAGzC;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,oBAAyB,EAClC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACtC,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAqBpC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export { output, isLexiconOutput } from "@intentius/chant/lexicon-output";
|
|
|
19
19
|
export type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
20
20
|
export { awsPlugin } from "./plugin.js";
|
|
21
21
|
export { observeResourcesDeepAws, awsDeepNormalizationHooks, hasOwnershipMarker, DEEP_READABLE_TYPES, AWS_READ_ONLY_NAMES, AWS_SERVICE_DEFAULTS, } from "./deep-observe.js";
|
|
22
|
-
export { describeStackResources, describeStackOutputs, getResource, listResources, parseResourceDescription, resolveEndpointOverride, serviceEndpointEnvVar, AwsReadError, type AwsReadHttp, type AwsReadClientOptions, type CloudControlDescription, type StackResource, type AwsCredentials, type AwsCredentialResolver, type AwsCredentialSource, } from "./api/read-client.js";
|
|
22
|
+
export { describeStackResources, describeStackOutputs, describeStackDetail, getResource, listResources, parseResourceDescription, resolveEndpointOverride, serviceEndpointEnvVar, AwsReadError, type AwsReadHttp, type AwsReadClientOptions, type CloudControlDescription, type StackResource, type StackDetail, type AwsCredentials, type AwsCredentialResolver, type AwsCredentialSource, } from "./api/read-client.js";
|
|
23
23
|
export { signRequest, resolveCredentials, EMPTY_PAYLOAD_SHA256, type SigV4Request, } from "./api/sigv4.js";
|
|
24
24
|
export { Sub, Ref, GetAtt, If, Join, Select, Split, Base64, GetAZs, SubIntrinsic, RefIntrinsic, GetAttIntrinsic, IfIntrinsic, JoinIntrinsic, SelectIntrinsic, SplitIntrinsic, Base64Intrinsic, GetAZsIntrinsic, } from "./intrinsics.js";
|
|
25
25
|
export { AWS, StackName, Region, AccountId, StackId, URLSuffix, NoValue, NotificationARNs, Partition, } from "./pseudo.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
5
|
-
"meta.json": "
|
|
4
|
+
"manifest.json": "2bc626775d04c8e8b162da0a076ac5efab827834fc0ea83ae1871b82479cf7ec",
|
|
5
|
+
"meta.json": "49cbb2ca9b454391a7644e14ee9cfd91bcf906c6af683bf46a182aa85047ecc4",
|
|
6
6
|
"types/index.d.ts": "6d6b37bd14a9b40fb3ff5c66e1cb49be1ae1032de47a4e167cf5f411c943b7f5",
|
|
7
7
|
"rules/hardcoded-region.ts": "5a0eaf7ab391231fe6cd51426ece29539cb4b36f31c8dd060956638fed55722a",
|
|
8
8
|
"rules/iam-wildcard.ts": "135d7217d278fef50939e605c5da32603ba35674b8c4b5c4d66a06c77903e945",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"rules/waw013.ts": "f9b37b955917415a2eb5f4aed7f28b76aff0a6abdb05228dcbb8bc77bc67672a",
|
|
16
16
|
"rules/waw014.ts": "889f1b06899b50f98941d05b029954cf375ac8a130933b98c9586079096216aa",
|
|
17
17
|
"rules/waw015.ts": "071c940fb6432cd8b42a44d083921e9491d5063417839109c10b2bd13dd2032e",
|
|
18
|
-
"rules/waw016.ts": "
|
|
18
|
+
"rules/waw016.ts": "b9538a9c44a58b687a08f74328caecba4236ad3c24c97759e6d69a3b9ce487c0",
|
|
19
19
|
"rules/waw017.ts": "658554008954f71036e23b514dece7063142198fbdca125f11d09026f1d21ab2",
|
|
20
20
|
"rules/waw018.ts": "a4aa83a25104f039a6c5e0f3e1f5b8b65e493b0ba162c985988190a726b90d2f",
|
|
21
21
|
"rules/waw019.ts": "f14ebf024a3cce7005d54c762e4e0979537d6a854fb123344107d40d92a94b5c",
|
|
@@ -72,5 +72,5 @@
|
|
|
72
72
|
"skills/chant-aws-eks.md": "8789255709ff004ad0a875fd5999edcdc66fc6e33d710db058d9f42703bcfdfe",
|
|
73
73
|
"skills/chant-aws-carve-terraform.md": "0ae4376590db47979be144d3ac6cdcfd56c9640d0536421aacf7330459d38e52"
|
|
74
74
|
},
|
|
75
|
-
"composite": "
|
|
75
|
+
"composite": "d3fda61a74692e9473bb1aeeb31723128b926b4c9a0014ba2d399dfab05473bb"
|
|
76
76
|
}
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* which also matches descriptions that mention the deprecation of a sibling
|
|
10
10
|
* property, an enum value, or the thing the property configures. Declared is a
|
|
11
11
|
* warning; inferred is reported at info and worded as a guess.
|
|
12
|
+
*
|
|
13
|
+
* A declared name is a flattened Registry pointer path (#1988), so most of them
|
|
14
|
+
* are nested — `Tags/TagKey`, `Source/Decryption/Url`. The template is walked
|
|
15
|
+
* along that path; matching top-level keys reached fewer than half of them.
|
|
12
16
|
*/
|
|
13
17
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
14
18
|
/** What a deprecation classification rests on. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"waw016.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/waw016.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"waw016.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/waw016.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAG9G,kDAAkD;AAClD,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,UAAU,CAAC;AA6DvD;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,gBAAgB,EACrB,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,GACrD,mBAAmB,EAAE,CAmCvB;AAED,eAAO,MAAM,MAAM,EAAE,cAOpB,CAAC"}
|
package/dist/manifest.json
CHANGED
package/dist/meta.json
CHANGED
|
@@ -13765,8 +13765,8 @@
|
|
|
13765
13765
|
"Id"
|
|
13766
13766
|
],
|
|
13767
13767
|
"deprecatedProperties": [
|
|
13768
|
-
"DistributionConfig/
|
|
13769
|
-
"DistributionConfig/
|
|
13768
|
+
"DistributionConfig/CustomOrigin",
|
|
13769
|
+
"DistributionConfig/S3Origin"
|
|
13770
13770
|
],
|
|
13771
13771
|
"tagging": {
|
|
13772
13772
|
"taggable": true,
|
|
@@ -21366,9 +21366,9 @@
|
|
|
21366
21366
|
"Id"
|
|
21367
21367
|
],
|
|
21368
21368
|
"deprecatedProperties": [
|
|
21369
|
-
"
|
|
21370
|
-
"
|
|
21371
|
-
"
|
|
21369
|
+
"ContinuousDeploymentPolicyConfig/Type",
|
|
21370
|
+
"ContinuousDeploymentPolicyConfig/SingleHeaderPolicyConfig",
|
|
21371
|
+
"ContinuousDeploymentPolicyConfig/SingleWeightPolicyConfig"
|
|
21372
21372
|
]
|
|
21373
21373
|
},
|
|
21374
21374
|
"ContinuousDeploymentPolicyConfig": {
|
|
@@ -68321,8 +68321,8 @@
|
|
|
68321
68321
|
"DomainName"
|
|
68322
68322
|
],
|
|
68323
68323
|
"conditionalCreateOnly": [
|
|
68324
|
-
"EncryptionAtRestOptions
|
|
68325
|
-
"AdvancedSecurityOptions/
|
|
68324
|
+
"EncryptionAtRestOptions",
|
|
68325
|
+
"AdvancedSecurityOptions/Enabled"
|
|
68326
68326
|
],
|
|
68327
68327
|
"tagging": {
|
|
68328
68328
|
"taggable": true,
|
package/dist/okf/rules/WAW016.md
CHANGED
package/dist/okf/types/Url.md
CHANGED
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAoN,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAoN,MAAM,0BAA0B,CAAC;AAuChR;yEACyE;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAiBnD,eAAO,MAAM,SAAS,EAAE,aAm/BvB,CAAC"}
|
package/dist/rules/waw016.ts
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* which also matches descriptions that mention the deprecation of a sibling
|
|
10
10
|
* property, an enum value, or the thing the property configures. Declared is a
|
|
11
11
|
* warning; inferred is reported at info and worded as a guess.
|
|
12
|
+
*
|
|
13
|
+
* A declared name is a flattened Registry pointer path (#1988), so most of them
|
|
14
|
+
* are nested — `Tags/TagKey`, `Source/Decryption/Url`. The template is walked
|
|
15
|
+
* along that path; matching top-level keys reached fewer than half of them.
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
18
|
import { readFileSync } from "fs";
|
|
@@ -56,6 +60,28 @@ function loadDeprecatedProperties(): Map<string, Map<string, DeprecationBasis>>
|
|
|
56
60
|
return map;
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
/**
|
|
64
|
+
* True when `value` expresses `segments` — the flattened Registry pointer path
|
|
65
|
+
* (#1988), which is a single key for a top-level property and `/`-joined for a
|
|
66
|
+
* nested one.
|
|
67
|
+
*
|
|
68
|
+
* An array met where the pointer named a property is descended anyway: the
|
|
69
|
+
* Registry elides the wildcard in some schemas, so `Tags/TagKey` addresses a
|
|
70
|
+
* key inside the Tags array's items.
|
|
71
|
+
*/
|
|
72
|
+
function expressesPath(value: unknown, segments: string[]): boolean {
|
|
73
|
+
if (segments.length === 0) return true;
|
|
74
|
+
if (Array.isArray(value)) {
|
|
75
|
+
const rest = segments[0] === "*" ? segments.slice(1) : segments;
|
|
76
|
+
return value.some((item) => expressesPath(item, rest));
|
|
77
|
+
}
|
|
78
|
+
if (segments[0] === "*") return false;
|
|
79
|
+
if (typeof value !== "object" || value === null) return false;
|
|
80
|
+
const obj = value as Record<string, unknown>;
|
|
81
|
+
if (!Object.prototype.hasOwnProperty.call(obj, segments[0])) return false;
|
|
82
|
+
return expressesPath(obj[segments[0]], segments.slice(1));
|
|
83
|
+
}
|
|
84
|
+
|
|
59
85
|
/**
|
|
60
86
|
* Core detection logic — exported for direct testing with synthetic data.
|
|
61
87
|
*/
|
|
@@ -76,9 +102,11 @@ export function checkDeprecatedProperties(
|
|
|
76
102
|
if (!deprProps) continue;
|
|
77
103
|
|
|
78
104
|
const props = resource.Properties ?? {};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
105
|
+
// Walk each declared path into the template, rather than matching
|
|
106
|
+
// top-level template keys: over half the declared names are nested
|
|
107
|
+
// (#1988), and a key comparison could never reach them.
|
|
108
|
+
for (const [propName, basis] of deprProps) {
|
|
109
|
+
if (!expressesPath(props, propName.split("/"))) continue;
|
|
82
110
|
|
|
83
111
|
diagnostics.push({
|
|
84
112
|
checkId: "WAW016",
|
package/dist/spec/parse.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* resources with properties and attributes, property types from definitions,
|
|
6
6
|
* and enum types from string enum definitions.
|
|
7
7
|
*/
|
|
8
|
+
import type { CFNSchema } from "./fetch.js";
|
|
8
9
|
import { constraintsIsEmpty as coreConstraintsIsEmpty, type PropertyConstraints } from "@intentius/chant/codegen/json-schema";
|
|
9
10
|
export type { PropertyConstraints } from "@intentius/chant/codegen/json-schema";
|
|
10
11
|
export interface ParsedProperty {
|
|
@@ -69,4 +70,19 @@ export declare function cfnShortName(typeName: string): string;
|
|
|
69
70
|
* Extract service name: "AWS::S3::Bucket" → "S3"
|
|
70
71
|
*/
|
|
71
72
|
export declare function cfnServiceName(typeName: string): string;
|
|
73
|
+
/**
|
|
74
|
+
* Flatten a CloudFormation Registry JSON pointer to the property path a
|
|
75
|
+
* template expresses, `/`-joined.
|
|
76
|
+
*
|
|
77
|
+
* Three shapes occur upstream. `/properties/BucketName` is the plain one.
|
|
78
|
+
* A pointer can re-enter the `properties` keyword mid-path
|
|
79
|
+
* (`/properties/DistributionConfig/properties/S3Origin`), which a template
|
|
80
|
+
* never writes. And a pointer can be scoped to a shared definition
|
|
81
|
+
* (`/definitions/ContinuousDeploymentPolicyConfig/properties/Type`), which a
|
|
82
|
+
* template expresses only through whichever property carries that definition.
|
|
83
|
+
*
|
|
84
|
+
* Array positions stay as `*`. `readOnlyProperties` attribute names are
|
|
85
|
+
* generated from this output, so dropping the wildcard would rename them.
|
|
86
|
+
*/
|
|
87
|
+
export declare function stripPointerPath(path: string, schema?: CFNSchema): string;
|
|
72
88
|
//# sourceMappingURL=parse.d.ts.map
|
package/dist/spec/parse.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/spec/parse.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/spec/parse.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAoC,MAAM,SAAS,CAAC;AAC3E,OAAO,EAGL,kBAAkB,IAAI,sBAAsB,EAE5C,KAAK,mBAAmB,EAIzB,MAAM,sCAAsC,CAAC;AAG9C,YAAY,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAkBhF,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,EAAE,mBAAmB,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,cAAc,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B;;;;;OAKG;IACH,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,mBAAmB,CAAC,EAAE,oBAAoB,GAAG,oBAAoB,CAAC;IAClE,OAAO,CAAC,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,WAAW,EAAE,OAAO,CAAC;QAAC,YAAY,EAAE,OAAO,CAAA;KAAE,CAAC;CAC9E;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,cAAc,CAAC;IACzB,aAAa,EAAE,kBAAkB,EAAE,CAAC;IACpC,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,iBAAiB,CAmJvE;AAiBD,eAAO,MAAM,kBAAkB,+BAAyB,CAAC;AAMzD;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGrD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGvD;AAuDD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,MAAM,CAYzE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-aws",
|
|
3
|
-
"version": "0.52.
|
|
3
|
+
"version": "0.52.2",
|
|
4
4
|
"description": "AWS CloudFormation lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"typescript": "^5.9.3"
|
|
81
81
|
},
|
|
82
82
|
"peerDependencies": {
|
|
83
|
-
"@intentius/chant": "^0.52.
|
|
83
|
+
"@intentius/chant": "^0.52.2",
|
|
84
84
|
"typescript": "^5.9.3"
|
|
85
85
|
}
|
|
86
86
|
}
|
package/src/api/read-client.ts
CHANGED
|
@@ -328,17 +328,40 @@ export async function describeStackResources(
|
|
|
328
328
|
}));
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
/** `DescribeStacks`
|
|
332
|
-
export
|
|
331
|
+
/** One stack's `DescribeStacks` answer: its outputs and its own tags. */
|
|
332
|
+
export interface StackDetail {
|
|
333
|
+
outputs: Record<string, string>;
|
|
334
|
+
/** The stack's own tags, as a flat map — where the ownership marker lives (#1222). */
|
|
335
|
+
tags: Record<string, string>;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* `DescribeStacks` for one stack, outputs and tags off the same response. The
|
|
340
|
+
* two ride together because they come from one call: reading the ownership
|
|
341
|
+
* marker on the observation path (#1998) costs no extra round trip.
|
|
342
|
+
*/
|
|
343
|
+
export async function describeStackDetail(
|
|
333
344
|
stackName: string,
|
|
334
345
|
options: AwsReadClientOptions = {},
|
|
335
|
-
): Promise<
|
|
346
|
+
): Promise<StackDetail> {
|
|
336
347
|
const xml = await cfnQuery("DescribeStacks", { StackName: stackName }, options);
|
|
337
348
|
const outputs: Record<string, string> = {};
|
|
338
349
|
for (const m of xmlMembers(xml, "Outputs")) {
|
|
339
350
|
if (m.OutputKey) outputs[m.OutputKey] = m.OutputValue ?? "";
|
|
340
351
|
}
|
|
341
|
-
|
|
352
|
+
const tags: Record<string, string> = {};
|
|
353
|
+
for (const m of xmlMembers(xml, "Tags")) {
|
|
354
|
+
if (m.Key) tags[m.Key] = m.Value ?? "";
|
|
355
|
+
}
|
|
356
|
+
return { outputs, tags };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** `DescribeStacks` outputs for one stack, as `key → value`. */
|
|
360
|
+
export async function describeStackOutputs(
|
|
361
|
+
stackName: string,
|
|
362
|
+
options: AwsReadClientOptions = {},
|
|
363
|
+
): Promise<Record<string, string>> {
|
|
364
|
+
return (await describeStackDetail(stackName, options)).outputs;
|
|
342
365
|
}
|
|
343
366
|
|
|
344
367
|
/* ── Cloud Control ────────────────────────────────────────────────────────── */
|
|
@@ -13765,8 +13765,8 @@
|
|
|
13765
13765
|
"Id"
|
|
13766
13766
|
],
|
|
13767
13767
|
"deprecatedProperties": [
|
|
13768
|
-
"DistributionConfig/
|
|
13769
|
-
"DistributionConfig/
|
|
13768
|
+
"DistributionConfig/CustomOrigin",
|
|
13769
|
+
"DistributionConfig/S3Origin"
|
|
13770
13770
|
],
|
|
13771
13771
|
"tagging": {
|
|
13772
13772
|
"taggable": true,
|
|
@@ -21366,9 +21366,9 @@
|
|
|
21366
21366
|
"Id"
|
|
21367
21367
|
],
|
|
21368
21368
|
"deprecatedProperties": [
|
|
21369
|
-
"
|
|
21370
|
-
"
|
|
21371
|
-
"
|
|
21369
|
+
"ContinuousDeploymentPolicyConfig/Type",
|
|
21370
|
+
"ContinuousDeploymentPolicyConfig/SingleHeaderPolicyConfig",
|
|
21371
|
+
"ContinuousDeploymentPolicyConfig/SingleWeightPolicyConfig"
|
|
21372
21372
|
]
|
|
21373
21373
|
},
|
|
21374
21374
|
"ContinuousDeploymentPolicyConfig": {
|
|
@@ -68321,8 +68321,8 @@
|
|
|
68321
68321
|
"DomainName"
|
|
68322
68322
|
],
|
|
68323
68323
|
"conditionalCreateOnly": [
|
|
68324
|
-
"EncryptionAtRestOptions
|
|
68325
|
-
"AdvancedSecurityOptions/
|
|
68324
|
+
"EncryptionAtRestOptions",
|
|
68325
|
+
"AdvancedSecurityOptions/Enabled"
|
|
68326
68326
|
],
|
|
68327
68327
|
"tagging": {
|
|
68328
68328
|
"taggable": true,
|
package/src/index.ts
CHANGED
|
@@ -67,6 +67,7 @@ export {
|
|
|
67
67
|
export {
|
|
68
68
|
describeStackResources,
|
|
69
69
|
describeStackOutputs,
|
|
70
|
+
describeStackDetail,
|
|
70
71
|
getResource,
|
|
71
72
|
listResources,
|
|
72
73
|
parseResourceDescription,
|
|
@@ -77,6 +78,7 @@ export {
|
|
|
77
78
|
type AwsReadClientOptions,
|
|
78
79
|
type CloudControlDescription,
|
|
79
80
|
type StackResource,
|
|
81
|
+
type StackDetail,
|
|
80
82
|
type AwsCredentials,
|
|
81
83
|
type AwsCredentialResolver,
|
|
82
84
|
type AwsCredentialSource,
|
|
@@ -33,6 +33,7 @@ const { liveEvidenceFromChangeSet, reconcileStatus } = await import("@intentius/
|
|
|
33
33
|
const { describeObservationConformance } = await import("@intentius/chant-test-utils");
|
|
34
34
|
const { observeResources } = await import("@intentius/chant/lifecycle/observe");
|
|
35
35
|
const { DECLARABLE_MARKER } = await import("@intentius/chant/declarable");
|
|
36
|
+
const { assertLiveEntity, LiveAssertionError } = await import("@intentius/chant/lifecycle/assert-live");
|
|
36
37
|
|
|
37
38
|
const liveTemplate = {
|
|
38
39
|
AWSTemplateFormatVersion: "2010-09-09",
|
|
@@ -58,10 +59,19 @@ const stackResourcesXml = (rows: Array<{ logicalId: string; type: string; physic
|
|
|
58
59
|
)
|
|
59
60
|
.join("")}</StackResources></DescribeStackResourcesResponse>`;
|
|
60
61
|
|
|
61
|
-
const stackOutputsXml = (outputs: Record<string, string> = {}) =>
|
|
62
|
+
const stackOutputsXml = (outputs: Record<string, string> = {}, tags: Record<string, string> = {}) =>
|
|
62
63
|
`<DescribeStacksResponse><Outputs>${Object.entries(outputs)
|
|
63
64
|
.map(([k, v]) => `<member><OutputKey>${k}</OutputKey><OutputValue>${v}</OutputValue></member>`)
|
|
64
|
-
.join("")}</Outputs
|
|
65
|
+
.join("")}</Outputs><Tags>${Object.entries(tags)
|
|
66
|
+
.map(([k, v]) => `<member><Key>${k}</Key><Value>${v}</Value></member>`)
|
|
67
|
+
.join("")}</Tags></DescribeStacksResponse>`;
|
|
68
|
+
|
|
69
|
+
/** The stack tags a chant apply stamps for one identity (#1222). */
|
|
70
|
+
const ownedStackTags = (stack: string, env: string) => ({
|
|
71
|
+
"chant:managed-by": "chant",
|
|
72
|
+
"chant:stack": stack,
|
|
73
|
+
"chant:env": env,
|
|
74
|
+
});
|
|
65
75
|
|
|
66
76
|
const queryErrorXml = (code: string, message: string) =>
|
|
67
77
|
`<ErrorResponse><Error><Code>${code}</Code><Message>${message}</Message></Error></ErrorResponse>`;
|
|
@@ -122,9 +132,10 @@ describe("aws lifecycle integration (#163)", () => {
|
|
|
122
132
|
}),
|
|
123
133
|
);
|
|
124
134
|
expect(observedNow.MyBucket?.type).toBe("AWS::S3::Bucket");
|
|
125
|
-
// Ownership verdicts are total (#1089)
|
|
126
|
-
// tags, so the verdict is
|
|
127
|
-
expect(observedNow.MyBucket?.ownership).toBe("
|
|
135
|
+
// Ownership verdicts are total (#1089). The stack answered and carries no
|
|
136
|
+
// chant tags, so the verdict is `foreign` — read, not merely unattempted.
|
|
137
|
+
expect(observedNow.MyBucket?.ownership).toBe("foreign");
|
|
138
|
+
expect(observedNow.MyBucket?.marker).toBeUndefined();
|
|
128
139
|
|
|
129
140
|
// Declared "MyQueue" is absent from live → create; live "MyBucket" is
|
|
130
141
|
// undeclared and unmarked → adopt (never delete without ownership).
|
|
@@ -371,16 +382,108 @@ describe("aws lifecycle integration (#163)", () => {
|
|
|
371
382
|
{ errors: [], warnings: [], entities, outputs: new Map([["aws", ""]]) } as never,
|
|
372
383
|
{ owned: true, stacks: ["prod-net", "prod-data", "prod-app", "prod-edge"] },
|
|
373
384
|
);
|
|
374
|
-
const ownership = result.notes.filter((n) => n.includes("ownership filter
|
|
385
|
+
const ownership = result.notes.filter((n) => n.includes("ownership filter not applied"));
|
|
375
386
|
expect(ownership).toHaveLength(1);
|
|
376
387
|
expect(ownership[0]).toBe(
|
|
377
|
-
"[aws] ownership filter
|
|
388
|
+
"[aws] ownership filter not applied on describeResources (this stack's own tags carry no chant marker, or DescribeStacks did not answer) — returning all, each with the verdict the read supports; use `chant import --from <env> --owned` for ownership-filtered export",
|
|
378
389
|
);
|
|
379
|
-
expect(result.warnings.join("\n")).not.toContain("ownership filter
|
|
390
|
+
expect(result.warnings.join("\n")).not.toContain("ownership filter not applied");
|
|
380
391
|
expect(warn).not.toHaveBeenCalled();
|
|
381
392
|
expect(error).not.toHaveBeenCalled();
|
|
382
393
|
});
|
|
383
394
|
|
|
395
|
+
test("an owned read of a marked stack resolves the marker and carries no note", async () => {
|
|
396
|
+
stubCfn((action) =>
|
|
397
|
+
action === "DescribeStackResources"
|
|
398
|
+
? { text: stackResourcesXml([{ logicalId: "MyBucket", type: "AWS::S3::Bucket", physicalId: "my-bucket" }]) }
|
|
399
|
+
: { text: stackOutputsXml({}, ownedStackTags("shop", "prod")) },
|
|
400
|
+
);
|
|
401
|
+
const observed = normalizeObservation(
|
|
402
|
+
await awsPlugin.describeResources!({
|
|
403
|
+
environment: "prod",
|
|
404
|
+
buildOutput: "",
|
|
405
|
+
entityNames: ["MyBucket"],
|
|
406
|
+
entities: new Map(),
|
|
407
|
+
owned: true,
|
|
408
|
+
}),
|
|
409
|
+
);
|
|
410
|
+
expect(observed.resources.MyBucket?.ownership).toBe("owned");
|
|
411
|
+
expect(observed.resources.MyBucket?.marker).toEqual({ stack: "shop", env: "prod" });
|
|
412
|
+
expect(observed.notes).toEqual([]);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// --- The ownership marker on the thin read (#1998) ---
|
|
416
|
+
|
|
417
|
+
test("a stack tagged for another environment is reported foreign, not unknown", async () => {
|
|
418
|
+
stubCfn((action) =>
|
|
419
|
+
action === "DescribeStackResources"
|
|
420
|
+
? { text: stackResourcesXml([{ logicalId: "MyBucket", type: "AWS::S3::Bucket", physicalId: "my-bucket" }]) }
|
|
421
|
+
: { text: stackOutputsXml({}, ownedStackTags("shop", "staging")) },
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const meta = normalizeObservation(
|
|
425
|
+
await awsPlugin.describeResources!({
|
|
426
|
+
environment: "prod",
|
|
427
|
+
buildOutput: "",
|
|
428
|
+
entityNames: ["MyBucket"],
|
|
429
|
+
entities: new Map(),
|
|
430
|
+
}),
|
|
431
|
+
).resources.MyBucket;
|
|
432
|
+
// The marker is the stack's, read back verbatim — which is what makes the
|
|
433
|
+
// env mismatch visible at all. Before this the field was absent and every
|
|
434
|
+
// caller had to pass the resource through unverified.
|
|
435
|
+
expect(meta?.marker).toEqual({ stack: "shop", env: "staging" });
|
|
436
|
+
|
|
437
|
+
await expect(
|
|
438
|
+
assertLiveEntity({
|
|
439
|
+
plugin: awsPlugin,
|
|
440
|
+
name: "MyBucket",
|
|
441
|
+
entityType: "AWS::S3::Bucket",
|
|
442
|
+
props: {},
|
|
443
|
+
buildOutput: "",
|
|
444
|
+
environment: "prod",
|
|
445
|
+
marker: { stack: "shop", env: "prod" },
|
|
446
|
+
}),
|
|
447
|
+
).rejects.toThrow(LiveAssertionError);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("assertLive passes when the stack's marker is this deploy's", async () => {
|
|
451
|
+
stubCfn((action) =>
|
|
452
|
+
action === "DescribeStackResources"
|
|
453
|
+
? { text: stackResourcesXml([{ logicalId: "MyBucket", type: "AWS::S3::Bucket", physicalId: "my-bucket" }]) }
|
|
454
|
+
: { text: stackOutputsXml({}, ownedStackTags("shop", "prod")) },
|
|
455
|
+
);
|
|
456
|
+
const meta = await assertLiveEntity({
|
|
457
|
+
plugin: awsPlugin,
|
|
458
|
+
name: "MyBucket",
|
|
459
|
+
entityType: "AWS::S3::Bucket",
|
|
460
|
+
props: {},
|
|
461
|
+
buildOutput: "",
|
|
462
|
+
environment: "prod",
|
|
463
|
+
marker: { stack: "shop", env: "prod" },
|
|
464
|
+
});
|
|
465
|
+
expect(meta.marker).toEqual({ stack: "shop", env: "prod" });
|
|
466
|
+
expect(meta.ownership).toBe("owned");
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("a stack whose tags could not be read stays unknown, never a guess", async () => {
|
|
470
|
+
stubCfn((action) =>
|
|
471
|
+
action === "DescribeStackResources"
|
|
472
|
+
? { text: stackResourcesXml([{ logicalId: "MyBucket", type: "AWS::S3::Bucket", physicalId: "my-bucket" }]) }
|
|
473
|
+
: { status: 400, text: queryErrorXml("Throttling", "Rate exceeded") },
|
|
474
|
+
);
|
|
475
|
+
const meta = normalizeObservation(
|
|
476
|
+
await awsPlugin.describeResources!({
|
|
477
|
+
environment: "prod",
|
|
478
|
+
buildOutput: "",
|
|
479
|
+
entityNames: ["MyBucket"],
|
|
480
|
+
entities: new Map(),
|
|
481
|
+
}),
|
|
482
|
+
).resources.MyBucket;
|
|
483
|
+
expect(meta?.ownership).toBe("unknown");
|
|
484
|
+
expect(meta?.marker).toBeUndefined();
|
|
485
|
+
});
|
|
486
|
+
|
|
384
487
|
test("an unowned read carries no ownership note", async () => {
|
|
385
488
|
stubCfn((action) =>
|
|
386
489
|
action === "DescribeStackResources"
|
|
@@ -428,10 +531,10 @@ describeObservationConformance({
|
|
|
428
531
|
},
|
|
429
532
|
},
|
|
430
533
|
{
|
|
431
|
-
// describe-stack-resources
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
name: "a healthy stack
|
|
534
|
+
// describe-stack-resources carries no per-resource tags, so a stack whose
|
|
535
|
+
// own tags carry no marker leaves `marker` (#1222) absent — absent means
|
|
536
|
+
// absent, never a guess.
|
|
537
|
+
name: "a healthy read of an unmarked stack",
|
|
435
538
|
declared: ["MyBucket"],
|
|
436
539
|
expectPresent: ["MyBucket"],
|
|
437
540
|
expectNoMarker: ["MyBucket"],
|
|
@@ -450,13 +553,54 @@ describeObservationConformance({
|
|
|
450
553
|
},
|
|
451
554
|
},
|
|
452
555
|
{
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
// from this path would be a claim the transport cannot support (#1348).
|
|
457
|
-
name: "an owned read on a path with no marker channel",
|
|
556
|
+
// The stack's own tags are this path's marker channel (#1998), so every
|
|
557
|
+
// member of a marked stack carries that stack's identity.
|
|
558
|
+
name: "a healthy read of a marked stack",
|
|
458
559
|
declared: ["MyBucket"],
|
|
459
560
|
expectPresent: ["MyBucket"],
|
|
561
|
+
expectMarker: { MyBucket: { stack: "shop", env: "prod" } },
|
|
562
|
+
run: () => {
|
|
563
|
+
stubCfn((action) =>
|
|
564
|
+
action === "DescribeStackResources"
|
|
565
|
+
? { text: stackResourcesXml([{ logicalId: "MyBucket", type: "AWS::S3::Bucket", physicalId: "my-bucket" }]) }
|
|
566
|
+
: { text: stackOutputsXml({}, ownedStackTags("shop", "prod")) },
|
|
567
|
+
);
|
|
568
|
+
return awsPlugin.describeResources!({
|
|
569
|
+
environment: "prod",
|
|
570
|
+
buildOutput: "",
|
|
571
|
+
entityNames: ["MyBucket"],
|
|
572
|
+
entities: new Map(),
|
|
573
|
+
});
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
// aws declares a marker channel here now, so the suite holds the verdict
|
|
578
|
+
// to `owned`/`foreign` — an `unknown` would mean the declaration is a
|
|
579
|
+
// claim the transport does not support (#1348).
|
|
580
|
+
name: "an owned read of a marked stack",
|
|
581
|
+
declared: ["MyBucket"],
|
|
582
|
+
expectPresent: ["MyBucket"],
|
|
583
|
+
owned: true,
|
|
584
|
+
run: () => {
|
|
585
|
+
stubCfn((action) =>
|
|
586
|
+
action === "DescribeStackResources"
|
|
587
|
+
? { text: stackResourcesXml([{ logicalId: "MyBucket", type: "AWS::S3::Bucket", physicalId: "my-bucket" }]) }
|
|
588
|
+
: { text: stackOutputsXml({}, ownedStackTags("shop", "prod")) },
|
|
589
|
+
);
|
|
590
|
+
return awsPlugin.describeResources!({
|
|
591
|
+
environment: "prod",
|
|
592
|
+
buildOutput: "",
|
|
593
|
+
entityNames: ["MyBucket"],
|
|
594
|
+
entities: new Map(),
|
|
595
|
+
owned: true,
|
|
596
|
+
});
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
name: "an owned read of an unmarked stack",
|
|
601
|
+
declared: ["MyBucket"],
|
|
602
|
+
expectPresent: ["MyBucket"],
|
|
603
|
+
expectNoMarker: ["MyBucket"],
|
|
460
604
|
owned: true,
|
|
461
605
|
run: () => {
|
|
462
606
|
stubCfn((action) =>
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
2
5
|
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
6
|
import { waw016, checkDeprecatedProperties, type DeprecationBasis } from "./waw016";
|
|
4
7
|
|
|
@@ -195,4 +198,151 @@ describe("WAW016: Deprecated Property Usage", () => {
|
|
|
195
198
|
expect(bySeverity.get("warning")).toContain("AccessControl");
|
|
196
199
|
expect(bySeverity.get("info")).toContain("ObjectLockConfiguration");
|
|
197
200
|
});
|
|
201
|
+
|
|
202
|
+
// --- Nested pointer paths (#1988) ---
|
|
203
|
+
|
|
204
|
+
test("a Tags/TagKey entry matches the key inside the Tags array", () => {
|
|
205
|
+
const ctx = makeCtx({
|
|
206
|
+
Resources: {
|
|
207
|
+
Block: {
|
|
208
|
+
Type: "AWS::AppStream::AppBlock",
|
|
209
|
+
Properties: {
|
|
210
|
+
Name: "block",
|
|
211
|
+
Tags: [{ TagKey: "team", TagValue: "platform" }],
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
const deprecated = new Map([
|
|
217
|
+
["AWS::AppStream::AppBlock", basisMap([["Tags/TagKey", "declared"], ["Tags/TagValue", "declared"]])],
|
|
218
|
+
]);
|
|
219
|
+
const diags = checkDeprecatedProperties(ctx, deprecated);
|
|
220
|
+
expect(diags).toHaveLength(2);
|
|
221
|
+
expect(diags.map((d) => d.message).join(" ")).toContain("Tags/TagKey");
|
|
222
|
+
expect(diags[0].severity).toBe("warning");
|
|
223
|
+
expect(diags[0].entity).toBe("Block");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("a Tags array carrying only the current keys is clean", () => {
|
|
227
|
+
const ctx = makeCtx({
|
|
228
|
+
Resources: {
|
|
229
|
+
Block: {
|
|
230
|
+
Type: "AWS::AppStream::AppBlock",
|
|
231
|
+
Properties: { Tags: [{ Key: "team", Value: "platform" }] },
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
const deprecated = new Map([
|
|
236
|
+
["AWS::AppStream::AppBlock", basisMap([["Tags/TagKey", "declared"]])],
|
|
237
|
+
]);
|
|
238
|
+
expect(checkDeprecatedProperties(ctx, deprecated)).toHaveLength(0);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("a nested object path matches", () => {
|
|
242
|
+
const ctx = makeCtx({
|
|
243
|
+
Resources: {
|
|
244
|
+
Flow: {
|
|
245
|
+
Type: "AWS::MediaConnect::Flow",
|
|
246
|
+
Properties: {
|
|
247
|
+
Source: { Name: "src", Decryption: { Url: "https://example.invalid" } },
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
const deprecated = new Map([
|
|
253
|
+
["AWS::MediaConnect::Flow", basisMap([["Source/Decryption/Url", "declared"], ["Source/SenderIpAddress", "declared"]])],
|
|
254
|
+
]);
|
|
255
|
+
const diags = checkDeprecatedProperties(ctx, deprecated);
|
|
256
|
+
expect(diags).toHaveLength(1);
|
|
257
|
+
expect(diags[0].message).toContain("Source/Decryption/Url");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("a wildcard segment requires an array member that carries the property", () => {
|
|
261
|
+
const deprecated = new Map([
|
|
262
|
+
["AWS::EC2::SecurityGroup", basisMap([["SecurityGroupEgress/*/SourceSecurityGroupId", "declared"]])],
|
|
263
|
+
]);
|
|
264
|
+
const hit = makeCtx({
|
|
265
|
+
Resources: {
|
|
266
|
+
Sg: {
|
|
267
|
+
Type: "AWS::EC2::SecurityGroup",
|
|
268
|
+
Properties: {
|
|
269
|
+
SecurityGroupEgress: [{ IpProtocol: "tcp" }, { IpProtocol: "tcp", SourceSecurityGroupId: "sg-1" }],
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
expect(checkDeprecatedProperties(hit, deprecated)).toHaveLength(1);
|
|
275
|
+
|
|
276
|
+
const miss = makeCtx({
|
|
277
|
+
Resources: {
|
|
278
|
+
Sg: {
|
|
279
|
+
Type: "AWS::EC2::SecurityGroup",
|
|
280
|
+
Properties: { SecurityGroupEgress: [{ IpProtocol: "tcp" }] },
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
expect(checkDeprecatedProperties(miss, deprecated)).toHaveLength(0);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("a nested path is not matched by a same-named top-level key", () => {
|
|
288
|
+
const ctx = makeCtx({
|
|
289
|
+
Resources: {
|
|
290
|
+
Insight: {
|
|
291
|
+
Type: "AWS::SecurityHub::Insight",
|
|
292
|
+
Properties: { Keyword: "unused-top-level" },
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
const deprecated = new Map([
|
|
297
|
+
["AWS::SecurityHub::Insight", basisMap([["Filters/Keyword", "declared"]])],
|
|
298
|
+
]);
|
|
299
|
+
expect(checkDeprecatedProperties(ctx, deprecated)).toHaveLength(0);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* The reachability count (#1988). Over half the declared names carry a nested
|
|
305
|
+
* pointer path, and a flattening regression makes them silently unmatchable
|
|
306
|
+
* rather than failing anything — so the count of unreachable names is pinned
|
|
307
|
+
* against the real generated data.
|
|
308
|
+
*/
|
|
309
|
+
describe("WAW016: declared deprecation reachability", () => {
|
|
310
|
+
const pkgDir = dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url)))));
|
|
311
|
+
const lexiconPath = join(pkgDir, "src", "generated", "lexicon-aws.json");
|
|
312
|
+
const hasGenerated = existsSync(lexiconPath);
|
|
313
|
+
|
|
314
|
+
interface Entry {
|
|
315
|
+
kind?: string;
|
|
316
|
+
resourceType?: string;
|
|
317
|
+
deprecatedProperties?: string[];
|
|
318
|
+
inferredDeprecations?: string[];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** A path a template can express: named segments and array wildcards, no JSON Schema keywords. */
|
|
322
|
+
function isTemplatePath(path: string): boolean {
|
|
323
|
+
const segments = path.split("/");
|
|
324
|
+
if (segments.length === 0 || segments[0] === "" || segments[0] === "*") return false;
|
|
325
|
+
return segments.every((s) => s === "*" || (s.length > 0 && s !== "properties" && s !== "definitions"));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
test.skipIf(!hasGenerated)("every declared deprecated name is a path a template can express", () => {
|
|
329
|
+
const data = JSON.parse(readFileSync(lexiconPath, "utf-8")) as Record<string, Entry>;
|
|
330
|
+
const declared: string[] = [];
|
|
331
|
+
for (const entry of Object.values(data)) {
|
|
332
|
+
if (entry.kind !== "resource" || !entry.resourceType) continue;
|
|
333
|
+
const inferred = new Set(entry.inferredDeprecations ?? []);
|
|
334
|
+
for (const name of entry.deprecatedProperties ?? []) {
|
|
335
|
+
if (!inferred.has(name)) declared.push(`${entry.resourceType} :: ${name}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const nested = declared.filter((d) => d.split(" :: ")[1].includes("/"));
|
|
340
|
+
const unreachable = declared.filter((d) => !isTemplatePath(d.split(" :: ")[1]));
|
|
341
|
+
|
|
342
|
+
// The spec pin moves, so the totals are a floor rather than an exact
|
|
343
|
+
// figure; the unreachable count is the one that must stay at zero.
|
|
344
|
+
expect(declared.length).toBeGreaterThan(0);
|
|
345
|
+
expect(nested.length).toBeGreaterThan(declared.length / 2);
|
|
346
|
+
expect(unreachable).toEqual([]);
|
|
347
|
+
});
|
|
198
348
|
});
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* which also matches descriptions that mention the deprecation of a sibling
|
|
10
10
|
* property, an enum value, or the thing the property configures. Declared is a
|
|
11
11
|
* warning; inferred is reported at info and worded as a guess.
|
|
12
|
+
*
|
|
13
|
+
* A declared name is a flattened Registry pointer path (#1988), so most of them
|
|
14
|
+
* are nested — `Tags/TagKey`, `Source/Decryption/Url`. The template is walked
|
|
15
|
+
* along that path; matching top-level keys reached fewer than half of them.
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
18
|
import { readFileSync } from "fs";
|
|
@@ -56,6 +60,28 @@ function loadDeprecatedProperties(): Map<string, Map<string, DeprecationBasis>>
|
|
|
56
60
|
return map;
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
/**
|
|
64
|
+
* True when `value` expresses `segments` — the flattened Registry pointer path
|
|
65
|
+
* (#1988), which is a single key for a top-level property and `/`-joined for a
|
|
66
|
+
* nested one.
|
|
67
|
+
*
|
|
68
|
+
* An array met where the pointer named a property is descended anyway: the
|
|
69
|
+
* Registry elides the wildcard in some schemas, so `Tags/TagKey` addresses a
|
|
70
|
+
* key inside the Tags array's items.
|
|
71
|
+
*/
|
|
72
|
+
function expressesPath(value: unknown, segments: string[]): boolean {
|
|
73
|
+
if (segments.length === 0) return true;
|
|
74
|
+
if (Array.isArray(value)) {
|
|
75
|
+
const rest = segments[0] === "*" ? segments.slice(1) : segments;
|
|
76
|
+
return value.some((item) => expressesPath(item, rest));
|
|
77
|
+
}
|
|
78
|
+
if (segments[0] === "*") return false;
|
|
79
|
+
if (typeof value !== "object" || value === null) return false;
|
|
80
|
+
const obj = value as Record<string, unknown>;
|
|
81
|
+
if (!Object.prototype.hasOwnProperty.call(obj, segments[0])) return false;
|
|
82
|
+
return expressesPath(obj[segments[0]], segments.slice(1));
|
|
83
|
+
}
|
|
84
|
+
|
|
59
85
|
/**
|
|
60
86
|
* Core detection logic — exported for direct testing with synthetic data.
|
|
61
87
|
*/
|
|
@@ -76,9 +102,11 @@ export function checkDeprecatedProperties(
|
|
|
76
102
|
if (!deprProps) continue;
|
|
77
103
|
|
|
78
104
|
const props = resource.Properties ?? {};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
105
|
+
// Walk each declared path into the template, rather than matching
|
|
106
|
+
// top-level template keys: over half the declared names are nested
|
|
107
|
+
// (#1988), and a key comparison could never reach them.
|
|
108
|
+
for (const [propName, basis] of deprProps) {
|
|
109
|
+
if (!expressesPath(props, propName.split("/"))) continue;
|
|
82
110
|
|
|
83
111
|
diagnostics.push({
|
|
84
112
|
checkId: "WAW016",
|
package/src/plugin.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { applyAwsEndpointArgv } from "./components/cloud-executor";
|
|
|
19
19
|
import { stackDoesNotExist } from "./stack-errors";
|
|
20
20
|
import {
|
|
21
21
|
AwsReadError,
|
|
22
|
-
|
|
22
|
+
describeStackDetail,
|
|
23
23
|
describeStackResources,
|
|
24
24
|
type AwsReadClientOptions,
|
|
25
25
|
type StackResource,
|
|
@@ -37,6 +37,7 @@ import { parseStackTemplate } from "./import/live-export";
|
|
|
37
37
|
import { awsCompletions } from "./lsp/completions";
|
|
38
38
|
import { awsHover } from "./lsp/hover";
|
|
39
39
|
import { AWS_TAG_OWNERSHIP_KEYS } from "./ownership";
|
|
40
|
+
import { readOwnership } from "@intentius/chant/ownership";
|
|
40
41
|
|
|
41
42
|
/** Re-exported from ./stack-errors so the long-standing import path (and its
|
|
42
43
|
* tests) keep working now that the deep reader shares the classifier. */
|
|
@@ -49,19 +50,24 @@ export { stackDoesNotExist } from "./stack-errors";
|
|
|
49
50
|
* import parsing, and code generation for AWS CloudFormation.
|
|
50
51
|
*/
|
|
51
52
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
53
|
+
* The stack's own tags did not resolve a marker for this read, so `owned: true`
|
|
54
|
+
* withheld nothing. Returned as a run-level note rather than printed (#1265):
|
|
55
|
+
* core says it once per run, after the answer, however many stacks were read.
|
|
56
|
+
* The text is the contract consumers grep for.
|
|
56
57
|
*/
|
|
57
|
-
const
|
|
58
|
-
"ownership filter
|
|
58
|
+
const OWNERSHIP_UNRESOLVED_NOTE =
|
|
59
|
+
"ownership filter not applied on describeResources (this stack's own tags carry no chant marker, or DescribeStacks did not answer) — returning all, each with the verdict the read supports; use `chant import --from <env> --owned` for ownership-filtered export";
|
|
59
60
|
|
|
60
61
|
export const awsPlugin: LexiconPlugin = {
|
|
61
62
|
name: "aws",
|
|
62
|
-
// The thin read is
|
|
63
|
-
//
|
|
64
|
-
|
|
63
|
+
// The thin read's marker channel is the STACK's own tags (#1998), read off
|
|
64
|
+
// the DescribeStacks call it already makes for the outputs.
|
|
65
|
+
// describe-stack-resources carries no per-resource tags, so the stack is the
|
|
66
|
+
// granularity — the same boundary teardown verifies on (#1222).
|
|
67
|
+
ownershipChannel: {
|
|
68
|
+
keys: AWS_TAG_OWNERSHIP_KEYS,
|
|
69
|
+
reads: ["describeResources", "observeResourcesDeep", "exportResources"],
|
|
70
|
+
},
|
|
65
71
|
serializer: awsSerializer,
|
|
66
72
|
// Local emulator (#920): Floci + the AWS env that redirects the SDK / observe.
|
|
67
73
|
emulator: FLOCI_EMULATOR,
|
|
@@ -568,14 +574,11 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
|
|
|
568
574
|
...(options.region ? { region: options.region } : {}),
|
|
569
575
|
};
|
|
570
576
|
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
|
|
576
|
-
// piping `graph --format ir` with `2>&1` had to skip lines to find the
|
|
577
|
-
// JSON. Core dedupes the note across stacks and prints it with the footer.
|
|
578
|
-
const notes = options.owned ? [OWNERSHIP_UNAVAILABLE_NOTE] : undefined;
|
|
577
|
+
// The note for the read paths that never reach the stack's tags at all: a
|
|
578
|
+
// missing stack, and a failed stack read. Core dedupes it across stacks and
|
|
579
|
+
// prints it once per run with the footer, rather than ahead of every answer
|
|
580
|
+
// (#1265).
|
|
581
|
+
const unresolvedNotes = options.owned ? [OWNERSHIP_UNRESOLVED_NOTE] : undefined;
|
|
579
582
|
|
|
580
583
|
// Derive stack name. A multi-stack project passes the explicit CloudFormation
|
|
581
584
|
// stack this observation targets (see `stacks` in ChantConfig); otherwise the
|
|
@@ -614,7 +617,7 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
|
|
|
614
617
|
{ ...resources, ...identity.resources, ...receiptObs.resources },
|
|
615
618
|
receiptHoles,
|
|
616
619
|
identity.queried,
|
|
617
|
-
|
|
620
|
+
unresolvedNotes,
|
|
618
621
|
);
|
|
619
622
|
}
|
|
620
623
|
// Any other failure (credentials, throttling, a region that can't be
|
|
@@ -636,23 +639,41 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
|
|
|
636
639
|
{ ...receiptObs.resources },
|
|
637
640
|
{ ...stackHoles, ...receiptObs.unobserved },
|
|
638
641
|
undefined,
|
|
639
|
-
|
|
642
|
+
unresolvedNotes,
|
|
640
643
|
);
|
|
641
644
|
}
|
|
642
645
|
|
|
643
646
|
// Map logical names from build to stack resources
|
|
644
647
|
const stackResourceMap = new Map(stackResources.map((r) => [r.logicalId, r]));
|
|
645
648
|
|
|
646
|
-
// Get stack outputs
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
+
// Get the stack's outputs and its own tags — one DescribeStacks call. A
|
|
650
|
+
// stack whose resources read fine but whose DescribeStacks does not is
|
|
651
|
+
// still a usable observation, so this failure is swallowed exactly as the
|
|
652
|
+
// non-zero exit code used to be. `undefined` tags mean the call did not
|
|
653
|
+
// answer, which is an unread channel rather than an absent marker.
|
|
649
654
|
let stackOutputs: Record<string, string> = {};
|
|
655
|
+
let stackTags: Record<string, string> | undefined;
|
|
650
656
|
try {
|
|
651
|
-
|
|
657
|
+
const detail = await describeStackDetail(stackName, client);
|
|
658
|
+
stackOutputs = detail.outputs;
|
|
659
|
+
stackTags = detail.tags;
|
|
652
660
|
} catch {
|
|
653
661
|
stackOutputs = {};
|
|
662
|
+
stackTags = undefined;
|
|
654
663
|
}
|
|
655
664
|
|
|
665
|
+
// The marker channel on this path (#1998). describe-stack-resources carries
|
|
666
|
+
// no per-resource tags, but the stack's own tags carry the identity the
|
|
667
|
+
// apply paths stamped from the template's `Metadata["chant:ownership"]`
|
|
668
|
+
// block — the same channel teardown verifies on (#1222). Every member of a
|
|
669
|
+
// marked stack belongs to that stack's identity, so the verdict and the
|
|
670
|
+
// marker are the stack's, read back verbatim.
|
|
671
|
+
const stackMarker = stackTags ? readOwnership(stackTags, AWS_TAG_OWNERSHIP_KEYS) : undefined;
|
|
672
|
+
const stackOwnership: ResourceMetadata["ownership"] =
|
|
673
|
+
stackTags === undefined ? "unknown" : stackMarker ? "owned" : "foreign";
|
|
674
|
+
// `owned: true` withheld nothing, so say why whenever the marker did not resolve.
|
|
675
|
+
const notes = options.owned && stackMarker === undefined ? unresolvedNotes : undefined;
|
|
676
|
+
|
|
656
677
|
// The outputs are the stack's, not any member's (#1279). They used to be
|
|
657
678
|
// copied onto every resource's `attributes`, so a VPC carried the stack's
|
|
658
679
|
// `expWebIp` and no `CidrBlock`. They ride the envelope once, keyed by the
|
|
@@ -672,14 +693,13 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
|
|
|
672
693
|
physicalId: stackResource.physicalId ?? "",
|
|
673
694
|
status: stackResource.status ?? "",
|
|
674
695
|
lastUpdated: stackResource.timestamp ?? "",
|
|
675
|
-
// Total verdict (#1089)
|
|
676
|
-
//
|
|
677
|
-
//
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
ownership: "unknown",
|
|
696
|
+
// Total verdict (#1089), from the stack's own tags. `unknown` is kept
|
|
697
|
+
// for the one case that earns it — DescribeStacks did not answer, so
|
|
698
|
+
// the channel was never read — and the change set never escalates
|
|
699
|
+
// `unknown` to a delete. `marker` is set only where the stack really
|
|
700
|
+
// carries one: absent means absent, never a guess.
|
|
701
|
+
ownership: stackOwnership,
|
|
702
|
+
...(stackMarker ? { marker: stackMarker } : {}),
|
|
683
703
|
};
|
|
684
704
|
}
|
|
685
705
|
|
package/src/spec/parse.test.ts
CHANGED
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
parseCFNSchema,
|
|
4
4
|
cfnShortName,
|
|
5
5
|
cfnServiceName,
|
|
6
|
+
stripPointerPath,
|
|
6
7
|
} from "./parse";
|
|
8
|
+
import type { CFNSchema } from "./fetch";
|
|
7
9
|
|
|
8
10
|
// Sample Registry JSON Schema for testing
|
|
9
11
|
const sampleBucketSchema = JSON.stringify({
|
|
@@ -401,6 +403,84 @@ describe("parseCFNSchema", () => {
|
|
|
401
403
|
});
|
|
402
404
|
});
|
|
403
405
|
|
|
406
|
+
describe("stripPointerPath (#1988)", () => {
|
|
407
|
+
const schema = {
|
|
408
|
+
typeName: "AWS::CloudFront::ContinuousDeploymentPolicy",
|
|
409
|
+
properties: {
|
|
410
|
+
ContinuousDeploymentPolicyConfig: { $ref: "#/definitions/ContinuousDeploymentPolicyConfig" },
|
|
411
|
+
Id: { type: "string" },
|
|
412
|
+
},
|
|
413
|
+
definitions: {
|
|
414
|
+
ContinuousDeploymentPolicyConfig: {
|
|
415
|
+
type: "object",
|
|
416
|
+
properties: {
|
|
417
|
+
Enabled: { type: "boolean" },
|
|
418
|
+
TrafficConfig: { $ref: "#/definitions/TrafficConfig" },
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
TrafficConfig: {
|
|
422
|
+
type: "object",
|
|
423
|
+
properties: { Type: { type: "string" } },
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
} as unknown as CFNSchema;
|
|
427
|
+
|
|
428
|
+
test("strips the top-level prefix", () => {
|
|
429
|
+
expect(stripPointerPath("/properties/BucketName")).toBe("BucketName");
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("keeps a nested path as a template writes it", () => {
|
|
433
|
+
expect(stripPointerPath("/properties/Tags/TagKey")).toBe("Tags/TagKey");
|
|
434
|
+
expect(stripPointerPath("/properties/Source/Decryption/Url")).toBe("Source/Decryption/Url");
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("drops a mid-path properties keyword", () => {
|
|
438
|
+
expect(stripPointerPath("/properties/DistributionConfig/properties/S3Origin")).toBe(
|
|
439
|
+
"DistributionConfig/S3Origin",
|
|
440
|
+
);
|
|
441
|
+
expect(stripPointerPath("/properties/EncryptionAtRestOptions/properties")).toBe(
|
|
442
|
+
"EncryptionAtRestOptions",
|
|
443
|
+
);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("keeps array wildcards", () => {
|
|
447
|
+
expect(stripPointerPath("/properties/SecurityGroupEgress/*/SourceSecurityGroupId")).toBe(
|
|
448
|
+
"SecurityGroupEgress/*/SourceSecurityGroupId",
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("resolves a definition-scoped pointer through the property that carries it", () => {
|
|
453
|
+
expect(stripPointerPath("/definitions/ContinuousDeploymentPolicyConfig/properties/Enabled", schema)).toBe(
|
|
454
|
+
"ContinuousDeploymentPolicyConfig/Enabled",
|
|
455
|
+
);
|
|
456
|
+
expect(stripPointerPath("/definitions/TrafficConfig/properties/Type", schema)).toBe(
|
|
457
|
+
"ContinuousDeploymentPolicyConfig/TrafficConfig/Type",
|
|
458
|
+
);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("leaves a definition nothing declared reaches as the Registry wrote it", () => {
|
|
462
|
+
expect(stripPointerPath("/definitions/Orphan/properties/Type", schema)).toBe(
|
|
463
|
+
"/definitions/Orphan/properties/Type",
|
|
464
|
+
);
|
|
465
|
+
expect(stripPointerPath("/definitions/Orphan/properties/Type")).toBe(
|
|
466
|
+
"/definitions/Orphan/properties/Type",
|
|
467
|
+
);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test("a definition reached through an array carries the wildcard", () => {
|
|
471
|
+
const arraySchema = {
|
|
472
|
+
typeName: "AWS::Test::Thing",
|
|
473
|
+
properties: { Rules: { type: "array", items: { $ref: "#/definitions/Rule" } } },
|
|
474
|
+
definitions: { Rule: { type: "object", properties: { Legacy: { type: "string" } } } },
|
|
475
|
+
} as unknown as CFNSchema;
|
|
476
|
+
expect(stripPointerPath("/definitions/Rule/properties/Legacy", arraySchema)).toBe("Rules/*/Legacy");
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("passes a non-pointer through", () => {
|
|
480
|
+
expect(stripPointerPath("BucketName")).toBe("BucketName");
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
404
484
|
describe("cfnShortName", () => {
|
|
405
485
|
test("extracts short name from full type", () => {
|
|
406
486
|
expect(cfnShortName("AWS::S3::Bucket")).toBe("Bucket");
|
package/src/spec/parse.ts
CHANGED
|
@@ -119,7 +119,7 @@ export function parseCFNSchema(data: string | Buffer): SchemaParseResult {
|
|
|
119
119
|
const attrs: ParsedAttribute[] = [];
|
|
120
120
|
const seenAttrs = new Set<string>();
|
|
121
121
|
for (const path of schema.readOnlyProperties ?? []) {
|
|
122
|
-
const attrName = stripPointerPath(path);
|
|
122
|
+
const attrName = stripPointerPath(path, schema);
|
|
123
123
|
|
|
124
124
|
// Flatten nested paths: "Endpoint/Address" → attr name "Endpoint.Address"
|
|
125
125
|
const cfnAttr = attrName.replace(/\//g, ".");
|
|
@@ -180,7 +180,7 @@ export function parseCFNSchema(data: string | Buffer): SchemaParseResult {
|
|
|
180
180
|
// can mention the deprecation of a sibling property, of an enum value, or of
|
|
181
181
|
// something the property merely configures.
|
|
182
182
|
const deprecatedSet = new Set<string>(
|
|
183
|
-
stripPointerPaths(schema.deprecatedProperties ?? []),
|
|
183
|
+
stripPointerPaths(schema.deprecatedProperties ?? [], schema),
|
|
184
184
|
);
|
|
185
185
|
const inferredDeprecations: string[] = [];
|
|
186
186
|
|
|
@@ -226,12 +226,12 @@ export function parseCFNSchema(data: string | Buffer): SchemaParseResult {
|
|
|
226
226
|
typeName: schema.typeName,
|
|
227
227
|
properties: props,
|
|
228
228
|
attributes: attrs,
|
|
229
|
-
createOnly: stripPointerPaths(schema.createOnlyProperties ?? []),
|
|
230
|
-
writeOnly: stripPointerPaths(schema.writeOnlyProperties ?? []),
|
|
231
|
-
primaryIdentifier: stripPointerPaths(schema.primaryIdentifier ?? []),
|
|
229
|
+
createOnly: stripPointerPaths(schema.createOnlyProperties ?? [], schema),
|
|
230
|
+
writeOnly: stripPointerPaths(schema.writeOnlyProperties ?? [], schema),
|
|
231
|
+
primaryIdentifier: stripPointerPaths(schema.primaryIdentifier ?? [], schema),
|
|
232
232
|
deprecatedProperties: [...deprecatedSet],
|
|
233
233
|
inferredDeprecations,
|
|
234
|
-
conditionalCreateOnly: stripPointerPaths(schema.conditionalCreateOnlyProperties ?? []),
|
|
234
|
+
conditionalCreateOnly: stripPointerPaths(schema.conditionalCreateOnlyProperties ?? [], schema),
|
|
235
235
|
...(replacementStrategy && { replacementStrategy }),
|
|
236
236
|
...(tagging && { tagging }),
|
|
237
237
|
},
|
|
@@ -277,15 +277,88 @@ export function cfnServiceName(typeName: string): string {
|
|
|
277
277
|
return parts.length >= 2 ? parts[1] : typeName;
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
/** Bound on the `$ref` walk that resolves a definition-scoped pointer. */
|
|
281
|
+
const MAX_DEFINITION_PATH_DEPTH = 6;
|
|
282
|
+
|
|
283
|
+
/** The definition a `#/definitions/Name` ref names, or undefined for anything else. */
|
|
284
|
+
function refDefinitionName(ref: string | undefined): string | undefined {
|
|
285
|
+
const prefix = "#/definitions/";
|
|
286
|
+
return ref?.startsWith(prefix) ? ref.slice(prefix.length) : undefined;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function pushProperties(
|
|
290
|
+
queue: Array<{ path: string[]; node: SchemaProperty }>,
|
|
291
|
+
path: string[],
|
|
292
|
+
properties: Record<string, SchemaProperty>,
|
|
293
|
+
): void {
|
|
294
|
+
for (const [name, prop] of Object.entries(properties)) queue.push({ path: [...path, name], node: prop });
|
|
295
|
+
}
|
|
296
|
+
|
|
280
297
|
/**
|
|
281
|
-
*
|
|
298
|
+
* The property path that reaches `defName`, searched breadth-first from the
|
|
299
|
+
* top-level properties so the shortest one wins. Undefined when nothing
|
|
300
|
+
* declared reaches the definition; the pointer is then left as the Registry
|
|
301
|
+
* wrote it rather than naming a key no template has.
|
|
282
302
|
*/
|
|
283
|
-
function
|
|
284
|
-
const
|
|
285
|
-
|
|
303
|
+
function definitionPropertyPath(defName: string, schema: CFNSchema): string | undefined {
|
|
304
|
+
const defs = schema.definitions ?? {};
|
|
305
|
+
const expanded = new Set<string>();
|
|
306
|
+
const queue: Array<{ path: string[]; node: SchemaProperty }> = [];
|
|
307
|
+
pushProperties(queue, [], schema.properties ?? {});
|
|
308
|
+
|
|
309
|
+
while (queue.length > 0) {
|
|
310
|
+
const { path, node } = queue.shift()!;
|
|
311
|
+
if (path.length > MAX_DEFINITION_PATH_DEPTH) continue;
|
|
312
|
+
|
|
313
|
+
const ref = refDefinitionName(node.$ref);
|
|
314
|
+
if (ref === defName) return path.join("/");
|
|
315
|
+
if (ref !== undefined) {
|
|
316
|
+
// Shortest path wins, so a definition already reached needs no second visit.
|
|
317
|
+
if (expanded.has(ref)) continue;
|
|
318
|
+
expanded.add(ref);
|
|
319
|
+
const def = defs[ref];
|
|
320
|
+
if (def?.properties) pushProperties(queue, path, def.properties);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (node.items) {
|
|
325
|
+
queue.push({ path: [...path, "*"], node: node.items });
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (node.properties) pushProperties(queue, path, node.properties);
|
|
329
|
+
}
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Flatten a CloudFormation Registry JSON pointer to the property path a
|
|
335
|
+
* template expresses, `/`-joined.
|
|
336
|
+
*
|
|
337
|
+
* Three shapes occur upstream. `/properties/BucketName` is the plain one.
|
|
338
|
+
* A pointer can re-enter the `properties` keyword mid-path
|
|
339
|
+
* (`/properties/DistributionConfig/properties/S3Origin`), which a template
|
|
340
|
+
* never writes. And a pointer can be scoped to a shared definition
|
|
341
|
+
* (`/definitions/ContinuousDeploymentPolicyConfig/properties/Type`), which a
|
|
342
|
+
* template expresses only through whichever property carries that definition.
|
|
343
|
+
*
|
|
344
|
+
* Array positions stay as `*`. `readOnlyProperties` attribute names are
|
|
345
|
+
* generated from this output, so dropping the wildcard would rename them.
|
|
346
|
+
*/
|
|
347
|
+
export function stripPointerPath(path: string, schema?: CFNSchema): string {
|
|
348
|
+
if (!path.startsWith("/")) return path;
|
|
349
|
+
const segments = path.split("/").filter((s) => s.length > 0);
|
|
350
|
+
|
|
351
|
+
if (segments[0] === "definitions") {
|
|
352
|
+
const prefix = segments[1] && schema ? definitionPropertyPath(segments[1], schema) : undefined;
|
|
353
|
+
if (prefix === undefined) return path;
|
|
354
|
+
return [prefix, ...segments.slice(2).filter((s) => s !== "properties")].join("/");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (segments[0] !== "properties") return path;
|
|
358
|
+
return segments.slice(1).filter((s) => s !== "properties").join("/");
|
|
286
359
|
}
|
|
287
360
|
|
|
288
|
-
function stripPointerPaths(paths: string[]): string[] {
|
|
361
|
+
function stripPointerPaths(paths: string[], schema?: CFNSchema): string[] {
|
|
289
362
|
if (paths.length === 0) return [];
|
|
290
|
-
return paths.map(stripPointerPath);
|
|
363
|
+
return paths.map((p) => stripPointerPath(p, schema));
|
|
291
364
|
}
|