@intentius/chant 0.44.2 → 0.44.3
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/build.d.ts +23 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/build.ts +51 -19
- package/src/cli/handlers/graph.test.ts +191 -15
- package/src/cli/handlers/graph.ts +99 -7
package/dist/build.d.ts
CHANGED
|
@@ -203,6 +203,29 @@ export declare function collectLexiconOutputs(entities: Map<string, Declarable>)
|
|
|
203
203
|
* @returns Array of auto-detected LexiconOutput instances
|
|
204
204
|
*/
|
|
205
205
|
export declare function detectCrossLexiconRefs(entities: Map<string, Declarable>): LexiconOutput[];
|
|
206
|
+
/** What merging build-root contributions produced: non-fatal render notes and
|
|
207
|
+
* fatal messages (a failed contributor, a name collision). */
|
|
208
|
+
export interface BuildRootMergeResult {
|
|
209
|
+
warnings: string[];
|
|
210
|
+
errors: string[];
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Run each build-root contributor (#1548 piece 3) and merge its rendered
|
|
214
|
+
* entities into `entities`, in place. THE one merge implementation — `build()`
|
|
215
|
+
* uses it for step 4b, and the graph handler's discover-based paths reuse it
|
|
216
|
+
* so a rendered kustomize root joins the graphed entity set under exactly the
|
|
217
|
+
* rules the build applies (#1626):
|
|
218
|
+
*
|
|
219
|
+
* - a contributed name colliding with an existing entity is an error, never a
|
|
220
|
+
* silent overwrite;
|
|
221
|
+
* - a contributor that throws becomes an error carrying its message (the k8s
|
|
222
|
+
* hook's missing-binary refusal names the binaries), not a stack trace.
|
|
223
|
+
*
|
|
224
|
+
* Errors come back as plain messages; each caller wraps them in its own error
|
|
225
|
+
* vocabulary (`BuildErrorClass` in `build()`, the CLI's `formatError` on the
|
|
226
|
+
* graph paths).
|
|
227
|
+
*/
|
|
228
|
+
export declare function mergeBuildRootEntities(entities: Map<string, Declarable>, contributors: ReadonlyArray<() => Promise<BuildRootContribution>>): Promise<BuildRootMergeResult>;
|
|
206
229
|
/**
|
|
207
230
|
* Builds a lexicon specification by discovering entities, sorting them
|
|
208
231
|
* topologically, and serializing them using the lexicon serializers.
|
package/dist/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/graph.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/graph.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAqGlD;;;;;;;;;GASG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAwCnE"}
|
package/package.json
CHANGED
package/src/build.ts
CHANGED
|
@@ -565,6 +565,53 @@ function generateManifest(
|
|
|
565
565
|
};
|
|
566
566
|
}
|
|
567
567
|
|
|
568
|
+
/** What merging build-root contributions produced: non-fatal render notes and
|
|
569
|
+
* fatal messages (a failed contributor, a name collision). */
|
|
570
|
+
export interface BuildRootMergeResult {
|
|
571
|
+
warnings: string[];
|
|
572
|
+
errors: string[];
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Run each build-root contributor (#1548 piece 3) and merge its rendered
|
|
577
|
+
* entities into `entities`, in place. THE one merge implementation — `build()`
|
|
578
|
+
* uses it for step 4b, and the graph handler's discover-based paths reuse it
|
|
579
|
+
* so a rendered kustomize root joins the graphed entity set under exactly the
|
|
580
|
+
* rules the build applies (#1626):
|
|
581
|
+
*
|
|
582
|
+
* - a contributed name colliding with an existing entity is an error, never a
|
|
583
|
+
* silent overwrite;
|
|
584
|
+
* - a contributor that throws becomes an error carrying its message (the k8s
|
|
585
|
+
* hook's missing-binary refusal names the binaries), not a stack trace.
|
|
586
|
+
*
|
|
587
|
+
* Errors come back as plain messages; each caller wraps them in its own error
|
|
588
|
+
* vocabulary (`BuildErrorClass` in `build()`, the CLI's `formatError` on the
|
|
589
|
+
* graph paths).
|
|
590
|
+
*/
|
|
591
|
+
export async function mergeBuildRootEntities(
|
|
592
|
+
entities: Map<string, Declarable>,
|
|
593
|
+
contributors: ReadonlyArray<() => Promise<BuildRootContribution>>,
|
|
594
|
+
): Promise<BuildRootMergeResult> {
|
|
595
|
+
const warnings: string[] = [];
|
|
596
|
+
const errors: string[] = [];
|
|
597
|
+
for (const contribute of contributors) {
|
|
598
|
+
try {
|
|
599
|
+
const contribution = await contribute();
|
|
600
|
+
warnings.push(...(contribution.warnings ?? []));
|
|
601
|
+
for (const [name, entity] of contribution.entities) {
|
|
602
|
+
if (entities.has(name)) {
|
|
603
|
+
errors.push(`Build-root entity "${name}" collides with a discovered entity of the same name`);
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
entities.set(name, entity);
|
|
607
|
+
}
|
|
608
|
+
} catch (error) {
|
|
609
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return { warnings, errors };
|
|
613
|
+
}
|
|
614
|
+
|
|
568
615
|
/**
|
|
569
616
|
* Builds a lexicon specification by discovering entities, sorting them
|
|
570
617
|
* topologically, and serializing them using the lexicon serializers.
|
|
@@ -680,25 +727,10 @@ async function buildFromDiscoveryResult(
|
|
|
680
727
|
// object, and re-running the contributors there would duplicate every
|
|
681
728
|
// contributed entity once per child.
|
|
682
729
|
if (!parentBuildStack && options?.buildRoots) {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
for (const [name, entity] of contribution.entities) {
|
|
688
|
-
if (discoveryResult.entities.has(name)) {
|
|
689
|
-
errors.push(
|
|
690
|
-
new BuildErrorClass(
|
|
691
|
-
"",
|
|
692
|
-
`Build-root entity "${name}" collides with a discovered entity of the same name`,
|
|
693
|
-
),
|
|
694
|
-
);
|
|
695
|
-
continue;
|
|
696
|
-
}
|
|
697
|
-
discoveryResult.entities.set(name, entity);
|
|
698
|
-
}
|
|
699
|
-
} catch (error) {
|
|
700
|
-
errors.push(new BuildErrorClass("", error instanceof Error ? error.message : String(error)));
|
|
701
|
-
}
|
|
730
|
+
const merged = await mergeBuildRootEntities(discoveryResult.entities, options.buildRoots);
|
|
731
|
+
warnings.push(...merged.warnings);
|
|
732
|
+
for (const message of merged.errors) {
|
|
733
|
+
errors.push(new BuildErrorClass("", message));
|
|
702
734
|
}
|
|
703
735
|
}
|
|
704
736
|
|
|
@@ -52,10 +52,16 @@ vi.mock("../../graph-layout", () => ({
|
|
|
52
52
|
// hit the Op-graph and source-view modes, so these mocks don't touch them).
|
|
53
53
|
const loadPluginsMock = vi.fn();
|
|
54
54
|
const resolveLexMock = vi.fn();
|
|
55
|
-
vi.mock("../plugins", () =>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
vi.mock("../plugins", async () => {
|
|
56
|
+
// Real `collectBuildRootContributors` (#1626): it is pure binding — the
|
|
57
|
+
// handler's build-root plumbing is exactly what the tests below assert.
|
|
58
|
+
const actual = await vi.importActual<typeof import("../plugins")>("../plugins");
|
|
59
|
+
return {
|
|
60
|
+
...actual,
|
|
61
|
+
loadPlugins: (...a: unknown[]) => loadPluginsMock(...a),
|
|
62
|
+
resolveProjectLexicons: (...a: unknown[]) => resolveLexMock(...a),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
59
65
|
const observeMock = vi.fn();
|
|
60
66
|
const replayMock = vi.fn();
|
|
61
67
|
const hasSnapshotMock = vi.fn((..._a: unknown[]) => Promise.resolve(false));
|
|
@@ -86,17 +92,22 @@ vi.mock("../../config", async () => {
|
|
|
86
92
|
};
|
|
87
93
|
});
|
|
88
94
|
const buildMock = vi.fn();
|
|
89
|
-
vi.mock("../../build", () =>
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
vi.mock("../../build", async () => {
|
|
96
|
+
// Real `mergeBuildRootEntities` (#1626): the graph paths reuse build's one
|
|
97
|
+
// merge implementation, so the tests exercise the real merge rules
|
|
98
|
+
// (collision → error) rather than a re-stubbed copy of them.
|
|
99
|
+
const actual = await vi.importActual<typeof import("../../build")>("../../build");
|
|
100
|
+
return {
|
|
101
|
+
...actual,
|
|
102
|
+
// Forwards its arguments so a test can assert what the live path passed —
|
|
103
|
+
// `buildParams` in particular (#1483), the same reason `discover` above
|
|
104
|
+
// forwards for #1359.
|
|
105
|
+
build: (...a: unknown[]) => {
|
|
106
|
+
buildMock(...a);
|
|
107
|
+
return Promise.resolve({ errors: [] });
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
});
|
|
100
111
|
|
|
101
112
|
const { runGraph } = await import("./graph");
|
|
102
113
|
|
|
@@ -812,4 +823,169 @@ describe("runGraph", () => {
|
|
|
812
823
|
});
|
|
813
824
|
});
|
|
814
825
|
});
|
|
826
|
+
|
|
827
|
+
// #1626 — #1612's `buildRoots` seam reached `chant build` and the lifecycle
|
|
828
|
+
// handlers but none of the graph command's four entity-collection sites, so
|
|
829
|
+
// a `k8s.kustomize.roots` estate graphed only its typed source: the rendered
|
|
830
|
+
// Deployment/Service existed in the build output and on the cluster, and
|
|
831
|
+
// nowhere in any graph. The shapes here mirror the kustomize-root fixture
|
|
832
|
+
// (lexicons/k8s/src/testdata/kustomize-root, rendered as `overlays/prod`):
|
|
833
|
+
// the contributor below returns exactly what `renderKustomizeRoots` returns
|
|
834
|
+
// for it — rendered-manifest entities keyed `overlays/prod/<kindName>`,
|
|
835
|
+
// props = the verbatim document, provenance annotation stamped.
|
|
836
|
+
describe("build-root contributions reach the graph (#1626)", () => {
|
|
837
|
+
const KUSTOMIZE_ROOT_ANNOTATION = "chant.intentius.io/kustomize-root";
|
|
838
|
+
|
|
839
|
+
/** A rendered kustomize document as `renderedManifestEntity` shapes it. */
|
|
840
|
+
const rendered = (group: string, kind: string, name: string, spec: Record<string, unknown>): Declarable =>
|
|
841
|
+
({
|
|
842
|
+
lexicon: "k8s",
|
|
843
|
+
entityType: `K8s::${group}::${kind}`,
|
|
844
|
+
kind: "resource",
|
|
845
|
+
props: {
|
|
846
|
+
apiVersion: group === "Core" ? "v1" : `${group.toLowerCase()}/v1`,
|
|
847
|
+
kind,
|
|
848
|
+
metadata: { name, annotations: { [KUSTOMIZE_ROOT_ANNOTATION]: "overlays/prod" } },
|
|
849
|
+
spec,
|
|
850
|
+
},
|
|
851
|
+
[DECLARABLE_MARKER]: true,
|
|
852
|
+
[Symbol.for("chant.k8s.renderedManifest")]: true,
|
|
853
|
+
}) as unknown as Declarable;
|
|
854
|
+
|
|
855
|
+
const fixtureContribution = () => ({
|
|
856
|
+
entities: new Map<string, Declarable>([
|
|
857
|
+
["overlays/prod/serviceProdWeb", rendered("Core", "Service", "prod-web", { selector: { app: "web" } })],
|
|
858
|
+
["overlays/prod/deploymentProdWeb", rendered("Apps", "Deployment", "prod-web", { replicas: 3 })],
|
|
859
|
+
]),
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
/** A k8s-shaped plugin whose `buildRoots` hook renders the fixture. */
|
|
863
|
+
const k8sPluginStub = (hook = async () => fixtureContribution()) => ({
|
|
864
|
+
name: "k8s",
|
|
865
|
+
serializer: {},
|
|
866
|
+
describeResources: () => Promise.resolve({}),
|
|
867
|
+
buildRoots: hook,
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
const declaredNamespace = (): void => {
|
|
871
|
+
discoverMock.mockResolvedValue({
|
|
872
|
+
entities: new Map<string, Declarable>([["ns", decl({ lexicon: "k8s", entityType: "K8s::Core::Namespace", kind: "resource", props: { metadata: { name: "web" } } })]]),
|
|
873
|
+
errors: [], dependencies: new Map(), sourceFiles: [],
|
|
874
|
+
});
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
const staticWithFixtureRoot = (hook?: () => Promise<{ entities: Map<string, Declarable> }>): void => {
|
|
878
|
+
lintMock.mockResolvedValue({ success: true });
|
|
879
|
+
declaredNamespace();
|
|
880
|
+
resolveLexMock.mockResolvedValue(["k8s"]);
|
|
881
|
+
loadPluginsMock.mockResolvedValue([k8sPluginStub(hook)]);
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
test("static --format ir emits the rendered nodes alongside the typed ones", async () => {
|
|
885
|
+
staticWithFixtureRoot();
|
|
886
|
+
const exit = await runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] });
|
|
887
|
+
expect(exit).toBe(0);
|
|
888
|
+
const ir = JSON.parse(stdoutBuf.join("\n"));
|
|
889
|
+
expect(ir.nodes.map((n: { id: string }) => n.id).sort()).toEqual([
|
|
890
|
+
"ns",
|
|
891
|
+
"overlays/prod/deploymentProdWeb",
|
|
892
|
+
"overlays/prod/serviceProdWeb",
|
|
893
|
+
]);
|
|
894
|
+
const deployment = ir.nodes.find((n: { id: string }) => n.id === "overlays/prod/deploymentProdWeb");
|
|
895
|
+
expect(deployment.kind).toBe("K8s::Apps::Deployment");
|
|
896
|
+
expect(deployment.lexicon).toBe("k8s");
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
// #1624 made attrs projection monotonic; the provenance annotation is a
|
|
900
|
+
// nested `metadata` tree, so it belongs to the full-property tier and only
|
|
901
|
+
// that tier — present at --detail 3, absent from --detail 1's topology view.
|
|
902
|
+
test("--detail 3 carries the provenance annotation in the rendered node's attrs; --detail 1 does not", async () => {
|
|
903
|
+
staticWithFixtureRoot();
|
|
904
|
+
expect(await runGraph({ args: makeArgs({ format: "ir", detail: 3 }), plugins: [], serializers: [] })).toBe(0);
|
|
905
|
+
const detailed = JSON.parse(stdoutBuf.join("\n"));
|
|
906
|
+
const at3 = detailed.nodes.find((n: { id: string }) => n.id === "overlays/prod/deploymentProdWeb");
|
|
907
|
+
expect(at3.attrs.metadata.annotations[KUSTOMIZE_ROOT_ANNOTATION]).toBe("overlays/prod");
|
|
908
|
+
|
|
909
|
+
stdoutBuf.length = 0;
|
|
910
|
+
staticWithFixtureRoot();
|
|
911
|
+
expect(await runGraph({ args: makeArgs({ format: "ir", detail: 1 }), plugins: [], serializers: [] })).toBe(0);
|
|
912
|
+
const coarse = JSON.parse(stdoutBuf.join("\n"));
|
|
913
|
+
const at1 = coarse.nodes.find((n: { id: string }) => n.id === "overlays/prod/deploymentProdWeb");
|
|
914
|
+
expect(at1).toBeDefined(); // the node survives the tier —
|
|
915
|
+
expect(at1.attrs.metadata).toBeUndefined(); // its property tree does not
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
test("a contributed name colliding with a discovered entity fails the graph like a discovery error", async () => {
|
|
919
|
+
staticWithFixtureRoot(async () => ({
|
|
920
|
+
entities: new Map<string, Declarable>([["ns", rendered("Core", "Namespace", "web", {})]]),
|
|
921
|
+
}));
|
|
922
|
+
const exit = await runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] });
|
|
923
|
+
expect(exit).toBe(1);
|
|
924
|
+
expect(stdoutBuf.join("\n")).toBe("");
|
|
925
|
+
expect(stderrBuf.join("\n")).toContain('"ns" collides');
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
test("--stacks joins the rendered entities into the stack partitions", async () => {
|
|
929
|
+
declaredNamespace();
|
|
930
|
+
resolveLexMock.mockResolvedValue(["k8s"]);
|
|
931
|
+
loadPluginsMock.mockResolvedValue([k8sPluginStub()]);
|
|
932
|
+
const exit = await runGraph({ args: makeArgs({ stacks: true, json: true }), plugins: [], serializers: [] });
|
|
933
|
+
expect(exit).toBe(0);
|
|
934
|
+
// partitionByLexicon/computeStackGraph are stubbed above; what matters is
|
|
935
|
+
// that the merge ran before them without erroring. The collision case
|
|
936
|
+
// proves the same merge is live on this path:
|
|
937
|
+
stderrBuf.length = 0;
|
|
938
|
+
declaredNamespace();
|
|
939
|
+
loadPluginsMock.mockResolvedValue([
|
|
940
|
+
k8sPluginStub(async () => ({ entities: new Map<string, Declarable>([["ns", rendered("Core", "Namespace", "web", {})]]) })),
|
|
941
|
+
]);
|
|
942
|
+
expect(await runGraph({ args: makeArgs({ stacks: true, json: true }), plugins: [], serializers: [] })).toBe(1);
|
|
943
|
+
expect(stderrBuf.join("\n")).toContain('"ns" collides');
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
test("--live passes the contributors through to build(), like lifecycle does", async () => {
|
|
947
|
+
resolveLexMock.mockResolvedValue(["k8s"]);
|
|
948
|
+
loadPluginsMock.mockResolvedValue([k8sPluginStub()]);
|
|
949
|
+
observeMock.mockResolvedValue({ observations: [], errors: [], warnings: [] });
|
|
950
|
+
const exit = await runGraph({ args: makeArgs({ format: "ir", live: true, env: "kcheck" }), plugins: [], serializers: [] });
|
|
951
|
+
expect(exit).toBe(0);
|
|
952
|
+
const [, , , options] = buildMock.mock.calls[0] as [string, unknown, unknown, { buildRoots?: unknown[] }];
|
|
953
|
+
expect(options.buildRoots).toHaveLength(1);
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
test("--live --overlay classifies an observed rendered node as managed, not foreign", async () => {
|
|
957
|
+
resolveLexMock.mockResolvedValue(["k8s"]);
|
|
958
|
+
loadPluginsMock.mockResolvedValue([k8sPluginStub()]);
|
|
959
|
+
declaredNamespace();
|
|
960
|
+
// The cluster has the namespace and the rendered Deployment — the
|
|
961
|
+
// latter observed through the same entityType/props flow as any
|
|
962
|
+
// declared entity (its props carry metadata.name like typed ones).
|
|
963
|
+
observeMock.mockResolvedValue({
|
|
964
|
+
observations: [{
|
|
965
|
+
lexicon: "k8s",
|
|
966
|
+
resources: {
|
|
967
|
+
ns: { type: "K8s::Core::Namespace", status: "OBSERVED", physicalId: "uid-ns", ownership: "owned" },
|
|
968
|
+
"overlays/prod/deploymentProdWeb": { type: "K8s::Apps::Deployment", status: "OBSERVED", physicalId: "uid-deploy", ownership: "owned" },
|
|
969
|
+
},
|
|
970
|
+
}],
|
|
971
|
+
errors: [], warnings: [],
|
|
972
|
+
});
|
|
973
|
+
const exit = await runGraph({
|
|
974
|
+
args: makeArgs({ format: "ir", live: true, overlay: true, env: "kcheck" }),
|
|
975
|
+
plugins: [], serializers: [],
|
|
976
|
+
});
|
|
977
|
+
expect(exit).toBe(0);
|
|
978
|
+
const ir = JSON.parse(stdoutBuf.join("\n"));
|
|
979
|
+
const deployment = ir.nodes.find((n: { id: string }) => n.id === "overlays/prod/deploymentProdWeb");
|
|
980
|
+
// Declared (via the merged contribution) + observed = managed. Before
|
|
981
|
+
// #1626 the declared canvas lacked the rendered node, so it read as
|
|
982
|
+
// foreign (`warn`) despite being chant's own build output.
|
|
983
|
+
expect(deployment.attrs._status).toBe("good");
|
|
984
|
+
expect(deployment.physicalId).toBe("uid-deploy");
|
|
985
|
+
// The rendered Service was declared but not observed — pending, the
|
|
986
|
+
// same verdict a typed entity would get.
|
|
987
|
+
const service = ir.nodes.find((n: { id: string }) => n.id === "overlays/prod/serviceProdWeb");
|
|
988
|
+
expect(service.attrs._status).toBe("accent");
|
|
989
|
+
});
|
|
990
|
+
});
|
|
815
991
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { dirname, resolve } from "node:path";
|
|
2
2
|
import { discoverOps } from "../../op/discover";
|
|
3
3
|
import { discover } from "../../discovery/index";
|
|
4
|
-
import { partitionByLexicon, computeStackGraph, build } from "../../build";
|
|
4
|
+
import { partitionByLexicon, computeStackGraph, build, mergeBuildRootEntities } from "../../build";
|
|
5
5
|
import { buildGraphIr, buildLiveGraphIr, collectUnobserved, overlayGraphs, sourceOverlayGraphs, type GraphIR, type IRPipeline, type LiveObservation } from "../../graph-ir";
|
|
6
6
|
import { buildDeclaredPerStack } from "../../graph-declared";
|
|
7
7
|
import { reconstructEdges, mergeCatalogs, containmentGroups, type ReferenceCatalog, type ContainmentPair } from "../../graph-refs";
|
|
@@ -15,7 +15,7 @@ import { toMermaid } from "../../graph-mermaid";
|
|
|
15
15
|
import { toDot } from "../../graph-dot";
|
|
16
16
|
import { getLayoutEngine, toLayoutInput, type NodeSize } from "../../graph-layout";
|
|
17
17
|
import { lintCommand } from "../commands/lint";
|
|
18
|
-
import { loadPlugins, resolveProjectLexicons } from "../plugins";
|
|
18
|
+
import { loadPlugins, resolveProjectLexicons, collectBuildRootContributors } from "../plugins";
|
|
19
19
|
import { readFileSync } from "node:fs";
|
|
20
20
|
import { formatError, formatWarning, formatBold } from "../format";
|
|
21
21
|
import type { CommandContext } from "../registry";
|
|
@@ -56,6 +56,69 @@ async function graphBuildParams(
|
|
|
56
56
|
return resolution.provenance;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The build-root contributors this invocation's project configures (#1626) —
|
|
61
|
+
* the same closures `chant build` and the lifecycle handlers hand to
|
|
62
|
+
* `BuildOptions.buildRoots`, bound here for the graph command's
|
|
63
|
+
* discover-based paths, which never run `build()` and so never ran the hook:
|
|
64
|
+
* a `k8s.kustomize.roots` estate graphed only its typed source while its
|
|
65
|
+
* built (and deployed) output carried the rendered manifests too.
|
|
66
|
+
*
|
|
67
|
+
* `graph` is not `requiresPlugins` (Op/source-graph modes must work without a
|
|
68
|
+
* lexicon), so plugins are loaded here when `ctx.plugins` is empty, mirroring
|
|
69
|
+
* `runGraphLive`. Rooted at the config's own directory, like
|
|
70
|
+
* `cli/commands/build.ts` — a relative kustomize root resolves against where
|
|
71
|
+
* it was declared, not against a sourceDir-scoped graph path. A project that
|
|
72
|
+
* fails to load plugins degrades to no contributors with a warning rather
|
|
73
|
+
* than failing modes that never needed plugins at all.
|
|
74
|
+
*/
|
|
75
|
+
async function graphBuildRootContributors(
|
|
76
|
+
ctx: CommandContext,
|
|
77
|
+
projectPath: string,
|
|
78
|
+
): Promise<Array<() => Promise<import("../../lexicon").BuildRootContribution>>> {
|
|
79
|
+
const { config, configPath } = await loadChantConfigUpward(projectPath).catch(
|
|
80
|
+
() => ({ config: {} as ChantConfig, configPath: undefined }),
|
|
81
|
+
);
|
|
82
|
+
let plugins = ctx.plugins;
|
|
83
|
+
if (plugins.length === 0) {
|
|
84
|
+
try {
|
|
85
|
+
plugins = await loadPlugins(await resolveProjectLexicons(projectPath));
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error(formatWarning({
|
|
88
|
+
message: `could not load the project's lexicons — build-root entities (kustomize roots) will be missing from the graph (${err instanceof Error ? err.message : String(err)})`,
|
|
89
|
+
}));
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return collectBuildRootContributors(
|
|
94
|
+
plugins,
|
|
95
|
+
config as unknown as Record<string, unknown>,
|
|
96
|
+
configPath ? dirname(configPath) : projectPath,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Merge build-root contributions into a discovered entity set — the exact
|
|
102
|
+
* merge `build()` performs (one implementation: `mergeBuildRootEntities`,
|
|
103
|
+
* ../../build.ts), surfaced in the graph command's error style. Returns false
|
|
104
|
+
* when a contributor failed or a name collided; the caller stops, matching
|
|
105
|
+
* how the discover-based paths treat discovery errors — a graph missing the
|
|
106
|
+
* entities the build would refuse over must not be emitted as if complete.
|
|
107
|
+
*/
|
|
108
|
+
async function mergeGraphBuildRoots(
|
|
109
|
+
entities: Map<string, import("../../declarable").Declarable>,
|
|
110
|
+
contributors: Array<() => Promise<import("../../lexicon").BuildRootContribution>>,
|
|
111
|
+
): Promise<boolean> {
|
|
112
|
+
if (contributors.length === 0) return true;
|
|
113
|
+
const merged = await mergeBuildRootEntities(entities, contributors);
|
|
114
|
+
for (const w of merged.warnings) console.error(formatWarning({ message: w }));
|
|
115
|
+
if (merged.errors.length > 0) {
|
|
116
|
+
for (const message of merged.errors) console.error(formatError({ message }));
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
59
122
|
/**
|
|
60
123
|
* `chant graph` — the Op dependency graph by default; `--stacks` renders the
|
|
61
124
|
* cross-stack apply-ordering graph (edges, order, waves) chant computes from
|
|
@@ -154,11 +217,17 @@ async function runGraphLive(
|
|
|
154
217
|
// a confidently wrong overlay rather than an empty one.
|
|
155
218
|
const liveBuildParams = await graphBuildParams(ctx, projectPath);
|
|
156
219
|
if (!liveBuildParams) return 1;
|
|
220
|
+
// #1626 — the same build-root contributors `chant build` runs (kustomize
|
|
221
|
+
// roots rendered into entities), so the scope handed to describeResources
|
|
222
|
+
// includes the rendered objects and the live read observes them like any
|
|
223
|
+
// declared entity. Without this the built estate carried them but the graph
|
|
224
|
+
// build here didn't, and the overlay called every one of them foreign.
|
|
225
|
+
const buildRoots = collectBuildRootContributors(plugins, config as unknown as Record<string, unknown>, projectPath);
|
|
157
226
|
const buildResult = await build(
|
|
158
227
|
resolve(args.src ?? config.sourceDir ?? "."),
|
|
159
228
|
plugins.map((p) => p.serializer),
|
|
160
229
|
undefined,
|
|
161
|
-
{ buildParams: liveBuildParams },
|
|
230
|
+
{ buildParams: liveBuildParams, buildRoots },
|
|
162
231
|
);
|
|
163
232
|
if (buildResult.errors.length > 0) {
|
|
164
233
|
console.error(formatError({ message: "Build failed — fix errors before graphing live state" }));
|
|
@@ -323,14 +392,23 @@ async function runGraphLive(
|
|
|
323
392
|
const declared = await discover(resolve(args.src ?? config.sourceDir ?? "."), {
|
|
324
393
|
...(declaredParams ? { buildParams: declaredParams } : {}),
|
|
325
394
|
});
|
|
326
|
-
|
|
395
|
+
// #1626 — the declared canvas must include what the build declares, and
|
|
396
|
+
// that is discovery PLUS build-root contributions (rendered kustomize
|
|
397
|
+
// objects). Same contributors, same merge as the `build()` above — so
|
|
398
|
+
// the rendered nodes classify `good`/`accent` like typed ones instead
|
|
399
|
+
// of reading as foreign. A merge that fails here (it just succeeded in
|
|
400
|
+
// the build above, so only a non-deterministic render gets this far)
|
|
401
|
+
// degrades exactly like discovery errors: overlay skipped, said once.
|
|
402
|
+
if (declared.errors.length > 0) {
|
|
403
|
+
console.error(formatWarning({ message: "overlay: source has discovery errors — showing the provisioned graph without the declared overlay" }));
|
|
404
|
+
} else if (!(await mergeGraphBuildRoots(declared.entities, buildRoots))) {
|
|
405
|
+
console.error(formatWarning({ message: "overlay: a build root failed to render — showing the provisioned graph without the declared overlay" }));
|
|
406
|
+
} else {
|
|
327
407
|
const declaredIr = buildGraphIr(declared.entities, projectPath);
|
|
328
408
|
ir =
|
|
329
409
|
args.overlayAnchor === "live"
|
|
330
410
|
? overlayGraphs(ir, declaredIr, overlayOpts)
|
|
331
411
|
: sourceOverlayGraphs(declaredIr, ir, overlayOpts);
|
|
332
|
-
} else {
|
|
333
|
-
console.error(formatWarning({ message: "overlay: source has discovery errors — showing the provisioned graph without the declared overlay" }));
|
|
334
412
|
}
|
|
335
413
|
}
|
|
336
414
|
}
|
|
@@ -584,6 +662,14 @@ async function runGraphView(
|
|
|
584
662
|
return 1;
|
|
585
663
|
}
|
|
586
664
|
|
|
665
|
+
// #1626 — join the build-root contributions (rendered kustomize objects) to
|
|
666
|
+
// the discovered set, the way `build()` does before partitioning, so the
|
|
667
|
+
// static graph shows the same estate the build emits. Fatal on failure,
|
|
668
|
+
// like discovery errors: this graph stands for what would build.
|
|
669
|
+
if (!(await mergeGraphBuildRoots(result.entities, await graphBuildRootContributors(ctx, projectPath)))) {
|
|
670
|
+
return 1;
|
|
671
|
+
}
|
|
672
|
+
|
|
587
673
|
// Build the base IR, focus with a lens (declarable-level, most precise), then
|
|
588
674
|
// apply the detail tier — so e.g. blast:<resource> works before any collapse.
|
|
589
675
|
let ir: GraphIR = buildGraphIr(result.entities, projectPath);
|
|
@@ -703,6 +789,12 @@ async function runStackGraph(ctx: CommandContext): Promise<number> {
|
|
|
703
789
|
return 1;
|
|
704
790
|
}
|
|
705
791
|
|
|
792
|
+
// #1626 — same join as runGraphView: contributed entities are part of the
|
|
793
|
+
// built estate, so they belong in its stack partitions too.
|
|
794
|
+
if (!(await mergeGraphBuildRoots(result.entities, await graphBuildRootContributors(ctx, projectPath)))) {
|
|
795
|
+
return 1;
|
|
796
|
+
}
|
|
797
|
+
|
|
706
798
|
const lexicons = [...partitionByLexicon(result.entities).keys()];
|
|
707
799
|
const graph = computeStackGraph(result.entities, lexicons);
|
|
708
800
|
|