@intentius/chant-lexicon-aws 0.15.2 → 0.16.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 +11 -0
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/integrity.json +4 -4
- package/dist/live-attrs.d.ts +4 -0
- package/dist/live-attrs.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/meta.json +118 -11
- package/dist/op/activities/aws-apply.d.ts +76 -0
- package/dist/op/activities/aws-apply.d.ts.map +1 -0
- package/dist/op/activities/floci.d.ts +11 -19
- package/dist/op/activities/floci.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +5 -1
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/reference-catalog.d.ts +14 -0
- package/dist/reference-catalog.d.ts.map +1 -0
- package/dist/types/index.d.ts +174 -19
- package/package.json +2 -2
- package/src/generated/index.d.ts +174 -19
- package/src/generated/index.ts +13 -2
- package/src/generated/lexicon-aws.json +118 -11
- package/src/live-attrs.test.ts +54 -0
- package/src/live-attrs.ts +40 -0
- package/src/op/activities/aws-apply.test.ts +143 -0
- package/src/op/activities/aws-apply.ts +194 -0
- package/src/op/activities/floci.ts +49 -91
- package/src/op/activities/index.ts +22 -1
- package/src/plugin.ts +13 -0
- package/src/reference-catalog.test.ts +74 -0
- package/src/reference-catalog.ts +82 -0
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
|
-
import { safeHeartbeat, sleep } from "@intentius/chant/op";
|
|
4
|
-
|
|
5
|
-
const execAsync = promisify(exec);
|
|
1
|
+
import { emulatorLifecycle } from "@intentius/chant/op";
|
|
6
2
|
|
|
7
3
|
const DEFAULT_NAME = "chant-floci";
|
|
8
4
|
const DEFAULT_PORT = 4566;
|
|
9
5
|
const DEFAULT_IMAGE = "floci/floci:latest";
|
|
10
6
|
const DEFAULT_REGION = "us-east-1";
|
|
11
7
|
const DEFAULT_READY_SERVICE = "cloudformation";
|
|
8
|
+
const DOCKER_SOCK = ["-v", "/var/run/docker.sock:/var/run/docker.sock"] as const;
|
|
12
9
|
|
|
13
10
|
export interface FlociUpArgs {
|
|
14
11
|
/** Container name. Default: `chant-floci`. */
|
|
@@ -34,30 +31,9 @@ export interface FlociDownArgs {
|
|
|
34
31
|
name?: string;
|
|
35
32
|
}
|
|
36
33
|
|
|
37
|
-
/**
|
|
38
|
-
export function
|
|
39
|
-
return `
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Build the `docker run` command that boots Floci. */
|
|
43
|
-
export function flociRunCommand(args: FlociUpArgs): string {
|
|
44
|
-
const name = args.name ?? DEFAULT_NAME;
|
|
45
|
-
const port = args.port ?? DEFAULT_PORT;
|
|
46
|
-
const image = args.image ?? DEFAULT_IMAGE;
|
|
47
|
-
const parts = ["docker", "run", "-d", "--rm", "--name", name, "-p", `${port}:4566`];
|
|
48
|
-
if (args.dockerSocket) parts.push("-v", "/var/run/docker.sock:/var/run/docker.sock");
|
|
49
|
-
parts.push(image);
|
|
50
|
-
return parts.join(" ");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Build the `docker rm -f` command. */
|
|
54
|
-
export function flociRmCommand(name: string): string {
|
|
55
|
-
return `docker rm -f ${name}`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** The Floci health endpoint URL for a host port. */
|
|
59
|
-
export function flociHealthUrl(port: number): string {
|
|
60
|
-
return `http://localhost:${port}/_localstack/health`;
|
|
34
|
+
/** True once the health body reports the required service (e.g. `"cloudformation"`). */
|
|
35
|
+
export function isFlociReady(healthBody: string, service: string): boolean {
|
|
36
|
+
return healthBody.includes(`"${service}"`);
|
|
61
37
|
}
|
|
62
38
|
|
|
63
39
|
/** The AWS env vars that point the `aws` CLI / SDK at a local Floci endpoint. */
|
|
@@ -70,9 +46,32 @@ export function flociEnv(port: number, region: string): Record<string, string> {
|
|
|
70
46
|
};
|
|
71
47
|
}
|
|
72
48
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
49
|
+
// AWS runs the LocalStack-compatible floci/floci image, so readiness is gated on
|
|
50
|
+
// a service (`cloudformation`) appearing in `/_localstack/health` — unlike the
|
|
51
|
+
// bespoke az/gcp fakes. Shared lifecycle: emulatorLifecycle (#746).
|
|
52
|
+
const flociFor = (readyService: string) =>
|
|
53
|
+
emulatorLifecycle({
|
|
54
|
+
name: DEFAULT_NAME,
|
|
55
|
+
image: DEFAULT_IMAGE,
|
|
56
|
+
containerPort: DEFAULT_PORT,
|
|
57
|
+
healthPath: "/_localstack/health",
|
|
58
|
+
ready: (body) => isFlociReady(body, readyService),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const builders = flociFor(DEFAULT_READY_SERVICE);
|
|
62
|
+
|
|
63
|
+
export const flociExistsCommand = builders.existsCommand;
|
|
64
|
+
export const flociRmCommand = builders.rmCommand;
|
|
65
|
+
export const flociHealthUrl = builders.healthUrl;
|
|
66
|
+
|
|
67
|
+
/** Build the `docker run` command that boots Floci. */
|
|
68
|
+
export function flociRunCommand(args: FlociUpArgs = {}): string {
|
|
69
|
+
return builders.runCommand({
|
|
70
|
+
name: args.name,
|
|
71
|
+
port: args.port,
|
|
72
|
+
image: args.image,
|
|
73
|
+
extraArgs: args.dockerSocket ? [...DOCKER_SOCK] : [],
|
|
74
|
+
});
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
/**
|
|
@@ -81,69 +80,28 @@ export function isFlociReady(healthBody: string, service: string): boolean {
|
|
|
81
80
|
* Idempotent: reuses a running container of the same name. Waits for the health
|
|
82
81
|
* endpoint to report `readyService`, then sets `AWS_ENDPOINT_URL` + test creds in
|
|
83
82
|
* the process environment so a following `nativeApply`/`cfn-deploy` targets the
|
|
84
|
-
* emulator. Env injection assumes the in-process
|
|
83
|
+
* emulator. Env injection assumes the in-process local executor; under a
|
|
85
84
|
* distributed Temporal worker, pass the endpoint explicitly instead.
|
|
86
|
-
*
|
|
87
|
-
* Uses longInfra profile — 20m timeout, heartbeat every poll (the image may pull).
|
|
88
85
|
*/
|
|
89
|
-
export async function flociUp(args: FlociUpArgs, signal?: AbortSignal): Promise<{ endpoint: string }> {
|
|
90
|
-
const name = args.name ?? DEFAULT_NAME;
|
|
91
|
-
const port = args.port ?? DEFAULT_PORT;
|
|
86
|
+
export async function flociUp(args: FlociUpArgs = {}, signal?: AbortSignal): Promise<{ endpoint: string }> {
|
|
92
87
|
const region = args.region ?? DEFAULT_REGION;
|
|
93
88
|
const service = args.readyService ?? DEFAULT_READY_SERVICE;
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
} else {
|
|
108
|
-
await execAsync(flociRunCommand({ ...args, name, port }), { signal });
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const url = flociHealthUrl(port);
|
|
112
|
-
const deadline = Date.now() + timeoutMs;
|
|
113
|
-
let ready = false;
|
|
114
|
-
while (Date.now() < deadline) {
|
|
115
|
-
if (signal?.aborted) throw new Error("flociUp aborted");
|
|
116
|
-
safeHeartbeat({ step: "flociUp", container: name });
|
|
117
|
-
try {
|
|
118
|
-
const { stdout } = await execAsync(`curl -fs ${url}`, { signal });
|
|
119
|
-
if (isFlociReady(stdout, service)) {
|
|
120
|
-
ready = true;
|
|
121
|
-
break;
|
|
122
|
-
}
|
|
123
|
-
} catch {
|
|
124
|
-
// Not up yet (connection refused / non-2xx) — retry.
|
|
125
|
-
}
|
|
126
|
-
await sleep(intervalMs, signal);
|
|
127
|
-
}
|
|
128
|
-
if (!ready) {
|
|
129
|
-
throw new Error(`Floci "${name}" did not become ready within ${timeoutMs}ms`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const env = flociEnv(port, region);
|
|
133
|
-
Object.assign(process.env, env);
|
|
134
|
-
console.log(`Floci ready on ${env.AWS_ENDPOINT_URL} (service: ${service})`);
|
|
135
|
-
return { endpoint: env.AWS_ENDPOINT_URL };
|
|
89
|
+
const { endpoint } = await flociFor(service).up(
|
|
90
|
+
{
|
|
91
|
+
name: args.name,
|
|
92
|
+
port: args.port,
|
|
93
|
+
image: args.image,
|
|
94
|
+
timeoutMs: args.timeoutMs,
|
|
95
|
+
intervalMs: args.intervalMs,
|
|
96
|
+
extraArgs: args.dockerSocket ? [...DOCKER_SOCK] : [],
|
|
97
|
+
},
|
|
98
|
+
signal,
|
|
99
|
+
);
|
|
100
|
+
Object.assign(process.env, flociEnv(args.port ?? DEFAULT_PORT, region));
|
|
101
|
+
return { endpoint };
|
|
136
102
|
}
|
|
137
103
|
|
|
138
|
-
/**
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
*/
|
|
142
|
-
export async function flociDown(args: FlociDownArgs, signal?: AbortSignal): Promise<void> {
|
|
143
|
-
const name = args.name ?? DEFAULT_NAME;
|
|
144
|
-
try {
|
|
145
|
-
await execAsync(flociRmCommand(name), { signal });
|
|
146
|
-
} catch {
|
|
147
|
-
// Already removed (`--rm` on exit, or never started) — treat as success.
|
|
148
|
-
}
|
|
104
|
+
/** Stop and remove the local Floci emulator container (no-op if already gone). */
|
|
105
|
+
export async function flociDown(args: FlociDownArgs = {}, signal?: AbortSignal): Promise<void> {
|
|
106
|
+
return builders.down(args, signal);
|
|
149
107
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AWS Op activities — resolved by the core activity registry when a project's
|
|
3
3
|
* `chant.config.ts` lists the `aws` lexicon. Contributes the local Floci AWS
|
|
4
|
-
* emulator lifecycle (`flociUp`/`flociDown`)
|
|
4
|
+
* emulator lifecycle (`flociUp`/`flociDown`) and the native CloudFormation
|
|
5
|
+
* applier (`awsApply`), which calls the CloudFormation API directly rather than
|
|
6
|
+
* shelling `aws` — the direct twin of `azApply`/`gcpApply`.
|
|
5
7
|
*/
|
|
6
8
|
export {
|
|
7
9
|
flociUp,
|
|
@@ -14,3 +16,22 @@ export {
|
|
|
14
16
|
isFlociReady,
|
|
15
17
|
} from "./floci";
|
|
16
18
|
export type { FlociUpArgs, FlociDownArgs } from "./floci";
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
awsApply,
|
|
22
|
+
awsDelete,
|
|
23
|
+
waitForStackSettled,
|
|
24
|
+
cfnUrl,
|
|
25
|
+
cfnForm,
|
|
26
|
+
capabilityParams,
|
|
27
|
+
xmlField,
|
|
28
|
+
stackStatus,
|
|
29
|
+
stackId,
|
|
30
|
+
cfnErrorMessage,
|
|
31
|
+
isStackMissing,
|
|
32
|
+
isNoUpdates,
|
|
33
|
+
isSuccessStatus,
|
|
34
|
+
isFailureStatus,
|
|
35
|
+
isTerminalStatus,
|
|
36
|
+
} from "./aws-apply";
|
|
37
|
+
export type { AwsApplyArgs, AwsHttp } from "./aws-apply";
|
package/src/plugin.ts
CHANGED
|
@@ -14,6 +14,8 @@ import { readFileSync } from "fs";
|
|
|
14
14
|
import { join, dirname } from "path";
|
|
15
15
|
import { fileURLToPath } from "url";
|
|
16
16
|
import { awsSerializer } from "./serializer";
|
|
17
|
+
import { awsReferenceCatalog } from "./reference-catalog";
|
|
18
|
+
import { resolveTemplateAttrs } from "./live-attrs";
|
|
17
19
|
import { CFParser } from "./import/parser";
|
|
18
20
|
import { CFGenerator } from "./import/generator";
|
|
19
21
|
import { parseStackTemplate } from "./import/live-export";
|
|
@@ -31,6 +33,8 @@ export const awsPlugin: LexiconPlugin = {
|
|
|
31
33
|
serializer: awsSerializer,
|
|
32
34
|
// Audit rule metadata for this lexicon's WAW* checks (#687).
|
|
33
35
|
auditCatalog: () => awsAuditCatalog,
|
|
36
|
+
// Live edge reconstruction for `chant graph --live` (#778).
|
|
37
|
+
referenceCatalog: awsReferenceCatalog,
|
|
34
38
|
|
|
35
39
|
lintRules(): LintRule[] {
|
|
36
40
|
const rulesDir = join(dirname(fileURLToPath(import.meta.url)), "lint", "rules");
|
|
@@ -580,6 +584,15 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
|
|
|
580
584
|
return parseStackTemplate(parsed.TemplateBody, options.selector, options.owned);
|
|
581
585
|
},
|
|
582
586
|
|
|
587
|
+
// Live attribute enrichment for `chant graph --live` edge reconstruction (#784).
|
|
588
|
+
// describe-stack-resources is too thin; the deployed template (exportResources)
|
|
589
|
+
// carries the references. Resolve its `{Ref}`/`{Fn::GetAtt}` intrinsics to bare
|
|
590
|
+
// logical ids so the reference resolver matches them.
|
|
591
|
+
async enrichLiveAttrs(options: { environment: string; owned?: boolean }): Promise<Record<string, Record<string, unknown>>> {
|
|
592
|
+
const template = await this.exportResources!({ environment: options.environment, owned: options.owned });
|
|
593
|
+
return resolveTemplateAttrs(template);
|
|
594
|
+
},
|
|
595
|
+
|
|
583
596
|
mcpTools() {
|
|
584
597
|
return [
|
|
585
598
|
{
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { reconstructEdges, containmentGroups } from "@intentius/chant/graph-refs";
|
|
3
|
+
import type { IRNode } from "@intentius/chant/graph-ir";
|
|
4
|
+
import { awsReferenceCatalog } from "./reference-catalog";
|
|
5
|
+
|
|
6
|
+
const n = (id: string, kind: string, attrs: Record<string, unknown>): IRNode => ({ id, kind, lexicon: "aws", attrs });
|
|
7
|
+
|
|
8
|
+
// A canonical 3-tier VPC (managed subgraph): internet → ALB (public subnets) →
|
|
9
|
+
// ECS service (private subnets) → RDS, with SG attachments. Attributes are the
|
|
10
|
+
// per-resource describe/export shape the catalog targets.
|
|
11
|
+
const nodes: IRNode[] = [
|
|
12
|
+
n("vpc", "AWS::EC2::VPC", { VpcId: "vpc-1" }),
|
|
13
|
+
n("pubA", "AWS::EC2::Subnet", { SubnetId: "subnet-pa", VpcId: "vpc-1" }),
|
|
14
|
+
n("pubB", "AWS::EC2::Subnet", { SubnetId: "subnet-pb", VpcId: "vpc-1" }),
|
|
15
|
+
n("privA", "AWS::EC2::Subnet", { SubnetId: "subnet-qa", VpcId: "vpc-1" }),
|
|
16
|
+
n("privB", "AWS::EC2::Subnet", { SubnetId: "subnet-qb", VpcId: "vpc-1" }),
|
|
17
|
+
n("albSg", "AWS::EC2::SecurityGroup", { GroupId: "sg-alb", VpcId: "vpc-1" }),
|
|
18
|
+
n("appSg", "AWS::EC2::SecurityGroup", { GroupId: "sg-app", VpcId: "vpc-1", IpPermissions: [{ UserIdGroupPairs: [{ GroupId: "sg-alb" }] }] }),
|
|
19
|
+
n("alb", "AWS::ElasticLoadBalancingV2::LoadBalancer", { LoadBalancerArn: "arn-alb", AvailabilityZones: [{ SubnetId: "subnet-pa" }, { SubnetId: "subnet-pb" }], SecurityGroups: ["sg-alb"] }),
|
|
20
|
+
n("tg", "AWS::ElasticLoadBalancingV2::TargetGroup", { TargetGroupArn: "arn-tg", VpcId: "vpc-1" }),
|
|
21
|
+
n("listener", "AWS::ElasticLoadBalancingV2::Listener", { LoadBalancerArn: "arn-alb", DefaultActions: [{ TargetGroupArn: "arn-tg" }] }),
|
|
22
|
+
n("cluster", "AWS::ECS::Cluster", { ClusterArn: "arn-cl" }),
|
|
23
|
+
n("taskdef", "AWS::ECS::TaskDefinition", { TaskDefinitionArn: "arn-td" }),
|
|
24
|
+
n("svc", "AWS::ECS::Service", { ServiceArn: "arn-svc", ClusterArn: "arn-cl", TaskDefinition: "arn-td", LoadBalancers: [{ TargetGroupArn: "arn-tg" }], NetworkConfiguration: { AwsvpcConfiguration: { Subnets: ["subnet-qa", "subnet-qb"], SecurityGroups: ["sg-app"] } } }),
|
|
25
|
+
n("rds", "AWS::RDS::DBInstance", { DBInstanceArn: "arn-rds", DBSubnetGroup: { Subnets: [{ SubnetIdentifier: "subnet-qa" }, { SubnetIdentifier: "subnet-qb" }] }, VpcSecurityGroups: [{ VpcSecurityGroupId: "sg-app" }] }),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
describe("awsReferenceCatalog — golden 3-tier VPC", () => {
|
|
29
|
+
const { edges, containment, dangling } = reconstructEdges(nodes, awsReferenceCatalog);
|
|
30
|
+
const hasEdge = (from: string, to: string, via?: string) =>
|
|
31
|
+
edges.some((e) => e.from === from && e.to === to && (via === undefined || e.viaAttr === via));
|
|
32
|
+
const hasCont = (child: string, parent: string) =>
|
|
33
|
+
containment.some((c) => c.child === child && c.parent === parent);
|
|
34
|
+
|
|
35
|
+
it("reconstructs the topology edges", () => {
|
|
36
|
+
expect(hasEdge("svc", "cluster", "in cluster")).toBe(true);
|
|
37
|
+
expect(hasEdge("svc", "taskdef", "runs")).toBe(true);
|
|
38
|
+
expect(hasEdge("svc", "tg", "registered in")).toBe(true);
|
|
39
|
+
expect(hasEdge("svc", "appSg", "sg")).toBe(true);
|
|
40
|
+
expect(hasEdge("listener", "alb", "on")).toBe(true);
|
|
41
|
+
expect(hasEdge("listener", "tg", "forwards to")).toBe(true);
|
|
42
|
+
expect(hasEdge("alb", "albSg", "sg")).toBe(true);
|
|
43
|
+
expect(hasEdge("appSg", "albSg", "allows")).toBe(true);
|
|
44
|
+
expect(hasEdge("rds", "appSg", "sg")).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("reconstructs network containment (→ #779 boundaries), not as edges", () => {
|
|
48
|
+
for (const s of ["pubA", "pubB", "privA", "privB", "albSg", "appSg", "tg"]) expect(hasCont(s, "vpc")).toBe(true);
|
|
49
|
+
expect(hasCont("alb", "pubA")).toBe(true);
|
|
50
|
+
expect(hasCont("alb", "pubB")).toBe(true);
|
|
51
|
+
expect(hasCont("svc", "privA")).toBe(true);
|
|
52
|
+
expect(hasCont("rds", "privB")).toBe(true);
|
|
53
|
+
// containment is never an edge
|
|
54
|
+
expect(edges.some((e) => e.to === "vpc")).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("has no dangling references — every target is in the observed set", () => {
|
|
58
|
+
expect(dangling).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("reference edges point holder → referenced", () => {
|
|
62
|
+
// e.g. the service references the cluster, not the reverse
|
|
63
|
+
expect(hasEdge("cluster", "svc")).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("containment groups nest VPC → subnets → resources (#779)", () => {
|
|
67
|
+
const groups = containmentGroups(containment);
|
|
68
|
+
// the VPC contains its subnets, SGs, and target group
|
|
69
|
+
expect(groups.vpc).toEqual(expect.arrayContaining(["pubA", "pubB", "privA", "privB", "albSg", "appSg", "tg"]));
|
|
70
|
+
// public subnets contain the ALB; private subnets contain the service + RDS
|
|
71
|
+
expect(groups.pubA).toContain("alb");
|
|
72
|
+
expect(groups.privA).toEqual(expect.arrayContaining(["svc", "rds"]));
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ReferenceCatalog } from "@intentius/chant/lexicon";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AWS reference catalog (#778, epic #776 v1) — how observed AWS resources
|
|
5
|
+
* reference each other, so `chant graph --live` can reconstruct the topology.
|
|
6
|
+
*
|
|
7
|
+
* Keyed to the per-resource describe/export attribute shape (`VpcId`,
|
|
8
|
+
* `SecurityGroups[].GroupId`, `TargetGroupArn`, …), not the thin
|
|
9
|
+
* `describe-stack-resources` metadata. Enough to draw the canonical 3-tier VPC
|
|
10
|
+
* (internet → ALB → ECS service → RDS, with SG attachments). `containment`
|
|
11
|
+
* relations (subnet ∈ VPC) feed #779's boundary boxes; `reference` relations are
|
|
12
|
+
* edges.
|
|
13
|
+
*/
|
|
14
|
+
export const awsReferenceCatalog: ReferenceCatalog = {
|
|
15
|
+
identities: [
|
|
16
|
+
{ kind: "AWS::EC2::VPC", ids: ["VpcId"] },
|
|
17
|
+
{ kind: "AWS::EC2::Subnet", ids: ["SubnetId"] },
|
|
18
|
+
{ kind: "AWS::EC2::SecurityGroup", ids: ["GroupId"] },
|
|
19
|
+
{ kind: "AWS::EC2::Instance", ids: ["InstanceId"] },
|
|
20
|
+
{ kind: "AWS::EC2::InternetGateway", ids: ["InternetGatewayId"] },
|
|
21
|
+
{ kind: "AWS::EC2::NatGateway", ids: ["NatGatewayId"] },
|
|
22
|
+
{ kind: "AWS::EC2::RouteTable", ids: ["RouteTableId"] },
|
|
23
|
+
{ kind: "AWS::ElasticLoadBalancingV2::LoadBalancer", ids: ["LoadBalancerArn", "DNSName"] },
|
|
24
|
+
{ kind: "AWS::ElasticLoadBalancingV2::TargetGroup", ids: ["TargetGroupArn"] },
|
|
25
|
+
{ kind: "AWS::ECS::Cluster", ids: ["ClusterArn", "ClusterName"] },
|
|
26
|
+
{ kind: "AWS::ECS::Service", ids: ["ServiceArn"] },
|
|
27
|
+
{ kind: "AWS::ECS::TaskDefinition", ids: ["TaskDefinitionArn"] },
|
|
28
|
+
{ kind: "AWS::RDS::DBInstance", ids: ["DBInstanceArn", "Endpoint.Address"] },
|
|
29
|
+
],
|
|
30
|
+
refs: [
|
|
31
|
+
// ── containment (→ boundary boxes, #779) ──
|
|
32
|
+
{ from: "AWS::EC2::Subnet", path: "VpcId", targetKind: "AWS::EC2::VPC", relation: "containment", label: "in VPC" },
|
|
33
|
+
{ from: "AWS::EC2::SecurityGroup", path: "VpcId", targetKind: "AWS::EC2::VPC", relation: "containment", label: "in VPC" },
|
|
34
|
+
{ from: "AWS::EC2::RouteTable", path: "VpcId", targetKind: "AWS::EC2::VPC", relation: "containment", label: "in VPC" },
|
|
35
|
+
{ from: "AWS::EC2::Instance", path: "SubnetId", targetKind: "AWS::EC2::Subnet", relation: "containment", label: "in subnet" },
|
|
36
|
+
{ from: "AWS::EC2::NatGateway", path: "SubnetId", targetKind: "AWS::EC2::Subnet", relation: "containment", label: "in subnet" },
|
|
37
|
+
{ from: "AWS::ElasticLoadBalancingV2::LoadBalancer", path: "AvailabilityZones[].SubnetId", targetKind: "AWS::EC2::Subnet", relation: "containment", label: "in subnet" },
|
|
38
|
+
{ from: "AWS::ElasticLoadBalancingV2::TargetGroup", path: "VpcId", targetKind: "AWS::EC2::VPC", relation: "containment", label: "in VPC" },
|
|
39
|
+
{ from: "AWS::ECS::Service", path: "NetworkConfiguration.AwsvpcConfiguration.Subnets[]", targetKind: "AWS::EC2::Subnet", relation: "containment", label: "in subnet" },
|
|
40
|
+
{ from: "AWS::RDS::DBInstance", path: "DBSubnetGroup.Subnets[].SubnetIdentifier", targetKind: "AWS::EC2::Subnet", relation: "containment", label: "in subnet" },
|
|
41
|
+
|
|
42
|
+
// ── references (→ edges) ──
|
|
43
|
+
{ from: "AWS::EC2::Instance", path: "SecurityGroups[].GroupId", targetKind: "AWS::EC2::SecurityGroup", relation: "reference", label: "sg" },
|
|
44
|
+
{ from: "AWS::EC2::SecurityGroup", path: "IpPermissions[].UserIdGroupPairs[].GroupId", targetKind: "AWS::EC2::SecurityGroup", relation: "reference", label: "allows" },
|
|
45
|
+
{ from: "AWS::EC2::Route", path: "GatewayId", targetKind: "AWS::EC2::InternetGateway", relation: "reference", label: "via" },
|
|
46
|
+
{ from: "AWS::EC2::Route", path: "NatGatewayId", targetKind: "AWS::EC2::NatGateway", relation: "reference", label: "via" },
|
|
47
|
+
{ from: "AWS::ElasticLoadBalancingV2::LoadBalancer", path: "SecurityGroups[]", targetKind: "AWS::EC2::SecurityGroup", relation: "reference", label: "sg" },
|
|
48
|
+
{ from: "AWS::ElasticLoadBalancingV2::Listener", path: "LoadBalancerArn", targetKind: "AWS::ElasticLoadBalancingV2::LoadBalancer", relation: "reference", label: "on" },
|
|
49
|
+
{ from: "AWS::ElasticLoadBalancingV2::Listener", path: "DefaultActions[].TargetGroupArn", targetKind: "AWS::ElasticLoadBalancingV2::TargetGroup", relation: "reference", label: "forwards to" },
|
|
50
|
+
{ from: "AWS::ECS::Service", path: "ClusterArn", targetKind: "AWS::ECS::Cluster", relation: "reference", label: "in cluster" },
|
|
51
|
+
{ from: "AWS::ECS::Service", path: "TaskDefinition", targetKind: "AWS::ECS::TaskDefinition", relation: "reference", label: "runs" },
|
|
52
|
+
{ from: "AWS::ECS::Service", path: "LoadBalancers[].TargetGroupArn", targetKind: "AWS::ElasticLoadBalancingV2::TargetGroup", relation: "reference", label: "registered in" },
|
|
53
|
+
{ from: "AWS::ECS::Service", path: "NetworkConfiguration.AwsvpcConfiguration.SecurityGroups[]", targetKind: "AWS::EC2::SecurityGroup", relation: "reference", label: "sg" },
|
|
54
|
+
{ from: "AWS::RDS::DBInstance", path: "VpcSecurityGroups[].VpcSecurityGroupId", targetKind: "AWS::EC2::SecurityGroup", relation: "reference", label: "sg" },
|
|
55
|
+
|
|
56
|
+
// ── CloudFormation *template* property paths (#784) ──
|
|
57
|
+
// `enrichLiveAttrs` resolves the deployed template's `{Ref}`/`{Fn::GetAtt}`
|
|
58
|
+
// intrinsics to bare logical ids (= node ids), so these match by node id; the
|
|
59
|
+
// property names are the CFN template shape (SecurityGroupIds, Cluster, …),
|
|
60
|
+
// which differs from the describe/SDK shape above. No targetKind needed —
|
|
61
|
+
// logical ids are unique.
|
|
62
|
+
{ from: "AWS::EC2::Subnet", path: "VpcId", relation: "containment", label: "in VPC" },
|
|
63
|
+
{ from: "AWS::EC2::SecurityGroup", path: "VpcId", relation: "containment", label: "in VPC" },
|
|
64
|
+
{ from: "AWS::EC2::RouteTable", path: "VpcId", relation: "containment", label: "in VPC" },
|
|
65
|
+
{ from: "AWS::EC2::Instance", path: "SubnetId", relation: "containment", label: "in subnet" },
|
|
66
|
+
{ from: "AWS::EC2::Instance", path: "SecurityGroupIds[]", relation: "reference", label: "sg" },
|
|
67
|
+
{ from: "AWS::EC2::NatGateway", path: "SubnetId", relation: "containment", label: "in subnet" },
|
|
68
|
+
{ from: "AWS::EC2::Route", path: "RouteTableId", relation: "reference", label: "in" },
|
|
69
|
+
{ from: "AWS::EC2::Route", path: "GatewayId", relation: "reference", label: "via" },
|
|
70
|
+
{ from: "AWS::EC2::Route", path: "NatGatewayId", relation: "reference", label: "via" },
|
|
71
|
+
{ from: "AWS::ElasticLoadBalancingV2::LoadBalancer", path: "Subnets[]", relation: "containment", label: "in subnet" },
|
|
72
|
+
{ from: "AWS::ElasticLoadBalancingV2::LoadBalancer", path: "SecurityGroups[]", relation: "reference", label: "sg" },
|
|
73
|
+
{ from: "AWS::ElasticLoadBalancingV2::TargetGroup", path: "VpcId", relation: "containment", label: "in VPC" },
|
|
74
|
+
{ from: "AWS::ElasticLoadBalancingV2::Listener", path: "LoadBalancerArn", relation: "reference", label: "on" },
|
|
75
|
+
{ from: "AWS::ElasticLoadBalancingV2::Listener", path: "DefaultActions[].TargetGroupArn", relation: "reference", label: "forwards to" },
|
|
76
|
+
{ from: "AWS::ECS::Service", path: "Cluster", relation: "reference", label: "in cluster" },
|
|
77
|
+
{ from: "AWS::ECS::Service", path: "TaskDefinition", relation: "reference", label: "runs" },
|
|
78
|
+
{ from: "AWS::ECS::Service", path: "LoadBalancers[].TargetGroupArn", relation: "reference", label: "registered in" },
|
|
79
|
+
{ from: "AWS::RDS::DBInstance", path: "DBSubnetGroupName", relation: "reference", label: "subnets" },
|
|
80
|
+
{ from: "AWS::RDS::DBInstance", path: "VPCSecurityGroups[]", relation: "reference", label: "sg" },
|
|
81
|
+
],
|
|
82
|
+
};
|