@intentius/chant 0.28.0 → 0.30.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/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +5 -3
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/config.d.ts +46 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +257 -0
- package/dist/deep-observation.d.ts.map +1 -0
- package/dist/discovery/fold-import.d.ts +153 -17
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/sandbox/config-wire.d.ts +3 -2
- package/dist/discovery/sandbox/config-wire.d.ts.map +1 -1
- package/dist/env.d.ts +5 -2
- package/dist/env.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +35 -3
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +9 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +73 -4
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/kubectl-context.d.ts +27 -0
- package/dist/kubectl-context.d.ts.map +1 -1
- package/dist/lexicon.d.ts +78 -6
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +26 -5
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/deep-diff.d.ts +103 -0
- package/dist/lifecycle/deep-diff.d.ts.map +1 -0
- package/dist/lifecycle/deep-observe.d.ts +62 -0
- package/dist/lifecycle/deep-observe.d.ts.map +1 -0
- package/dist/lifecycle/index.d.ts +3 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/live-diff.d.ts +25 -1
- package/dist/lifecycle/live-diff.d.ts.map +1 -1
- package/dist/lifecycle/observation-baseline.d.ts +118 -0
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -0
- package/dist/lifecycle/observe.d.ts +4 -2
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/status.d.ts +26 -1
- package/dist/lifecycle/status.d.ts.map +1 -1
- package/dist/lifecycle/types.d.ts +8 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/live-endpoint.d.ts +92 -0
- package/dist/live-endpoint.d.ts.map +1 -0
- package/dist/observation.d.ts +123 -0
- package/dist/observation.d.ts.map +1 -0
- package/dist/stack-output.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/components.test.ts +63 -4
- package/src/cli/handlers/components.ts +78 -35
- package/src/cli/handlers/graph.test.ts +155 -6
- package/src/cli/handlers/graph.ts +124 -29
- package/src/cli/handlers/lifecycle.test.ts +410 -6
- package/src/cli/handlers/lifecycle.ts +475 -182
- package/src/cli/main.test.ts +6 -0
- package/src/cli/main.ts +12 -0
- package/src/cli/registry.ts +14 -0
- package/src/config.test.ts +75 -0
- package/src/config.ts +61 -3
- package/src/deep-observation.test.ts +234 -0
- package/src/deep-observation.ts +489 -0
- package/src/discovery/fold-composite.test.ts +594 -0
- package/src/discovery/fold-import.test.ts +372 -1
- package/src/discovery/fold-import.ts +1216 -116
- package/src/discovery/sandbox/config-wire.ts +3 -2
- package/src/env.test.ts +12 -0
- package/src/env.ts +12 -4
- package/src/fold/fold.test.ts +105 -0
- package/src/fold/fold.ts +100 -20
- package/src/fold/subset.test.ts +38 -7
- package/src/fold/subset.ts +9 -0
- package/src/graph-ir-live.test.ts +28 -1
- package/src/graph-ir.ts +115 -12
- package/src/index.ts +2 -0
- package/src/kubectl-context.ts +81 -0
- package/src/lexicon.ts +100 -6
- package/src/lifecycle/change-set.test.ts +93 -1
- package/src/lifecycle/change-set.ts +65 -13
- package/src/lifecycle/deep-diff.test.ts +157 -0
- package/src/lifecycle/deep-diff.ts +213 -0
- package/src/lifecycle/deep-observe.test.ts +174 -0
- package/src/lifecycle/deep-observe.ts +173 -0
- package/src/lifecycle/index.ts +3 -0
- package/src/lifecycle/live-diff.test.ts +39 -0
- package/src/lifecycle/live-diff.ts +51 -5
- package/src/lifecycle/observation-baseline.test.ts +99 -0
- package/src/lifecycle/observation-baseline.ts +217 -0
- package/src/lifecycle/observe.test.ts +74 -3
- package/src/lifecycle/observe.ts +82 -22
- package/src/lifecycle/snapshot.test.ts +39 -1
- package/src/lifecycle/snapshot.ts +40 -20
- package/src/lifecycle/status.test.ts +89 -8
- package/src/lifecycle/status.ts +53 -3
- package/src/lifecycle/types.ts +8 -0
- package/src/live-endpoint.test.ts +115 -0
- package/src/live-endpoint.ts +148 -0
- package/src/observation.test.ts +96 -0
- package/src/observation.ts +213 -0
- package/src/stack-output.test.ts +55 -0
- package/src/stack-output.ts +41 -20
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { describe, test, expect, vi, beforeEach } from "vitest";
|
|
1
|
+
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { sep } from "node:path";
|
|
3
|
-
import { createMockPlugin, staticDescribeResources, staticListArtifacts } from "@intentius/chant-test-utils";
|
|
3
|
+
import { createMockPlugin, staticDescribeResources, staticObservation, staticDeepObservation, staticListArtifacts } from "@intentius/chant-test-utils";
|
|
4
4
|
import type { LexiconPlugin, ResourceMetadata } from "../../lexicon";
|
|
5
5
|
import type { BuildResult } from "../../build";
|
|
6
6
|
import type { ParsedArgs } from "../registry";
|
|
@@ -12,23 +12,35 @@ const readEnvironmentSnapshotsMock = vi.fn();
|
|
|
12
12
|
const listSnapshotsMock = vi.fn();
|
|
13
13
|
const takeSnapshotMock = vi.fn();
|
|
14
14
|
const loadChantConfigMock = vi.fn();
|
|
15
|
+
const pushLifecycleMock = vi.fn();
|
|
16
|
+
const readBlobFromPathMock = vi.fn();
|
|
17
|
+
const writeBlobToPathMock = vi.fn();
|
|
15
18
|
|
|
16
19
|
vi.mock("../../build", () => ({ build: (...args: unknown[]) => buildMock(...args) }));
|
|
17
20
|
vi.mock("../../lifecycle/git", () => ({
|
|
18
21
|
fetchLifecycle: () => fetchLifecycleMock(),
|
|
22
|
+
pushLifecycle: () => pushLifecycleMock(),
|
|
19
23
|
readSnapshot: (...args: unknown[]) => readSnapshotMock(...args),
|
|
20
24
|
readEnvironmentSnapshots: (...args: unknown[]) => readEnvironmentSnapshotsMock(...args),
|
|
21
25
|
listSnapshots: (...args: unknown[]) => listSnapshotsMock(...args),
|
|
22
26
|
snapshotStorageKey: (lexicon: string, stack?: string) => (stack ? `${stack}__${lexicon}` : lexicon),
|
|
27
|
+
// The accepted-observation baseline (#1014) rides the same orphan-branch
|
|
28
|
+
// plumbing as the snapshots, so it is mocked at the same seam.
|
|
29
|
+
readBlobFromPath: (...args: unknown[]) => readBlobFromPathMock(...args),
|
|
30
|
+
writeBlobToPath: (...args: unknown[]) => writeBlobToPathMock(...args),
|
|
23
31
|
}));
|
|
24
32
|
vi.mock("../../lifecycle/snapshot", () => ({
|
|
25
33
|
takeSnapshot: (...args: unknown[]) => takeSnapshotMock(...args),
|
|
26
34
|
}));
|
|
27
|
-
vi.mock("../../config", () =>
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
vi.mock("../../config", async () => {
|
|
36
|
+
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
37
|
+
return {
|
|
38
|
+
...actual,
|
|
39
|
+
loadChantConfig: (...args: unknown[]) => loadChantConfigMock(...args),
|
|
40
|
+
};
|
|
41
|
+
});
|
|
30
42
|
|
|
31
|
-
const { runLifecycleDiff, runLifecycleSnapshot, runLifecycleShow, runLifecycleLog, runLifecycleUnknown } = await import("./lifecycle");
|
|
43
|
+
const { runLifecycleDiff, runLifecyclePlan, runLifecycleSnapshot, runLifecycleShow, runLifecycleLog, runLifecycleUnknown } = await import("./lifecycle");
|
|
32
44
|
|
|
33
45
|
function makeArgs(overrides: Partial<ParsedArgs>): ParsedArgs {
|
|
34
46
|
return {
|
|
@@ -89,6 +101,12 @@ describe("runLifecycleDiff --live", () => {
|
|
|
89
101
|
readSnapshotMock.mockReset();
|
|
90
102
|
loadChantConfigMock.mockReset();
|
|
91
103
|
loadChantConfigMock.mockResolvedValue({ config: {} });
|
|
104
|
+
readBlobFromPathMock.mockReset();
|
|
105
|
+
readBlobFromPathMock.mockResolvedValue(null); // no accepted baseline recorded
|
|
106
|
+
writeBlobToPathMock.mockReset();
|
|
107
|
+
writeBlobToPathMock.mockResolvedValue("sha");
|
|
108
|
+
pushLifecycleMock.mockReset();
|
|
109
|
+
pushLifecycleMock.mockResolvedValue(true);
|
|
92
110
|
});
|
|
93
111
|
|
|
94
112
|
test("surfaces drift between previous snapshot and live state", async () => {
|
|
@@ -128,6 +146,64 @@ describe("runLifecycleDiff --live", () => {
|
|
|
128
146
|
expect(output).toContain("UPDATE_COMPLETE");
|
|
129
147
|
});
|
|
130
148
|
|
|
149
|
+
// #1089 — a hole in the read is rendered as a hole, and never silently
|
|
150
|
+
// inflates the missing/drift counts that the all-clear line reads.
|
|
151
|
+
test("renders an UNOBSERVED section and qualifies the all-clear", async () => {
|
|
152
|
+
buildMock.mockResolvedValue(makeBuildResult({ k8s: ["widget"] }));
|
|
153
|
+
fetchLifecycleMock.mockResolvedValue(undefined);
|
|
154
|
+
readSnapshotMock.mockResolvedValue(null);
|
|
155
|
+
|
|
156
|
+
const plugins: LexiconPlugin[] = [
|
|
157
|
+
createMockPlugin({
|
|
158
|
+
name: "k8s",
|
|
159
|
+
describeResources: staticObservation({}, {
|
|
160
|
+
widget: { type: "K8s::X::Widget", reason: "unsupported-kind", detail: "no kubectl mapping" },
|
|
161
|
+
}),
|
|
162
|
+
}),
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const exit = await runLifecycleDiff({
|
|
166
|
+
args: makeArgs({ path: "diff", extraPositional: "prod", live: true }),
|
|
167
|
+
plugins,
|
|
168
|
+
serializers: plugins.map((p) => p.serializer),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(exit).toBe(0);
|
|
172
|
+
const stdout = stdoutBuf.join("\n");
|
|
173
|
+
expect(stdout).toContain("UNOBSERVED");
|
|
174
|
+
expect(stdout).toContain("widget");
|
|
175
|
+
expect(stdout).toContain("no reader for this resource kind");
|
|
176
|
+
// Not counted as missing — "declared, not in cloud" is a claim we can't make.
|
|
177
|
+
expect(stdout).toContain("0 missing");
|
|
178
|
+
expect(stderrBuf.join("\n")).toContain("could not be observed");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("a throwing describeResources reports its entities unobserved instead of skipping the lexicon", async () => {
|
|
182
|
+
buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
|
|
183
|
+
fetchLifecycleMock.mockResolvedValue(undefined);
|
|
184
|
+
readSnapshotMock.mockResolvedValue(null);
|
|
185
|
+
|
|
186
|
+
const plugins: LexiconPlugin[] = [
|
|
187
|
+
createMockPlugin({
|
|
188
|
+
name: "aws",
|
|
189
|
+
describeResources: async () => { throw new Error("Unable to locate credentials"); },
|
|
190
|
+
}),
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
const exit = await runLifecycleDiff({
|
|
194
|
+
args: makeArgs({ path: "diff", extraPositional: "prod", live: true }),
|
|
195
|
+
plugins,
|
|
196
|
+
serializers: plugins.map((p) => p.serializer),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(exit).toBe(0);
|
|
200
|
+
expect(stderrBuf.join("\n")).toContain("not as absent");
|
|
201
|
+
const stdout = stdoutBuf.join("\n");
|
|
202
|
+
expect(stdout).toContain("UNOBSERVED");
|
|
203
|
+
expect(stdout).toContain("bucket");
|
|
204
|
+
expect(stdout).toContain("0 missing");
|
|
205
|
+
});
|
|
206
|
+
|
|
131
207
|
test("builds from config.sourceDir on a mixed-layout project", async () => {
|
|
132
208
|
buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
|
|
133
209
|
fetchLifecycleMock.mockResolvedValue(undefined);
|
|
@@ -289,6 +365,298 @@ describe("runLifecycleDiff --live", () => {
|
|
|
289
365
|
expect(output).toContain("bucket");
|
|
290
366
|
expect(output).toContain("added");
|
|
291
367
|
});
|
|
368
|
+
|
|
369
|
+
// #1014 — property-level drift, gated purely on the deep capability.
|
|
370
|
+
describe("deep observation (#1014)", () => {
|
|
371
|
+
const withDeep = (over: Parameters<typeof createMockPlugin>[0] = {}) =>
|
|
372
|
+
createMockPlugin({
|
|
373
|
+
name: "aws",
|
|
374
|
+
describeResources: staticObservation({ bucket: meta() }),
|
|
375
|
+
observeResourcesDeep: staticDeepObservation({
|
|
376
|
+
bucket: {
|
|
377
|
+
type: "AWS::S3::Bucket",
|
|
378
|
+
properties: { Versioning: "Suspended", Logging: { Target: "audit" } },
|
|
379
|
+
},
|
|
380
|
+
}),
|
|
381
|
+
...over,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const runDiff = async (plugins: LexiconPlugin[], args: Partial<ParsedArgs> = {}) => {
|
|
385
|
+
buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
|
|
386
|
+
// Declared: versioning on, nothing about logging.
|
|
387
|
+
const build = makeBuildResult({ aws: ["bucket"] });
|
|
388
|
+
build.entities.set("bucket", {
|
|
389
|
+
lexicon: "aws",
|
|
390
|
+
entityType: "AWS::S3::Bucket",
|
|
391
|
+
props: { Versioning: "Enabled" },
|
|
392
|
+
} as never);
|
|
393
|
+
buildMock.mockResolvedValue(build);
|
|
394
|
+
fetchLifecycleMock.mockResolvedValue(undefined);
|
|
395
|
+
readSnapshotMock.mockResolvedValue(null);
|
|
396
|
+
return runLifecycleDiff({
|
|
397
|
+
args: makeArgs({ command: "state", path: "diff", extraPositional: "prod", live: true, ...args }),
|
|
398
|
+
plugins,
|
|
399
|
+
serializers: plugins.map((p) => p.serializer),
|
|
400
|
+
} as never);
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
test("reports the changed property and the undeclared one", async () => {
|
|
404
|
+
await runDiff([withDeep()]);
|
|
405
|
+
const output = stdoutBuf.join("\n");
|
|
406
|
+
expect(output).toContain("aws (properties)");
|
|
407
|
+
expect(output).toContain("Versioning: Enabled → Suspended");
|
|
408
|
+
expect(output).toContain("Logging.Target: <undeclared> → audit");
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("a lexicon with no deep reader prints nothing extra", async () => {
|
|
412
|
+
await runDiff([createMockPlugin({ name: "aws", describeResources: staticObservation({ bucket: meta() }) })]);
|
|
413
|
+
expect(stdoutBuf.join("\n")).not.toContain("(properties)");
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("an accepted deviation in the baseline stops re-alerting", async () => {
|
|
417
|
+
readBlobFromPathMock.mockResolvedValue(
|
|
418
|
+
JSON.stringify({
|
|
419
|
+
baseline: "v1",
|
|
420
|
+
environment: "prod",
|
|
421
|
+
lexicons: { aws: { bucket: { accepted: [{ path: "Logging.Target", value: "audit" }] } } },
|
|
422
|
+
}),
|
|
423
|
+
);
|
|
424
|
+
await runDiff([withDeep()]);
|
|
425
|
+
const output = stdoutBuf.join("\n");
|
|
426
|
+
expect(output).toContain("Versioning: Enabled → Suspended");
|
|
427
|
+
expect(output).not.toContain("Logging.Target: <undeclared>");
|
|
428
|
+
expect(output).toContain("ACCEPTED (in the baseline; not drift)");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("--json carries the property drift under the lexicon's `deep` key", async () => {
|
|
432
|
+
await runDiff([withDeep()], { json: true });
|
|
433
|
+
const payload = JSON.parse(stdoutBuf.join("\n")) as {
|
|
434
|
+
lexicons: { aws: { deep: { drifted: Array<{ changes: Array<{ path: string }> }> } } };
|
|
435
|
+
};
|
|
436
|
+
expect(payload.lexicons.aws.deep.drifted[0].changes.map((c) => c.path).sort()).toEqual([
|
|
437
|
+
"Logging.Target",
|
|
438
|
+
"Versioning",
|
|
439
|
+
]);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("a deep read that could not look is a hole, not drift", async () => {
|
|
443
|
+
await runDiff([
|
|
444
|
+
withDeep({
|
|
445
|
+
observeResourcesDeep: staticDeepObservation(
|
|
446
|
+
{},
|
|
447
|
+
{ bucket: { type: "AWS::S3::Bucket", reason: "no-credentials", detail: "token expired" } },
|
|
448
|
+
),
|
|
449
|
+
}),
|
|
450
|
+
]);
|
|
451
|
+
const output = `${stdoutBuf.join("\n")}\n${stderrBuf.join("\n")}`;
|
|
452
|
+
expect(output).toContain("PROPERTIES UNOBSERVED");
|
|
453
|
+
expect(output).toContain("no credentials");
|
|
454
|
+
expect(output).toContain("could not be observed — that part of the estate is unknown, not clean");
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("--update-baseline writes what was reported and pushes it", async () => {
|
|
458
|
+
await runDiff([withDeep()], { updateBaseline: true });
|
|
459
|
+
expect(writeBlobToPathMock).toHaveBeenCalledTimes(1);
|
|
460
|
+
const [environment, filename, content] = writeBlobToPathMock.mock.calls[0] as [string, string, string];
|
|
461
|
+
expect(environment).toBe("prod");
|
|
462
|
+
expect(filename).toBe("observation-baseline.json");
|
|
463
|
+
const written = JSON.parse(content) as {
|
|
464
|
+
lexicons: { aws: { bucket: { accepted: Array<{ path: string; value: unknown }> } } };
|
|
465
|
+
};
|
|
466
|
+
expect(written.lexicons.aws.bucket.accepted.map((a) => a.path)).toEqual(["Logging.Target", "Versioning"]);
|
|
467
|
+
expect(pushLifecycleMock).toHaveBeenCalled();
|
|
468
|
+
expect(stderrBuf.join("\n")).toContain("accepted 2 deviation(s)");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("--update-baseline with nothing reported writes nothing", async () => {
|
|
472
|
+
await runDiff([
|
|
473
|
+
withDeep({ observeResourcesDeep: staticDeepObservation({}) }),
|
|
474
|
+
], { updateBaseline: true });
|
|
475
|
+
expect(writeBlobToPathMock).not.toHaveBeenCalled();
|
|
476
|
+
expect(stderrBuf.join("\n")).toContain("nothing to accept");
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// #1166 — an environment can declare its own endpoint (a local emulator like
|
|
481
|
+
// Floci), applied to the ambient var of every observing lexicon that has one
|
|
482
|
+
// unless the ambient shell already set it.
|
|
483
|
+
describe("declared endpoint (#1166)", () => {
|
|
484
|
+
const prevEndpoint = process.env.AWS_ENDPOINT_URL;
|
|
485
|
+
|
|
486
|
+
afterEach(() => {
|
|
487
|
+
if (prevEndpoint === undefined) delete process.env.AWS_ENDPOINT_URL;
|
|
488
|
+
else process.env.AWS_ENDPOINT_URL = prevEndpoint;
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("applies the declared endpoint to AWS_ENDPOINT_URL for the live describe, then restores it", async () => {
|
|
492
|
+
delete process.env.AWS_ENDPOINT_URL;
|
|
493
|
+
buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
|
|
494
|
+
fetchLifecycleMock.mockResolvedValue(undefined);
|
|
495
|
+
readSnapshotMock.mockResolvedValue(null);
|
|
496
|
+
loadChantConfigMock.mockResolvedValue({
|
|
497
|
+
config: { environments: [{ name: "floci", endpoint: "http://localhost:4566" }] },
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
let seenDuringDescribe: string | undefined;
|
|
501
|
+
const plugins: LexiconPlugin[] = [
|
|
502
|
+
createMockPlugin({
|
|
503
|
+
name: "aws",
|
|
504
|
+
describeResources: async () => {
|
|
505
|
+
seenDuringDescribe = process.env.AWS_ENDPOINT_URL;
|
|
506
|
+
return {};
|
|
507
|
+
},
|
|
508
|
+
}),
|
|
509
|
+
];
|
|
510
|
+
|
|
511
|
+
const exit = await runLifecycleDiff({
|
|
512
|
+
args: makeArgs({ path: "diff", extraPositional: "floci", live: true }),
|
|
513
|
+
plugins,
|
|
514
|
+
serializers: plugins.map((p) => p.serializer),
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
expect(exit).toBe(0);
|
|
518
|
+
expect(seenDuringDescribe).toBe("http://localhost:4566");
|
|
519
|
+
expect(process.env.AWS_ENDPOINT_URL).toBeUndefined(); // restored
|
|
520
|
+
expect(stderrBuf.join("\n")).toMatch(/environment "floci" declares endpoint http:\/\/localhost:4566/);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test("ambient AWS_ENDPOINT_URL still wins over the declared endpoint", async () => {
|
|
524
|
+
process.env.AWS_ENDPOINT_URL = "http://real-endpoint.example";
|
|
525
|
+
buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
|
|
526
|
+
fetchLifecycleMock.mockResolvedValue(undefined);
|
|
527
|
+
readSnapshotMock.mockResolvedValue(null);
|
|
528
|
+
loadChantConfigMock.mockResolvedValue({
|
|
529
|
+
config: { environments: [{ name: "floci", endpoint: "http://localhost:4566" }] },
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
let seenDuringDescribe: string | undefined;
|
|
533
|
+
const plugins: LexiconPlugin[] = [
|
|
534
|
+
createMockPlugin({
|
|
535
|
+
name: "aws",
|
|
536
|
+
describeResources: async () => {
|
|
537
|
+
seenDuringDescribe = process.env.AWS_ENDPOINT_URL;
|
|
538
|
+
return {};
|
|
539
|
+
},
|
|
540
|
+
}),
|
|
541
|
+
];
|
|
542
|
+
|
|
543
|
+
const exit = await runLifecycleDiff({
|
|
544
|
+
args: makeArgs({ path: "diff", extraPositional: "floci", live: true }),
|
|
545
|
+
plugins,
|
|
546
|
+
serializers: plugins.map((p) => p.serializer),
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
expect(exit).toBe(0);
|
|
550
|
+
expect(seenDuringDescribe).toBe("http://real-endpoint.example");
|
|
551
|
+
expect(process.env.AWS_ENDPOINT_URL).toBe("http://real-endpoint.example");
|
|
552
|
+
expect(stderrBuf.join("\n")).toMatch(/ambient AWS_ENDPOINT_URL already set/);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
describe("runLifecyclePlan", () => {
|
|
558
|
+
let stdoutBuf: string[];
|
|
559
|
+
let stderrBuf: string[];
|
|
560
|
+
|
|
561
|
+
beforeEach(() => {
|
|
562
|
+
stdoutBuf = [];
|
|
563
|
+
stderrBuf = [];
|
|
564
|
+
vi.spyOn(console, "log").mockImplementation((s: string) => { stdoutBuf.push(s); });
|
|
565
|
+
vi.spyOn(console, "error").mockImplementation((s: string) => { stderrBuf.push(s); });
|
|
566
|
+
buildMock.mockReset();
|
|
567
|
+
fetchLifecycleMock.mockReset();
|
|
568
|
+
fetchLifecycleMock.mockResolvedValue(undefined);
|
|
569
|
+
readSnapshotMock.mockReset();
|
|
570
|
+
readSnapshotMock.mockResolvedValue(null);
|
|
571
|
+
loadChantConfigMock.mockReset();
|
|
572
|
+
loadChantConfigMock.mockResolvedValue({ config: {} });
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test("happy path: proposes a create for a declared, unobserved-nowhere-else entity", async () => {
|
|
576
|
+
buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
|
|
577
|
+
const plugins: LexiconPlugin[] = [
|
|
578
|
+
createMockPlugin({ name: "aws", describeResources: staticDescribeResources({}) }),
|
|
579
|
+
];
|
|
580
|
+
const exit = await runLifecyclePlan({
|
|
581
|
+
args: makeArgs({ path: "plan", extraPositional: "prod" }),
|
|
582
|
+
plugins,
|
|
583
|
+
serializers: plugins.map((p) => p.serializer),
|
|
584
|
+
});
|
|
585
|
+
expect(exit).toBe(0);
|
|
586
|
+
expect(stdoutBuf.join("\n")).toContain("bucket");
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// #1166 — plan is always a live read (no `--live` flag of its own), so a
|
|
590
|
+
// declared environment endpoint applies here exactly as it does for
|
|
591
|
+
// `chant graph --live` / `chant lifecycle diff --live`.
|
|
592
|
+
describe("declared endpoint (#1166)", () => {
|
|
593
|
+
const prevEndpoint = process.env.AWS_ENDPOINT_URL;
|
|
594
|
+
|
|
595
|
+
afterEach(() => {
|
|
596
|
+
if (prevEndpoint === undefined) delete process.env.AWS_ENDPOINT_URL;
|
|
597
|
+
else process.env.AWS_ENDPOINT_URL = prevEndpoint;
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test("applies the declared endpoint to AWS_ENDPOINT_URL for the plan's describe, then restores it", async () => {
|
|
601
|
+
delete process.env.AWS_ENDPOINT_URL;
|
|
602
|
+
buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
|
|
603
|
+
loadChantConfigMock.mockResolvedValue({
|
|
604
|
+
config: { environments: [{ name: "floci", endpoint: "http://localhost:4566" }] },
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
let seenDuringDescribe: string | undefined;
|
|
608
|
+
const plugins: LexiconPlugin[] = [
|
|
609
|
+
createMockPlugin({
|
|
610
|
+
name: "aws",
|
|
611
|
+
describeResources: async () => {
|
|
612
|
+
seenDuringDescribe = process.env.AWS_ENDPOINT_URL;
|
|
613
|
+
return {};
|
|
614
|
+
},
|
|
615
|
+
}),
|
|
616
|
+
];
|
|
617
|
+
|
|
618
|
+
const exit = await runLifecyclePlan({
|
|
619
|
+
args: makeArgs({ path: "plan", extraPositional: "floci" }),
|
|
620
|
+
plugins,
|
|
621
|
+
serializers: plugins.map((p) => p.serializer),
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
expect(exit).toBe(0);
|
|
625
|
+
expect(seenDuringDescribe).toBe("http://localhost:4566");
|
|
626
|
+
expect(process.env.AWS_ENDPOINT_URL).toBeUndefined();
|
|
627
|
+
expect(stderrBuf.join("\n")).toMatch(/environment "floci" declares endpoint http:\/\/localhost:4566/);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
test("ambient AWS_ENDPOINT_URL still wins over the declared endpoint", async () => {
|
|
631
|
+
process.env.AWS_ENDPOINT_URL = "http://real-endpoint.example";
|
|
632
|
+
buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
|
|
633
|
+
loadChantConfigMock.mockResolvedValue({
|
|
634
|
+
config: { environments: [{ name: "floci", endpoint: "http://localhost:4566" }] },
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
let seenDuringDescribe: string | undefined;
|
|
638
|
+
const plugins: LexiconPlugin[] = [
|
|
639
|
+
createMockPlugin({
|
|
640
|
+
name: "aws",
|
|
641
|
+
describeResources: async () => {
|
|
642
|
+
seenDuringDescribe = process.env.AWS_ENDPOINT_URL;
|
|
643
|
+
return {};
|
|
644
|
+
},
|
|
645
|
+
}),
|
|
646
|
+
];
|
|
647
|
+
|
|
648
|
+
const exit = await runLifecyclePlan({
|
|
649
|
+
args: makeArgs({ path: "plan", extraPositional: "floci" }),
|
|
650
|
+
plugins,
|
|
651
|
+
serializers: plugins.map((p) => p.serializer),
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
expect(exit).toBe(0);
|
|
655
|
+
expect(seenDuringDescribe).toBe("http://real-endpoint.example");
|
|
656
|
+
expect(process.env.AWS_ENDPOINT_URL).toBe("http://real-endpoint.example");
|
|
657
|
+
expect(stderrBuf.join("\n")).toMatch(/ambient AWS_ENDPOINT_URL already set/);
|
|
658
|
+
});
|
|
659
|
+
});
|
|
292
660
|
});
|
|
293
661
|
|
|
294
662
|
describe("runLifecycleSnapshot", () => {
|
|
@@ -365,6 +733,42 @@ describe("runLifecycleSnapshot", () => {
|
|
|
365
733
|
expect(stderrBuf.join("\n")).toContain("Snapshot saved");
|
|
366
734
|
expect(takeSnapshotMock).toHaveBeenCalledTimes(1);
|
|
367
735
|
});
|
|
736
|
+
|
|
737
|
+
// #1166 — a snapshot is always a live read, so a declared environment
|
|
738
|
+
// endpoint applies here too, unless the ambient shell already set it.
|
|
739
|
+
describe("declared endpoint (#1166)", () => {
|
|
740
|
+
const prevEndpoint = process.env.AWS_ENDPOINT_URL;
|
|
741
|
+
|
|
742
|
+
afterEach(() => {
|
|
743
|
+
if (prevEndpoint === undefined) delete process.env.AWS_ENDPOINT_URL;
|
|
744
|
+
else process.env.AWS_ENDPOINT_URL = prevEndpoint;
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
test("applies the declared endpoint to AWS_ENDPOINT_URL for takeSnapshot, then restores it", async () => {
|
|
748
|
+
delete process.env.AWS_ENDPOINT_URL;
|
|
749
|
+
buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
|
|
750
|
+
loadChantConfigMock.mockResolvedValue({
|
|
751
|
+
config: { environments: [{ name: "floci", endpoint: "http://localhost:4566" }] },
|
|
752
|
+
});
|
|
753
|
+
let seenDuringSnapshot: string | undefined;
|
|
754
|
+
takeSnapshotMock.mockImplementation(async () => {
|
|
755
|
+
seenDuringSnapshot = process.env.AWS_ENDPOINT_URL;
|
|
756
|
+
return { snapshots: [], commit: "sha", warnings: [], errors: [] };
|
|
757
|
+
});
|
|
758
|
+
const plugins: LexiconPlugin[] = [
|
|
759
|
+
createMockPlugin({ name: "aws", describeResources: staticDescribeResources({}) }),
|
|
760
|
+
];
|
|
761
|
+
const exit = await runLifecycleSnapshot({
|
|
762
|
+
args: makeArgs({ command: "state", path: "snapshot", extraPositional: "floci" }),
|
|
763
|
+
plugins,
|
|
764
|
+
serializers: plugins.map((p) => p.serializer),
|
|
765
|
+
});
|
|
766
|
+
expect(exit).toBe(0);
|
|
767
|
+
expect(seenDuringSnapshot).toBe("http://localhost:4566");
|
|
768
|
+
expect(process.env.AWS_ENDPOINT_URL).toBeUndefined();
|
|
769
|
+
expect(stderrBuf.join("\n")).toMatch(/environment "floci" declares endpoint http:\/\/localhost:4566/);
|
|
770
|
+
});
|
|
771
|
+
});
|
|
368
772
|
});
|
|
369
773
|
|
|
370
774
|
describe("runLifecycleShow", () => {
|