@intentius/chant 0.18.30 → 0.18.32
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/cli/handlers/components.d.ts +27 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +13 -0
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/components.ts +8 -2
- package/src/cli/handlers/graph.test.ts +92 -0
- package/src/cli/handlers/graph.ts +27 -1
- package/src/lifecycle/observe.test.ts +48 -0
- package/src/lifecycle/observe.ts +37 -8
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CommandContext } from "../registry.js";
|
|
2
|
+
import type { Phase } from "../../components/component.js";
|
|
2
3
|
/**
|
|
3
4
|
* chant components release <env> --component <name> --digest <sha256:...>
|
|
4
5
|
* [--git-sha <sha>] [--run-id <id>] [--actor <name>]
|
|
@@ -17,6 +18,32 @@ import type { CommandContext } from "../registry.js";
|
|
|
17
18
|
* timestamp from the environment/CLI at record time."
|
|
18
19
|
*/
|
|
19
20
|
export declare function runComponentsReleaseRecord(ctx: CommandContext): Promise<number>;
|
|
21
|
+
/**
|
|
22
|
+
* chant components status [env] [--live] [--json] [--compare-to <env>]
|
|
23
|
+
*
|
|
24
|
+
* Answers "what's built" and "what's deployed where," joined by digest.
|
|
25
|
+
* Without `--live`, reports the release ledger alone (recorded vs unrecorded
|
|
26
|
+
* is meaningless without live evidence, so every recorded row reports
|
|
27
|
+
* `unknown` reconciliation — still useful as a pure ledger read). With
|
|
28
|
+
* `--live`, joins against `lifecycle diff --live` + ownership (reusing
|
|
29
|
+
* `buildChangeSet`, the same classification `lifecycle plan` already
|
|
30
|
+
* trusts) to flag unrecorded deploys and drift.
|
|
31
|
+
*
|
|
32
|
+
* Omitting `<env>` reports every environment that has release records.
|
|
33
|
+
* `--compare-to <env>` answers the single-query cross-environment question
|
|
34
|
+
* the epic names explicitly: "which build is in `<env>`, and is it the one
|
|
35
|
+
* tested in `<compare-to>`."
|
|
36
|
+
*/
|
|
37
|
+
/** Every distinct `cfn-deploy` stack name a component's deploy phases target. A
|
|
38
|
+
* step may itself be a nested `Phase`, so walk recursively; a resolved component
|
|
39
|
+
* carries the stack as a concrete string.
|
|
40
|
+
*
|
|
41
|
+
* Exported for `chant graph --live` (../handlers/graph.ts's `runGraphLive`,
|
|
42
|
+
* #57): the same per-component stack resolution this file uses for `chant
|
|
43
|
+
* components status --live` is also how the live graph learns which stacks to
|
|
44
|
+
* observe on a multi-stack, per-component project (`describeResources`'s
|
|
45
|
+
* single-stack-named-after-the-environment convention never matches one). */
|
|
46
|
+
export declare function cfnDeployStacks(deploy: Phase[]): string[];
|
|
20
47
|
export declare function runComponentsStatus(ctx: CommandContext): Promise<number>;
|
|
21
48
|
/** Fallback for `chant components` with no/unknown subcommand. */
|
|
22
49
|
export declare function runComponentsUnknown(ctx: CommandContext): Promise<number>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/components.ts"],"names":[],"mappings":"AA6CA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/components.ts"],"names":[],"mappings":"AA6CA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,KAAK,EAAE,KAAK,EAAa,MAAM,4BAA4B,CAAC;AAEnE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,0BAA0B,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAuErF;AAyCD;;;;;;;;;;;;;;;GAeG;AACH;;;;;;;;6EAQ6E;AAC7E,wBAAgB,eAAe,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,EAAE,CAiBzD;AAwCD,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAuN9E;AAED,kEAAkE;AAClE,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAM/E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/graph.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/graph.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAKlD;;;;;;;;;GASG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAmBnE"}
|
|
@@ -24,8 +24,21 @@ export interface ObserveResult {
|
|
|
24
24
|
* marker channel logs and returns everything (its own contract). Plugins that
|
|
25
25
|
* throw are collected into `errors` and skipped — one failing lexicon never
|
|
26
26
|
* sinks the whole graph.
|
|
27
|
+
*
|
|
28
|
+
* `stacks` (#57) is for a multi-stack, per-component project (e.g. loomster)
|
|
29
|
+
* where there is no single stack named after the environment — AWS's
|
|
30
|
+
* single-stack convention (`lexicons/aws/src/plugin.ts`'s `describeResources`,
|
|
31
|
+
* absent an explicit `stack`) queries a stack that simply doesn't exist there,
|
|
32
|
+
* so the single-call path always observes zero nodes. When `stacks` is
|
|
33
|
+
* present and non-empty, each observing plugin's `describeResources` is
|
|
34
|
+
* called once per stack (same `environment`/`entities`/`entityNames`, only
|
|
35
|
+
* `stack` varies) and the returned resource maps are unioned — a resource
|
|
36
|
+
* appears under whichever stack contains its logical id. When `stacks` is
|
|
37
|
+
* absent or empty, behavior is exactly the single call of before (no `stack`
|
|
38
|
+
* key at all), so a single-stack project is unaffected.
|
|
27
39
|
*/
|
|
28
40
|
export declare function observeResources(environment: string, plugins: ObservationLexicon[], buildResult: BuildResult, opts?: {
|
|
29
41
|
owned?: boolean;
|
|
42
|
+
stacks?: string[];
|
|
30
43
|
}): Promise<ObserveResult>;
|
|
31
44
|
//# sourceMappingURL=observe.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observe.d.ts","sourceRoot":"","sources":["../../src/lifecycle/observe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAoB,MAAM,YAAY,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,eAAe,EAAE,CAAC;IAChC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED
|
|
1
|
+
{"version":3,"file":"observe.d.ts","sourceRoot":"","sources":["../../src/lifecycle/observe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAoB,MAAM,YAAY,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,eAAe,EAAE,CAAC;IAChC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,kBAAkB,EAAE,EAC7B,WAAW,EAAE,WAAW,EACxB,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAC5C,OAAO,CAAC,aAAa,CAAC,CAgExB"}
|
package/package.json
CHANGED
|
@@ -194,8 +194,14 @@ interface StatusJsonRow {
|
|
|
194
194
|
*/
|
|
195
195
|
/** Every distinct `cfn-deploy` stack name a component's deploy phases target. A
|
|
196
196
|
* step may itself be a nested `Phase`, so walk recursively; a resolved component
|
|
197
|
-
* carries the stack as a concrete string.
|
|
198
|
-
|
|
197
|
+
* carries the stack as a concrete string.
|
|
198
|
+
*
|
|
199
|
+
* Exported for `chant graph --live` (../handlers/graph.ts's `runGraphLive`,
|
|
200
|
+
* #57): the same per-component stack resolution this file uses for `chant
|
|
201
|
+
* components status --live` is also how the live graph learns which stacks to
|
|
202
|
+
* observe on a multi-stack, per-component project (`describeResources`'s
|
|
203
|
+
* single-stack-named-after-the-environment convention never matches one). */
|
|
204
|
+
export function cfnDeployStacks(deploy: Phase[]): string[] {
|
|
199
205
|
const stacks = new Set<string>();
|
|
200
206
|
const walkSteps = (steps: Phase["steps"]): void => {
|
|
201
207
|
for (const step of steps) {
|
|
@@ -44,6 +44,14 @@ const observeMock = vi.fn();
|
|
|
44
44
|
vi.mock("../../lifecycle/observe", () => ({
|
|
45
45
|
observeResources: (...a: unknown[]) => observeMock(...a),
|
|
46
46
|
}));
|
|
47
|
+
// `runGraphLive` resolves each component's `cfn-deploy` stack(s) (#57) so a
|
|
48
|
+
// multi-stack, per-component project observes the right stacks; default to
|
|
49
|
+
// "no components" (empty map, no errors) so the existing single-stack --live
|
|
50
|
+
// test is unaffected unless a test opts into a component layout below.
|
|
51
|
+
const discoverComponentsMock = vi.fn();
|
|
52
|
+
vi.mock("../../components/discover", () => ({
|
|
53
|
+
discoverComponents: (...a: unknown[]) => discoverComponentsMock(...a),
|
|
54
|
+
}));
|
|
47
55
|
vi.mock("../../config", () => ({
|
|
48
56
|
loadChantConfig: () => Promise.resolve({ config: {} }),
|
|
49
57
|
}));
|
|
@@ -93,6 +101,12 @@ describe("runGraph", () => {
|
|
|
93
101
|
lintMock.mockReset();
|
|
94
102
|
layoutMock.mockReset();
|
|
95
103
|
componentGraphMock.mockReset();
|
|
104
|
+
discoverComponentsMock.mockReset();
|
|
105
|
+
// Default: no components — the single-stack --live path most tests exercise.
|
|
106
|
+
discoverComponentsMock.mockResolvedValue({ components: new Map(), sourceFiles: [], errors: [] });
|
|
107
|
+
observeMock.mockReset();
|
|
108
|
+
loadPluginsMock.mockReset();
|
|
109
|
+
resolveLexMock.mockReset();
|
|
96
110
|
});
|
|
97
111
|
|
|
98
112
|
describe("Op graph (default)", () => {
|
|
@@ -326,5 +340,83 @@ describe("runGraph", () => {
|
|
|
326
340
|
expect(out).not.toContain("No lexicons implement describeResources");
|
|
327
341
|
expect(out).toContain("web-vpc");
|
|
328
342
|
});
|
|
343
|
+
|
|
344
|
+
// Regression for #57: a single-stack project (no `*.component.ts` files —
|
|
345
|
+
// discoverComponents returns an empty map) must observe exactly as before,
|
|
346
|
+
// no `stacks` collected.
|
|
347
|
+
test("single-stack project (no components): observeResources gets an empty stacks list", async () => {
|
|
348
|
+
resolveLexMock.mockResolvedValue(["aws"]);
|
|
349
|
+
loadPluginsMock.mockResolvedValue([
|
|
350
|
+
{ name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
|
|
351
|
+
]);
|
|
352
|
+
observeMock.mockResolvedValue({ observations: [], errors: [] });
|
|
353
|
+
const exit = await runGraph({ args: makeArgs({ format: "ir", live: true, env: "prod" }), plugins: [], serializers: [] });
|
|
354
|
+
expect(exit).toBe(0);
|
|
355
|
+
expect(observeMock).toHaveBeenCalledWith("prod", expect.anything(), expect.anything(), {
|
|
356
|
+
owned: true,
|
|
357
|
+
stacks: [],
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// The bug this branch fixes (#57): a multi-stack, per-component project
|
|
362
|
+
// (loomster/Floci) has no stack literally named after the environment, so
|
|
363
|
+
// the live graph must resolve each component's own `cfn-deploy` stack(s)
|
|
364
|
+
// and pass them through to `observeResources` for the per-stack union.
|
|
365
|
+
test("multi-stack component project: resolves each component's cfn-deploy stack(s) and passes them to observeResources", async () => {
|
|
366
|
+
resolveLexMock.mockResolvedValue(["aws"]);
|
|
367
|
+
loadPluginsMock.mockResolvedValue([
|
|
368
|
+
{ name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
|
|
369
|
+
]);
|
|
370
|
+
discoverComponentsMock.mockResolvedValue({
|
|
371
|
+
errors: [],
|
|
372
|
+
sourceFiles: [],
|
|
373
|
+
components: new Map([
|
|
374
|
+
["loom-db", { component: { name: "loom-db", dependsOn: [], deploy: [
|
|
375
|
+
{ phase: "deploy", steps: [{ kind: "cfn-deploy", stack: "loom-local-a-loom-db" }] },
|
|
376
|
+
] }, exportName: "loomDb", filePath: "components/loom-db.component.ts" }],
|
|
377
|
+
// Multi-stack component: two cfn-deploy steps, nested under a sub-phase.
|
|
378
|
+
["loom-backend", { component: { name: "loom-backend", dependsOn: ["loom-db"], deploy: [
|
|
379
|
+
{ phase: "deploy", steps: [
|
|
380
|
+
{ phase: "nested", steps: [{ kind: "cfn-deploy", stack: "loom-local-a-loom-backend" }] },
|
|
381
|
+
{ kind: "cfn-deploy", stack: "loom-local-a-loom-backend-jobs" },
|
|
382
|
+
] },
|
|
383
|
+
] }, exportName: "loomBackend", filePath: "components/loom-backend.component.ts" }],
|
|
384
|
+
// Non-aws / no cfn-deploy component: contributes no stacks.
|
|
385
|
+
["loom-frontend", { component: { name: "loom-frontend", dependsOn: [], deploy: [
|
|
386
|
+
{ phase: "deploy", steps: [{ kind: "s3-sync" }] },
|
|
387
|
+
] }, exportName: "loomFrontend", filePath: "components/loom-frontend.component.ts" }],
|
|
388
|
+
]),
|
|
389
|
+
});
|
|
390
|
+
observeMock.mockResolvedValue({
|
|
391
|
+
observations: [{ lexicon: "aws", resources: { "loom-db": { type: "AWS::RDS::DBInstance", status: "OK" } } }],
|
|
392
|
+
errors: [],
|
|
393
|
+
});
|
|
394
|
+
const exit = await runGraph({ args: makeArgs({ format: "ir", live: true, env: "local" }), plugins: [], serializers: [] });
|
|
395
|
+
expect(exit).toBe(0);
|
|
396
|
+
expect(observeMock).toHaveBeenCalledTimes(1);
|
|
397
|
+
const [, , , opts] = observeMock.mock.calls[0];
|
|
398
|
+
expect((opts as { stacks: string[] }).stacks.sort()).toEqual([
|
|
399
|
+
"loom-local-a-loom-backend",
|
|
400
|
+
"loom-local-a-loom-backend-jobs",
|
|
401
|
+
"loom-local-a-loom-db",
|
|
402
|
+
]);
|
|
403
|
+
expect((opts as { owned: boolean }).owned).toBe(true);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("component discovery errors: falls back to the single-stack path with a warning", async () => {
|
|
407
|
+
resolveLexMock.mockResolvedValue(["aws"]);
|
|
408
|
+
loadPluginsMock.mockResolvedValue([
|
|
409
|
+
{ name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
|
|
410
|
+
]);
|
|
411
|
+
discoverComponentsMock.mockResolvedValue({ errors: [{ message: "bad component" }], sourceFiles: [], components: new Map() });
|
|
412
|
+
observeMock.mockResolvedValue({ observations: [], errors: [] });
|
|
413
|
+
const exit = await runGraph({ args: makeArgs({ format: "ir", live: true, env: "prod" }), plugins: [], serializers: [] });
|
|
414
|
+
expect(exit).toBe(0);
|
|
415
|
+
expect(observeMock).toHaveBeenCalledWith("prod", expect.anything(), expect.anything(), {
|
|
416
|
+
owned: true,
|
|
417
|
+
stacks: [],
|
|
418
|
+
});
|
|
419
|
+
expect(stderrBuf.join("\n")).toMatch(/component discovery failed/i);
|
|
420
|
+
});
|
|
329
421
|
});
|
|
330
422
|
});
|
|
@@ -17,6 +17,8 @@ import { readFileSync } from "node:fs";
|
|
|
17
17
|
import { formatError, formatWarning, formatBold } from "../format";
|
|
18
18
|
import type { CommandContext } from "../registry";
|
|
19
19
|
import { computeComponentGraph } from "../../components/cli-support";
|
|
20
|
+
import { discoverComponents } from "../../components/discover";
|
|
21
|
+
import { cfnDeployStacks } from "./components";
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* `chant graph` — the Op dependency graph by default; `--stacks` renders the
|
|
@@ -96,7 +98,31 @@ async function runGraphLive(
|
|
|
96
98
|
return 1;
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
|
|
101
|
+
// Multi-stack, per-component projects (loomster/Floci, #57): AWS's
|
|
102
|
+
// single-stack convention (`describeResources` defaults to a stack named
|
|
103
|
+
// after the environment, lexicons/aws/src/plugin.ts) never matches a
|
|
104
|
+
// per-component layout (e.g. `loom-local-a-<component>`), so the plain
|
|
105
|
+
// single call always observes zero nodes. Resolve every discovered
|
|
106
|
+
// component's `cfn-deploy` stack(s) — the same walk `chant components
|
|
107
|
+
// status --live` uses (`cfnDeployStacks`, ./components.ts) — and hand them
|
|
108
|
+
// to `observeResources`, which queries `describeResources` once per stack
|
|
109
|
+
// and unions the results. A project with no components (or whose discovery
|
|
110
|
+
// errors) yields no stacks, so `observeResources` falls back to its
|
|
111
|
+
// original single-stack call — unchanged.
|
|
112
|
+
const componentsDiscovery = await discoverComponents(resolve(args.src ?? config.sourceDir ?? "."));
|
|
113
|
+
const stacks = new Set<string>();
|
|
114
|
+
if (componentsDiscovery.errors.length === 0) {
|
|
115
|
+
for (const { component } of componentsDiscovery.components.values()) {
|
|
116
|
+
for (const stack of cfnDeployStacks(component.deploy)) stacks.add(stack);
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
console.error(formatWarning({ message: "component discovery failed — observing the single-stack convention instead" }));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const { observations, errors } = await observeResources(environment, observing, buildResult, {
|
|
123
|
+
owned: true,
|
|
124
|
+
stacks: [...stacks],
|
|
125
|
+
});
|
|
100
126
|
for (const e of errors) console.error(formatWarning({ message: e }));
|
|
101
127
|
|
|
102
128
|
let ir: GraphIR = buildLiveGraphIr(observations);
|
|
@@ -62,4 +62,52 @@ describe("observeResources", () => {
|
|
|
62
62
|
const { observations } = await observeResources("prod", [empty, noObserve], mockBuild());
|
|
63
63
|
expect(observations).toEqual([]);
|
|
64
64
|
});
|
|
65
|
+
|
|
66
|
+
it("with no stacks: calls describeResources exactly once with no `stack` key (unchanged single-stack path)", async () => {
|
|
67
|
+
const calls: Array<{ stack?: string }> = [];
|
|
68
|
+
const plugins = [
|
|
69
|
+
awsPlugin((opts) => {
|
|
70
|
+
calls.push(opts as { stack?: string });
|
|
71
|
+
return { "web-vpc": { type: "AWS::EC2::VPC", status: "CREATE_COMPLETE" } };
|
|
72
|
+
}),
|
|
73
|
+
];
|
|
74
|
+
const { observations } = await observeResources("prod", plugins, mockBuild());
|
|
75
|
+
expect(calls).toHaveLength(1);
|
|
76
|
+
expect(calls[0]).not.toHaveProperty("stack");
|
|
77
|
+
expect(Object.keys(observations[0].resources)).toEqual(["web-vpc"]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("with stacks: [s1, s2] — calls describeResources once per stack and unions the results", async () => {
|
|
81
|
+
const calls: Array<string | undefined> = [];
|
|
82
|
+
const plugins = [
|
|
83
|
+
awsPlugin((opts) => {
|
|
84
|
+
const stack = (opts as { stack?: string }).stack;
|
|
85
|
+
calls.push(stack);
|
|
86
|
+
// Different resources per stack — the multi-stack, per-component case.
|
|
87
|
+
const resources: Record<string, ResourceMetadata> =
|
|
88
|
+
stack === "s1"
|
|
89
|
+
? { "db-a": { type: "AWS::RDS::DBInstance", status: "AVAILABLE" } }
|
|
90
|
+
: { "db-b": { type: "AWS::RDS::DBInstance", status: "AVAILABLE" } };
|
|
91
|
+
return resources;
|
|
92
|
+
}),
|
|
93
|
+
];
|
|
94
|
+
const { observations, errors } = await observeResources("prod", plugins, mockBuild(), { stacks: ["s1", "s2"] });
|
|
95
|
+
expect(calls).toEqual(["s1", "s2"]);
|
|
96
|
+
expect(errors).toEqual([]);
|
|
97
|
+
expect(observations).toHaveLength(1);
|
|
98
|
+
expect(Object.keys(observations[0].resources).sort()).toEqual(["db-a", "db-b"]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("with an empty stacks array — falls back to the single unstacked call", async () => {
|
|
102
|
+
const calls: Array<{ stack?: string }> = [];
|
|
103
|
+
const plugins = [
|
|
104
|
+
awsPlugin((opts) => {
|
|
105
|
+
calls.push(opts as { stack?: string });
|
|
106
|
+
return { "web-vpc": { type: "AWS::EC2::VPC", status: "CREATE_COMPLETE" } };
|
|
107
|
+
}),
|
|
108
|
+
];
|
|
109
|
+
await observeResources("prod", plugins, mockBuild(), { stacks: [] });
|
|
110
|
+
expect(calls).toHaveLength(1);
|
|
111
|
+
expect(calls[0]).not.toHaveProperty("stack");
|
|
112
|
+
});
|
|
65
113
|
});
|
package/src/lifecycle/observe.ts
CHANGED
|
@@ -27,14 +27,27 @@ export interface ObserveResult {
|
|
|
27
27
|
* marker channel logs and returns everything (its own contract). Plugins that
|
|
28
28
|
* throw are collected into `errors` and skipped — one failing lexicon never
|
|
29
29
|
* sinks the whole graph.
|
|
30
|
+
*
|
|
31
|
+
* `stacks` (#57) is for a multi-stack, per-component project (e.g. loomster)
|
|
32
|
+
* where there is no single stack named after the environment — AWS's
|
|
33
|
+
* single-stack convention (`lexicons/aws/src/plugin.ts`'s `describeResources`,
|
|
34
|
+
* absent an explicit `stack`) queries a stack that simply doesn't exist there,
|
|
35
|
+
* so the single-call path always observes zero nodes. When `stacks` is
|
|
36
|
+
* present and non-empty, each observing plugin's `describeResources` is
|
|
37
|
+
* called once per stack (same `environment`/`entities`/`entityNames`, only
|
|
38
|
+
* `stack` varies) and the returned resource maps are unioned — a resource
|
|
39
|
+
* appears under whichever stack contains its logical id. When `stacks` is
|
|
40
|
+
* absent or empty, behavior is exactly the single call of before (no `stack`
|
|
41
|
+
* key at all), so a single-stack project is unaffected.
|
|
30
42
|
*/
|
|
31
43
|
export async function observeResources(
|
|
32
44
|
environment: string,
|
|
33
45
|
plugins: ObservationLexicon[],
|
|
34
46
|
buildResult: BuildResult,
|
|
35
|
-
opts?: { owned?: boolean },
|
|
47
|
+
opts?: { owned?: boolean; stacks?: string[] },
|
|
36
48
|
): Promise<ObserveResult> {
|
|
37
49
|
const owned = opts?.owned ?? true;
|
|
50
|
+
const stacks = opts?.stacks ?? [];
|
|
38
51
|
const observations: LiveObservation[] = [];
|
|
39
52
|
const warnings: string[] = [];
|
|
40
53
|
const errors: string[] = [];
|
|
@@ -64,13 +77,29 @@ export async function observeResources(
|
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
let resources: Record<string, ResourceMetadata>;
|
|
81
|
+
if (stacks.length > 0) {
|
|
82
|
+
resources = {};
|
|
83
|
+
for (const stack of stacks) {
|
|
84
|
+
const perStack = await plugin.describeResources({
|
|
85
|
+
environment,
|
|
86
|
+
buildOutput,
|
|
87
|
+
entityNames,
|
|
88
|
+
entities,
|
|
89
|
+
owned,
|
|
90
|
+
stack,
|
|
91
|
+
});
|
|
92
|
+
Object.assign(resources, perStack);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
resources = await plugin.describeResources({
|
|
96
|
+
environment,
|
|
97
|
+
buildOutput,
|
|
98
|
+
entityNames,
|
|
99
|
+
entities,
|
|
100
|
+
owned,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
74
103
|
if (Object.keys(resources).length > 0) {
|
|
75
104
|
observations.push({ lexicon: plugin.name, resources });
|
|
76
105
|
}
|