@intentius/chant-lexicon-aws 0.124.43 → 0.124.44
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/codegen/generate-lexicon.d.ts +2 -6
- package/dist/codegen/generate-lexicon.d.ts.map +1 -1
- package/dist/deep-observe.d.ts +6 -2
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/deep-observe.js +17 -7
- package/dist/import/live-export.d.ts.map +1 -1
- package/dist/import/live-export.js +8 -4
- package/dist/integrity.json +5 -4
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-catalog.js +1 -0
- package/dist/lint/audit-lineage.d.ts.map +1 -1
- package/dist/lint/audit-lineage.js +4 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/index.js +2 -0
- package/dist/lint/post-synth/waw017.d.ts.map +1 -1
- package/dist/lint/post-synth/waw017.js +3 -2
- package/dist/lint/post-synth/waw071.d.ts +19 -0
- package/dist/lint/post-synth/waw071.d.ts.map +1 -0
- package/dist/lint/post-synth/waw071.js +76 -0
- package/dist/manifest.json +1 -1
- package/dist/meta.json +3618 -1206
- package/dist/okf/index.md +1 -0
- package/dist/okf/rules/WAW071.md +19 -0
- package/dist/okf/types/Key.md +1 -0
- package/dist/okf/types/Map.md +1 -0
- package/dist/okf/types/SsmParameter.md +4 -0
- package/dist/okf/types/Type.md +1 -0
- package/dist/okf/types/UserPool.md +1 -0
- package/dist/rules/waw017.ts +3 -2
- package/dist/rules/waw071.ts +85 -0
- package/dist/serializer.d.ts +12 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +44 -10
- package/dist/spec/parse.d.ts +27 -5
- package/dist/spec/parse.d.ts.map +1 -1
- package/dist/spec/parse.js +56 -0
- package/dist/taggable.d.ts +28 -0
- package/dist/taggable.d.ts.map +1 -1
- package/dist/taggable.js +60 -9
- package/package.json +2 -2
- package/src/codegen/generate-lexicon.ts +2 -2
- package/src/deep-observe.test.ts +19 -0
- package/src/deep-observe.ts +17 -6
- package/src/generated/lexicon-aws.json +3618 -1206
- package/src/import/live-export.test.ts +18 -0
- package/src/import/live-export.ts +8 -4
- package/src/lint/audit-catalog.ts +1 -0
- package/src/lint/audit-lineage.ts +4 -0
- package/src/lint/post-synth/index.ts +2 -0
- package/src/lint/post-synth/waw017.ts +3 -2
- package/src/lint/post-synth/waw071.test.ts +50 -0
- package/src/lint/post-synth/waw071.ts +85 -0
- package/src/serializer-tag-shapes.test.ts +83 -0
- package/src/serializer.ts +46 -9
- package/src/spec/parse.test.ts +39 -0
- package/src/spec/parse.ts +83 -1
- package/src/taggable.ts +78 -11
package/dist/okf/index.md
CHANGED
|
@@ -1937,3 +1937,4 @@ okf_version: '0.2'
|
|
|
1937
1937
|
* [WAW068](/rules/WAW068.md) - VPN Gateway or Transit Gateway has only one attached VPN Connection — no redundant path for hybrid connectivity
|
|
1938
1938
|
* [WAW069](/rules/WAW069.md) - Template references a condition it never declares — resource/output Condition keys, Fn::If, and Condition references must name an entry in the Conditions sec...
|
|
1939
1939
|
* [WAW070](/rules/WAW070.md) - Ref or Fn::GetAtt names something the template doesn't declare
|
|
1940
|
+
* [WAW071](/rules/WAW071.md) - Tags are in the wrong property or the wrong shape for the resource type
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: post-synth-check
|
|
3
|
+
title: WAW071
|
|
4
|
+
description: Tags are in the wrong property or the wrong shape for the resource type
|
|
5
|
+
id: WAW071
|
|
6
|
+
severity: error
|
|
7
|
+
category: post-synth
|
|
8
|
+
lexicon: aws
|
|
9
|
+
docs: https://intentius.io/chant/lexicons/aws/rules/
|
|
10
|
+
---
|
|
11
|
+
Tags are in the wrong property or the wrong shape for the resource type
|
|
12
|
+
|
|
13
|
+
## Applies to
|
|
14
|
+
|
|
15
|
+
- [Key](/types/Key.md)
|
|
16
|
+
- [Map](/types/Map.md)
|
|
17
|
+
- [SsmParameter](/types/SsmParameter.md)
|
|
18
|
+
- [Type](/types/Type.md)
|
|
19
|
+
- [UserPool](/types/UserPool.md)
|
package/dist/okf/types/Key.md
CHANGED
|
@@ -34,3 +34,4 @@ resource_type: AWS::PaymentCryptography::Key
|
|
|
34
34
|
|
|
35
35
|
- [WAW024](/rules/WAW024.md): Application Load Balancer does not have access logging enabled
|
|
36
36
|
- [WAW043](/rules/WAW043.md): KMS customer-managed key does not have automatic key rotation enabled
|
|
37
|
+
- [WAW071](/rules/WAW071.md): Tags are in the wrong property or the wrong shape for the resource type
|
package/dist/okf/types/Map.md
CHANGED
|
@@ -40,3 +40,4 @@ resource_type: AWS::Location::Map
|
|
|
40
40
|
- [WAW066](/rules/WAW066.md): Private subnet's route table has no default route, or its default route targets a gateway that does not exist in the template
|
|
41
41
|
- [WAW067](/rules/WAW067.md): A single-AZ NAT gateway serves private subnets across multiple Availability Zones — single point of failure for cross-AZ egress
|
|
42
42
|
- [WAW068](/rules/WAW068.md): VPN Gateway or Transit Gateway has only one attached VPN Connection — no redundant path for hybrid connectivity
|
|
43
|
+
- [WAW071](/rules/WAW071.md): Tags are in the wrong property or the wrong shape for the resource type
|
package/dist/okf/types/Type.md
CHANGED
|
@@ -83,3 +83,4 @@ resource_type: AWS::Cassandra::Type
|
|
|
83
83
|
- [WAW066](/rules/WAW066.md): Private subnet's route table has no default route, or its default route targets a gateway that does not exist in the template
|
|
84
84
|
- [WAW067](/rules/WAW067.md): A single-AZ NAT gateway serves private subnets across multiple Availability Zones — single point of failure for cross-AZ egress
|
|
85
85
|
- [WAW068](/rules/WAW068.md): VPN Gateway or Transit Gateway has only one attached VPN Connection — no redundant path for hybrid connectivity
|
|
86
|
+
- [WAW071](/rules/WAW071.md): Tags are in the wrong property or the wrong shape for the resource type
|
|
@@ -59,3 +59,4 @@ resource_type: AWS::Cognito::UserPool
|
|
|
59
59
|
- [WAW050](/rules/WAW050.md): Cognito UserPool does not have advanced security (risk-based auth) enabled
|
|
60
60
|
- [WAW051](/rules/WAW051.md): Cognito UserPoolClient allows the deprecated implicit OAuth grant
|
|
61
61
|
- [WAW052](/rules/WAW052.md): Cognito UserPool does not require MFA (full tier)
|
|
62
|
+
- [WAW071](/rules/WAW071.md): Tags are in the wrong property or the wrong shape for the resource type
|
package/dist/rules/waw017.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
9
9
|
import { parseCFTemplate } from "./cf-refs";
|
|
10
|
-
import { loadTaggableResources } from "../../taggable";
|
|
10
|
+
import { loadTaggableResources, tagPlacementFor } from "../../taggable";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Core detection logic — exported for direct testing with synthetic data.
|
|
@@ -28,7 +28,8 @@ export function checkMissingTags(
|
|
|
28
28
|
if (!taggable.has(resource.Type)) continue;
|
|
29
29
|
|
|
30
30
|
const props = resource.Properties ?? {};
|
|
31
|
-
|
|
31
|
+
const tagProperty = tagPlacementFor(resource.Type)?.property ?? "Tags";
|
|
32
|
+
if (!(tagProperty in props)) {
|
|
32
33
|
diagnostics.push({
|
|
33
34
|
checkId: "WAW017",
|
|
34
35
|
severity: "warning",
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW071: Tags are in the wrong property or the wrong shape for the type
|
|
3
|
+
*
|
|
4
|
+
* Each taggable type names its tag property and shape in its schema
|
|
5
|
+
* (`tagging.tagProperty`): a `[{Key, Value}]` list for most types, a map for
|
|
6
|
+
* types such as `AWS::SSM::Parameter`, and a property other than `Tags` for
|
|
7
|
+
* types such as `AWS::Cognito::UserPool` (`UserPoolTags`). CloudFormation
|
|
8
|
+
* rejects a template that puts tags anywhere else. chant stamped the ownership
|
|
9
|
+
* marker as a `Tags` list on every type until #4350; this check catches the
|
|
10
|
+
* next such template.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
14
|
+
import { parseCFTemplate } from "./cf-refs";
|
|
15
|
+
import { isTagIntrinsic, loadTagPlacements, type TagPlacement } from "../../taggable";
|
|
16
|
+
|
|
17
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
18
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Why `value` doesn't fit `placement`, or undefined when it does. */
|
|
22
|
+
function shapeProblem(value: unknown, placement: TagPlacement): string | undefined {
|
|
23
|
+
if (isTagIntrinsic(value)) return undefined;
|
|
24
|
+
if (placement.shape === "map") {
|
|
25
|
+
return isPlainObject(value) ? undefined : "a list, but the type takes a key-to-value map";
|
|
26
|
+
}
|
|
27
|
+
if (!Array.isArray(value)) return "a map, but the type takes a [{Key, Value}] list";
|
|
28
|
+
const bad = value.some((t) => !isTagIntrinsic(t) && !(isPlainObject(t) && "Key" in t && "Value" in t));
|
|
29
|
+
return bad ? "a list with an entry that has no Key and Value" : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Core detection logic — exported for direct testing with synthetic data.
|
|
34
|
+
*/
|
|
35
|
+
export function checkTagShapes(
|
|
36
|
+
ctx: PostSynthContext,
|
|
37
|
+
placements: Map<string, TagPlacement | null>,
|
|
38
|
+
): PostSynthDiagnostic[] {
|
|
39
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
40
|
+
|
|
41
|
+
for (const [lexicon, output] of ctx.outputs) {
|
|
42
|
+
if (lexicon !== "aws") continue;
|
|
43
|
+
const template = parseCFTemplate(output);
|
|
44
|
+
if (!template?.Resources) continue;
|
|
45
|
+
|
|
46
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
47
|
+
const placement = placements.get(resource.Type);
|
|
48
|
+
if (!placement) continue;
|
|
49
|
+
const props = resource.Properties ?? {};
|
|
50
|
+
|
|
51
|
+
if (placement.property !== "Tags" && "Tags" in props) {
|
|
52
|
+
diagnostics.push({
|
|
53
|
+
checkId: "WAW071",
|
|
54
|
+
severity: "error",
|
|
55
|
+
message: `Resource "${logicalId}" (${resource.Type}) has Tags, but the type takes its tags in ${placement.property}`,
|
|
56
|
+
entity: logicalId,
|
|
57
|
+
lexicon: "aws",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!(placement.property in props)) continue;
|
|
62
|
+
const problem = shapeProblem(props[placement.property], placement);
|
|
63
|
+
if (problem) {
|
|
64
|
+
diagnostics.push({
|
|
65
|
+
checkId: "WAW071",
|
|
66
|
+
severity: "error",
|
|
67
|
+
message: `Resource "${logicalId}" (${resource.Type}) has ${placement.property} as ${problem}`,
|
|
68
|
+
entity: logicalId,
|
|
69
|
+
lexicon: "aws",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return diagnostics;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const waw071: PostSynthCheck = {
|
|
79
|
+
id: "WAW071",
|
|
80
|
+
description: "Tags are in the wrong property or the wrong shape for the resource type",
|
|
81
|
+
|
|
82
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
83
|
+
return checkTagShapes(ctx, loadTagPlacements());
|
|
84
|
+
},
|
|
85
|
+
};
|
package/dist/serializer.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import type { Serializer } from "@intentius/chant/serializer";
|
|
2
|
+
import { type TagPlacement } from "./taggable.js";
|
|
3
|
+
/**
|
|
4
|
+
* Merge stamped default tags under a resource's explicit tags, in the type's
|
|
5
|
+
* tag shape (#4350). Explicit tags win on a key clash. Explicit tags of the
|
|
6
|
+
* other shape are converted, so a map written on a list type still merges.
|
|
7
|
+
* Returns undefined when the explicit value is an intrinsic, which can't be
|
|
8
|
+
* merged into.
|
|
9
|
+
*/
|
|
10
|
+
export declare function mergeTags(explicit: unknown, stamped: Array<{
|
|
11
|
+
Key: string;
|
|
12
|
+
Value: unknown;
|
|
13
|
+
}>, placement: TagPlacement): unknown;
|
|
2
14
|
/**
|
|
3
15
|
* AWS CloudFormation serializer implementation
|
|
4
16
|
*/
|
package/dist/serializer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAsC,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAsC,MAAM,6BAA6B,CAAC;AA0BlG,OAAO,EAAmD,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AA0QhG;;;;;;GAMG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,OAAO,EACjB,OAAO,EAAE,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,EAC/C,SAAS,EAAE,YAAY,GACtB,OAAO,CAcT;AAkWD;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,UAgF3B,CAAC"}
|
package/dist/serializer.js
CHANGED
|
@@ -14,7 +14,7 @@ import { isMapping } from "./mapping.js";
|
|
|
14
14
|
import { isDefaultTags } from "./default-tags.js";
|
|
15
15
|
import { isTemplateTransform } from "./template-transform.js";
|
|
16
16
|
import { isTemplateSection } from "./template-sections.js";
|
|
17
|
-
import {
|
|
17
|
+
import { isTagIntrinsic, loadTagPlacements, readTagValue } from "./taggable.js";
|
|
18
18
|
/**
|
|
19
19
|
* Check if a declarable is a CoreParameter
|
|
20
20
|
*/
|
|
@@ -172,6 +172,29 @@ function renderReceiptRows(receipts, ownership) {
|
|
|
172
172
|
}
|
|
173
173
|
return rows;
|
|
174
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Merge stamped default tags under a resource's explicit tags, in the type's
|
|
177
|
+
* tag shape (#4350). Explicit tags win on a key clash. Explicit tags of the
|
|
178
|
+
* other shape are converted, so a map written on a list type still merges.
|
|
179
|
+
* Returns undefined when the explicit value is an intrinsic, which can't be
|
|
180
|
+
* merged into.
|
|
181
|
+
*/
|
|
182
|
+
export function mergeTags(explicit, stamped, placement) {
|
|
183
|
+
if (explicit !== undefined && (isTagIntrinsic(explicit) || (typeof explicit !== "object" || explicit === null))) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
if (placement.shape === "map") {
|
|
187
|
+
const merged = {};
|
|
188
|
+
for (const t of stamped)
|
|
189
|
+
merged[t.Key] = t.Value;
|
|
190
|
+
return { ...merged, ...readTagValue(explicit) };
|
|
191
|
+
}
|
|
192
|
+
const explicitList = Array.isArray(explicit)
|
|
193
|
+
? explicit
|
|
194
|
+
: Object.entries(readTagValue(explicit)).map(([Key, Value]) => ({ Key, Value }));
|
|
195
|
+
const explicitKeys = new Set(explicitList.map(t => t?.Key));
|
|
196
|
+
return [...stamped.filter(t => !explicitKeys.has(t.Key)), ...explicitList];
|
|
197
|
+
}
|
|
175
198
|
/**
|
|
176
199
|
* Serialize a set of entities into a CFTemplate object (without JSON.stringify).
|
|
177
200
|
*/
|
|
@@ -371,22 +394,33 @@ function serializeToTemplate(entities, outputs, extraParameters, extraOutputs, o
|
|
|
371
394
|
template.Resources[name] = resource;
|
|
372
395
|
}
|
|
373
396
|
}
|
|
374
|
-
// Inject default tags into taggable resources
|
|
397
|
+
// Inject default tags into taggable resources, each through its own tag
|
|
398
|
+
// property and shape (#4350): `UserPoolTags` on a Cognito user pool, a map on
|
|
399
|
+
// an SSM parameter, a `[{Key, Value}]` list elsewhere.
|
|
375
400
|
if (defaultTagEntries.length > 0) {
|
|
376
|
-
const
|
|
401
|
+
const placements = loadTagPlacements();
|
|
377
402
|
for (const [, resource] of Object.entries(template.Resources)) {
|
|
378
|
-
|
|
403
|
+
const placement = placements.get(resource.Type);
|
|
404
|
+
if (placement === undefined)
|
|
379
405
|
continue;
|
|
380
406
|
const resolved = defaultTagEntries.map(t => ({
|
|
381
407
|
Key: t.Key,
|
|
382
408
|
Value: toCFValue(t.Value, entityNames),
|
|
383
409
|
}));
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
resource.Properties =
|
|
389
|
-
|
|
410
|
+
const stamped = placement ? mergeTags(resource.Properties?.[placement.property], resolved, placement) : undefined;
|
|
411
|
+
if (stamped !== undefined) {
|
|
412
|
+
if (!resource.Properties)
|
|
413
|
+
resource.Properties = {};
|
|
414
|
+
resource.Properties[placement.property] = stamped;
|
|
415
|
+
}
|
|
416
|
+
else if (ownership) {
|
|
417
|
+
// Nowhere valid to write the tags, so the marker goes on the resource's
|
|
418
|
+
// own Metadata instead of an invalid property.
|
|
419
|
+
resource.Metadata = {
|
|
420
|
+
...(resource.Metadata ?? {}),
|
|
421
|
+
[OWNERSHIP_METADATA_KEY]: ownershipEntries(AWS_TAG_OWNERSHIP_KEYS, ownership),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
390
424
|
}
|
|
391
425
|
}
|
|
392
426
|
// Emit StackOutput entities as CF Outputs
|
package/dist/spec/parse.d.ts
CHANGED
|
@@ -46,11 +46,22 @@ export interface ParsedResource {
|
|
|
46
46
|
inferredDeprecations: string[];
|
|
47
47
|
conditionalCreateOnly: string[];
|
|
48
48
|
replacementStrategy?: "delete_then_create" | "create_then_delete";
|
|
49
|
-
tagging?:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
tagging?: ResourceTagging;
|
|
50
|
+
}
|
|
51
|
+
/** How a type's tags are written: a `[{Key, Value}]` list or a key-to-value map. */
|
|
52
|
+
export type TagShape = "list" | "map";
|
|
53
|
+
export interface ResourceTagging {
|
|
54
|
+
taggable: boolean;
|
|
55
|
+
tagOnCreate: boolean;
|
|
56
|
+
tagUpdatable: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* The top-level property the type's tags live in (`Tags`, `UserPoolTags`, ...)
|
|
59
|
+
* and its shape, read from the schema's `tagging.tagProperty` (#4350). Both are
|
|
60
|
+
* absent when the tags sit somewhere the serializer can't stamp, such as
|
|
61
|
+
* a path inside an array (`TagSpecifications[].Tags`) or an `{Items: [...]}` wrapper.
|
|
62
|
+
*/
|
|
63
|
+
tagProperty?: string;
|
|
64
|
+
tagShape?: TagShape;
|
|
54
65
|
}
|
|
55
66
|
export interface SchemaParseResult {
|
|
56
67
|
resource: ParsedResource;
|
|
@@ -61,6 +72,17 @@ export interface SchemaParseResult {
|
|
|
61
72
|
* Parse a CloudFormation Registry JSON Schema into typed structures.
|
|
62
73
|
*/
|
|
63
74
|
export declare function parseCFNSchema(data: string | Buffer): SchemaParseResult;
|
|
75
|
+
/**
|
|
76
|
+
* Where a taggable type's tags go and in which shape (#4350). The property is
|
|
77
|
+
* `tagging.tagProperty` when it names one top-level property, else `Tags` when
|
|
78
|
+
* the type has one. The shape is a list when the items carry `Key` and `Value`
|
|
79
|
+
* (directly or as one `oneOf` branch) and a map when the property is an object
|
|
80
|
+
* with no fixed properties. Anything else returns `{}`, so the type is skipped.
|
|
81
|
+
*/
|
|
82
|
+
export declare function tagPlacement(schema: CFNSchema): {
|
|
83
|
+
tagProperty?: string;
|
|
84
|
+
tagShape?: TagShape;
|
|
85
|
+
};
|
|
64
86
|
export declare const constraintsIsEmpty: typeof coreConstraintsIsEmpty;
|
|
65
87
|
/**
|
|
66
88
|
* Extract short resource name: "AWS::S3::Bucket" → "Bucket"
|
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;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;
|
|
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,eAAe,CAAC;CAC3B;AAED,oFAAoF;AACpF,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,KAAK,CAAC;AAEtC,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,EAAE,OAAO,CAAC;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;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,CAoJvE;AAID;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,QAAQ,CAAA;CAAE,CAe7F;AAuDD,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/dist/spec/parse.js
CHANGED
|
@@ -122,6 +122,7 @@ export function parseCFNSchema(data) {
|
|
|
122
122
|
taggable: true,
|
|
123
123
|
tagOnCreate: schema.tagging.tagOnCreate ?? false,
|
|
124
124
|
tagUpdatable: schema.tagging.tagUpdatable ?? false,
|
|
125
|
+
...tagPlacement(schema),
|
|
125
126
|
};
|
|
126
127
|
}
|
|
127
128
|
// --- Replacement strategy ---
|
|
@@ -151,6 +152,61 @@ export function parseCFNSchema(data) {
|
|
|
151
152
|
};
|
|
152
153
|
}
|
|
153
154
|
// --- Type resolution (delegated to core) ---
|
|
155
|
+
/**
|
|
156
|
+
* Where a taggable type's tags go and in which shape (#4350). The property is
|
|
157
|
+
* `tagging.tagProperty` when it names one top-level property, else `Tags` when
|
|
158
|
+
* the type has one. The shape is a list when the items carry `Key` and `Value`
|
|
159
|
+
* (directly or as one `oneOf` branch) and a map when the property is an object
|
|
160
|
+
* with no fixed properties. Anything else returns `{}`, so the type is skipped.
|
|
161
|
+
*/
|
|
162
|
+
export function tagPlacement(schema) {
|
|
163
|
+
const pointer = schema.tagging?.tagProperty;
|
|
164
|
+
let name;
|
|
165
|
+
if (pointer !== undefined) {
|
|
166
|
+
const m = /^\/properties\/([^/]+)$/.exec(pointer);
|
|
167
|
+
if (!m)
|
|
168
|
+
return {};
|
|
169
|
+
name = m[1];
|
|
170
|
+
}
|
|
171
|
+
else if (schema.properties?.Tags) {
|
|
172
|
+
name = "Tags";
|
|
173
|
+
}
|
|
174
|
+
if (name === undefined)
|
|
175
|
+
return {};
|
|
176
|
+
const prop = resolveLocalRef(schema.properties?.[name], schema);
|
|
177
|
+
if (!prop)
|
|
178
|
+
return {};
|
|
179
|
+
const shape = tagShapeOf(prop, schema);
|
|
180
|
+
return shape ? { tagProperty: name, tagShape: shape } : {};
|
|
181
|
+
}
|
|
182
|
+
function resolveLocalRef(value, schema) {
|
|
183
|
+
let node = value;
|
|
184
|
+
for (let i = 0; i < 8 && node && typeof node.$ref === "string"; i++) {
|
|
185
|
+
const def = refDefinitionName(node.$ref);
|
|
186
|
+
node = def ? schema.definitions?.[def] : undefined;
|
|
187
|
+
}
|
|
188
|
+
return node && typeof node === "object" ? node : undefined;
|
|
189
|
+
}
|
|
190
|
+
function hasType(node, t) {
|
|
191
|
+
return node.type === t || (Array.isArray(node.type) && node.type.includes(t));
|
|
192
|
+
}
|
|
193
|
+
function isKeyValueItem(node, schema) {
|
|
194
|
+
if (!node)
|
|
195
|
+
return false;
|
|
196
|
+
if (node.properties && "Key" in node.properties && "Value" in node.properties)
|
|
197
|
+
return true;
|
|
198
|
+
const branches = [...(node.oneOf ?? []), ...(node.anyOf ?? [])];
|
|
199
|
+
return branches.some((b) => isKeyValueItem(resolveLocalRef(b, schema), schema));
|
|
200
|
+
}
|
|
201
|
+
function tagShapeOf(prop, schema) {
|
|
202
|
+
if (hasType(prop, "array")) {
|
|
203
|
+
return isKeyValueItem(resolveLocalRef(prop.items, schema), schema) ? "list" : undefined;
|
|
204
|
+
}
|
|
205
|
+
const objectish = hasType(prop, "object") || prop.patternProperties !== undefined || prop.additionalProperties !== undefined;
|
|
206
|
+
if (objectish && (!prop.properties || Object.keys(prop.properties).length === 0))
|
|
207
|
+
return "map";
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
154
210
|
function resolvePropertyType(prop, schema) {
|
|
155
211
|
const shortName = cfnShortName(schema.typeName);
|
|
156
212
|
return coreResolvePropertyType(prop, schema, (defName) => `${shortName}_${defName}`);
|
package/dist/taggable.d.ts
CHANGED
|
@@ -3,9 +3,37 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Lazy-loaded and cached for the lifetime of the process.
|
|
5
5
|
*/
|
|
6
|
+
/** How a type's tags are written: a `[{Key, Value}]` list or a key-to-value map. */
|
|
7
|
+
export type TagShape = "list" | "map";
|
|
8
|
+
/** The property a type's tags live in, and its shape (#4350). */
|
|
9
|
+
export interface TagPlacement {
|
|
10
|
+
property: string;
|
|
11
|
+
shape: TagShape;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Every taggable type, mapped to where its tags go. `null` means the schema
|
|
15
|
+
* puts the tags somewhere chant can't stamp (inside an array, or under a
|
|
16
|
+
* wrapper object), so the type gets no stamped tags.
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadTagPlacements(): Map<string, TagPlacement | null>;
|
|
6
19
|
/**
|
|
7
20
|
* Load taggable resource types from the lexicon JSON.
|
|
8
21
|
* Result is cached after first call.
|
|
9
22
|
*/
|
|
10
23
|
export declare function loadTaggableResources(): Set<string>;
|
|
24
|
+
/** Where `type`'s tags go, or undefined when it isn't taggable or can't be placed. */
|
|
25
|
+
export declare function tagPlacementFor(type: string): TagPlacement | undefined;
|
|
26
|
+
/** True for a CloudFormation intrinsic (`Ref`, `Fn::*`), which can't be merged into. */
|
|
27
|
+
export declare function isTagIntrinsic(value: unknown): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Read a tag value of either shape into a key-to-value map: a `[{Key, Value}]`
|
|
30
|
+
* list or a plain map. An intrinsic or anything else reads as `{}`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function readTagValue(value: unknown): Record<string, unknown>;
|
|
33
|
+
/**
|
|
34
|
+
* The tags on a resource's property tree, read through the type's own tag
|
|
35
|
+
* property (`UserPoolTags` on a Cognito user pool), falling back to `Tags`
|
|
36
|
+
* when the type is unknown. Either shape reads (#4350).
|
|
37
|
+
*/
|
|
38
|
+
export declare function readResourceTags(type: string | undefined, properties: Record<string, unknown> | undefined): Record<string, unknown>;
|
|
11
39
|
//# sourceMappingURL=taggable.d.ts.map
|
package/dist/taggable.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"taggable.d.ts","sourceRoot":"","sources":["../src/taggable.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"taggable.d.ts","sourceRoot":"","sources":["../src/taggable.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,oFAAoF;AACpF,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,KAAK,CAAC;AAEtC,iEAAiE;AACjE,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,QAAQ,CAAC;CACjB;AAiBD;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,GAAG,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC,CAoBpE;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,GAAG,CAAC,MAAM,CAAC,CAEnD;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAEtE;AAMD,wFAAwF;AACxF,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAItD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUpE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAInI"}
|
package/dist/taggable.js
CHANGED
|
@@ -8,26 +8,77 @@ import { join } from "path";
|
|
|
8
8
|
import { moduleSourceDir } from "@intentius/chant/module-source";
|
|
9
9
|
let _cached;
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Every taggable type, mapped to where its tags go. `null` means the schema
|
|
12
|
+
* puts the tags somewhere chant can't stamp (inside an array, or under a
|
|
13
|
+
* wrapper object), so the type gets no stamped tags.
|
|
13
14
|
*/
|
|
14
|
-
export function
|
|
15
|
+
export function loadTagPlacements() {
|
|
15
16
|
if (_cached)
|
|
16
17
|
return _cached;
|
|
17
|
-
const
|
|
18
|
+
const map = new Map();
|
|
18
19
|
try {
|
|
19
20
|
const lexiconPath = join(moduleSourceDir(import.meta.url), "generated", "lexicon-aws.json");
|
|
20
21
|
const content = readFileSync(lexiconPath, "utf-8");
|
|
21
22
|
const data = JSON.parse(content);
|
|
22
23
|
for (const [_name, entry] of Object.entries(data)) {
|
|
23
|
-
if (entry.kind
|
|
24
|
-
|
|
25
|
-
}
|
|
24
|
+
if (entry.kind !== "resource" || !entry.resourceType || !entry.tagging?.taggable)
|
|
25
|
+
continue;
|
|
26
|
+
const { tagProperty, tagShape } = entry.tagging;
|
|
27
|
+
map.set(entry.resourceType, tagProperty && tagShape ? { property: tagProperty, shape: tagShape } : null);
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
catch {
|
|
29
31
|
// Lexicon not available — skip
|
|
30
32
|
}
|
|
31
|
-
_cached =
|
|
32
|
-
return
|
|
33
|
+
_cached = map;
|
|
34
|
+
return map;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Load taggable resource types from the lexicon JSON.
|
|
38
|
+
* Result is cached after first call.
|
|
39
|
+
*/
|
|
40
|
+
export function loadTaggableResources() {
|
|
41
|
+
return new Set(loadTagPlacements().keys());
|
|
42
|
+
}
|
|
43
|
+
/** Where `type`'s tags go, or undefined when it isn't taggable or can't be placed. */
|
|
44
|
+
export function tagPlacementFor(type) {
|
|
45
|
+
return loadTagPlacements().get(type) ?? undefined;
|
|
46
|
+
}
|
|
47
|
+
function isPlainObject(value) {
|
|
48
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49
|
+
}
|
|
50
|
+
/** True for a CloudFormation intrinsic (`Ref`, `Fn::*`), which can't be merged into. */
|
|
51
|
+
export function isTagIntrinsic(value) {
|
|
52
|
+
if (!isPlainObject(value))
|
|
53
|
+
return false;
|
|
54
|
+
const keys = Object.keys(value);
|
|
55
|
+
return keys.length === 1 && (keys[0] === "Ref" || keys[0].startsWith("Fn::"));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Read a tag value of either shape into a key-to-value map: a `[{Key, Value}]`
|
|
59
|
+
* list or a plain map. An intrinsic or anything else reads as `{}`.
|
|
60
|
+
*/
|
|
61
|
+
export function readTagValue(value) {
|
|
62
|
+
const out = {};
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
for (const t of value) {
|
|
65
|
+
if (isPlainObject(t) && typeof t.Key === "string")
|
|
66
|
+
out[t.Key] = t.Value;
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
if (isPlainObject(value) && !isTagIntrinsic(value))
|
|
71
|
+
return { ...value };
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The tags on a resource's property tree, read through the type's own tag
|
|
76
|
+
* property (`UserPoolTags` on a Cognito user pool), falling back to `Tags`
|
|
77
|
+
* when the type is unknown. Either shape reads (#4350).
|
|
78
|
+
*/
|
|
79
|
+
export function readResourceTags(type, properties) {
|
|
80
|
+
if (!properties)
|
|
81
|
+
return {};
|
|
82
|
+
const placement = type ? tagPlacementFor(type) : undefined;
|
|
83
|
+
return readTagValue(properties[placement?.property ?? "Tags"]);
|
|
33
84
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-aws",
|
|
3
|
-
"version": "0.124.
|
|
3
|
+
"version": "0.124.44",
|
|
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,6 +80,6 @@
|
|
|
80
80
|
"typescript": "^5.9.3"
|
|
81
81
|
},
|
|
82
82
|
"peerDependencies": {
|
|
83
|
-
"@intentius/chant": "^0.124.
|
|
83
|
+
"@intentius/chant": "^0.124.44"
|
|
84
84
|
}
|
|
85
85
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Output is a Record<string, LexiconEntry> keyed by generated class name.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { SchemaParseResult } from "../spec/parse";
|
|
7
|
+
import type { ResourceTagging, SchemaParseResult } from "../spec/parse";
|
|
8
8
|
import { cfnShortName } from "../spec/parse";
|
|
9
9
|
import type { NamingStrategy } from "./naming";
|
|
10
10
|
import type { ExtensionConstraint } from "./extensions";
|
|
@@ -33,7 +33,7 @@ export interface LexiconEntry {
|
|
|
33
33
|
inferredDeprecations?: string[];
|
|
34
34
|
conditionalCreateOnly?: string[];
|
|
35
35
|
replacementStrategy?: "delete_then_create" | "create_then_delete";
|
|
36
|
-
tagging?:
|
|
36
|
+
tagging?: ResourceTagging;
|
|
37
37
|
runtimeDeprecations?: Record<string, string>;
|
|
38
38
|
}
|
|
39
39
|
|
package/src/deep-observe.test.ts
CHANGED
|
@@ -353,6 +353,25 @@ describe("hasOwnershipMarker", () => {
|
|
|
353
353
|
expect(hasOwnershipMarker({ Tags: [{ Key: "env", Value: "prod" }] })).toBe(false);
|
|
354
354
|
expect(hasOwnershipMarker({})).toBe(false);
|
|
355
355
|
});
|
|
356
|
+
|
|
357
|
+
test("reads a map, and the type's own tag property (#4350)", () => {
|
|
358
|
+
expect(hasOwnershipMarker({ Tags: { "chant:managed-by": "chant" } }, "AWS::SSM::Parameter")).toBe(true);
|
|
359
|
+
expect(hasOwnershipMarker({ UserPoolTags: { "chant:managed-by": "chant" } }, "AWS::Cognito::UserPool")).toBe(true);
|
|
360
|
+
expect(hasOwnershipMarker({ Tags: { "chant:managed-by": "chant" } }, "AWS::Cognito::UserPool")).toBe(false);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("prunes the live map entry of the marker on a map-tagged type (#4350)", () => {
|
|
364
|
+
const out = normalizeDeepProperties(
|
|
365
|
+
{ UserPoolTags: { "chant:managed-by": "chant", team: "a" } },
|
|
366
|
+
{
|
|
367
|
+
entityType: "AWS::Cognito::UserPool",
|
|
368
|
+
side: "live",
|
|
369
|
+
hooks: awsDeepNormalizationHooks,
|
|
370
|
+
counterpartPaths: new Set<string>(),
|
|
371
|
+
},
|
|
372
|
+
);
|
|
373
|
+
expect(out).toEqual({ UserPoolTags: { team: "a" } });
|
|
374
|
+
});
|
|
356
375
|
});
|
|
357
376
|
|
|
358
377
|
describe("observeResourcesDeepAws", () => {
|
package/src/deep-observe.ts
CHANGED
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
} from "./api/read-client";
|
|
64
64
|
import { declaredIdentifier } from "./identity-observe";
|
|
65
65
|
import { AWS_TAG_OWNERSHIP_KEYS } from "./ownership";
|
|
66
|
+
import { readResourceTags, tagPlacementFor } from "./taggable";
|
|
66
67
|
import {
|
|
67
68
|
AWS_DEEP_BLIND_SPOTS,
|
|
68
69
|
EC2_TOPOLOGY_GENERATED_NAMES,
|
|
@@ -409,6 +410,8 @@ export const awsDeepNormalizationHooks: DeepNormalizationHooks = {
|
|
|
409
410
|
// drift. Counterpart-gated, so a template that declares the tag itself is
|
|
410
411
|
// still compared against what is live.
|
|
411
412
|
if (isOwnershipTag(node.value)) return true;
|
|
413
|
+
// The same marker as a map entry, on a map-tagged type (#4350).
|
|
414
|
+
if (OWNERSHIP_TAG_KEYS.has(node.key) && isMapTagEntry(node.entityType, node.pattern, node.key)) return true;
|
|
412
415
|
|
|
413
416
|
// A name the service generated because source did not supply one.
|
|
414
417
|
if (AWS_GENERATED_NAMES[node.entityType]?.has(node.pattern)) return true;
|
|
@@ -533,11 +536,19 @@ function isOwnershipTag(value: unknown): boolean {
|
|
|
533
536
|
return isRecord(value) && typeof value.Key === "string" && OWNERSHIP_TAG_KEYS.has(value.Key);
|
|
534
537
|
}
|
|
535
538
|
|
|
536
|
-
/** True when
|
|
537
|
-
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
539
|
+
/** True when `pattern` is `<tagProperty>.<key>` on a type whose tags are a map. */
|
|
540
|
+
function isMapTagEntry(type: string, pattern: string, key: string): boolean {
|
|
541
|
+
const placement = tagPlacementFor(type);
|
|
542
|
+
return placement?.shape === "map" && pattern === `${placement.property}.${key}`;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* True when the live property tree carries chant's ownership marker tag. With
|
|
547
|
+
* a `type`, the tags are read from that type's own tag property
|
|
548
|
+
* (`UserPoolTags`); either way a list or a map reads (#4350).
|
|
549
|
+
*/
|
|
550
|
+
export function hasOwnershipMarker(properties: Record<string, unknown>, type?: string): boolean {
|
|
551
|
+
return readResourceTags(type, properties)[AWS_TAG_OWNERSHIP_KEYS.managedBy] !== undefined;
|
|
541
552
|
}
|
|
542
553
|
|
|
543
554
|
export interface AwsDeepObserveOptions {
|
|
@@ -680,7 +691,7 @@ export async function observeResourcesDeepAws(
|
|
|
680
691
|
// thin path this one can answer the ownership question (#1015's open note).
|
|
681
692
|
// A resource withheld by the filter is `filtered`, never absent: it exists,
|
|
682
693
|
// it just isn't chant's.
|
|
683
|
-
const owned = hasOwnershipMarker(properties);
|
|
694
|
+
const owned = hasOwnershipMarker(properties, type);
|
|
684
695
|
if (options.owned && !owned) {
|
|
685
696
|
unobserved[entityName] = {
|
|
686
697
|
type,
|