@intentius/chant-lexicon-helm 0.45.0 → 0.49.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/archive-render.d.ts +94 -0
- package/dist/archive-render.d.ts.map +1 -0
- package/dist/commands.d.ts +54 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/config.d.ts +135 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/deep-observe-hooks.d.ts +2 -0
- package/dist/deep-observe-hooks.d.ts.map +1 -0
- package/dist/deep-observe.d.ts +54 -0
- package/dist/deep-observe.d.ts.map +1 -0
- package/dist/describe-resources.d.ts +17 -0
- package/dist/describe-resources.d.ts.map +1 -0
- package/dist/index.d.ts +18 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +5 -3
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/whm503.d.ts +34 -0
- package/dist/lint/post-synth/whm503.d.ts.map +1 -0
- package/dist/lint/post-synth/whm504.d.ts +20 -0
- package/dist/lint/post-synth/whm504.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +2 -0
- package/dist/okf/rules/WHM503.md +15 -0
- package/dist/okf/rules/WHM504.md +11 -0
- package/dist/okf/types/Values.md +1 -0
- package/dist/op/activities/cluster-probe.d.ts +44 -0
- package/dist/op/activities/cluster-probe.d.ts.map +1 -0
- package/dist/op/activities/helm.d.ts +179 -5
- package/dist/op/activities/helm.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +4 -2
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/pinnability/actions.d.ts +76 -0
- package/dist/pinnability/actions.d.ts.map +1 -0
- package/dist/pinnability/classify.d.ts +129 -0
- package/dist/pinnability/classify.d.ts.map +1 -0
- package/dist/pinnability/conditions.d.ts +109 -0
- package/dist/pinnability/conditions.d.ts.map +1 -0
- package/dist/pinnability/index.d.ts +17 -0
- package/dist/pinnability/index.d.ts.map +1 -0
- package/dist/pinnability/localize.d.ts +124 -0
- package/dist/pinnability/localize.d.ts.map +1 -0
- package/dist/pinnability/render-stream.d.ts +27 -0
- package/dist/pinnability/render-stream.d.ts.map +1 -0
- package/dist/pinnability/values.d.ts +69 -0
- package/dist/pinnability/values.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/release-observe.d.ts +109 -0
- package/dist/release-observe.d.ts.map +1 -0
- package/dist/render-diff.d.ts +192 -0
- package/dist/render-diff.d.ts.map +1 -0
- package/dist/render-digest.d.ts +135 -0
- package/dist/render-digest.d.ts.map +1 -0
- package/dist/render-store.d.ts +310 -0
- package/dist/render-store.d.ts.map +1 -0
- package/dist/render-wrapper.d.ts +140 -0
- package/dist/render-wrapper.d.ts.map +1 -0
- package/dist/render.d.ts +81 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/rules/whm503.ts +101 -0
- package/dist/rules/whm504.ts +52 -0
- package/dist/skills/chant-helm.md +7 -0
- package/dist/values-probe.d.ts +177 -0
- package/dist/values-probe.d.ts.map +1 -0
- package/dist/wrapper-chart.d.ts +61 -0
- package/dist/wrapper-chart.d.ts.map +1 -0
- package/package.json +10 -4
- package/src/archive-render.test.ts +201 -0
- package/src/archive-render.ts +119 -0
- package/src/commands.test.ts +487 -0
- package/src/commands.ts +445 -0
- package/src/config.test.ts +130 -0
- package/src/config.ts +220 -0
- package/src/deep-observe-hooks.ts +20 -0
- package/src/deep-observe.test.ts +165 -0
- package/src/deep-observe.ts +121 -0
- package/src/describe-resources.test.ts +255 -0
- package/src/describe-resources.ts +198 -0
- package/src/index.ts +98 -2
- package/src/lifecycle-integration.test.ts +289 -0
- package/src/lint/audit-catalog.ts +2 -0
- package/src/lint/post-synth/index.ts +4 -0
- package/src/lint/post-synth/whm503.test.ts +134 -0
- package/src/lint/post-synth/whm503.ts +101 -0
- package/src/lint/post-synth/whm504.test.ts +105 -0
- package/src/lint/post-synth/whm504.ts +52 -0
- package/src/op/activities/cluster-probe.test.ts +66 -0
- package/src/op/activities/cluster-probe.ts +148 -0
- package/src/op/activities/helm.test.ts +824 -0
- package/src/op/activities/helm.ts +628 -5
- package/src/op/activities/index.ts +17 -2
- package/src/pinnability/actions.ts +263 -0
- package/src/pinnability/classify.ts +579 -0
- package/src/pinnability/conditions.ts +0 -0
- package/src/pinnability/index.ts +77 -0
- package/src/pinnability/localize.test.ts +234 -0
- package/src/pinnability/localize.ts +644 -0
- package/src/pinnability/pinnability.test.ts +560 -0
- package/src/pinnability/render-stream.ts +59 -0
- package/src/pinnability/values.ts +205 -0
- package/src/plugin.test.ts +3 -1
- package/src/plugin.ts +47 -0
- package/src/release-observe.ts +392 -0
- package/src/render-diff.test.ts +366 -0
- package/src/render-diff.ts +407 -0
- package/src/render-digest.test.ts +186 -0
- package/src/render-digest.ts +207 -0
- package/src/render-store.test.ts +412 -0
- package/src/render-store.ts +621 -0
- package/src/render-wrapper.test.ts +259 -0
- package/src/render-wrapper.ts +274 -0
- package/src/render.test.ts +383 -4
- package/src/render.ts +319 -8
- package/src/skills/chant-helm.md +7 -0
- package/src/values-probe.test.ts +400 -0
- package/src/values-probe.ts +627 -0
- package/src/wrapper-chart.test.ts +110 -0
- package/src/wrapper-chart.ts +124 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { normalizeObservation } from "@intentius/chant/observation";
|
|
3
|
+
import { describeResources } from "./describe-resources";
|
|
4
|
+
import { parseReleaseDocuments } from "./release-observe";
|
|
5
|
+
import type { HelmRunner } from "./release-observe";
|
|
6
|
+
|
|
7
|
+
const LIST = JSON.stringify([
|
|
8
|
+
{
|
|
9
|
+
name: "web-app",
|
|
10
|
+
namespace: "prod",
|
|
11
|
+
revision: "3",
|
|
12
|
+
updated: "2026-08-01 10:00:00.000000000 +0000 UTC",
|
|
13
|
+
status: "deployed",
|
|
14
|
+
chart: "web-app-0.1.0",
|
|
15
|
+
app_version: "1.0.0",
|
|
16
|
+
},
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const MANIFEST = `---
|
|
20
|
+
# Source: web-app/templates/deployment.yaml
|
|
21
|
+
apiVersion: apps/v1
|
|
22
|
+
kind: Deployment
|
|
23
|
+
metadata:
|
|
24
|
+
name: web-app
|
|
25
|
+
labels:
|
|
26
|
+
app.kubernetes.io/managed-by: chant
|
|
27
|
+
chant.intentius.io/stack: shop
|
|
28
|
+
chant.intentius.io/env: prod
|
|
29
|
+
spec:
|
|
30
|
+
replicas: 2
|
|
31
|
+
---
|
|
32
|
+
# Source: web-app/templates/service.yaml
|
|
33
|
+
apiVersion: v1
|
|
34
|
+
kind: Service
|
|
35
|
+
metadata:
|
|
36
|
+
name: web-app
|
|
37
|
+
namespace: elsewhere
|
|
38
|
+
spec:
|
|
39
|
+
ports:
|
|
40
|
+
- port: 80
|
|
41
|
+
---
|
|
42
|
+
# Source: web-app/crds/widgets.yaml
|
|
43
|
+
apiVersion: apiextensions.k8s.io/v1
|
|
44
|
+
kind: CustomResourceDefinition
|
|
45
|
+
metadata:
|
|
46
|
+
name: widgets.example.com
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
const HOOKS = `---
|
|
50
|
+
# Source: web-app/templates/migrate-job.yaml
|
|
51
|
+
apiVersion: batch/v1
|
|
52
|
+
kind: Job
|
|
53
|
+
metadata:
|
|
54
|
+
name: web-app-migrate
|
|
55
|
+
annotations:
|
|
56
|
+
"helm.sh/hook": pre-install,pre-upgrade
|
|
57
|
+
"helm.sh/hook-weight": "1"
|
|
58
|
+
"helm.sh/hook-delete-policy": hook-succeeded
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
/** A scripted helm CLI: canned stdout per subcommand, recorded commands. */
|
|
62
|
+
function scriptedHelm(
|
|
63
|
+
outputs: { list?: string; manifest?: string; hooks?: string },
|
|
64
|
+
commands: string[] = [],
|
|
65
|
+
): HelmRunner {
|
|
66
|
+
return async (command) => {
|
|
67
|
+
commands.push(command);
|
|
68
|
+
if (command.startsWith("helm list")) return { stdout: outputs.list ?? "[]" };
|
|
69
|
+
if (command.startsWith("helm get manifest")) {
|
|
70
|
+
if (outputs.manifest === undefined) throw new Error("release: not found");
|
|
71
|
+
return { stdout: outputs.manifest };
|
|
72
|
+
}
|
|
73
|
+
if (command.startsWith("helm get hooks")) {
|
|
74
|
+
if (outputs.hooks === undefined) throw new Error("release: not found");
|
|
75
|
+
return { stdout: outputs.hooks };
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`unexpected command: ${command}`);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const chartEntities = new Map([
|
|
82
|
+
["chart", { entityType: "Helm::Chart", props: { name: "web-app" } }],
|
|
83
|
+
["valuesSchema", { entityType: "Helm::Values", props: { replicaCount: 2 } }],
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
const baseOptions = {
|
|
87
|
+
environment: "prod",
|
|
88
|
+
buildOutput: "",
|
|
89
|
+
entityNames: [...chartEntities.keys()],
|
|
90
|
+
entities: chartEntities,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
describe("helm describeResources (#1246)", () => {
|
|
94
|
+
test("reads BOTH channels: manifest resources and hook resources are rows", async () => {
|
|
95
|
+
const commands: string[] = [];
|
|
96
|
+
const result = await describeResources(baseOptions, scriptedHelm({ list: LIST, manifest: MANIFEST, hooks: HOOKS }, commands));
|
|
97
|
+
const { resources, unobserved } = normalizeObservation(result);
|
|
98
|
+
|
|
99
|
+
expect(commands.some((c) => c.startsWith("helm get manifest 'web-app' -n 'prod'"))).toBe(true);
|
|
100
|
+
expect(commands.some((c) => c.startsWith("helm get hooks 'web-app' -n 'prod'"))).toBe(true);
|
|
101
|
+
|
|
102
|
+
// The release row, keyed by the chart entity.
|
|
103
|
+
expect(resources.chart).toMatchObject({
|
|
104
|
+
type: "Helm::Release",
|
|
105
|
+
physicalId: "prod/web-app",
|
|
106
|
+
status: "deployed",
|
|
107
|
+
ownership: "owned",
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Manifest channel: namespace-silent documents inherit the release
|
|
111
|
+
// namespace; explicit namespaces are kept; cluster-scoped kinds get none.
|
|
112
|
+
expect(resources["Deployment/prod/web-app"]).toMatchObject({
|
|
113
|
+
type: "K8s::Apps::Deployment",
|
|
114
|
+
attributes: expect.objectContaining({ channel: "manifest", release: "web-app" }),
|
|
115
|
+
});
|
|
116
|
+
expect(resources["Service/elsewhere/web-app"]).toBeDefined();
|
|
117
|
+
expect(resources["cluster:CustomResourceDefinition/widgets.example.com"]).toBeDefined();
|
|
118
|
+
|
|
119
|
+
// Hooks channel: excluded from `helm get manifest`, so this row only
|
|
120
|
+
// exists because both channels were read.
|
|
121
|
+
expect(resources["Job/prod/web-app-migrate"]).toMatchObject({
|
|
122
|
+
type: "K8s::Batch::Job",
|
|
123
|
+
attributes: expect.objectContaining({
|
|
124
|
+
channel: "hooks",
|
|
125
|
+
hook: "pre-install,pre-upgrade",
|
|
126
|
+
hookWeight: "1",
|
|
127
|
+
hookDeletePolicy: "hook-succeeded",
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(unobserved).toEqual({});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("every rendered resource is owned via release identity and a runtime child of its chart (#1077)", async () => {
|
|
135
|
+
const result = await describeResources(baseOptions, scriptedHelm({ list: LIST, manifest: MANIFEST, hooks: HOOKS }));
|
|
136
|
+
const { resources } = normalizeObservation(result);
|
|
137
|
+
for (const key of ["Deployment/prod/web-app", "Service/elsewhere/web-app", "Job/prod/web-app-migrate"]) {
|
|
138
|
+
expect(resources[key].ownership).toBe("owned");
|
|
139
|
+
expect(resources[key].ownerChain).toEqual({ root: "declared", entity: "chart" });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("surfaces chant's stack/env marker verbatim where the rendered labels carry it (#1222)", async () => {
|
|
144
|
+
const result = await describeResources(baseOptions, scriptedHelm({ list: LIST, manifest: MANIFEST, hooks: HOOKS }));
|
|
145
|
+
const { resources } = normalizeObservation(result);
|
|
146
|
+
expect(resources["Deployment/prod/web-app"].marker).toEqual({ stack: "shop", env: "prod" });
|
|
147
|
+
expect(resources["Service/elsewhere/web-app"].marker).toBeUndefined();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("chart-authoring satellites ride the single chart's release verdict", async () => {
|
|
151
|
+
const present = normalizeObservation(
|
|
152
|
+
await describeResources(baseOptions, scriptedHelm({ list: LIST, manifest: MANIFEST, hooks: HOOKS })),
|
|
153
|
+
);
|
|
154
|
+
expect(present.resources.valuesSchema).toMatchObject({
|
|
155
|
+
type: "Helm::Values",
|
|
156
|
+
status: "deployed",
|
|
157
|
+
ownership: "owned",
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Release absent → chart AND satellites in neither map (create is right).
|
|
161
|
+
const absent = normalizeObservation(await describeResources(baseOptions, scriptedHelm({ list: "[]" })));
|
|
162
|
+
expect(absent.resources).toEqual({});
|
|
163
|
+
expect(absent.unobserved).toEqual({});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("missing helm binary: every declared entity is unobserved with no-credentials, never a clean empty snapshot", async () => {
|
|
167
|
+
const result = await describeResources(baseOptions, async () => {
|
|
168
|
+
throw Object.assign(new Error("/bin/sh: helm: command not found"), { code: 127 });
|
|
169
|
+
});
|
|
170
|
+
const { resources, unobserved } = normalizeObservation(result);
|
|
171
|
+
expect(resources).toEqual({});
|
|
172
|
+
expect(unobserved.chart.reason).toBe("no-credentials");
|
|
173
|
+
expect(unobserved.valuesSchema.reason).toBe("no-credentials");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("unreachable cluster (kubeconfig): no-credentials with the CLI's own words", async () => {
|
|
177
|
+
const result = await describeResources(baseOptions, async () => {
|
|
178
|
+
throw new Error('Error: Kubernetes cluster unreachable: Get "https://127.0.0.1:6443/version": connect: connection refused');
|
|
179
|
+
});
|
|
180
|
+
const { unobserved } = normalizeObservation(result);
|
|
181
|
+
expect(unobserved.chart.reason).toBe("no-credentials");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("an unreachable release is unobserved with a reason, never absent (#1246)", async () => {
|
|
185
|
+
// `helm list` succeeds, `helm get manifest` fails.
|
|
186
|
+
const result = await describeResources(baseOptions, scriptedHelm({ list: LIST }));
|
|
187
|
+
const { resources, unobserved } = normalizeObservation(result);
|
|
188
|
+
expect(resources.chart).toBeUndefined();
|
|
189
|
+
expect(unobserved.chart.reason).toBe("read-failed");
|
|
190
|
+
// The satellite shares the hole, with the same reason.
|
|
191
|
+
expect(unobserved.valuesSchema.reason).toBe("read-failed");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("a release deployed under another name resolves through its chart identity", async () => {
|
|
195
|
+
const list = JSON.stringify([
|
|
196
|
+
{ name: "web-prod", namespace: "prod", status: "deployed", chart: "web-app-0.1.0", revision: "1" },
|
|
197
|
+
]);
|
|
198
|
+
const { resources } = normalizeObservation(
|
|
199
|
+
await describeResources(baseOptions, scriptedHelm({ list, manifest: MANIFEST, hooks: "" })),
|
|
200
|
+
);
|
|
201
|
+
expect(resources.chart).toMatchObject({ physicalId: "prod/web-prod" });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("the deploy unit (`stack`) names the release for a single-chart project", async () => {
|
|
205
|
+
const list = JSON.stringify([
|
|
206
|
+
{ name: "custom-name", namespace: "apps", status: "deployed", chart: "something-else-2.0.0" },
|
|
207
|
+
]);
|
|
208
|
+
const { resources } = normalizeObservation(
|
|
209
|
+
await describeResources(
|
|
210
|
+
{ ...baseOptions, stack: "apps/custom-name" },
|
|
211
|
+
scriptedHelm({ list, manifest: "", hooks: "" }),
|
|
212
|
+
),
|
|
213
|
+
);
|
|
214
|
+
expect(resources.chart).toMatchObject({ physicalId: "apps/custom-name" });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("the same release name in several namespaces is unobserved, not guessed and not absent", async () => {
|
|
218
|
+
const list = JSON.stringify([
|
|
219
|
+
{ name: "web-app", namespace: "a", status: "deployed", chart: "web-app-0.1.0" },
|
|
220
|
+
{ name: "web-app", namespace: "b", status: "deployed", chart: "web-app-0.1.0" },
|
|
221
|
+
]);
|
|
222
|
+
const { resources, unobserved } = normalizeObservation(
|
|
223
|
+
await describeResources(baseOptions, scriptedHelm({ list })),
|
|
224
|
+
);
|
|
225
|
+
expect(resources.chart).toBeUndefined();
|
|
226
|
+
expect(unobserved.chart.reason).toBe("read-failed");
|
|
227
|
+
expect(unobserved.chart.detail).toContain("several namespaces");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("--owned filters nothing and says so in a note — every row is release-scoped", async () => {
|
|
231
|
+
const result = await describeResources(
|
|
232
|
+
{ ...baseOptions, owned: true },
|
|
233
|
+
scriptedHelm({ list: LIST, manifest: MANIFEST, hooks: HOOKS }),
|
|
234
|
+
);
|
|
235
|
+
const { resources, notes } = normalizeObservation(result);
|
|
236
|
+
expect(Object.keys(resources).length).toBeGreaterThan(0);
|
|
237
|
+
expect(notes.some((n) => n.includes("--owned"))).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("helm release document parsing", () => {
|
|
242
|
+
test("tolerates comments, empty documents and non-object documents", () => {
|
|
243
|
+
const rows = parseReleaseDocuments(
|
|
244
|
+
`# just a comment\n---\n---\n"scalar"\n---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: cm\n`,
|
|
245
|
+
"manifest",
|
|
246
|
+
"prod",
|
|
247
|
+
);
|
|
248
|
+
expect(rows).toHaveLength(1);
|
|
249
|
+
expect(rows[0]).toMatchObject({ key: "ConfigMap/prod/cm", entityType: "K8s::Core::ConfigMap" });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("unparseable YAML yields no rows rather than throwing", () => {
|
|
253
|
+
expect(parseReleaseDocuments("{{ not yaml", "manifest", "prod")).toEqual([]);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live introspection of Helm releases per resource — the helm lexicon's
|
|
3
|
+
* `describeResources` (#1246, epic #1228 phase 5).
|
|
4
|
+
*
|
|
5
|
+
* `listArtifacts` reports release *metadata* (name, revision, status) and no
|
|
6
|
+
* resource state, which is why `lifecycle diff --live` had nothing to compare
|
|
7
|
+
* for a Helm-managed estate. This read goes one level down: for each release
|
|
8
|
+
* a declared `Helm::Chart` deploys, it fetches what the release holds —
|
|
9
|
+
* `helm get manifest` AND `helm get hooks`, both channels, because hooks are
|
|
10
|
+
* excluded from the manifest channel and reading one would report every hook
|
|
11
|
+
* resource as drift — and reports one row per rendered resource.
|
|
12
|
+
*
|
|
13
|
+
* ## Verdicts (#1089)
|
|
14
|
+
*
|
|
15
|
+
* - The chart entity itself is OBSERVED-PRESENT as the release
|
|
16
|
+
* (`Helm::Release`), OBSERVED-ABSENT when `helm list` was asked and did
|
|
17
|
+
* not report it (only that becomes a `create`), and NOT-OBSERVED with a
|
|
18
|
+
* total reason when helm or the cluster could not be reached — a missing
|
|
19
|
+
* binary or kubeconfig is `no-credentials`, never a clean empty snapshot.
|
|
20
|
+
* - Every rendered resource is reported `ownership: "owned"` — helm-managed
|
|
21
|
+
* via release identity: the read is scoped to the release, so everything
|
|
22
|
+
* it returns is by construction managed by a release this project
|
|
23
|
+
* declares. Rows additionally surface chant's own stack/env marker
|
|
24
|
+
* (#1222) verbatim when the rendered labels carry it.
|
|
25
|
+
* - Every rendered resource carries `ownerChain: { root: "declared" }`
|
|
26
|
+
* pointing at its chart entity, so the diff engine classifies it
|
|
27
|
+
* `runtime` rather than `orphan` (#1077) — an unpinned release is not
|
|
28
|
+
* drift. Controller-created children (Pods under a chart's Deployment)
|
|
29
|
+
* are the k8s lexicon's rows; its own owner-chain walk classifies them.
|
|
30
|
+
*
|
|
31
|
+
* ## Chart-authoring satellites
|
|
32
|
+
*
|
|
33
|
+
* `Helm::Values`, `Helm::Notes`, `Helm::Test` and the other authoring
|
|
34
|
+
* entities have no runtime identity of their own — their deployed form IS
|
|
35
|
+
* the release (helm stores the applied values and rendered NOTES.txt in the
|
|
36
|
+
* release record). In a single-chart project they ride the chart's verdict:
|
|
37
|
+
* present when the release is, absent when it is not, unobserved with the
|
|
38
|
+
* same reason otherwise. A multi-chart project cannot attribute them to one
|
|
39
|
+
* release, so they are NOT-OBSERVED (`unsupported-kind`) with a detail
|
|
40
|
+
* saying so.
|
|
41
|
+
*/
|
|
42
|
+
import type { ResourceMetadata, UnobservedEntity } from "@intentius/chant/lexicon";
|
|
43
|
+
import type { ObservationResult } from "@intentius/chant/observation";
|
|
44
|
+
import { observation } from "@intentius/chant/observation";
|
|
45
|
+
import { LABEL_OWNERSHIP_KEYS, readOwnership } from "@intentius/chant/ownership";
|
|
46
|
+
import {
|
|
47
|
+
HELM_CHART_ENTITY_TYPE,
|
|
48
|
+
HELM_RELEASE_TYPE,
|
|
49
|
+
defaultHelmRunner,
|
|
50
|
+
observeReleases,
|
|
51
|
+
type HelmRunner,
|
|
52
|
+
type ObservedRelease,
|
|
53
|
+
} from "./release-observe";
|
|
54
|
+
|
|
55
|
+
export interface HelmDescribeOptions {
|
|
56
|
+
environment: string;
|
|
57
|
+
buildOutput: string;
|
|
58
|
+
entityNames: string[];
|
|
59
|
+
entities: Map<string, { entityType: string; props: Record<string, unknown> }>;
|
|
60
|
+
stack?: string;
|
|
61
|
+
region?: string;
|
|
62
|
+
owned?: boolean;
|
|
63
|
+
namespace?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function pruneUndefined<T extends Record<string, unknown>>(obj: T): Record<string, unknown> {
|
|
67
|
+
const out: Record<string, unknown> = {};
|
|
68
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
69
|
+
if (v !== undefined) out[k] = v;
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function releaseRow(release: ObservedRelease): ResourceMetadata {
|
|
75
|
+
return {
|
|
76
|
+
type: HELM_RELEASE_TYPE,
|
|
77
|
+
physicalId: `${release.namespace}/${release.release}`,
|
|
78
|
+
status: release.status ?? "unknown",
|
|
79
|
+
...(release.updated ? { lastUpdated: release.updated } : {}),
|
|
80
|
+
ownership: "owned",
|
|
81
|
+
attributes: pruneUndefined({
|
|
82
|
+
chart: release.chart,
|
|
83
|
+
revision: release.revision,
|
|
84
|
+
appVersion: release.appVersion,
|
|
85
|
+
namespace: release.namespace,
|
|
86
|
+
resources: release.resources.length,
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function describeResources(
|
|
92
|
+
options: HelmDescribeOptions,
|
|
93
|
+
run: HelmRunner = defaultHelmRunner,
|
|
94
|
+
): Promise<ObservationResult> {
|
|
95
|
+
const satellites: Array<{ entityName: string; entityType: string }> = [];
|
|
96
|
+
for (const [entityName, entity] of options.entities) {
|
|
97
|
+
if (entity.entityType !== HELM_CHART_ENTITY_TYPE) {
|
|
98
|
+
satellites.push({ entityName, entityType: entity.entityType });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const observed = await observeReleases(
|
|
103
|
+
{ environment: options.environment, entities: options.entities, stack: options.stack },
|
|
104
|
+
run,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const chartCount = options.entities.size - satellites.length;
|
|
108
|
+
const resources: Record<string, ResourceMetadata> = {};
|
|
109
|
+
const unobserved: Record<string, UnobservedEntity> = { ...observed.unobserved };
|
|
110
|
+
const notes: string[] = [];
|
|
111
|
+
|
|
112
|
+
if (chartCount === 0 && satellites.length > 0) {
|
|
113
|
+
for (const { entityName, entityType } of satellites) {
|
|
114
|
+
unobserved[entityName] = {
|
|
115
|
+
type: entityType,
|
|
116
|
+
reason: "unsupported-kind",
|
|
117
|
+
detail: "chart-authoring entity with no declared Helm::Chart to attribute a release to",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return observation(resources, unobserved, observed.queried, notes);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const release of observed.releases) {
|
|
124
|
+
resources[release.entityName] = releaseRow(release);
|
|
125
|
+
|
|
126
|
+
for (const row of release.resources) {
|
|
127
|
+
const labels = (row.doc.metadata as { labels?: Record<string, unknown> } | undefined)?.labels;
|
|
128
|
+
const marker = readOwnership(labels, LABEL_OWNERSHIP_KEYS);
|
|
129
|
+
resources[row.key] = {
|
|
130
|
+
type: row.entityType,
|
|
131
|
+
physicalId: row.namespace ? `${row.namespace}/${row.name}` : row.name,
|
|
132
|
+
status: release.status ?? "unknown",
|
|
133
|
+
// Owned via release identity (#1246): the read is scoped to a release
|
|
134
|
+
// this project declares, so every document it returns is helm-managed
|
|
135
|
+
// by construction — the release IS the marker channel here.
|
|
136
|
+
ownership: "owned",
|
|
137
|
+
...(marker ? { marker } : {}),
|
|
138
|
+
// The release's resources are what the declared chart deploys —
|
|
139
|
+
// expected runtime, never orphan/delete candidates (#1077).
|
|
140
|
+
ownerChain: { root: "declared", entity: release.entityName },
|
|
141
|
+
attributes: pruneUndefined({
|
|
142
|
+
release: release.release,
|
|
143
|
+
releaseNamespace: release.namespace,
|
|
144
|
+
namespace: row.namespace,
|
|
145
|
+
channel: row.channel,
|
|
146
|
+
hook: row.hook?.hook,
|
|
147
|
+
hookWeight: row.hook?.weight,
|
|
148
|
+
hookDeletePolicy: row.hook?.deletePolicy,
|
|
149
|
+
}),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Chart-authoring satellites ride the single chart's verdict; see the
|
|
155
|
+
// module doc. Their deployed form is the release itself, so the release
|
|
156
|
+
// read is the honest answer for them — present, absent, or unobserved
|
|
157
|
+
// exactly as the chart is.
|
|
158
|
+
if (satellites.length > 0) {
|
|
159
|
+
if (chartCount === 1) {
|
|
160
|
+
const [chartEntity] = [...options.entities].find(
|
|
161
|
+
([, e]) => e.entityType === HELM_CHART_ENTITY_TYPE,
|
|
162
|
+
)!;
|
|
163
|
+
const release = observed.releases.find((r) => r.entityName === chartEntity);
|
|
164
|
+
const hole = unobserved[chartEntity];
|
|
165
|
+
for (const { entityName, entityType } of satellites) {
|
|
166
|
+
if (release) {
|
|
167
|
+
resources[entityName] = {
|
|
168
|
+
type: entityType,
|
|
169
|
+
physicalId: `${release.namespace}/${release.release}`,
|
|
170
|
+
status: release.status ?? "unknown",
|
|
171
|
+
ownership: "owned",
|
|
172
|
+
attributes: { release: release.release, namespace: release.namespace, via: "release-identity" },
|
|
173
|
+
};
|
|
174
|
+
} else if (hole) {
|
|
175
|
+
unobserved[entityName] = { type: entityType, reason: hole.reason, ...(hole.detail ? { detail: hole.detail } : {}) };
|
|
176
|
+
}
|
|
177
|
+
// Release confirmed absent → the satellite is absent too: in neither
|
|
178
|
+
// map, and deploying the chart is what creates it.
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
for (const { entityName, entityType } of satellites) {
|
|
182
|
+
unobserved[entityName] = {
|
|
183
|
+
type: entityType,
|
|
184
|
+
reason: "unsupported-kind",
|
|
185
|
+
detail: "chart-authoring entity in a multi-chart project — cannot attribute it to a single release",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (options.owned) {
|
|
192
|
+
notes.push(
|
|
193
|
+
"helm scopes reads to declared releases, so every row is helm-managed (owned) by construction — --owned withholds nothing",
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return observation(resources, unobserved, observed.queried, notes);
|
|
198
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,8 +14,91 @@ export { helmUpgradeCapability, createHelmUpgradeCapability, type HelmUpgradeInp
|
|
|
14
14
|
export { Chart, Values, ValuesOverride, HelmTest, HelmNotes, HelmHook, HelmDependency, HelmMaintainer, HelmCRD } from "./resources";
|
|
15
15
|
|
|
16
16
|
// HelmRender — render an upstream chart at chant build time
|
|
17
|
-
export { HelmRender } from "./render";
|
|
18
|
-
export type { HelmRenderProps } from "./render";
|
|
17
|
+
export { HelmRender, getHelmRenderRecords, clearHelmRenderRecords } from "./render";
|
|
18
|
+
export type { HelmRenderProps, HelmRenderRecord } from "./render";
|
|
19
|
+
|
|
20
|
+
// #1237 — render canonicalization + the contentDigest/inputDigest split.
|
|
21
|
+
export { canonicalizeRender, helmContentDigest, helmInputDigest, renderStability } from "./render-digest";
|
|
22
|
+
export type { HelmInputDigestSource, RenderStabilityGroup, RenderStabilityReport } from "./render-digest";
|
|
23
|
+
|
|
24
|
+
// #1238 — RenderManifest + content-addressed render storage.
|
|
25
|
+
export {
|
|
26
|
+
helmValuesDigest,
|
|
27
|
+
indexRenderDocuments,
|
|
28
|
+
renderCacheKey,
|
|
29
|
+
renderStoreRoot,
|
|
30
|
+
persistHelmRender,
|
|
31
|
+
loadRenderManifest,
|
|
32
|
+
loadRenderContent,
|
|
33
|
+
listRenderManifests,
|
|
34
|
+
findRenderByCacheKey,
|
|
35
|
+
readRenderDocument,
|
|
36
|
+
} from "./render-store";
|
|
37
|
+
export type {
|
|
38
|
+
RenderManifest,
|
|
39
|
+
RenderDocumentEntry,
|
|
40
|
+
RenderDocumentRef,
|
|
41
|
+
RenderCacheKeySource,
|
|
42
|
+
RenderInputsIndexEntry,
|
|
43
|
+
PersistHelmRenderInput,
|
|
44
|
+
PersistedHelmRender,
|
|
45
|
+
} from "./render-store";
|
|
46
|
+
|
|
47
|
+
// #1239 — structure-preserving render wrapper: segment-based CRD routing.
|
|
48
|
+
export { routeRender, routeStoredRender } from "./render-wrapper";
|
|
49
|
+
export type {
|
|
50
|
+
RoutedRender,
|
|
51
|
+
RoutedDocument,
|
|
52
|
+
RoutedRenderWarning,
|
|
53
|
+
RoutedRenderWarningCode,
|
|
54
|
+
RouteRenderOptions,
|
|
55
|
+
} from "./render-wrapper";
|
|
56
|
+
|
|
57
|
+
// #1242 — pinned install path: wrapper-chart materialization.
|
|
58
|
+
export { materializeWrapperChart, wrapperChartName } from "./wrapper-chart";
|
|
59
|
+
export type { MaterializedWrapperChart } from "./wrapper-chart";
|
|
60
|
+
|
|
61
|
+
// #1240 — addArchiveHelmRender: fold a pinned render into a build archive.
|
|
62
|
+
export { addArchiveHelmRender, HELM_RENDER_MEDIA_TYPE } from "./archive-render";
|
|
63
|
+
export type { AddArchiveHelmRenderInput, AddArchiveHelmRenderOutput } from "./archive-render";
|
|
64
|
+
|
|
65
|
+
// #1251/#1252 — build-time coalesced-values probe and its provenance products.
|
|
66
|
+
export {
|
|
67
|
+
probeCoalescedValues,
|
|
68
|
+
coalescedValuesDigest,
|
|
69
|
+
computeValueSources,
|
|
70
|
+
findDeadAssignments,
|
|
71
|
+
rootCoalescedValues,
|
|
72
|
+
getValuesProbeRecords,
|
|
73
|
+
recordValuesProbe,
|
|
74
|
+
clearValuesProbeRecords,
|
|
75
|
+
} from "./values-probe";
|
|
76
|
+
export type {
|
|
77
|
+
ValuesProbeOptions,
|
|
78
|
+
CoalescedValuesProbe,
|
|
79
|
+
CoalescedChartValues,
|
|
80
|
+
DisabledDependency,
|
|
81
|
+
SuppliedValuesLayer,
|
|
82
|
+
ValueOrigin,
|
|
83
|
+
ValuesAttributionInput,
|
|
84
|
+
DeadAssignment,
|
|
85
|
+
HelmValuesProbeRecord,
|
|
86
|
+
} from "./values-probe";
|
|
87
|
+
|
|
88
|
+
// Capability profiles — per-cluster render inputs (#1235, epic #1228)
|
|
89
|
+
export {
|
|
90
|
+
helmConfigSchema,
|
|
91
|
+
helmCapabilityProfileSchema,
|
|
92
|
+
resolveCapabilityProfile,
|
|
93
|
+
validateCapabilityProfile,
|
|
94
|
+
KUBE_VERSION_PATTERN,
|
|
95
|
+
} from "./config";
|
|
96
|
+
export type {
|
|
97
|
+
HelmChantConfig,
|
|
98
|
+
HelmCapabilityProfile,
|
|
99
|
+
HelmCapabilityProfileConfig,
|
|
100
|
+
HelmCapabilityProfileRef,
|
|
101
|
+
} from "./config";
|
|
19
102
|
|
|
20
103
|
// Intrinsics
|
|
21
104
|
export {
|
|
@@ -113,6 +196,19 @@ export { HelmGenerator } from "./import/generator";
|
|
|
113
196
|
export { stripTemplateExpressions, classifyExpression } from "./import/template-stripper";
|
|
114
197
|
export type { StrippedExpression, StripResult, ExpressionKind } from "./import/template-stripper";
|
|
115
198
|
|
|
199
|
+
// Pinnability gate (#1234, epic #1228) — what the pinned-render pipeline
|
|
200
|
+
// consults before anything is pinned
|
|
201
|
+
export { classifyChart } from "./pinnability";
|
|
202
|
+
export type {
|
|
203
|
+
PinnabilityVerdict,
|
|
204
|
+
PinnabilityReport,
|
|
205
|
+
ClassifyChartOptions,
|
|
206
|
+
CapabilityRequirement,
|
|
207
|
+
ClosedInput,
|
|
208
|
+
ConditionalHazard,
|
|
209
|
+
RenderEvidence,
|
|
210
|
+
} from "./pinnability";
|
|
211
|
+
|
|
116
212
|
// LSP providers
|
|
117
213
|
export { helmCompletions } from "./lsp/completions";
|
|
118
214
|
export { helmHover } from "./lsp/hover";
|