@intentius/chant-lexicon-helm 0.46.0 → 0.50.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.
Files changed (122) hide show
  1. package/dist/archive-render.d.ts +94 -0
  2. package/dist/archive-render.d.ts.map +1 -0
  3. package/dist/commands.d.ts +54 -0
  4. package/dist/commands.d.ts.map +1 -0
  5. package/dist/config.d.ts +135 -0
  6. package/dist/config.d.ts.map +1 -0
  7. package/dist/deep-observe-hooks.d.ts +2 -0
  8. package/dist/deep-observe-hooks.d.ts.map +1 -0
  9. package/dist/deep-observe.d.ts +54 -0
  10. package/dist/deep-observe.d.ts.map +1 -0
  11. package/dist/describe-resources.d.ts +17 -0
  12. package/dist/describe-resources.d.ts.map +1 -0
  13. package/dist/index.d.ts +19 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/integrity.json +5 -3
  16. package/dist/lint/audit-catalog.d.ts.map +1 -1
  17. package/dist/lint/post-synth/index.d.ts.map +1 -1
  18. package/dist/lint/post-synth/whm503.d.ts +34 -0
  19. package/dist/lint/post-synth/whm503.d.ts.map +1 -0
  20. package/dist/lint/post-synth/whm504.d.ts +20 -0
  21. package/dist/lint/post-synth/whm504.d.ts.map +1 -0
  22. package/dist/manifest.json +25 -13
  23. package/dist/okf/index.md +2 -0
  24. package/dist/okf/rules/WHM503.md +15 -0
  25. package/dist/okf/rules/WHM504.md +11 -0
  26. package/dist/okf/types/Values.md +1 -0
  27. package/dist/op/activities/cluster-probe.d.ts +44 -0
  28. package/dist/op/activities/cluster-probe.d.ts.map +1 -0
  29. package/dist/op/activities/helm.d.ts +179 -5
  30. package/dist/op/activities/helm.d.ts.map +1 -1
  31. package/dist/op/activities/index.d.ts +4 -2
  32. package/dist/op/activities/index.d.ts.map +1 -1
  33. package/dist/op/builders.d.ts +47 -0
  34. package/dist/op/builders.d.ts.map +1 -0
  35. package/dist/pinnability/actions.d.ts +76 -0
  36. package/dist/pinnability/actions.d.ts.map +1 -0
  37. package/dist/pinnability/classify.d.ts +129 -0
  38. package/dist/pinnability/classify.d.ts.map +1 -0
  39. package/dist/pinnability/conditions.d.ts +109 -0
  40. package/dist/pinnability/conditions.d.ts.map +1 -0
  41. package/dist/pinnability/index.d.ts +17 -0
  42. package/dist/pinnability/index.d.ts.map +1 -0
  43. package/dist/pinnability/localize.d.ts +124 -0
  44. package/dist/pinnability/localize.d.ts.map +1 -0
  45. package/dist/pinnability/render-stream.d.ts +27 -0
  46. package/dist/pinnability/render-stream.d.ts.map +1 -0
  47. package/dist/pinnability/values.d.ts +69 -0
  48. package/dist/pinnability/values.d.ts.map +1 -0
  49. package/dist/plugin.d.ts.map +1 -1
  50. package/dist/release-observe.d.ts +109 -0
  51. package/dist/release-observe.d.ts.map +1 -0
  52. package/dist/render-diff.d.ts +192 -0
  53. package/dist/render-diff.d.ts.map +1 -0
  54. package/dist/render-digest.d.ts +135 -0
  55. package/dist/render-digest.d.ts.map +1 -0
  56. package/dist/render-store.d.ts +310 -0
  57. package/dist/render-store.d.ts.map +1 -0
  58. package/dist/render-wrapper.d.ts +140 -0
  59. package/dist/render-wrapper.d.ts.map +1 -0
  60. package/dist/render.d.ts +81 -0
  61. package/dist/render.d.ts.map +1 -1
  62. package/dist/rules/whm503.ts +101 -0
  63. package/dist/rules/whm504.ts +52 -0
  64. package/dist/skills/chant-helm.md +7 -0
  65. package/dist/values-probe.d.ts +177 -0
  66. package/dist/values-probe.d.ts.map +1 -0
  67. package/dist/wrapper-chart.d.ts +61 -0
  68. package/dist/wrapper-chart.d.ts.map +1 -0
  69. package/package.json +10 -4
  70. package/src/archive-render.test.ts +201 -0
  71. package/src/archive-render.ts +119 -0
  72. package/src/commands.test.ts +487 -0
  73. package/src/commands.ts +445 -0
  74. package/src/config.test.ts +130 -0
  75. package/src/config.ts +220 -0
  76. package/src/deep-observe-hooks.ts +20 -0
  77. package/src/deep-observe.test.ts +165 -0
  78. package/src/deep-observe.ts +121 -0
  79. package/src/describe-resources.test.ts +255 -0
  80. package/src/describe-resources.ts +198 -0
  81. package/src/index.ts +106 -2
  82. package/src/lifecycle-integration.test.ts +289 -0
  83. package/src/lint/audit-catalog.ts +2 -0
  84. package/src/lint/post-synth/index.ts +4 -0
  85. package/src/lint/post-synth/whm503.test.ts +134 -0
  86. package/src/lint/post-synth/whm503.ts +101 -0
  87. package/src/lint/post-synth/whm504.test.ts +105 -0
  88. package/src/lint/post-synth/whm504.ts +52 -0
  89. package/src/op/activities/cluster-probe.test.ts +66 -0
  90. package/src/op/activities/cluster-probe.ts +148 -0
  91. package/src/op/activities/helm.test.ts +824 -0
  92. package/src/op/activities/helm.ts +628 -5
  93. package/src/op/activities/index.ts +17 -2
  94. package/src/op/builders.test.ts +70 -0
  95. package/src/op/builders.ts +66 -0
  96. package/src/pinnability/actions.ts +263 -0
  97. package/src/pinnability/classify.ts +579 -0
  98. package/src/pinnability/conditions.ts +0 -0
  99. package/src/pinnability/index.ts +77 -0
  100. package/src/pinnability/localize.test.ts +234 -0
  101. package/src/pinnability/localize.ts +644 -0
  102. package/src/pinnability/pinnability.test.ts +560 -0
  103. package/src/pinnability/render-stream.ts +59 -0
  104. package/src/pinnability/values.ts +205 -0
  105. package/src/plugin.test.ts +3 -1
  106. package/src/plugin.ts +97 -12
  107. package/src/release-observe.ts +392 -0
  108. package/src/render-diff.test.ts +366 -0
  109. package/src/render-diff.ts +407 -0
  110. package/src/render-digest.test.ts +186 -0
  111. package/src/render-digest.ts +207 -0
  112. package/src/render-store.test.ts +412 -0
  113. package/src/render-store.ts +621 -0
  114. package/src/render-wrapper.test.ts +259 -0
  115. package/src/render-wrapper.ts +274 -0
  116. package/src/render.test.ts +383 -4
  117. package/src/render.ts +319 -8
  118. package/src/skills/chant-helm.md +7 -0
  119. package/src/values-probe.test.ts +400 -0
  120. package/src/values-probe.ts +627 -0
  121. package/src/wrapper-chart.test.ts +110 -0
  122. package/src/wrapper-chart.ts +124 -0
