@intentius/chant-lexicon-aws 0.46.0 → 0.50.0
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 +10 -2
- package/dist/api/read-client.d.ts.map +1 -1
- package/dist/codegen/generate-lexicon.d.ts +5 -0
- package/dist/codegen/generate-lexicon.d.ts.map +1 -1
- package/dist/components/host-delivery.d.ts +4 -3
- package/dist/components/host-delivery.d.ts.map +1 -1
- package/dist/composites/agentcore-agent.d.ts +26 -1
- package/dist/composites/agentcore-agent.d.ts.map +1 -1
- package/dist/composites/bucket-deployment.d.ts +82 -0
- package/dist/composites/bucket-deployment.d.ts.map +1 -0
- package/dist/composites/dynamodb-table.d.ts +45 -0
- package/dist/composites/dynamodb-table.d.ts.map +1 -0
- package/dist/composites/ec2-instance-bundle.d.ts +61 -0
- package/dist/composites/ec2-instance-bundle.d.ts.map +1 -0
- package/dist/composites/ecr-repository.d.ts +49 -0
- package/dist/composites/ecr-repository.d.ts.map +1 -0
- package/dist/composites/fargate-service.d.ts +16 -0
- package/dist/composites/fargate-service.d.ts.map +1 -1
- package/dist/composites/index.d.ts +16 -2
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/composites/monitoring-stack.d.ts +99 -0
- package/dist/composites/monitoring-stack.d.ts.map +1 -0
- package/dist/composites/nlb-service.d.ts +48 -0
- package/dist/composites/nlb-service.d.ts.map +1 -0
- package/dist/composites/step-functions-workflow.d.ts +36 -0
- package/dist/composites/step-functions-workflow.d.ts.map +1 -0
- package/dist/deep-observe.d.ts +45 -0
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/deep-topology.d.ts +104 -0
- package/dist/deep-topology.d.ts.map +1 -0
- package/dist/effect-receipt-row.d.ts +92 -0
- package/dist/effect-receipt-row.d.ts.map +1 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +14 -6
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/cf-refs.d.ts +17 -0
- package/dist/lint/post-synth/cf-refs.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/waw016.d.ts +10 -2
- package/dist/lint/post-synth/waw016.d.ts.map +1 -1
- package/dist/lint/post-synth/waw061.d.ts +19 -0
- package/dist/lint/post-synth/waw061.d.ts.map +1 -0
- package/dist/lint/post-synth/waw062.d.ts +19 -0
- package/dist/lint/post-synth/waw062.d.ts.map +1 -0
- package/dist/lint/post-synth/waw063.d.ts +35 -0
- package/dist/lint/post-synth/waw063.d.ts.map +1 -0
- package/dist/lint/post-synth/waw064.d.ts +17 -0
- package/dist/lint/post-synth/waw064.d.ts.map +1 -0
- package/dist/lint/post-synth/waw065.d.ts +23 -0
- package/dist/lint/post-synth/waw065.d.ts.map +1 -0
- package/dist/lint/post-synth/waw066.d.ts +25 -0
- package/dist/lint/post-synth/waw066.d.ts.map +1 -0
- package/dist/lint/post-synth/waw067.d.ts +24 -0
- package/dist/lint/post-synth/waw067.d.ts.map +1 -0
- package/dist/lint/post-synth/waw068.d.ts +20 -0
- package/dist/lint/post-synth/waw068.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/meta.json +34 -0
- package/dist/okf/index.md +8 -0
- package/dist/okf/rules/WAW061.md +17 -0
- package/dist/okf/rules/WAW062.md +27 -0
- package/dist/okf/rules/WAW063.md +19 -0
- package/dist/okf/rules/WAW064.md +18 -0
- package/dist/okf/rules/WAW065.md +25 -0
- package/dist/okf/rules/WAW066.md +22 -0
- package/dist/okf/rules/WAW067.md +20 -0
- package/dist/okf/rules/WAW068.md +19 -0
- package/dist/okf/types/Action.md +1 -0
- package/dist/okf/types/AutoScalingGroup.md +1 -0
- package/dist/okf/types/Bucket.md +1 -0
- package/dist/okf/types/EC2Route.md +2 -0
- package/dist/okf/types/ECRRepository.md +1 -0
- package/dist/okf/types/Function.md +1 -0
- package/dist/okf/types/IamPolicy.md +1 -0
- package/dist/okf/types/InternetGateway.md +4 -0
- package/dist/okf/types/LaunchTemplate.md +1 -0
- package/dist/okf/types/LoadBalancer.md +1 -0
- package/dist/okf/types/ManagedPolicy.md +2 -0
- package/dist/okf/types/Map.md +5 -0
- package/dist/okf/types/NatGateway.md +4 -0
- package/dist/okf/types/Role.md +2 -0
- package/dist/okf/types/RouteTable.md +4 -0
- package/dist/okf/types/SecurityGroup.md +1 -0
- package/dist/okf/types/StateMachine.md +4 -0
- package/dist/okf/types/Subnet.md +6 -0
- package/dist/okf/types/SubnetRouteTableAssociation.md +5 -0
- package/dist/okf/types/Table.md +4 -0
- package/dist/okf/types/TransitGateway.md +6 -0
- package/dist/okf/types/TransitGatewayAttachment.md +4 -0
- package/dist/okf/types/TransitGatewayPeeringAttachment.md +4 -0
- package/dist/okf/types/TransitGatewayRoute.md +5 -0
- package/dist/okf/types/TransitGatewayRouteTable.md +4 -0
- package/dist/okf/types/TransitGatewayRouteTableAssociation.md +4 -0
- package/dist/okf/types/TransitGatewayRouteTablePropagation.md +4 -0
- package/dist/okf/types/TransitGatewayVpcAttachment.md +4 -0
- package/dist/okf/types/Type.md +8 -0
- package/dist/okf/types/VPNConnection.md +4 -0
- package/dist/okf/types/VPNGateway.md +4 -0
- package/dist/okf/types/Vpc.md +1 -0
- package/dist/op/activities/index.d.ts +6 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/builders.d.ts +47 -0
- package/dist/op/builders.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/receipt-store.d.ts +109 -0
- package/dist/receipt-store.d.ts.map +1 -0
- package/dist/rules/cf-refs.ts +40 -0
- package/dist/rules/waw016.ts +37 -16
- package/dist/rules/waw061.ts +67 -0
- package/dist/rules/waw062.ts +0 -0
- package/dist/rules/waw063.ts +0 -0
- package/dist/rules/waw064.ts +52 -0
- package/dist/rules/waw065.ts +110 -0
- package/dist/rules/waw066.ts +155 -0
- package/dist/rules/waw067.ts +114 -0
- package/dist/rules/waw068.ts +78 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/skills/chant-aws-carve-terraform.md +26 -23
- package/dist/spec/parse.d.ts +7 -0
- package/dist/spec/parse.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/api/read-client.test.ts +9 -0
- package/src/api/read-client.ts +15 -2
- package/src/codegen/fallback.ts +1 -0
- package/src/codegen/generate-lexicon.ts +6 -0
- package/src/codegen/sam.ts +9 -0
- package/src/components/host-delivery.ts +4 -3
- package/src/composites/agentcore-agent.ts +35 -13
- package/src/composites/bucket-deployment.test.ts +84 -0
- package/src/composites/bucket-deployment.ts +153 -0
- package/src/composites/composites.test.ts +325 -0
- package/src/composites/dynamodb-table.test.ts +154 -0
- package/src/composites/dynamodb-table.ts +124 -0
- package/src/composites/ec2-instance-bundle.ts +134 -0
- package/src/composites/ecr-repository.test.ts +118 -0
- package/src/composites/ecr-repository.ts +117 -0
- package/src/composites/fargate-service.ts +19 -3
- package/src/composites/index.ts +21 -2
- package/src/composites/monitoring-stack.test.ts +130 -0
- package/src/composites/monitoring-stack.ts +164 -0
- package/src/composites/nlb-service.ts +117 -0
- package/src/composites/step-functions-workflow.ts +127 -0
- package/src/deep-observe.test.ts +201 -1
- package/src/deep-observe.ts +204 -0
- package/src/deep-topology.test.ts +392 -0
- package/src/deep-topology.ts +237 -0
- package/src/effect-receipt-row.test.ts +164 -0
- package/src/effect-receipt-row.ts +147 -0
- package/src/generated/lexicon-aws.json +34 -0
- package/src/index.ts +36 -7
- package/src/lint/audit-catalog.ts +16 -0
- package/src/lint/post-synth/cf-refs.ts +40 -0
- package/src/lint/post-synth/index.ts +16 -0
- package/src/lint/post-synth/waw016.test.ts +61 -4
- package/src/lint/post-synth/waw016.ts +37 -16
- package/src/lint/post-synth/waw061.test.ts +99 -0
- package/src/lint/post-synth/waw061.ts +67 -0
- package/src/lint/post-synth/waw062.test.ts +109 -0
- package/src/lint/post-synth/waw062.ts +0 -0
- package/src/lint/post-synth/waw063.test.ts +220 -0
- package/src/lint/post-synth/waw063.ts +0 -0
- package/src/lint/post-synth/waw064.test.ts +79 -0
- package/src/lint/post-synth/waw064.ts +52 -0
- package/src/lint/post-synth/waw065.test.ts +90 -0
- package/src/lint/post-synth/waw065.ts +110 -0
- package/src/lint/post-synth/waw066.test.ts +108 -0
- package/src/lint/post-synth/waw066.ts +155 -0
- package/src/lint/post-synth/waw067.test.ts +79 -0
- package/src/lint/post-synth/waw067.ts +114 -0
- package/src/lint/post-synth/waw068.test.ts +74 -0
- package/src/lint/post-synth/waw068.ts +78 -0
- package/src/lsp/hover.ts +17 -2
- package/src/op/activities/index.ts +15 -0
- package/src/op/builders.test.ts +61 -0
- package/src/op/builders.ts +72 -0
- package/src/plugin.ts +41 -10
- package/src/receipt-store.test.ts +204 -0
- package/src/receipt-store.ts +315 -0
- package/src/serializer.ts +105 -2
- package/src/skills/chant-aws-carve-terraform.md +26 -23
- package/src/spec/parse.test.ts +74 -0
- package/src/spec/parse.ts +26 -6
package/src/deep-observe.ts
CHANGED
|
@@ -56,10 +56,19 @@ import {
|
|
|
56
56
|
AwsReadError,
|
|
57
57
|
describeStackResources,
|
|
58
58
|
getResource,
|
|
59
|
+
listResources,
|
|
59
60
|
type AwsReadClientOptions,
|
|
60
61
|
type AwsReadHttp,
|
|
62
|
+
type StackResource,
|
|
61
63
|
} from "./api/read-client";
|
|
64
|
+
import { declaredIdentifier } from "./identity-observe";
|
|
62
65
|
import { AWS_TAG_OWNERSHIP_KEYS } from "./ownership";
|
|
66
|
+
import {
|
|
67
|
+
AWS_DEEP_BLIND_SPOTS,
|
|
68
|
+
EC2_TOPOLOGY_GENERATED_NAMES,
|
|
69
|
+
EC2_TOPOLOGY_SERVICE_DEFAULTS,
|
|
70
|
+
EC2_TOPOLOGY_SOURCES,
|
|
71
|
+
} from "./deep-topology";
|
|
63
72
|
import { applyAwsEndpointArgv } from "./components/cloud-executor";
|
|
64
73
|
import { toIngressRules } from "./dependencies";
|
|
65
74
|
import { createRequire } from "node:module";
|
|
@@ -106,6 +115,9 @@ export const DEEP_SOURCES: Record<string, DeepSource> = {
|
|
|
106
115
|
id: "GroupId",
|
|
107
116
|
toModel: securityGroupToModel,
|
|
108
117
|
},
|
|
118
|
+
// The EC2 topology types the folds fold over (#1269), sources and
|
|
119
|
+
// translations alongside their type in ./deep-topology.ts.
|
|
120
|
+
...EC2_TOPOLOGY_SOURCES,
|
|
109
121
|
};
|
|
110
122
|
|
|
111
123
|
/**
|
|
@@ -114,6 +126,74 @@ export const DEEP_SOURCES: Record<string, DeepSource> = {
|
|
|
114
126
|
*/
|
|
115
127
|
export const DEEP_READABLE_TYPES: ReadonlySet<string> = new Set(Object.keys(DEEP_SOURCES));
|
|
116
128
|
|
|
129
|
+
/**
|
|
130
|
+
* A child type this reader enumerates under one declared parent (#1015's
|
|
131
|
+
* out-of-band case) — the console-added SNS subscription or inline role
|
|
132
|
+
* policy that no property diff can see, because it is not a property of
|
|
133
|
+
* anything declared: it is a whole resource CloudFormation never made.
|
|
134
|
+
*
|
|
135
|
+
* Cloud Control's `ListResources` takes the parent as a `ResourceModel`
|
|
136
|
+
* scope, which is what `model` builds from the physical id the stack read
|
|
137
|
+
* already resolved. Every listed child is then sorted into declared (it is
|
|
138
|
+
* some stack resource's physical id, or some declared entity's spelled
|
|
139
|
+
* identity) or out-of-band; the out-of-band ones ride into the observation
|
|
140
|
+
* as live resources nobody declared, which the deep diff reports as
|
|
141
|
+
* undeclared entities.
|
|
142
|
+
*/
|
|
143
|
+
export interface DeepChildSource {
|
|
144
|
+
/** The child's CloudFormation type — what `ListResources` is asked for. */
|
|
145
|
+
childType: string;
|
|
146
|
+
/** The `ResourceModel` scoping the listing to one parent. */
|
|
147
|
+
model: (parentPhysicalId: string) => Record<string, unknown>;
|
|
148
|
+
/**
|
|
149
|
+
* Legacy declared forms of the same live child. `AWS::IAM::Policy` attached
|
|
150
|
+
* to a role creates the same inline policy `AWS::IAM::RolePolicy` lists, but
|
|
151
|
+
* under its own physical id — the policy name, which is the second part of
|
|
152
|
+
* the RolePolicy identifier. Without this leg every legacy inline policy
|
|
153
|
+
* reads as out-of-band on a stack that declared it.
|
|
154
|
+
*/
|
|
155
|
+
declaredVia?: Array<{
|
|
156
|
+
type: string;
|
|
157
|
+
/** The part of the child identifier that carries that type's physical id. */
|
|
158
|
+
part: (identifier: string) => string | undefined;
|
|
159
|
+
}>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Parent type → the child types worth listing under it. Scoped like
|
|
164
|
+
* {@link DEEP_SOURCES}: the high-signal cases first — the issue's own
|
|
165
|
+
* examples — and widened per type, not by wildcard. A child type Cloud
|
|
166
|
+
* Control cannot list parent-scoped does not belong here.
|
|
167
|
+
*/
|
|
168
|
+
export const DEEP_CHILD_SOURCES: Record<string, readonly DeepChildSource[]> = {
|
|
169
|
+
"AWS::IAM::Role": [
|
|
170
|
+
{
|
|
171
|
+
childType: "AWS::IAM::RolePolicy",
|
|
172
|
+
model: (roleName) => ({ RoleName: roleName }),
|
|
173
|
+
// A RolePolicy identifier is `PolicyName|RoleName` — the schema's own
|
|
174
|
+
// primaryIdentifier order, which is what the manifest records and what
|
|
175
|
+
// `declaredIdentifier` joins by. The legacy type's physical id is the
|
|
176
|
+
// policy name, the first part.
|
|
177
|
+
declaredVia: [{ type: "AWS::IAM::Policy", part: (id) => id.split("|")[0] }],
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
"AWS::SNS::Topic": [
|
|
181
|
+
{
|
|
182
|
+
childType: "AWS::SNS::Subscription",
|
|
183
|
+
model: (topicArn) => ({ TopicArn: topicArn }),
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The name an out-of-band child reports under. There is no chant entity name
|
|
190
|
+
* to use — nobody declared it — so the name has to carry the finding on its
|
|
191
|
+
* own: what kind of thing, and which one.
|
|
192
|
+
*/
|
|
193
|
+
export function outOfBandChildName(childType: string, identifier: string): string {
|
|
194
|
+
return `${childType}:${identifier}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
117
197
|
/**
|
|
118
198
|
* `describe-security-groups` -> the `AWS::EC2::SecurityGroup` resource model.
|
|
119
199
|
*
|
|
@@ -205,6 +285,7 @@ export const AWS_SERVICE_DEFAULTS: Record<string, Record<string, unknown>> = {
|
|
|
205
285
|
"SecurityGroupEgress[].CidrIp": "0.0.0.0/0",
|
|
206
286
|
"SecurityGroupEgress[].IpProtocol": "-1",
|
|
207
287
|
},
|
|
288
|
+
...EC2_TOPOLOGY_SERVICE_DEFAULTS,
|
|
208
289
|
};
|
|
209
290
|
|
|
210
291
|
/**
|
|
@@ -218,6 +299,7 @@ export const AWS_SERVICE_DEFAULTS: Record<string, Record<string, unknown>> = {
|
|
|
218
299
|
*/
|
|
219
300
|
export const AWS_GENERATED_NAMES: Record<string, ReadonlySet<string>> = {
|
|
220
301
|
"AWS::EC2::SecurityGroup": new Set(["GroupName"]),
|
|
302
|
+
...EC2_TOPOLOGY_GENERATED_NAMES,
|
|
221
303
|
};
|
|
222
304
|
|
|
223
305
|
/** Stable JSON with sorted keys — the fallback ordering key for a set-like array. */
|
|
@@ -303,6 +385,18 @@ export const awsDeepNormalizationHooks: DeepNormalizationHooks = {
|
|
|
303
385
|
if (schemaReadOnlyPatterns(node.entityType).has(node.pattern)) return true;
|
|
304
386
|
if (AWS_READ_ONLY_NAMES.has(lastSegment(node.pattern))) return true;
|
|
305
387
|
|
|
388
|
+
// A declared property this type's source cannot see (#1269). With nothing
|
|
389
|
+
// live to stand against it would report `value -> <absent>` on every clean
|
|
390
|
+
// apply — a blind spot rendered as drift. Counterpart-gated the other way
|
|
391
|
+
// round: a live tree that does carry the field is still compared.
|
|
392
|
+
if (
|
|
393
|
+
node.side === "declared" &&
|
|
394
|
+
node.counterpart === "absent" &&
|
|
395
|
+
AWS_DEEP_BLIND_SPOTS[node.entityType]?.has(node.pattern)
|
|
396
|
+
) {
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
|
|
306
400
|
// Provider defaults, on the live side only, and only where source is silent
|
|
307
401
|
// about the property. `"unknown"` (a one-sided normalization) never prunes:
|
|
308
402
|
// the reader must not decide this before the declared tree is in hand.
|
|
@@ -606,9 +700,119 @@ export async function observeResourcesDeepAws(
|
|
|
606
700
|
};
|
|
607
701
|
});
|
|
608
702
|
|
|
703
|
+
// The out-of-band pass (#1015): under each declared parent whose type names
|
|
704
|
+
// child sources, list what actually exists and keep what nothing declared.
|
|
705
|
+
// A parent `--owned` withheld keeps its children out too — reporting a
|
|
706
|
+
// foreign resource's children while withholding the resource itself would
|
|
707
|
+
// be the filter half-applied. A parent with no deep reader of its own
|
|
708
|
+
// (`unsupported-kind`) still gets its children enumerated: listing under it
|
|
709
|
+
// needs only the physical id the stack already resolved.
|
|
710
|
+
const parents: Array<{ resource: StackResource; sources: readonly DeepChildSource[] }> = [];
|
|
711
|
+
for (const entityName of options.entityNames) {
|
|
712
|
+
const resource = byLogicalId.get(entityName);
|
|
713
|
+
const sources = resource ? DEEP_CHILD_SOURCES[resource.type] : undefined;
|
|
714
|
+
if (!resource?.physicalId || !sources) continue;
|
|
715
|
+
if (unobserved[entityName]?.reason === "filtered") continue;
|
|
716
|
+
parents.push({ resource, sources });
|
|
717
|
+
}
|
|
718
|
+
const children = await enumerateOutOfBandChildren({
|
|
719
|
+
parents,
|
|
720
|
+
allStackResources: stackResources,
|
|
721
|
+
entities: options.entities,
|
|
722
|
+
client,
|
|
723
|
+
normalize: (childType, properties) =>
|
|
724
|
+
normalizeDeepProperties(properties, {
|
|
725
|
+
entityType: childType,
|
|
726
|
+
side: "live",
|
|
727
|
+
hooks: awsDeepNormalizationHooks,
|
|
728
|
+
}),
|
|
729
|
+
concurrently: boundedConcurrently,
|
|
730
|
+
});
|
|
731
|
+
for (const [name, child] of Object.entries(children)) {
|
|
732
|
+
if (!resources[name]) resources[name] = child;
|
|
733
|
+
}
|
|
734
|
+
|
|
609
735
|
return deepObservation(resources, unobserved);
|
|
610
736
|
}
|
|
611
737
|
|
|
738
|
+
/**
|
|
739
|
+
* List every child-sourced type under its declared parents and return the
|
|
740
|
+
* resources nothing declared, named by {@link outOfBandChildName}.
|
|
741
|
+
*
|
|
742
|
+
* "Declared" is answered three ways, in order of how directly the stack says
|
|
743
|
+
* it: the child identifier is some stack resource's physical id (the normal
|
|
744
|
+
* case — a declared `AWS::SNS::Subscription` deploys under its ARN); a
|
|
745
|
+
* `declaredVia` legacy form's physical id is the identifier's relevant part;
|
|
746
|
+
* or a declared entity's props spell the identifier (`declaredIdentifier`,
|
|
747
|
+
* the carve case — declared in source, not yet in any stack).
|
|
748
|
+
*
|
|
749
|
+
* A listing that fails is silence about that parent's children, never a
|
|
750
|
+
* finding and never a hole: there is no declared entity to hang a hole on,
|
|
751
|
+
* and fabricating one would put an undeclared name in the unobserved report.
|
|
752
|
+
* `UnsupportedOperation` — an emulator without parent-scoped listing — lands
|
|
753
|
+
* here too, which is what keeps the Floci lanes quiet.
|
|
754
|
+
*/
|
|
755
|
+
async function enumerateOutOfBandChildren(args: {
|
|
756
|
+
parents: Array<{ resource: StackResource; sources: readonly DeepChildSource[] }>;
|
|
757
|
+
allStackResources: StackResource[];
|
|
758
|
+
entities?: Map<string, { entityType: string; props: Record<string, unknown> }>;
|
|
759
|
+
client: AwsReadClientOptions;
|
|
760
|
+
normalize: (childType: string, properties: Record<string, unknown>) => Record<string, unknown>;
|
|
761
|
+
concurrently: <T>(items: T[], fn: (item: T) => Promise<void>) => Promise<unknown>;
|
|
762
|
+
}): Promise<Record<string, DeepResourceObservation>> {
|
|
763
|
+
const out: Record<string, DeepResourceObservation> = {};
|
|
764
|
+
if (args.parents.length === 0) return out;
|
|
765
|
+
|
|
766
|
+
// Physical ids by type, over the WHOLE stack — a stack resource that is not
|
|
767
|
+
// a declared chant entity (a nested stack's row, a resource another tool
|
|
768
|
+
// added to the same stack) is still stack-managed, not out-of-band.
|
|
769
|
+
const addTo = (map: Map<string, Set<string>>, key: string, value: string) => {
|
|
770
|
+
const set = map.get(key) ?? new Set<string>();
|
|
771
|
+
set.add(value);
|
|
772
|
+
map.set(key, set);
|
|
773
|
+
};
|
|
774
|
+
const stackIdsByType = new Map<string, Set<string>>();
|
|
775
|
+
for (const r of args.allStackResources) {
|
|
776
|
+
if (r.physicalId) addTo(stackIdsByType, r.type, r.physicalId);
|
|
777
|
+
}
|
|
778
|
+
const declaredIdsByType = new Map<string, Set<string>>();
|
|
779
|
+
for (const entity of args.entities?.values() ?? []) {
|
|
780
|
+
const id = declaredIdentifier(entity.entityType, entity.props ?? {});
|
|
781
|
+
if (id) addTo(declaredIdsByType, entity.entityType, id);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const isDeclared = (source: DeepChildSource, identifier: string): boolean => {
|
|
785
|
+
if (stackIdsByType.get(source.childType)?.has(identifier)) return true;
|
|
786
|
+
if (declaredIdsByType.get(source.childType)?.has(identifier)) return true;
|
|
787
|
+
for (const via of source.declaredVia ?? []) {
|
|
788
|
+
const part = via.part(identifier);
|
|
789
|
+
if (part && stackIdsByType.get(via.type)?.has(part)) return true;
|
|
790
|
+
}
|
|
791
|
+
return false;
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
const listings = args.parents.flatMap(({ resource, sources }) =>
|
|
795
|
+
sources.map((source) => ({ physicalId: resource.physicalId as string, source })),
|
|
796
|
+
);
|
|
797
|
+
await args.concurrently(listings, async ({ physicalId, source }) => {
|
|
798
|
+
let listed;
|
|
799
|
+
try {
|
|
800
|
+
listed = await listResources(source.childType, args.client, source.model(physicalId));
|
|
801
|
+
} catch {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
for (const child of listed) {
|
|
805
|
+
if (!child.identifier || isDeclared(source, child.identifier)) continue;
|
|
806
|
+
out[outOfBandChildName(source.childType, child.identifier)] = {
|
|
807
|
+
type: source.childType,
|
|
808
|
+
physicalId: child.identifier,
|
|
809
|
+
properties: args.normalize(source.childType, child.properties),
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
return out;
|
|
814
|
+
}
|
|
815
|
+
|
|
612
816
|
/**
|
|
613
817
|
* Read every `ec2`-sourced type in bulk, keyed by physical id.
|
|
614
818
|
*
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-read coverage for the EC2 topology types (#1269).
|
|
3
|
+
*
|
|
4
|
+
* The folds' inputs — VPC, subnet, route table, internet gateway, and the
|
|
5
|
+
* joins between them — were exactly the types the deep reader could not read,
|
|
6
|
+
* so a snapshot-backed query could answer identity questions and not a single
|
|
7
|
+
* fold question. These tests drive each added type through the real reader
|
|
8
|
+
* against faked describes, and close with the fold-level proof: a question
|
|
9
|
+
* that was previously unanswerable (is this VPC the provider's default?)
|
|
10
|
+
* answered from the deep read alone.
|
|
11
|
+
*
|
|
12
|
+
* Same seams as deep-observe.test.ts: Cloud Control and CloudFormation are
|
|
13
|
+
* faked at the injected `http` transport, the EC2 describes at the runtime's
|
|
14
|
+
* `spawn`.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, test, expect, vi, beforeEach } from "vitest";
|
|
17
|
+
|
|
18
|
+
const spawnMock = vi.fn();
|
|
19
|
+
vi.mock("@intentius/chant/runtime-adapter", async (importOriginal) => {
|
|
20
|
+
const actual = await importOriginal<typeof import("@intentius/chant/runtime-adapter")>();
|
|
21
|
+
return { ...actual, getRuntime: () => ({ ...actual.getRuntime(), spawn: spawnMock }) };
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const { observeResourcesDeepAws, DEEP_READABLE_TYPES, awsDeepNormalizationHooks } = await import("./deep-observe");
|
|
25
|
+
const { vpcToModel, subnetToModel, routeTableToModel, internetGatewayToModel } = await import("./deep-topology");
|
|
26
|
+
const { stampProviderDefaults } = await import("./defaults");
|
|
27
|
+
const { normalizeDeepObservation } = await import("@intentius/chant/deep-observation");
|
|
28
|
+
const { diffDeepObservation } = await import("@intentius/chant/lifecycle/deep-observe");
|
|
29
|
+
|
|
30
|
+
const ok = (text: string) => ({ status: 200, text });
|
|
31
|
+
|
|
32
|
+
/** A Cloud Control `GetResource` body — the model arrives as a JSON string. */
|
|
33
|
+
const cloudControl = (identifier: string, properties: Record<string, unknown>) =>
|
|
34
|
+
ok(JSON.stringify({ ResourceDescription: { Identifier: identifier, Properties: JSON.stringify(properties) } }));
|
|
35
|
+
|
|
36
|
+
const stackResources = (rows: Array<[string, string, string]>) =>
|
|
37
|
+
ok(
|
|
38
|
+
`<DescribeStackResourcesResponse><DescribeStackResourcesResult><StackResources>${rows
|
|
39
|
+
.map(
|
|
40
|
+
([logicalId, type, physicalId]) =>
|
|
41
|
+
`<member><LogicalResourceId>${logicalId}</LogicalResourceId><ResourceType>${type}</ResourceType>` +
|
|
42
|
+
`<PhysicalResourceId>${physicalId}</PhysicalResourceId><ResourceStatus>CREATE_COMPLETE</ResourceStatus>` +
|
|
43
|
+
`<Timestamp>2026-01-01T00:00:00Z</Timestamp></member>`,
|
|
44
|
+
)
|
|
45
|
+
.join("")}</StackResources></DescribeStackResourcesResult></DescribeStackResourcesResponse>`,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
type FakeResponse = { status: number; text: string };
|
|
49
|
+
|
|
50
|
+
/** Route on the Cloud Control identifier; `undefined` is the CloudFormation call. */
|
|
51
|
+
function httpFake(route: (identifier: string | undefined) => FakeResponse) {
|
|
52
|
+
return async (_url: string, init: { headers: Record<string, string>; body: string }) => {
|
|
53
|
+
const target = init.headers["x-amz-target"];
|
|
54
|
+
const identifier = target ? (JSON.parse(init.body) as { Identifier?: string }).Identifier : undefined;
|
|
55
|
+
return route(identifier);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Route each EC2 bulk describe on its subcommand (`argv[2]`). */
|
|
60
|
+
function ec2Describes(byCommand: Record<string, Record<string, unknown[]>>) {
|
|
61
|
+
spawnMock.mockImplementation((argv: string[]) => {
|
|
62
|
+
const body = byCommand[argv[2]];
|
|
63
|
+
if (!body) return Promise.resolve({ stdout: "", stderr: `no fake for ${argv[2]}`, exitCode: 255 });
|
|
64
|
+
return Promise.resolve({ stdout: JSON.stringify(body), stderr: "", exitCode: 0 });
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const entities = (
|
|
69
|
+
record: Record<string, { entityType: string; props: Record<string, unknown> }>,
|
|
70
|
+
): Map<string, { entityType: string; props: Record<string, unknown> }> => new Map(Object.entries(record));
|
|
71
|
+
|
|
72
|
+
const vpcRow = {
|
|
73
|
+
VpcId: "vpc-01",
|
|
74
|
+
State: "available",
|
|
75
|
+
OwnerId: "111122223333",
|
|
76
|
+
CidrBlock: "10.0.0.0/16",
|
|
77
|
+
DhcpOptionsId: "dopt-01",
|
|
78
|
+
InstanceTenancy: "default",
|
|
79
|
+
IsDefault: false,
|
|
80
|
+
CidrBlockAssociationSet: [{ AssociationId: "vpc-cidr-assoc-01", CidrBlock: "10.0.0.0/16" }],
|
|
81
|
+
Tags: [{ Key: "env", Value: "prod" }],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Braces matter: `mockReset()` returns the mock, and a function returned from
|
|
85
|
+
// a vitest hook is called as a cleanup hook — the mock invoked with no argv.
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
spawnMock.mockReset();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("the topology translations", () => {
|
|
91
|
+
test("a VPC row maps onto the resource model, keeping the provider's default marker", () => {
|
|
92
|
+
expect(vpcToModel(vpcRow)).toEqual({
|
|
93
|
+
VpcId: "vpc-01",
|
|
94
|
+
CidrBlock: "10.0.0.0/16",
|
|
95
|
+
InstanceTenancy: "default",
|
|
96
|
+
IsDefault: false,
|
|
97
|
+
Tags: [{ Key: "env", Value: "prod" }],
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("a subnet row keeps both fold inputs: the VPC hop and MapPublicIpOnLaunch", () => {
|
|
102
|
+
expect(
|
|
103
|
+
subnetToModel({
|
|
104
|
+
SubnetId: "subnet-01",
|
|
105
|
+
VpcId: "vpc-01",
|
|
106
|
+
State: "available",
|
|
107
|
+
OwnerId: "111122223333",
|
|
108
|
+
CidrBlock: "10.0.1.0/24",
|
|
109
|
+
AvailabilityZone: "us-east-1a",
|
|
110
|
+
AvailabilityZoneId: "use1-az1",
|
|
111
|
+
AvailableIpAddressCount: 250,
|
|
112
|
+
MapPublicIpOnLaunch: true,
|
|
113
|
+
AssignIpv6AddressOnCreation: false,
|
|
114
|
+
DefaultForAz: false,
|
|
115
|
+
Ipv6Native: false,
|
|
116
|
+
PrivateDnsNameOptionsOnLaunch: { HostnameType: "ip-name" },
|
|
117
|
+
}),
|
|
118
|
+
).toEqual({
|
|
119
|
+
SubnetId: "subnet-01",
|
|
120
|
+
VpcId: "vpc-01",
|
|
121
|
+
CidrBlock: "10.0.1.0/24",
|
|
122
|
+
AvailabilityZone: "us-east-1a",
|
|
123
|
+
AvailabilityZoneId: "use1-az1",
|
|
124
|
+
MapPublicIpOnLaunch: true,
|
|
125
|
+
AssignIpv6AddressOnCreation: false,
|
|
126
|
+
Ipv6Native: false,
|
|
127
|
+
DefaultForAz: false,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("a route table's routes and associations are its own declared resources, not carried twice", () => {
|
|
132
|
+
expect(
|
|
133
|
+
routeTableToModel({
|
|
134
|
+
RouteTableId: "rtb-01",
|
|
135
|
+
VpcId: "vpc-01",
|
|
136
|
+
Routes: [{ DestinationCidrBlock: "0.0.0.0/0", GatewayId: "igw-01" }],
|
|
137
|
+
Associations: [{ Main: false, SubnetId: "subnet-01" }],
|
|
138
|
+
PropagatingVgws: [],
|
|
139
|
+
}),
|
|
140
|
+
).toEqual({ RouteTableId: "rtb-01", VpcId: "vpc-01" });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("an internet gateway is tags and nothing else; an empty tag set is absence, not a value", () => {
|
|
144
|
+
expect(internetGatewayToModel({ InternetGatewayId: "igw-01", Attachments: [], Tags: [] })).toEqual({
|
|
145
|
+
InternetGatewayId: "igw-01",
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("deep-reading an EC2 topology stack", () => {
|
|
151
|
+
const topologyStack = stackResources([
|
|
152
|
+
["Net", "AWS::EC2::VPC", "vpc-01"],
|
|
153
|
+
["PublicSubnet", "AWS::EC2::Subnet", "subnet-01"],
|
|
154
|
+
["PublicRoutes", "AWS::EC2::RouteTable", "rtb-01"],
|
|
155
|
+
["Igw", "AWS::EC2::InternetGateway", "igw-01"],
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
test("every topology type from #1269 is readable", () => {
|
|
159
|
+
for (const type of [
|
|
160
|
+
"AWS::EC2::VPC",
|
|
161
|
+
"AWS::EC2::Subnet",
|
|
162
|
+
"AWS::EC2::RouteTable",
|
|
163
|
+
"AWS::EC2::Route",
|
|
164
|
+
"AWS::EC2::SubnetRouteTableAssociation",
|
|
165
|
+
"AWS::EC2::InternetGateway",
|
|
166
|
+
"AWS::EC2::VPCGatewayAttachment",
|
|
167
|
+
"AWS::EC2::Instance",
|
|
168
|
+
"AWS::EC2::LaunchTemplate",
|
|
169
|
+
]) {
|
|
170
|
+
expect(DEEP_READABLE_TYPES.has(type)).toBe(true);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("one bulk describe per type, and every tree is recorded", async () => {
|
|
175
|
+
ec2Describes({
|
|
176
|
+
"describe-vpcs": { Vpcs: [vpcRow] },
|
|
177
|
+
"describe-subnets": {
|
|
178
|
+
Subnets: [{ SubnetId: "subnet-01", VpcId: "vpc-01", CidrBlock: "10.0.1.0/24", MapPublicIpOnLaunch: true }],
|
|
179
|
+
},
|
|
180
|
+
"describe-route-tables": { RouteTables: [{ RouteTableId: "rtb-01", VpcId: "vpc-01", Routes: [] }] },
|
|
181
|
+
"describe-internet-gateways": { InternetGateways: [{ InternetGatewayId: "igw-01", Attachments: [] }] },
|
|
182
|
+
});
|
|
183
|
+
const result = normalizeDeepObservation(
|
|
184
|
+
await observeResourcesDeepAws({
|
|
185
|
+
environment: "prod",
|
|
186
|
+
entityNames: ["Net", "PublicSubnet", "PublicRoutes", "Igw"],
|
|
187
|
+
http: httpFake(() => topologyStack),
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect(Object.keys(result.resources).sort()).toEqual(["Igw", "Net", "PublicRoutes", "PublicSubnet"]);
|
|
192
|
+
expect(result.unobserved).toEqual({});
|
|
193
|
+
|
|
194
|
+
// The containment hop and the public-IP fact the folds read.
|
|
195
|
+
expect(result.resources.PublicSubnet.properties).toMatchObject({
|
|
196
|
+
VpcId: "vpc-01",
|
|
197
|
+
MapPublicIpOnLaunch: true,
|
|
198
|
+
});
|
|
199
|
+
// Server-minted ids are pruned by the schema's own readOnlyProperties.
|
|
200
|
+
expect(result.resources.Net.properties.VpcId).toBeUndefined();
|
|
201
|
+
expect(result.resources.PublicSubnet.properties.SubnetId).toBeUndefined();
|
|
202
|
+
// The provider's default marker survives into the snapshot for the fold.
|
|
203
|
+
expect(result.resources.Net.properties.IsDefault).toBe(false);
|
|
204
|
+
|
|
205
|
+
// Four types, four describes — never one call per resource.
|
|
206
|
+
expect(spawnMock).toHaveBeenCalledTimes(4);
|
|
207
|
+
const commands = spawnMock.mock.calls.map((c) => (c[0] as string[])[2]).sort();
|
|
208
|
+
expect(commands).toEqual([
|
|
209
|
+
"describe-internet-gateways",
|
|
210
|
+
"describe-route-tables",
|
|
211
|
+
"describe-subnets",
|
|
212
|
+
"describe-vpcs",
|
|
213
|
+
]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("the routing-chain joins read through Cloud Control by their physical id", async () => {
|
|
217
|
+
const http = httpFake((identifier) => {
|
|
218
|
+
if (identifier === undefined) {
|
|
219
|
+
return stackResources([
|
|
220
|
+
["DefaultRoute", "AWS::EC2::Route", "rtb-01|0.0.0.0/0"],
|
|
221
|
+
["SubnetRoutes", "AWS::EC2::SubnetRouteTableAssociation", "rtbassoc-01"],
|
|
222
|
+
["IgwAttachment", "AWS::EC2::VPCGatewayAttachment", "IGW|vpc-01"],
|
|
223
|
+
]);
|
|
224
|
+
}
|
|
225
|
+
if (identifier === "rtb-01|0.0.0.0/0") {
|
|
226
|
+
return cloudControl(identifier, {
|
|
227
|
+
RouteTableId: "rtb-01",
|
|
228
|
+
DestinationCidrBlock: "0.0.0.0/0",
|
|
229
|
+
GatewayId: "igw-01",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (identifier === "rtbassoc-01") {
|
|
233
|
+
return cloudControl(identifier, { Id: "rtbassoc-01", RouteTableId: "rtb-01", SubnetId: "subnet-01" });
|
|
234
|
+
}
|
|
235
|
+
return cloudControl(identifier, { InternetGatewayId: "igw-01", VpcId: "vpc-01" });
|
|
236
|
+
});
|
|
237
|
+
const result = normalizeDeepObservation(
|
|
238
|
+
await observeResourcesDeepAws({
|
|
239
|
+
environment: "prod",
|
|
240
|
+
entityNames: ["DefaultRoute", "SubnetRoutes", "IgwAttachment"],
|
|
241
|
+
http,
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// The `0.0.0.0/0` -> igw edge `internetFacing` walks.
|
|
246
|
+
expect(result.resources.DefaultRoute.properties).toEqual({
|
|
247
|
+
RouteTableId: "rtb-01",
|
|
248
|
+
DestinationCidrBlock: "0.0.0.0/0",
|
|
249
|
+
GatewayId: "igw-01",
|
|
250
|
+
});
|
|
251
|
+
// Subnet -> route table, with the association's own minted id pruned.
|
|
252
|
+
expect(result.resources.SubnetRoutes.properties).toEqual({
|
|
253
|
+
RouteTableId: "rtb-01",
|
|
254
|
+
SubnetId: "subnet-01",
|
|
255
|
+
});
|
|
256
|
+
// Igw -> VPC.
|
|
257
|
+
expect(result.resources.IgwAttachment.properties).toEqual({
|
|
258
|
+
InternetGatewayId: "igw-01",
|
|
259
|
+
VpcId: "vpc-01",
|
|
260
|
+
});
|
|
261
|
+
expect(spawnMock).not.toHaveBeenCalled();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("a type without a source is still a stated hole, not a silent one", async () => {
|
|
265
|
+
const result = normalizeDeepObservation(
|
|
266
|
+
await observeResourcesDeepAws({
|
|
267
|
+
environment: "prod",
|
|
268
|
+
entityNames: ["Ip"],
|
|
269
|
+
http: httpFake(() => stackResources([["Ip", "AWS::EC2::EIP", "203.0.113.7"]])),
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
expect(result.resources).toEqual({});
|
|
273
|
+
expect(result.unobserved.Ip.reason).toBe("unsupported-kind");
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe("the fold answers (#1269)", () => {
|
|
278
|
+
const vpcStack = stackResources([
|
|
279
|
+
["Net", "AWS::EC2::VPC", "vpc-01"],
|
|
280
|
+
["Legacy", "AWS::EC2::VPC", "vpc-02"],
|
|
281
|
+
]);
|
|
282
|
+
|
|
283
|
+
test("IsDefault on a VPC — previously unanswerable — answers from the deep read", async () => {
|
|
284
|
+
ec2Describes({
|
|
285
|
+
"describe-vpcs": {
|
|
286
|
+
Vpcs: [vpcRow, { ...vpcRow, VpcId: "vpc-02", CidrBlock: "172.31.0.0/16", IsDefault: true, Tags: [] }],
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
const observed = normalizeDeepObservation(
|
|
290
|
+
await observeResourcesDeepAws({
|
|
291
|
+
environment: "prod",
|
|
292
|
+
entityNames: ["Net", "Legacy"],
|
|
293
|
+
http: httpFake(() => vpcStack),
|
|
294
|
+
}),
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
// The classification fold reads the provider's own marker off the
|
|
298
|
+
// recorded properties — nothing to re-fetch, no heuristics.
|
|
299
|
+
const stamped = stampProviderDefaults(
|
|
300
|
+
Object.fromEntries(
|
|
301
|
+
Object.entries(observed.resources).map(([name, r]) => [
|
|
302
|
+
name,
|
|
303
|
+
{ type: r.type, status: "CREATE_COMPLETE", attributes: r.properties },
|
|
304
|
+
]),
|
|
305
|
+
),
|
|
306
|
+
);
|
|
307
|
+
expect(stamped.Legacy.attributes?.providerDefault).toBe(true);
|
|
308
|
+
expect(stamped.Net.attributes?.providerDefault).toBeUndefined();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("the marker feeds the fold without surfacing as drift on a declared VPC", async () => {
|
|
312
|
+
ec2Describes({ "describe-vpcs": { Vpcs: [vpcRow] } });
|
|
313
|
+
const observed = normalizeDeepObservation(
|
|
314
|
+
await observeResourcesDeepAws({
|
|
315
|
+
environment: "prod",
|
|
316
|
+
entityNames: ["Net"],
|
|
317
|
+
http: httpFake(() => stackResources([["Net", "AWS::EC2::VPC", "vpc-01"]])),
|
|
318
|
+
}),
|
|
319
|
+
);
|
|
320
|
+
const declared = entities({
|
|
321
|
+
Net: {
|
|
322
|
+
entityType: "AWS::EC2::VPC",
|
|
323
|
+
props: { CidrBlock: "10.0.0.0/16", Tags: [{ Key: "env", Value: "prod" }] },
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// In the snapshot for a query to read…
|
|
328
|
+
expect(observed.resources.Net.properties.IsDefault).toBe(false);
|
|
329
|
+
// …and subtracted in the diff, where source never declared it.
|
|
330
|
+
const diff = diffDeepObservation(declared, observed, awsDeepNormalizationHooks);
|
|
331
|
+
expect(diff.drifted).toEqual([]);
|
|
332
|
+
expect(diff.unchanged).toEqual(["Net"]);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("declared DNS attributes are a source blind spot, not absence drift; a visible change still reports", async () => {
|
|
336
|
+
ec2Describes({ "describe-vpcs": { Vpcs: [{ ...vpcRow, CidrBlock: "10.9.0.0/16", Tags: [] }] } });
|
|
337
|
+
const observed = normalizeDeepObservation(
|
|
338
|
+
await observeResourcesDeepAws({
|
|
339
|
+
environment: "prod",
|
|
340
|
+
entityNames: ["Net"],
|
|
341
|
+
http: httpFake(() => stackResources([["Net", "AWS::EC2::VPC", "vpc-01"]])),
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
const declared = entities({
|
|
345
|
+
Net: {
|
|
346
|
+
entityType: "AWS::EC2::VPC",
|
|
347
|
+
props: { CidrBlock: "10.0.0.0/16", EnableDnsSupport: true, EnableDnsHostnames: true },
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const diff = diffDeepObservation(declared, observed, awsDeepNormalizationHooks);
|
|
352
|
+
expect(diff.drifted).toEqual([
|
|
353
|
+
{
|
|
354
|
+
name: "Net",
|
|
355
|
+
type: "AWS::EC2::VPC",
|
|
356
|
+
changes: [
|
|
357
|
+
{ path: "CidrBlock", kind: "changed", declared: "10.0.0.0/16", live: "10.9.0.0/16" },
|
|
358
|
+
],
|
|
359
|
+
},
|
|
360
|
+
]);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("an out-of-band MapPublicIpOnLaunch flip reports; the service default does not", async () => {
|
|
364
|
+
const subnetRow = (mapPublic: boolean) => ({
|
|
365
|
+
Subnets: [{ SubnetId: "subnet-01", VpcId: "vpc-01", CidrBlock: "10.0.1.0/24", MapPublicIpOnLaunch: mapPublic }],
|
|
366
|
+
});
|
|
367
|
+
const declared = entities({
|
|
368
|
+
PublicSubnet: { entityType: "AWS::EC2::Subnet", props: { VpcId: "vpc-01", CidrBlock: "10.0.1.0/24" } },
|
|
369
|
+
});
|
|
370
|
+
const observe = async () =>
|
|
371
|
+
normalizeDeepObservation(
|
|
372
|
+
await observeResourcesDeepAws({
|
|
373
|
+
environment: "prod",
|
|
374
|
+
entityNames: ["PublicSubnet"],
|
|
375
|
+
http: httpFake(() => stackResources([["PublicSubnet", "AWS::EC2::Subnet", "subnet-01"]])),
|
|
376
|
+
}),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
ec2Describes({ "describe-subnets": subnetRow(false) });
|
|
380
|
+
expect(diffDeepObservation(declared, await observe(), awsDeepNormalizationHooks).drifted).toEqual([]);
|
|
381
|
+
|
|
382
|
+
ec2Describes({ "describe-subnets": subnetRow(true) });
|
|
383
|
+
const diff = diffDeepObservation(declared, await observe(), awsDeepNormalizationHooks);
|
|
384
|
+
expect(diff.drifted).toEqual([
|
|
385
|
+
{
|
|
386
|
+
name: "PublicSubnet",
|
|
387
|
+
type: "AWS::EC2::Subnet",
|
|
388
|
+
changes: [{ path: "MapPublicIpOnLaunch", kind: "undeclared", live: true }],
|
|
389
|
+
},
|
|
390
|
+
]);
|
|
391
|
+
});
|
|
392
|
+
});
|