@intentius/chant-lexicon-aws 0.124.52 → 0.124.54
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 -0
- package/dist/codegen/generate-lexicon.d.ts.map +1 -1
- package/dist/codegen/generate-lexicon.js +1 -0
- package/dist/components/__tests__/mock-cloud-executor.d.ts +4 -0
- package/dist/components/__tests__/mock-cloud-executor.d.ts.map +1 -1
- package/dist/components/__tests__/mock-cloud-executor.js +18 -1
- package/dist/components/apply.d.ts +44 -2
- package/dist/components/apply.d.ts.map +1 -1
- package/dist/components/apply.js +260 -29
- package/dist/components/cfn-adopt.d.ts +99 -0
- package/dist/components/cfn-adopt.d.ts.map +1 -0
- package/dist/components/cfn-adopt.js +278 -0
- package/dist/components/cfn-imports.d.ts +84 -0
- package/dist/components/cfn-imports.d.ts.map +1 -0
- package/dist/components/cfn-imports.js +239 -0
- package/dist/components/cloud-executor.d.ts +31 -2
- package/dist/components/cloud-executor.d.ts.map +1 -1
- package/dist/components/cloud-executor.js +64 -2
- package/dist/importable.d.ts +30 -0
- package/dist/importable.d.ts.map +1 -0
- package/dist/importable.js +72 -0
- package/dist/index-identity.d.ts +19 -0
- package/dist/index-identity.d.ts.map +1 -0
- package/dist/index-identity.js +79 -0
- package/dist/index-import.d.ts +17 -0
- package/dist/index-import.d.ts.map +1 -0
- package/dist/index-import.js +115 -0
- package/dist/integrity.json +3 -3
- package/dist/listed-types.d.ts +11 -0
- package/dist/listed-types.d.ts.map +1 -0
- package/dist/listed-types.js +52 -0
- package/dist/manifest.json +1 -1
- package/dist/meta.json +13206 -1237
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +9 -0
- package/dist/reference-catalog.d.ts.map +1 -1
- package/dist/reference-catalog.js +62 -20
- package/dist/spec/fetch.d.ts +5 -0
- package/dist/spec/fetch.d.ts.map +1 -1
- package/dist/spec/parse.d.ts +8 -0
- package/dist/spec/parse.d.ts.map +1 -1
- package/dist/spec/parse.js +3 -0
- package/package.json +2 -2
- package/src/codegen/generate-lexicon.ts +3 -0
- package/src/components/__tests__/mock-cloud-executor.ts +20 -1
- package/src/components/apply.ts +282 -29
- package/src/components/cfn-adopt.test.ts +284 -0
- package/src/components/cfn-adopt.ts +340 -0
- package/src/components/cfn-exports-in-use.test.ts +146 -0
- package/src/components/cfn-imports-preview.test.ts +178 -0
- package/src/components/cfn-imports-three-stacks.test.ts +107 -0
- package/src/components/cfn-imports.test.ts +213 -0
- package/src/components/cfn-imports.ts +246 -0
- package/src/components/cfn-plan-shape-conformance.test.ts +83 -0
- package/src/components/cloud-executor.ts +102 -5
- package/src/components/demo-composed-stacks-floci.e2e.test.ts +235 -0
- package/src/derived-edges.test.ts +142 -0
- package/src/generated/lexicon-aws.json +13206 -1237
- package/src/importable.test.ts +86 -0
- package/src/importable.ts +83 -0
- package/src/index-identity.test.ts +134 -0
- package/src/index-identity.ts +98 -0
- package/src/index-import.test.ts +242 -0
- package/src/index-import.ts +140 -0
- package/src/index-search.test.ts +87 -0
- package/src/listed-types.test.ts +97 -0
- package/src/listed-types.ts +56 -0
- package/src/plugin.ts +10 -0
- package/src/reference-catalog.ts +63 -20
- package/src/spec/fetch.ts +5 -0
- package/src/spec/parse.test.ts +15 -0
- package/src/spec/parse.ts +12 -0
- package/src/testdata/indexed/identity.listing.json +65 -0
- package/src/testdata/indexed/import.listing.json +126 -0
- package/src/testdata/indexed/listing.json +69 -0
- package/src/testdata/indexed/topology.listing.json +350 -0
- package/src/testdata/three-cloud/dns.graph.json +336 -0
- package/src/three-cloud-parity.test.ts +49 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `examples/demo-composed-stacks` through the pull-request loop against
|
|
3
|
+
* Floci (#4448): each scenario from a fresh base, through `chant
|
|
4
|
+
* components pr-plan`, one approval and `chant components pr-apply`, as
|
|
5
|
+
* #4430 ran them by hand.
|
|
6
|
+
*
|
|
7
|
+
* - The edit act: every value known at plan, one approval, pr-apply exits 0,
|
|
8
|
+
* and any round after an upstream applied is covered by that approval.
|
|
9
|
+
* - The create act: app's read of network's new `SubnetBId` is known after
|
|
10
|
+
* apply, one approval, pr-apply exits 0, the round that plans app again is
|
|
11
|
+
* `covered` with `filledInputs` naming the id, and app's parameter holds
|
|
12
|
+
* network's real subnet id, and so does edge's, through app's
|
|
13
|
+
* `WorkerSubnetId` output.
|
|
14
|
+
* - The imports act (#4466): the stacks read each other only through
|
|
15
|
+
* `Fn::ImportValue`. pr-plan orders them by the imports and lists both
|
|
16
|
+
* as known after apply, one approval covers both rounds, and edge holds
|
|
17
|
+
* the id of the interface app created in network's new subnet.
|
|
18
|
+
*
|
|
19
|
+
* Each act copies the example with its stack names and SSM names made the
|
|
20
|
+
* run's own, builds the templates with the example's `npm run build`, and
|
|
21
|
+
* deletes its stacks after. Set CHANT_FLOCI_ENDPOINT to a running Floci, for
|
|
22
|
+
* example `docker run -d -p 4611:4566 floci/floci` and
|
|
23
|
+
* `CHANT_FLOCI_ENDPOINT=http://localhost:4611`; the `aws` CLI must be on
|
|
24
|
+
* PATH. Skipped otherwise, with the reason named.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { execFileSync } from "node:child_process";
|
|
28
|
+
import { join } from "node:path";
|
|
29
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
30
|
+
import type { PrMember, PrReport } from "@intentius/chant/pr-loop";
|
|
31
|
+
import { openScenarioLoop, type ScenarioLoop } from "@intentius/chant-test-utils/pr-loop-harness";
|
|
32
|
+
import { awsPlugin } from "../plugin";
|
|
33
|
+
|
|
34
|
+
const REPO = join(import.meta.dirname, "..", "..", "..", "..");
|
|
35
|
+
const EXAMPLE = join(REPO, "examples", "demo-composed-stacks");
|
|
36
|
+
const TIMEOUT = 900_000;
|
|
37
|
+
const STACKS = ["network", "app", "edge"];
|
|
38
|
+
|
|
39
|
+
const endpoint = process.env.CHANT_FLOCI_ENDPOINT?.replace(/\/+$/, "");
|
|
40
|
+
function hasAwsCli(): boolean {
|
|
41
|
+
try {
|
|
42
|
+
execFileSync("aws", ["--version"], { stdio: "ignore" });
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const missing = !endpoint
|
|
49
|
+
? "CHANT_FLOCI_ENDPOINT is not set (for example `docker run -d -p 4611:4566 floci/floci` and CHANT_FLOCI_ENDPOINT=http://localhost:4611)"
|
|
50
|
+
: !hasAwsCli()
|
|
51
|
+
? "the aws CLI is not on PATH"
|
|
52
|
+
: undefined;
|
|
53
|
+
|
|
54
|
+
const aws = (...args: string[]): unknown => JSON.parse(execFileSync("aws", ["--endpoint-url", endpoint!, "--output", "json", ...args], { encoding: "utf8" }) || "{}");
|
|
55
|
+
|
|
56
|
+
interface Stack {
|
|
57
|
+
Outputs?: { OutputKey: string; OutputValue: string }[];
|
|
58
|
+
Parameters?: { ParameterKey: string; ParameterValue: string }[];
|
|
59
|
+
}
|
|
60
|
+
const stack = (name: string): Stack => (aws("cloudformation", "describe-stacks", "--stack-name", name) as { Stacks: Stack[] }).Stacks[0]!;
|
|
61
|
+
const ssm = (name: string): string => (aws("ssm", "get-parameter", "--name", name) as { Parameter: { Value: string } }).Parameter.Value;
|
|
62
|
+
const byComponent = (r: PrReport): Record<string, PrMember> => Object.fromEntries(r.members.map((m) => [m.component, m]));
|
|
63
|
+
// The report lists members by the change set's order, which the report does
|
|
64
|
+
// not define as dependency order; `selection.waves` holds that.
|
|
65
|
+
const statuses = (r: PrReport): string[][] => r.members.map((m) => [m.component, m.status]).sort(([a], [b]) => a!.localeCompare(b!));
|
|
66
|
+
const total = (r: PrReport, action: "create" | "update" | "replace" | "delete"): number => r.members.reduce((n, m) => n + m.counts[action], 0);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The example's names made this run's own: the CloudFormation stack each
|
|
70
|
+
* component deploys (and `stacks[]`, which names the same stacks), and the
|
|
71
|
+
* SSM parameter paths and tags. Component names, and so `stackOutput` reads,
|
|
72
|
+
* stay as they are.
|
|
73
|
+
*/
|
|
74
|
+
function rewrite(id: string) {
|
|
75
|
+
return (rel: string, text: string): string => {
|
|
76
|
+
let out = text.replaceAll("chant-compose", id);
|
|
77
|
+
if (rel === "chant.config.ts") out = out.replace(/name: "(network|app|edge)", src:/g, `name: "${id}-$1", src:`);
|
|
78
|
+
if (rel.endsWith(".component.ts")) out = out.replace(/(kind: "cfn-deploy",\s*stack: ")(network|app|edge)"/g, `$1${id}-$2"`);
|
|
79
|
+
return out;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function actSuite(scenario: string, tests: (ctx: { loop: () => ScenarioLoop; id: string }) => void): void {
|
|
84
|
+
describe.skipIf(missing !== undefined)(`demo-composed-stacks, ${scenario}, on Floci${missing ? `: skipped, ${missing}` : ""}`, () => {
|
|
85
|
+
const id = `cs-${scenario.replace("chant-compose-", "")}-${Date.now().toString(36)}`;
|
|
86
|
+
let loop: ScenarioLoop | undefined;
|
|
87
|
+
|
|
88
|
+
beforeAll(async () => {
|
|
89
|
+
loop = await openScenarioLoop({
|
|
90
|
+
example: EXAMPLE,
|
|
91
|
+
repo: REPO,
|
|
92
|
+
plugins: [awsPlugin],
|
|
93
|
+
env: "demo",
|
|
94
|
+
rewrite: rewrite(id),
|
|
95
|
+
// The templates, as the pipeline's "Build the templates" step builds them.
|
|
96
|
+
prepare: (dir) => void execFileSync("npm", ["run", "build"], { cwd: dir, encoding: "utf8" }),
|
|
97
|
+
processEnv: { AWS_ENDPOINT_URL: endpoint!, AWS_ACCESS_KEY_ID: "test", AWS_SECRET_ACCESS_KEY: "test", AWS_REGION: "us-east-1", AWS_DEFAULT_REGION: "us-east-1" },
|
|
98
|
+
});
|
|
99
|
+
// The base, as the demo host applies it before a scenario.
|
|
100
|
+
expect(await loop.applyBase()).toBe(0);
|
|
101
|
+
// describe-stacks throws on a stack the base did not create.
|
|
102
|
+
for (const s of STACKS) stack(`${id}-${s}`);
|
|
103
|
+
}, TIMEOUT);
|
|
104
|
+
|
|
105
|
+
afterAll(async () => {
|
|
106
|
+
await loop?.close();
|
|
107
|
+
for (const s of [...STACKS].reverse()) {
|
|
108
|
+
try {
|
|
109
|
+
aws("cloudformation", "delete-stack", "--stack-name", `${id}-${s}`);
|
|
110
|
+
} catch {
|
|
111
|
+
// never created
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
tests({ loop: () => loop!, id });
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
actSuite("chant-compose-edit", ({ loop, id }) => {
|
|
121
|
+
test("one pr-plan digest over the three stacks, every value known at plan", async () => {
|
|
122
|
+
const { code, report: plan } = await loop().open(1, "chant-compose-edit");
|
|
123
|
+
expect(code).toBe(0);
|
|
124
|
+
expect(plan.status).toBe("planned");
|
|
125
|
+
expect([...plan.selection.changed].sort()).toEqual(["app", "edge", "network"]);
|
|
126
|
+
expect(plan.selection.waves).toEqual([["network"], ["app"], ["edge"]]);
|
|
127
|
+
expect(statuses(plan)).toEqual([
|
|
128
|
+
["app", "planned"],
|
|
129
|
+
["edge", "planned"],
|
|
130
|
+
["network", "planned"],
|
|
131
|
+
]);
|
|
132
|
+
for (const m of plan.members) expect(m.unknownInputs).toBeUndefined();
|
|
133
|
+
expect([total(plan, "create"), total(plan, "update"), total(plan, "delete")]).toEqual([3, 3, 0]);
|
|
134
|
+
expect(plan.changeSet.digest).toBe(plan.digest);
|
|
135
|
+
expect(plan.approval).toEqual({ status: "pending" });
|
|
136
|
+
}, TIMEOUT);
|
|
137
|
+
|
|
138
|
+
test("one approval, and pr-apply exits 0: no round asks for an approval of its own", async () => {
|
|
139
|
+
const plan = loop().report("plan");
|
|
140
|
+
await loop().approve(1, "pr-apply", plan.digest);
|
|
141
|
+
expect(await loop().merge(1)).toBe(0);
|
|
142
|
+
const applied = loop().report("apply");
|
|
143
|
+
expect(applied.digest).toBe(plan.digest);
|
|
144
|
+
expect(applied.status).toBe("applied");
|
|
145
|
+
for (const r of applied.rounds ?? []) {
|
|
146
|
+
expect(r.status).toBe("applied");
|
|
147
|
+
expect(r.covered?.digest).toBe(plan.digest);
|
|
148
|
+
}
|
|
149
|
+
expect(statuses(applied)).toEqual([
|
|
150
|
+
["app", "applied"],
|
|
151
|
+
["edge", "applied"],
|
|
152
|
+
["network", "applied"],
|
|
153
|
+
]);
|
|
154
|
+
expect(ssm(`/${id}/upstream`)).toBe("http://app:8090");
|
|
155
|
+
expect(ssm(`/${id}/admin-upstream`)).toBe("http://app:9000");
|
|
156
|
+
}, TIMEOUT);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
actSuite("chant-compose-create", ({ loop, id }) => {
|
|
160
|
+
test("one pr-plan digest, app's and edge's reads of the new subnet id known after apply", async () => {
|
|
161
|
+
const { code, report: plan } = await loop().open(1, "chant-compose-create");
|
|
162
|
+
expect(code).toBe(0);
|
|
163
|
+
expect(plan.status).toBe("planned");
|
|
164
|
+
const m = byComponent(plan);
|
|
165
|
+
expect(m.app!.unknownInputs).toEqual(["network.SubnetBId"]);
|
|
166
|
+
expect(m.app!.reads?.find((r) => r.upstream === "network")?.unknown).toEqual(["SubnetBId"]);
|
|
167
|
+
// edge reads app's ListenerPort, which the preview knows, and app's WorkerSubnetId, which it does not.
|
|
168
|
+
expect(m.edge!.unknownInputs).toEqual(["app.WorkerSubnetId"]);
|
|
169
|
+
expect(m.edge!.reads?.find((r) => r.upstream === "app")?.unknown).toEqual(["WorkerSubnetId"]);
|
|
170
|
+
expect([total(plan, "create"), total(plan, "update"), total(plan, "delete")]).toEqual([3, 1, 0]);
|
|
171
|
+
expect(plan.approval).toEqual({ status: "pending" });
|
|
172
|
+
}, TIMEOUT);
|
|
173
|
+
|
|
174
|
+
test("one approval, and pr-apply exits 0: the round is covered with filledInputs, and app and edge hold the real id", async () => {
|
|
175
|
+
const plan = loop().report("plan");
|
|
176
|
+
await loop().approve(1, "pr-apply", plan.digest);
|
|
177
|
+
expect(await loop().merge(1)).toBe(0);
|
|
178
|
+
const applied = loop().report("apply");
|
|
179
|
+
expect(applied.digest).toBe(plan.digest);
|
|
180
|
+
expect(applied.status).toBe("applied");
|
|
181
|
+
const rounds = applied.rounds ?? [];
|
|
182
|
+
for (const r of rounds) expect(r.status).toBe("applied");
|
|
183
|
+
const filled = rounds.find((r) => r.components.includes("app"));
|
|
184
|
+
expect(filled?.covered?.digest).toBe(plan.digest);
|
|
185
|
+
expect(filled?.covered?.filledInputs).toContain("network.SubnetBId");
|
|
186
|
+
expect(filled?.notCovered).toBeUndefined();
|
|
187
|
+
const edgeRound = rounds.find((r) => r.components.includes("edge"));
|
|
188
|
+
expect(edgeRound?.covered?.digest).toBe(plan.digest);
|
|
189
|
+
expect(edgeRound?.covered?.filledInputs).toContain("app.WorkerSubnetId");
|
|
190
|
+
|
|
191
|
+
const subnetB = stack(`${id}-network`).Outputs?.find((o) => o.OutputKey === "SubnetBId")?.OutputValue;
|
|
192
|
+
expect(subnetB).toMatch(/^subnet-/);
|
|
193
|
+
expect(stack(`${id}-app`).Parameters?.find((p) => p.ParameterKey === "appSubnetBId")?.ParameterValue).toBe(subnetB);
|
|
194
|
+
expect(stack(`${id}-edge`).Parameters?.find((p) => p.ParameterKey === "edgeWorkerSubnetId")?.ParameterValue).toBe(subnetB);
|
|
195
|
+
expect(ssm(`/${id}/worker-subnet`)).toBe(subnetB);
|
|
196
|
+
expect(ssm(`/${id}/allowlist`)).toBe("10.0.1.0/24,10.0.2.0/24");
|
|
197
|
+
}, TIMEOUT);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
actSuite("chant-compose-imports", ({ loop, id }) => {
|
|
201
|
+
test("one pr-plan digest: the imports in the templates order the stacks, and both are known after apply (#4466)", async () => {
|
|
202
|
+
const { code, report: plan } = await loop().open(1, "chant-compose-imports");
|
|
203
|
+
expect(code).toBe(0);
|
|
204
|
+
expect(plan.status).toBe("planned");
|
|
205
|
+
expect(plan.selection.waves).toEqual([["network"], ["app"], ["edge"]]);
|
|
206
|
+
const m = byComponent(plan);
|
|
207
|
+
expect(m.app!.unknownInputs).toEqual(["network.SubnetCId"]);
|
|
208
|
+
expect(m.app!.reads?.find((r) => r.upstream === "network")?.unknown).toEqual(["SubnetCId"]);
|
|
209
|
+
expect(m.edge!.unknownInputs).toEqual(["app.ImportsInterfaceId"]);
|
|
210
|
+
expect(m.edge!.reads?.find((r) => r.upstream === "app")?.unknown).toEqual(["ImportsInterfaceId"]);
|
|
211
|
+
expect([total(plan, "create"), total(plan, "update"), total(plan, "delete")]).toEqual([3, 0, 0]);
|
|
212
|
+
expect(plan.approval).toEqual({ status: "pending" });
|
|
213
|
+
}, TIMEOUT);
|
|
214
|
+
|
|
215
|
+
test("one approval, and pr-apply exits 0: both rounds are covered, and edge records the interface app put in subnet c", async () => {
|
|
216
|
+
const plan = loop().report("plan");
|
|
217
|
+
await loop().approve(1, "pr-apply", plan.digest);
|
|
218
|
+
expect(await loop().merge(1)).toBe(0);
|
|
219
|
+
const applied = loop().report("apply");
|
|
220
|
+
expect(applied.digest).toBe(plan.digest);
|
|
221
|
+
expect(applied.status).toBe("applied");
|
|
222
|
+
const rounds = applied.rounds ?? [];
|
|
223
|
+
for (const r of rounds) expect(r.status).toBe("applied");
|
|
224
|
+
const appRound = rounds.find((r) => r.components.includes("app"));
|
|
225
|
+
expect(appRound?.covered?.digest).toBe(plan.digest);
|
|
226
|
+
expect(appRound?.covered?.filledInputs).toContain("network.SubnetCId");
|
|
227
|
+
const edgeRound = [...rounds].reverse().find((r) => r.components.includes("edge"));
|
|
228
|
+
expect(edgeRound?.covered?.digest).toBe(plan.digest);
|
|
229
|
+
expect(edgeRound?.covered?.filledInputs).toContain("app.ImportsInterfaceId");
|
|
230
|
+
|
|
231
|
+
const iface = stack(`${id}-app`).Outputs?.find((o) => o.OutputKey === "ImportsInterfaceId")?.OutputValue;
|
|
232
|
+
expect(iface).toMatch(/^eni-/);
|
|
233
|
+
expect(ssm(`/${id}/imports-interface`)).toBe(iface);
|
|
234
|
+
}, TIMEOUT);
|
|
235
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #4522 on the aws lexicon: the reference catalog over an indexed fixture
|
|
3
|
+
* with a known topology (instance, security group, subnet, route table,
|
|
4
|
+
* internet gateway), in Cloud Control's CloudFormation property shape.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import type { GraphIR, IREdge } from "@intentius/chant/graph-ir";
|
|
10
|
+
import { typeListing, type ListingLexicon, type ListingResult } from "@intentius/chant/lifecycle/type-listing";
|
|
11
|
+
import { deriveEdges } from "@intentius/chant/lifecycle/derive-edges";
|
|
12
|
+
import { CapabilityEngine } from "@intentius/chant/capabilities/matcher";
|
|
13
|
+
import { buildView, displayName } from "@intentius/chant/capabilities/view";
|
|
14
|
+
import { resolvePacks, shippedRules } from "@intentius/chant/capabilities/catalog";
|
|
15
|
+
import { awsCapabilityPack } from "./capabilities/pack";
|
|
16
|
+
import { awsKindForListedType } from "./listed-types";
|
|
17
|
+
import { awsReferenceCatalog } from "./reference-catalog";
|
|
18
|
+
import { awsPlugin } from "./plugin";
|
|
19
|
+
|
|
20
|
+
const here = import.meta.dirname;
|
|
21
|
+
const surface = JSON.parse(readFileSync(join(here, "..", "surface.snapshot.json"), "utf-8")) as {
|
|
22
|
+
entries: Record<string, { kind: string; resourceType: string }>;
|
|
23
|
+
};
|
|
24
|
+
const KINDS: ReadonlySet<string> = new Set(
|
|
25
|
+
Object.values(surface.entries).filter((e) => e.kind === "resource").map((e) => e.resourceType),
|
|
26
|
+
);
|
|
27
|
+
const listing = JSON.parse(readFileSync(join(here, "testdata", "indexed", "topology.listing.json"), "utf-8")) as ListingResult;
|
|
28
|
+
|
|
29
|
+
const aws: ListingLexicon = {
|
|
30
|
+
name: awsPlugin.name,
|
|
31
|
+
deepNormalizationHooks: awsPlugin.deepNormalizationHooks,
|
|
32
|
+
listedTypes: { ...awsPlugin.listedTypes!, kindFor: (t) => awsKindForListedType(t, KINDS) },
|
|
33
|
+
};
|
|
34
|
+
const doc = deriveEdges(typeListing(listing, [aws]), [{ name: "aws", referenceCatalog: awsPlugin.referenceCatalog }]);
|
|
35
|
+
const short = (e: IREdge) => `${displayName(e.from)} -> ${displayName(e.to)} [${e.viaAttr}] ${e.relation}`;
|
|
36
|
+
|
|
37
|
+
// Hand-checked against the fixture. displayName keeps the native id's last
|
|
38
|
+
// path segment, so the route `rtb-0pub|0.0.0.0/0` reads as `0`.
|
|
39
|
+
const EXPECTED = [
|
|
40
|
+
"IGW|vpc-0a1 -> igw-0a1 [InternetGatewayId] reference",
|
|
41
|
+
"IGW|vpc-0a1 -> vpc-0a1 [VpcId] reference",
|
|
42
|
+
"i-0batch -> sg-0web [SecurityGroupIds] reference",
|
|
43
|
+
"i-0batch -> subnet-0priv [SubnetId] containment",
|
|
44
|
+
"i-0web -> sg-0web [SecurityGroupIds] reference",
|
|
45
|
+
"i-0web -> subnet-0pub [SubnetId] containment",
|
|
46
|
+
"0 -> igw-0a1 [GatewayId] reference",
|
|
47
|
+
"0 -> rtb-0pub [RouteTableId] reference",
|
|
48
|
+
"rtb-0pub -> vpc-0a1 [VpcId] containment",
|
|
49
|
+
"rtbassoc-0pub -> rtb-0pub [RouteTableId] reference",
|
|
50
|
+
"rtbassoc-0pub -> subnet-0pub [SubnetId] reference",
|
|
51
|
+
"sg-0db -> sg-0web [SecurityGroupIngress] reference",
|
|
52
|
+
"sg-0db -> vpc-0a1 [VpcId] containment",
|
|
53
|
+
"sg-0web -> vpc-0a1 [VpcId] containment",
|
|
54
|
+
"subnet-0priv -> vpc-0a1 [VpcId] containment",
|
|
55
|
+
"subnet-0pub -> shop [Tags] reference",
|
|
56
|
+
"subnet-0pub -> vpc-0a1 [VpcId] containment",
|
|
57
|
+
].sort();
|
|
58
|
+
|
|
59
|
+
describe("derived edges over the aws topology fixture", () => {
|
|
60
|
+
it("produces the topology's edges, each labelled derived with its rule", () => {
|
|
61
|
+
expect(doc.graph.edges.map(short).sort()).toEqual(EXPECTED);
|
|
62
|
+
for (const e of doc.graph.edges) {
|
|
63
|
+
expect(e.origin).toBe("derived");
|
|
64
|
+
expect(e.grain).toBe("property");
|
|
65
|
+
expect(e.rule).toMatch(/^aws:AWS::[A-Za-z0-9]+::[A-Za-z0-9]+:/);
|
|
66
|
+
}
|
|
67
|
+
expect(doc.graph.dangling).toBeUndefined();
|
|
68
|
+
expect((doc.notes ?? []).filter((n) => n.kind === "ambiguous-reference" || n.kind === "no-catalog")).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("names the rule that drew each edge", () => {
|
|
72
|
+
const rules = new Set(doc.graph.edges.map((e) => e.rule));
|
|
73
|
+
expect(rules).toContain("aws:AWS::EC2::Instance:SubnetId");
|
|
74
|
+
expect(rules).toContain("aws:AWS::EC2::Route:GatewayId");
|
|
75
|
+
expect(rules).toContain("aws:AWS::EC2::VPCGatewayAttachment:InternetGatewayId");
|
|
76
|
+
expect(rules).toContain("aws:AWS::EC2::SecurityGroup:SecurityGroupIngress[].SourceSecurityGroupId");
|
|
77
|
+
expect(rules).toContain("aws:AWS::EC2::Subnet:tag:kubernetes.io/cluster/");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("boxes subnets, route tables and security groups in their VPC", () => {
|
|
81
|
+
const vpc = doc.graph.nodes.find((n) => n.nativeId === "vpc-0a1")!.id;
|
|
82
|
+
expect(doc.graph.groups.byContainer?.[vpc]?.map(displayName).sort()).toEqual(["rtb-0pub", "sg-0db", "sg-0web", "subnet-0priv", "subnet-0pub"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("internet-reachable answers the same over the index as over the declared graph", () => {
|
|
86
|
+
const n = (id: string, kind: string, attrs: Record<string, unknown>) => ({ id, kind, lexicon: "aws", attrs });
|
|
87
|
+
const ref = (from: string, to: string, viaAttr: string): IREdge => ({ from, to, kind: "ref", viaAttr });
|
|
88
|
+
// The same estate as CloudFormation source declares it: references are AttrRefs, named by property.
|
|
89
|
+
const declared: GraphIR = {
|
|
90
|
+
nodes: [
|
|
91
|
+
n("vpc-0a1", "AWS::EC2::VPC", { CidrBlock: "10.0.0.0/16" }),
|
|
92
|
+
n("subnet-0pub", "AWS::EC2::Subnet", { CidrBlock: "10.0.1.0/24" }),
|
|
93
|
+
n("subnet-0priv", "AWS::EC2::Subnet", { CidrBlock: "10.0.2.0/24" }),
|
|
94
|
+
n("igw-0a1", "AWS::EC2::InternetGateway", {}),
|
|
95
|
+
n("rtb-0pub", "AWS::EC2::RouteTable", {}),
|
|
96
|
+
n("route", "AWS::EC2::Route", { DestinationCidrBlock: "0.0.0.0/0" }),
|
|
97
|
+
n("rtbassoc-0pub", "AWS::EC2::SubnetRouteTableAssociation", {}),
|
|
98
|
+
n("sg-0web", "AWS::EC2::SecurityGroup", { GroupDescription: "web", SecurityGroupIngress: [{ IpProtocol: "tcp", FromPort: 22, ToPort: 22, CidrIp: "0.0.0.0/0" }] }),
|
|
99
|
+
n("i-0web", "AWS::EC2::Instance", { ImageId: "ami-0abc", InstanceType: "t3.micro" }),
|
|
100
|
+
n("i-0batch", "AWS::EC2::Instance", { ImageId: "ami-0abc", InstanceType: "t3.micro" }),
|
|
101
|
+
],
|
|
102
|
+
edges: [
|
|
103
|
+
ref("subnet-0pub", "vpc-0a1", "VpcId"),
|
|
104
|
+
ref("subnet-0priv", "vpc-0a1", "VpcId"),
|
|
105
|
+
ref("rtb-0pub", "vpc-0a1", "VpcId"),
|
|
106
|
+
ref("route", "rtb-0pub", "RouteTableId"),
|
|
107
|
+
ref("route", "igw-0a1", "GatewayId"),
|
|
108
|
+
ref("rtbassoc-0pub", "subnet-0pub", "SubnetId"),
|
|
109
|
+
ref("rtbassoc-0pub", "rtb-0pub", "RouteTableId"),
|
|
110
|
+
ref("sg-0web", "vpc-0a1", "VpcId"),
|
|
111
|
+
ref("i-0web", "subnet-0pub", "SubnetId"),
|
|
112
|
+
ref("i-0web", "sg-0web", "SecurityGroupIds"),
|
|
113
|
+
ref("i-0batch", "subnet-0priv", "SubnetId"),
|
|
114
|
+
ref("i-0batch", "sg-0web", "SecurityGroupIds"),
|
|
115
|
+
],
|
|
116
|
+
groups: {},
|
|
117
|
+
};
|
|
118
|
+
const answer = (g: GraphIR) => {
|
|
119
|
+
const e = new CapabilityEngine(buildView(g, resolvePacks([awsCapabilityPack])), shippedRules().rules);
|
|
120
|
+
return [...e.set("internet-reachable", e.bind("internet-reachable", [] as never)).keys()].map(displayName).sort();
|
|
121
|
+
};
|
|
122
|
+
expect(answer(declared)).toEqual(["i-0web"]);
|
|
123
|
+
expect(answer(doc.graph)).toEqual(answer(declared));
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("the aws catalog over indexed nodes", () => {
|
|
128
|
+
it("gives every untargeted rule a target kind for indexed nodes, and keeps it untargeted for enrichLiveAttrs", () => {
|
|
129
|
+
const untargeted = awsReferenceCatalog.refs.filter((r) => r.targetKind === undefined);
|
|
130
|
+
expect(untargeted).toHaveLength(19);
|
|
131
|
+
expect(untargeted.filter((r) => r.indexedTargetKind === undefined)).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("keys every identity and target to a kind the lexicon has", () => {
|
|
135
|
+
const kinds = [
|
|
136
|
+
...awsReferenceCatalog.identities.map((i) => i.kind),
|
|
137
|
+
...awsReferenceCatalog.refs.flatMap((r) => [r.from, r.targetKind ?? r.indexedTargetKind!]),
|
|
138
|
+
...(awsReferenceCatalog.tagRefs ?? []).map((r) => r.targetKind),
|
|
139
|
+
];
|
|
140
|
+
expect([...new Set(kinds)].filter((k) => !KINDS.has(k))).toEqual([]);
|
|
141
|
+
});
|
|
142
|
+
});
|