@@ -0,0 +1,366 @@
1
+ import { afterEach, describe, expect, test } from "vitest";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ import { fakeCluster, objectKey } from "@intentius/chant-lexicon-k8s/api/fake-cluster";
7
+
8
+ import type { HelmCapabilityProfile } from "./config";
9
+ import {
10
+ diffRenderLive,
11
+ diffRenders,
12
+ documentKey,
13
+ renderDiffIsEmpty,
14
+ resolveDocumentNamespace,
15
+ } from "./render-diff";
16
+ import { persistHelmRender } from "./render-store";
17
+ import type { RenderManifest } from "./render-store";
18
+
19
+ /**
20
+ * Render diffing (#1249 offline, #1250 live — epic #1228 Phase 6).
21
+ *
22
+ * The offline half resolves two content digests already in a fresh store —
23
+ * no cluster, no helm binary, purely the stored-vs-stored path. The live
24
+ * half drives the real `diffRenderLive` against a render persisted through
25
+ * the real `persistHelmRender` (so the document index, byte offsets and
26
+ * digests are exactly what the store would hold) and a fake k8s cluster at
27
+ * the API edge — the same seam `deep-observe.test.ts` (#1247) uses for the
28
+ * release-scoped read.
29
+ */
30
+
31
+ const PROFILE: HelmCapabilityProfile = {
32
+ name: "prod",
33
+ kubeVersion: "1.33.6",
34
+ apiVersions: ["batch/v1"],
35
+ };
36
+
37
+ function freshRoot(): string {
38
+ return mkdtempSync(join(tmpdir(), "chant-helm-render-diff-"));
39
+ }
40
+
41
+ function persist(root: string, rendered: string, overrides?: Partial<Parameters<typeof persistHelmRender>[0]>) {
42
+ return persistHelmRender({
43
+ rendered,
44
+ releaseName: "rel",
45
+ chart: "tiny",
46
+ chartVersion: "0.1.0",
47
+ namespace: "web",
48
+ capabilityProfile: PROFILE,
49
+ root,
50
+ ...overrides,
51
+ });
52
+ }
53
+
54
+ const BASE = [
55
+ "---",
56
+ "# Source: tiny/templates/configmap.yaml",
57
+ "apiVersion: v1",
58
+ "kind: ConfigMap",
59
+ "metadata:",
60
+ " name: tiny-config",
61
+ " namespace: web",
62
+ "data:",
63
+ " zeta: last",
64
+ " alpha: first",
65
+ "---",
66
+ "# Source: tiny/templates/serviceaccount.yaml",
67
+ "apiVersion: v1",
68
+ "kind: ServiceAccount",
69
+ "metadata:",
70
+ " name: tiny-sa",
71
+ " namespace: web",
72
+ "",
73
+ ].join("\n");
74
+
75
+ describe("diffRenders", () => {
76
+ test("throws naming the missing digest when either side was never persisted", () => {
77
+ const root = freshRoot();
78
+ const { manifest } = persist(root, BASE);
79
+ expect(() => diffRenders(manifest.contentDigest, `sha256:${"0".repeat(64)}`, { root })).toThrow(
80
+ /no stored render for sha256:0{64}/,
81
+ );
82
+ expect(() => diffRenders(`sha256:${"0".repeat(64)}`, manifest.contentDigest, { root })).toThrow(
83
+ /no stored render for sha256:0{64}/,
84
+ );
85
+ });
86
+
87
+ test("two identical renders diff to nothing", () => {
88
+ const root = freshRoot();
89
+ const { manifest: a } = persist(root, BASE);
90
+ const { manifest: b } = persist(root, BASE, { namespace: "web", releaseName: "rel" });
91
+ expect(a.contentDigest).toBe(b.contentDigest);
92
+
93
+ const diff = diffRenders(a.contentDigest, b.contentDigest, { root });
94
+ expect(renderDiffIsEmpty(diff)).toBe(true);
95
+ expect(diff.added).toEqual([]);
96
+ expect(diff.removed).toEqual([]);
97
+ expect(diff.changed).toEqual([]);
98
+ expect(diff.unchanged).toHaveLength(2);
99
+ expect(diff.unindexed).toEqual({ from: 0, to: 0 });
100
+ });
101
+
102
+ test("reports a field-level change inside a document whose bytes differ", () => {
103
+ const root = freshRoot();
104
+ const { manifest: from } = persist(root, BASE);
105
+ const bumped = BASE.replace(" zeta: last", " zeta: LAST-BUMPED").replace(
106
+ " alpha: first",
107
+ " alpha: first\n gamma: new",
108
+ );
109
+ const { manifest: to } = persist(root, bumped);
110
+ expect(from.contentDigest).not.toBe(to.contentDigest);
111
+
112
+ const diff = diffRenders(from.contentDigest, to.contentDigest, { root });
113
+ expect(diff.added).toEqual([]);
114
+ expect(diff.removed).toEqual([]);
115
+ expect(diff.changed).toHaveLength(1);
116
+ const [cm] = diff.changed;
117
+ expect(cm.kind).toBe("ConfigMap");
118
+ expect(cm.name).toBe("tiny-config");
119
+ expect(cm.namespace).toBe("web");
120
+ expect(cm.beforeDigest).not.toBe(cm.afterDigest);
121
+
122
+ const byPath = Object.fromEntries(cm.changes.map((c) => [c.path, c]));
123
+ expect(byPath["data.zeta"]).toEqual({ path: "data.zeta", kind: "changed", before: "last", after: "LAST-BUMPED" });
124
+ expect(byPath["data.gamma"]).toEqual({ path: "data.gamma", kind: "added", after: "new" });
125
+
126
+ // The untouched ServiceAccount document is unchanged, not re-diffed.
127
+ expect(diff.unchanged).toHaveLength(1);
128
+ expect(diff.unchanged[0].kind).toBe("ServiceAccount");
129
+ });
130
+
131
+ test("reports a document present on only one side as added or removed", () => {
132
+ const root = freshRoot();
133
+ const { manifest: from } = persist(root, BASE);
134
+ const withExtra =
135
+ BASE +
136
+ ["---", "# Source: tiny/templates/secret.yaml", "apiVersion: v1", "kind: Secret", "metadata:", " name: tiny-secret", " namespace: web", ""].join(
137
+ "\n",
138
+ );
139
+ const { manifest: to } = persist(root, withExtra);
140
+
141
+ const diff = diffRenders(from.contentDigest, to.contentDigest, { root });
142
+ expect(diff.changed).toEqual([]);
143
+ expect(diff.removed).toEqual([]);
144
+ expect(diff.added).toHaveLength(1);
145
+ expect(diff.added[0]).toMatchObject({ kind: "Secret", name: "tiny-secret", namespace: "web" });
146
+
147
+ // Reversed direction: the same document reports as removed.
148
+ const reverse = diffRenders(to.contentDigest, from.contentDigest, { root });
149
+ expect(reverse.added).toEqual([]);
150
+ expect(reverse.removed).toHaveLength(1);
151
+ expect(reverse.removed[0]).toMatchObject({ kind: "Secret", name: "tiny-secret", namespace: "web" });
152
+ });
153
+
154
+ test("matches duplicate documents positionally within their identity group", () => {
155
+ const root = freshRoot();
156
+ const crd = [
157
+ "---",
158
+ "# Source: tiny/crds/widgets.yaml",
159
+ "apiVersion: apiextensions.k8s.io/v1",
160
+ "kind: CustomResourceDefinition",
161
+ "metadata:",
162
+ " name: widgets.example.com",
163
+ "spec:",
164
+ " group: example.com",
165
+ ].join("\n");
166
+ const oneCopy = crd + "\n";
167
+ const twoCopies = crd + "\n" + crd + "\n";
168
+
169
+ const { manifest: from } = persist(root, oneCopy, { chart: "tiny-one" });
170
+ const { manifest: to } = persist(root, twoCopies, { chart: "tiny-two" });
171
+
172
+ const diff = diffRenders(from.contentDigest, to.contentDigest, { root });
173
+ // One matched pair (identical bytes -> unchanged), one surplus -> added.
174
+ expect(diff.unchanged).toHaveLength(1);
175
+ expect(diff.added).toHaveLength(1);
176
+ expect(diff.added[0].kind).toBe("CustomResourceDefinition");
177
+ expect(diff.removed).toEqual([]);
178
+ expect(diff.changed).toEqual([]);
179
+ });
180
+
181
+ test("counts unindexed documents on each side without dropping them silently", () => {
182
+ const root = freshRoot();
183
+ const withJunk = BASE + "---\nnot: an object with kind or name\n";
184
+ const { manifest: from } = persist(root, BASE);
185
+ const { manifest: to } = persist(root, withJunk, { releaseName: "rel2" });
186
+
187
+ const diff = diffRenders(from.contentDigest, to.contentDigest, { root });
188
+ expect(diff.unindexed).toEqual({ from: 0, to: 1 });
189
+ });
190
+
191
+ test("carries chart/releaseName identity for both sides in the result header", () => {
192
+ const root = freshRoot();
193
+ const { manifest: from } = persist(root, BASE, { chart: "tiny", releaseName: "rel-a" });
194
+ const { manifest: to } = persist(root, BASE.replace("last", "LAST"), { chart: "tiny", releaseName: "rel-b" });
195
+
196
+ const diff = diffRenders(from.contentDigest, to.contentDigest, { root });
197
+ expect(diff.from).toEqual({ contentDigest: from.contentDigest, chart: "tiny", releaseName: "rel-a" });
198
+ expect(diff.to).toEqual({ contentDigest: to.contentDigest, chart: "tiny", releaseName: "rel-b" });
199
+ });
200
+ });
201
+
202
+ const RENDERED = `---
203
+ # Source: tiny/templates/deployment.yaml
204
+ apiVersion: apps/v1
205
+ kind: Deployment
206
+ metadata:
207
+ name: web
208
+ namespace: shop
209
+ spec:
210
+ replicas: 2
211
+ ---
212
+ # Source: tiny/templates/config.yaml
213
+ apiVersion: v1
214
+ kind: ConfigMap
215
+ metadata:
216
+ name: web-config
217
+ data:
218
+ key: value
219
+ ---
220
+ # Source: tiny/templates/migrate-job.yaml
221
+ apiVersion: batch/v1
222
+ kind: Job
223
+ metadata:
224
+ name: web-migrate
225
+ annotations:
226
+ helm.sh/hook: pre-install
227
+ spec:
228
+ template:
229
+ spec:
230
+ containers:
231
+ - name: migrate
232
+ image: migrate:1.0
233
+ ---
234
+ # Source: tiny/templates/clusterrole.yaml
235
+ apiVersion: rbac.authorization.k8s.io/v1
236
+ kind: ClusterRole
237
+ metadata:
238
+ name: web-reader
239
+ rules: []
240
+ `;
241
+
242
+ const roots: string[] = [];
243
+ function tmpRoot(): string {
244
+ const dir = mkdtempSync(join(tmpdir(), "chant-helm-render-diff-"));
245
+ roots.push(dir);
246
+ return dir;
247
+ }
248
+
249
+ afterEach(() => {
250
+ while (roots.length > 0) rmSync(roots.pop() as string, { recursive: true, force: true });
251
+ });
252
+
253
+ function persistFixture(root: string): RenderManifest {
254
+ const { manifest } = persistHelmRender({
255
+ rendered: RENDERED,
256
+ releaseName: "web",
257
+ chart: "tiny",
258
+ namespace: "shop",
259
+ capabilityProfile: { name: "test-profile", kubeVersion: "1.33.0", apiVersions: [] },
260
+ root,
261
+ });
262
+ return manifest;
263
+ }
264
+
265
+ describe("resolveDocumentNamespace / documentKey (pure helpers)", () => {
266
+ test("a namespace-silent namespaced kind defaults to the render's target namespace", () => {
267
+ expect(resolveDocumentNamespace("ConfigMap", null, "shop")).toBe("shop");
268
+ expect(documentKey("ConfigMap", "shop", "web-config")).toBe("ConfigMap/shop/web-config");
269
+ });
270
+
271
+ test("a namespace-silent cluster-scoped kind is never defaulted", () => {
272
+ expect(resolveDocumentNamespace("ClusterRole", null, "shop")).toBeUndefined();
273
+ expect(documentKey("ClusterRole", undefined, "web-reader")).toBe("cluster:ClusterRole/web-reader");
274
+ });
275
+
276
+ test("an explicit document namespace always wins over the render's target namespace", () => {
277
+ expect(resolveDocumentNamespace("Deployment", "other-ns", "shop")).toBe("other-ns");
278
+ });
279
+ });
280
+
281
+ describe("diffRenderLive (#1250)", () => {
282
+ test("no such digest in the store — refused, never a false clean diff", async () => {
283
+ const root = tmpRoot();
284
+ const outcome = await diffRenderLive({ contentDigest: `sha256:${"0".repeat(64)}`, environment: "prod", root });
285
+ expect(outcome.found).toBe(false);
286
+ });
287
+
288
+ test("every document in the render becomes a live-diff row, hook resources included — a hook is just another rendered document", async () => {
289
+ const root = tmpRoot();
290
+ const manifest = persistFixture(root);
291
+
292
+ const cluster = fakeCluster({
293
+ objects: {
294
+ [objectKey("apps/v1", "Deployment", "web", "shop")]: {
295
+ apiVersion: "apps/v1",
296
+ kind: "Deployment",
297
+ metadata: { name: "web", namespace: "shop" },
298
+ // Live has drifted from the pinned render — 3 replicas, not 2.
299
+ spec: { replicas: 3 },
300
+ },
301
+ [objectKey("v1", "ConfigMap", "web-config", "shop")]: {
302
+ apiVersion: "v1",
303
+ kind: "ConfigMap",
304
+ metadata: { name: "web-config", namespace: "shop" },
305
+ data: { key: "value" },
306
+ },
307
+ [objectKey("batch/v1", "Job", "web-migrate", "shop")]: {
308
+ apiVersion: "batch/v1",
309
+ kind: "Job",
310
+ metadata: { name: "web-migrate", namespace: "shop", annotations: { "helm.sh/hook": "pre-install" } },
311
+ // The hook resource's image drifted too — reachable only because
312
+ // the render's document index carries hook docs like any other.
313
+ spec: { template: { spec: { containers: [{ name: "migrate", image: "migrate:2.0" }] } } },
314
+ },
315
+ [objectKey("rbac.authorization.k8s.io/v1", "ClusterRole", "web-reader")]: {
316
+ apiVersion: "rbac.authorization.k8s.io/v1",
317
+ kind: "ClusterRole",
318
+ metadata: { name: "web-reader" },
319
+ rules: [],
320
+ },
321
+ },
322
+ });
323
+
324
+ const outcome = await diffRenderLive(
325
+ { contentDigest: manifest.contentDigest, environment: "prod", root },
326
+ { connect: cluster.connector },
327
+ );
328
+ expect(outcome.found).toBe(true);
329
+ if (!outcome.found) return;
330
+
331
+ const byName = Object.fromEntries(outcome.diff.drifted.map((e) => [e.name, e]));
332
+ expect(byName["Deployment/shop/web"]).toBeDefined();
333
+ expect(byName["Deployment/shop/web"].changes.some((c) => c.path === "spec.replicas")).toBe(true);
334
+
335
+ // Hook resource: reported exactly like a non-hook document.
336
+ expect(byName["Job/shop/web-migrate"]).toBeDefined();
337
+
338
+ // Namespace-silent document defaulted to the render's namespace and matched clean.
339
+ expect(outcome.diff.unchanged).toContain("ConfigMap/shop/web-config");
340
+ // Cluster-scoped document never gets a namespace and matched clean.
341
+ expect(outcome.diff.unchanged).toContain("cluster:ClusterRole/web-reader");
342
+
343
+ // The read is identity-scoped (exactly the render's own documents, by
344
+ // kind/namespace/name) rather than a cluster sweep — same as the
345
+ // release-scoped deep read (#1247) — so there is never anything for
346
+ // `undeclaredEntities` to report here.
347
+ expect(outcome.diff.undeclaredEntities).toEqual([]);
348
+ });
349
+
350
+ test("a document the live cluster never got is neither drift nor a hole — the thin read owns existence, not this one", async () => {
351
+ const root = tmpRoot();
352
+ const manifest = persistFixture(root);
353
+ // Cluster has nothing at all — every render document is simply absent live.
354
+ const cluster = fakeCluster();
355
+
356
+ const outcome = await diffRenderLive(
357
+ { contentDigest: manifest.contentDigest, environment: "prod", root },
358
+ { connect: cluster.connector },
359
+ );
360
+ expect(outcome.found).toBe(true);
361
+ if (!outcome.found) return;
362
+ expect(outcome.diff.drifted).toEqual([]);
363
+ expect(outcome.diff.unchanged).toEqual([]);
364
+ expect(outcome.diff.unobserved).toEqual([]);
365
+ });
366
+ });