@intentius/chant-lexicon-aws 0.32.0 → 0.33.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/generated/index.d.ts +24 -15
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/integrity.json +4 -4
- package/dist/manifest.json +1 -1
- package/dist/meta.json +249 -151
- package/dist/plugin.d.ts +0 -6
- package/dist/plugin.d.ts.map +1 -1
- package/dist/types/index.d.ts +316 -203
- package/package.json +2 -2
- package/src/generated/index.d.ts +316 -203
- package/src/generated/index.ts +24 -15
- package/src/generated/lexicon-aws.json +249 -151
- package/src/plugin.ts +89 -3
package/src/plugin.ts
CHANGED
|
@@ -36,6 +36,59 @@ export { stackDoesNotExist } from "./stack-errors";
|
|
|
36
36
|
* Provides serializer, lint rules, template detection,
|
|
37
37
|
* import parsing, and code generation for AWS CloudFormation.
|
|
38
38
|
*/
|
|
39
|
+
/**
|
|
40
|
+
* Resolve `internetFacing` per instance LOGICAL id from LIVE route tables — a
|
|
41
|
+
* subnet is internet-facing iff its route table (an explicit association, else
|
|
42
|
+
* the VPC's main table) has a default route to an Internet Gateway. This covers
|
|
43
|
+
* the account's default VPC, whose routing chant does not model declaratively
|
|
44
|
+
* (the instance references it through a parameter). Best-effort: any failure
|
|
45
|
+
* returns what it has, so search still works on the declared topology.
|
|
46
|
+
*/
|
|
47
|
+
async function liveInternetFacing(regionArgs: string[], stackName: string): Promise<Record<string, string>> {
|
|
48
|
+
const { getRuntime } = await import("@intentius/chant/runtime-adapter");
|
|
49
|
+
const rt = getRuntime();
|
|
50
|
+
const run = (args: string[]) =>
|
|
51
|
+
rt.spawn(applyAwsEndpointArgv(["aws", ...args, ...regionArgs, "--output", "json"], process.env.AWS_ENDPOINT_URL));
|
|
52
|
+
const out: Record<string, string> = {};
|
|
53
|
+
try {
|
|
54
|
+
const res = await run(["cloudformation", "describe-stack-resources", "--stack-name", stackName]);
|
|
55
|
+
if (res.exitCode !== 0) return out;
|
|
56
|
+
const stackRes = (JSON.parse(res.stdout).StackResources ?? []) as Array<{ LogicalResourceId: string; ResourceType: string; PhysicalResourceId: string }>;
|
|
57
|
+
const instByLogical = new Map<string, string>();
|
|
58
|
+
for (const r of stackRes) if (r.ResourceType === "AWS::EC2::Instance") instByLogical.set(r.LogicalResourceId, r.PhysicalResourceId);
|
|
59
|
+
if (instByLogical.size === 0) return out;
|
|
60
|
+
|
|
61
|
+
const di = await run(["ec2", "describe-instances", "--instance-ids", ...instByLogical.values()]);
|
|
62
|
+
if (di.exitCode !== 0) return out;
|
|
63
|
+
const locByInst = new Map<string, { subnet?: string; vpc?: string }>();
|
|
64
|
+
for (const rsv of (JSON.parse(di.stdout).Reservations ?? []) as Array<{ Instances?: Array<{ InstanceId: string; SubnetId?: string; VpcId?: string }> }>)
|
|
65
|
+
for (const i of rsv.Instances ?? []) locByInst.set(i.InstanceId, { subnet: i.SubnetId, vpc: i.VpcId });
|
|
66
|
+
|
|
67
|
+
const rtRes = await run(["ec2", "describe-route-tables"]);
|
|
68
|
+
if (rtRes.exitCode !== 0) return out;
|
|
69
|
+
const subnetIgw = new Map<string, string>();
|
|
70
|
+
const vpcMainIgw = new Map<string, string>();
|
|
71
|
+
for (const t of (JSON.parse(rtRes.stdout).RouteTables ?? []) as Array<{ RouteTableId?: string; VpcId?: string; Routes?: Array<{ GatewayId?: string }>; Associations?: Array<{ SubnetId?: string; Main?: boolean }> }>) {
|
|
72
|
+
const igwRoute = (t.Routes ?? []).find((r) => typeof r.GatewayId === "string" && r.GatewayId.startsWith("igw-"));
|
|
73
|
+
if (!igwRoute) continue;
|
|
74
|
+
const ev = `${t.RouteTableId ?? "rtb"} → ${igwRoute.GatewayId}`;
|
|
75
|
+
for (const a of t.Associations ?? []) {
|
|
76
|
+
if (a.SubnetId) subnetIgw.set(a.SubnetId, ev);
|
|
77
|
+
if (a.Main && t.VpcId) vpcMainIgw.set(t.VpcId, ev);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const [logical, instId] of instByLogical) {
|
|
82
|
+
const loc = locByInst.get(instId);
|
|
83
|
+
const ev = loc ? ((loc.subnet && subnetIgw.get(loc.subnet)) || (loc.vpc && vpcMainIgw.get(loc.vpc))) : undefined;
|
|
84
|
+
if (ev) out[logical] = ev;
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
/* best-effort */
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
39
92
|
export const awsPlugin: LexiconPlugin = {
|
|
40
93
|
name: "aws",
|
|
41
94
|
serializer: awsSerializer,
|
|
@@ -532,12 +585,15 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
|
|
|
532
585
|
buildOutput: string;
|
|
533
586
|
entityNames: string[];
|
|
534
587
|
stack?: string;
|
|
588
|
+
region?: string;
|
|
535
589
|
owned?: boolean;
|
|
536
590
|
}): Promise<ObservationResult> {
|
|
537
591
|
const { getRuntime } = await import("@intentius/chant/runtime-adapter");
|
|
538
592
|
const { observation, unobservedAll } = await import("@intentius/chant/observation");
|
|
539
593
|
const rt = getRuntime();
|
|
540
594
|
const resources: Record<string, ResourceMetadata> = {};
|
|
595
|
+
// Multi-region estates: target this stack's region, not the ambient one.
|
|
596
|
+
const regionArgs = options.region ? ["--region", options.region] : [];
|
|
541
597
|
|
|
542
598
|
if (options.owned) {
|
|
543
599
|
// describe-stack-resources does not return tags, so ownership cannot be
|
|
@@ -559,6 +615,7 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
|
|
|
559
615
|
const listResult = await rt.spawn(applyAwsEndpointArgv([
|
|
560
616
|
"aws", "cloudformation", "describe-stack-resources",
|
|
561
617
|
"--stack-name", stackName,
|
|
618
|
+
...regionArgs,
|
|
562
619
|
"--output", "json",
|
|
563
620
|
], process.env.AWS_ENDPOINT_URL));
|
|
564
621
|
|
|
@@ -607,6 +664,7 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
|
|
|
607
664
|
const describeResult = await rt.spawn(applyAwsEndpointArgv([
|
|
608
665
|
"aws", "cloudformation", "describe-stacks",
|
|
609
666
|
"--stack-name", stackName,
|
|
667
|
+
...regionArgs,
|
|
610
668
|
"--output", "json",
|
|
611
669
|
], process.env.AWS_ENDPOINT_URL));
|
|
612
670
|
|
|
@@ -712,6 +770,7 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
|
|
|
712
770
|
async exportResources(options: {
|
|
713
771
|
environment: string;
|
|
714
772
|
stack?: string;
|
|
773
|
+
region?: string;
|
|
715
774
|
selector?: ResourceSelector;
|
|
716
775
|
owned?: boolean;
|
|
717
776
|
}): Promise<ExportedTemplate> {
|
|
@@ -721,10 +780,12 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
|
|
|
721
780
|
// Same stack-name convention as describeResources: an explicit multi-stack
|
|
722
781
|
// stack name (#932), else the stack named after the environment.
|
|
723
782
|
const stackName = options.stack ?? `${options.environment}`;
|
|
783
|
+
const regionArgs = options.region ? ["--region", options.region] : [];
|
|
724
784
|
|
|
725
785
|
const result = await rt.spawn(applyAwsEndpointArgv([
|
|
726
786
|
"aws", "cloudformation", "get-template",
|
|
727
787
|
"--stack-name", stackName,
|
|
788
|
+
...regionArgs,
|
|
728
789
|
"--template-stage", "Original",
|
|
729
790
|
"--output", "json",
|
|
730
791
|
], process.env.AWS_ENDPOINT_URL));
|
|
@@ -750,9 +811,34 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
|
|
|
750
811
|
// describe-stack-resources is too thin; the deployed template (exportResources)
|
|
751
812
|
// carries the references. Resolve its `{Ref}`/`{Fn::GetAtt}` intrinsics to bare
|
|
752
813
|
// logical ids so the reference resolver matches them.
|
|
753
|
-
async enrichLiveAttrs(options: { environment: string; stack?: string; owned?: boolean }): Promise<Record<string, Record<string, unknown>>> {
|
|
754
|
-
|
|
755
|
-
|
|
814
|
+
async enrichLiveAttrs(options: { environment: string; stack?: string; stacks?: Array<string | { name: string; region?: string }>; owned?: boolean }): Promise<Record<string, Record<string, unknown>>> {
|
|
815
|
+
// Multi-stack (#1161): a project declaring ChantConfig.stacks passes them
|
|
816
|
+
// here; enrich per-stack and merge. Otherwise the single-stack convention.
|
|
817
|
+
const stackRefs = options.stacks && options.stacks.length > 0
|
|
818
|
+
? options.stacks.map((st) => (typeof st === "string" ? { name: st } : st))
|
|
819
|
+
: [{ name: options.stack ?? options.environment }];
|
|
820
|
+
const merged: Record<string, Record<string, unknown>> = {};
|
|
821
|
+
const multi = options.stacks && options.stacks.length > 0;
|
|
822
|
+
for (const ref of stackRefs) {
|
|
823
|
+
try {
|
|
824
|
+
const template = await this.exportResources!({ environment: options.environment, stack: ref.name, region: ref.region, owned: options.owned });
|
|
825
|
+
const attrs = resolveTemplateAttrs(template);
|
|
826
|
+
// Stack-qualify keys to match the observed node ids (#1162).
|
|
827
|
+
for (const [logicalId, v] of Object.entries(attrs)) {
|
|
828
|
+
merged[multi ? `${ref.name}::${logicalId}` : logicalId] = v;
|
|
829
|
+
}
|
|
830
|
+
// Resolve internetFacing from live route tables (covers the default VPC),
|
|
831
|
+
// carrying the route-table → IGW evidence so search can justify the match.
|
|
832
|
+
const facing = await liveInternetFacing(ref.region ? ["--region", ref.region] : [], ref.name);
|
|
833
|
+
for (const [logicalId, via] of Object.entries(facing)) {
|
|
834
|
+
const key = multi ? `${ref.name}::${logicalId}` : logicalId;
|
|
835
|
+
merged[key] = { ...merged[key], internetFacing: true, internetFacingVia: via };
|
|
836
|
+
}
|
|
837
|
+
} catch {
|
|
838
|
+
// A stack that isn't deployed yet contributes no live attrs — skip it.
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return merged;
|
|
756
842
|
},
|
|
757
843
|
|
|
758
844
|
mcpTools() {
|