@intentius/chant-lexicon-helm 0.0.16

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 (110) hide show
  1. package/README.md +22 -0
  2. package/dist/integrity.json +36 -0
  3. package/dist/manifest.json +37 -0
  4. package/dist/meta.json +208 -0
  5. package/dist/rules/chart-metadata.ts +64 -0
  6. package/dist/rules/helm-helpers.ts +64 -0
  7. package/dist/rules/no-hardcoded-image.ts +62 -0
  8. package/dist/rules/values-no-secrets.ts +82 -0
  9. package/dist/rules/whm101.ts +46 -0
  10. package/dist/rules/whm102.ts +33 -0
  11. package/dist/rules/whm103.ts +59 -0
  12. package/dist/rules/whm104.ts +35 -0
  13. package/dist/rules/whm105.ts +30 -0
  14. package/dist/rules/whm201.ts +36 -0
  15. package/dist/rules/whm202.ts +50 -0
  16. package/dist/rules/whm203.ts +39 -0
  17. package/dist/rules/whm204.ts +60 -0
  18. package/dist/rules/whm301.ts +41 -0
  19. package/dist/rules/whm302.ts +40 -0
  20. package/dist/rules/whm401.ts +57 -0
  21. package/dist/rules/whm402.ts +45 -0
  22. package/dist/rules/whm403.ts +45 -0
  23. package/dist/rules/whm404.ts +36 -0
  24. package/dist/rules/whm405.ts +53 -0
  25. package/dist/rules/whm406.ts +34 -0
  26. package/dist/rules/whm407.ts +83 -0
  27. package/dist/rules/whm501.ts +103 -0
  28. package/dist/rules/whm502.ts +94 -0
  29. package/dist/skills/chant-helm-chart-patterns.md +229 -0
  30. package/dist/skills/chant-helm-chart-security-patterns.md +192 -0
  31. package/dist/skills/chant-helm-create-chart.md +211 -0
  32. package/dist/types/index.d.ts +132 -0
  33. package/package.json +34 -0
  34. package/src/codegen/docs-cli.ts +4 -0
  35. package/src/codegen/docs.ts +483 -0
  36. package/src/codegen/generate-cli.ts +28 -0
  37. package/src/codegen/generate.ts +249 -0
  38. package/src/codegen/naming.ts +38 -0
  39. package/src/codegen/package.ts +64 -0
  40. package/src/composites/composites.test.ts +1050 -0
  41. package/src/composites/helm-batch-job.ts +209 -0
  42. package/src/composites/helm-crd-lifecycle.ts +184 -0
  43. package/src/composites/helm-cron-job.ts +177 -0
  44. package/src/composites/helm-daemon-set.ts +169 -0
  45. package/src/composites/helm-external-secret.ts +93 -0
  46. package/src/composites/helm-library.ts +51 -0
  47. package/src/composites/helm-microservice.ts +331 -0
  48. package/src/composites/helm-monitored-service.ts +252 -0
  49. package/src/composites/helm-namespace-env.ts +154 -0
  50. package/src/composites/helm-secure-ingress.ts +114 -0
  51. package/src/composites/helm-stateful-service.ts +213 -0
  52. package/src/composites/helm-web-app.ts +264 -0
  53. package/src/composites/helm-worker.ts +207 -0
  54. package/src/composites/index.ts +38 -0
  55. package/src/coverage.test.ts +21 -0
  56. package/src/coverage.ts +50 -0
  57. package/src/generated/index.d.ts +132 -0
  58. package/src/generated/index.ts +13 -0
  59. package/src/generated/lexicon-helm.json +208 -0
  60. package/src/helpers.test.ts +51 -0
  61. package/src/helpers.ts +100 -0
  62. package/src/import/generator.ts +285 -0
  63. package/src/import/import.test.ts +224 -0
  64. package/src/import/parser.ts +160 -0
  65. package/src/import/template-stripper.ts +123 -0
  66. package/src/index.ts +108 -0
  67. package/src/intrinsics.test.ts +380 -0
  68. package/src/intrinsics.ts +484 -0
  69. package/src/lint/post-synth/helm-helpers.ts +64 -0
  70. package/src/lint/post-synth/post-synth.test.ts +504 -0
  71. package/src/lint/post-synth/whm101.ts +46 -0
  72. package/src/lint/post-synth/whm102.ts +33 -0
  73. package/src/lint/post-synth/whm103.ts +59 -0
  74. package/src/lint/post-synth/whm104.ts +35 -0
  75. package/src/lint/post-synth/whm105.ts +30 -0
  76. package/src/lint/post-synth/whm201.ts +36 -0
  77. package/src/lint/post-synth/whm202.ts +50 -0
  78. package/src/lint/post-synth/whm203.ts +39 -0
  79. package/src/lint/post-synth/whm204.ts +60 -0
  80. package/src/lint/post-synth/whm301.ts +41 -0
  81. package/src/lint/post-synth/whm302.ts +40 -0
  82. package/src/lint/post-synth/whm401.ts +57 -0
  83. package/src/lint/post-synth/whm402.ts +45 -0
  84. package/src/lint/post-synth/whm403.ts +45 -0
  85. package/src/lint/post-synth/whm404.ts +36 -0
  86. package/src/lint/post-synth/whm405.ts +53 -0
  87. package/src/lint/post-synth/whm406.ts +34 -0
  88. package/src/lint/post-synth/whm407.ts +83 -0
  89. package/src/lint/post-synth/whm501.ts +103 -0
  90. package/src/lint/post-synth/whm502.ts +94 -0
  91. package/src/lint/rules/chart-metadata.ts +64 -0
  92. package/src/lint/rules/lint-rules.test.ts +97 -0
  93. package/src/lint/rules/no-hardcoded-image.ts +62 -0
  94. package/src/lint/rules/values-no-secrets.ts +82 -0
  95. package/src/lsp/completions.test.ts +72 -0
  96. package/src/lsp/completions.ts +20 -0
  97. package/src/lsp/hover.test.ts +46 -0
  98. package/src/lsp/hover.ts +46 -0
  99. package/src/package-cli.ts +28 -0
  100. package/src/plugin.test.ts +71 -0
  101. package/src/plugin.ts +206 -0
  102. package/src/resources.ts +77 -0
  103. package/src/serializer.test.ts +930 -0
  104. package/src/serializer.ts +835 -0
  105. package/src/skills/chart-patterns.md +229 -0
  106. package/src/skills/chart-security-patterns.md +192 -0
  107. package/src/skills/create-chart.md +211 -0
  108. package/src/validate-cli.ts +21 -0
  109. package/src/validate.test.ts +37 -0
  110. package/src/validate.ts +36 -0
@@ -0,0 +1,930 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { createResource, createProperty } from "@intentius/chant/runtime";
3
+ import type { Declarable } from "@intentius/chant/declarable";
4
+ import type { SerializerResult } from "@intentius/chant/serializer";
5
+ import { helmSerializer } from "./serializer";
6
+ import { Chart, Values, HelmNotes, HelmTest, HelmHook, HelmDependency, HelmMaintainer, HelmCRD } from "./resources";
7
+ import { values, include, printf, toYaml, quote, helmDefault, required, If, ElseIf, Range, With, Release, ChartRef, Capabilities } from "./intrinsics";
8
+
9
+ function makeEntities(...pairs: [string, Record<string, unknown>][]): Map<string, Declarable> {
10
+ const entities = new Map<string, Declarable>();
11
+ for (const [name, entity] of pairs) {
12
+ entities.set(name, entity as unknown as Declarable);
13
+ }
14
+ return entities;
15
+ }
16
+
17
+ // K8s resource constructors for testing
18
+ const Deployment = createResource("K8s::Apps::Deployment", "k8s", {});
19
+ const Service = createResource("K8s::Core::Service", "k8s", {});
20
+ const ConfigMap = createResource("K8s::Core::ConfigMap", "k8s", {});
21
+ const Ingress = createResource("K8s::Networking::Ingress", "k8s", {});
22
+
23
+ describe("helmSerializer", () => {
24
+ test("name and rulePrefix", () => {
25
+ expect(helmSerializer.name).toBe("helm");
26
+ expect(helmSerializer.rulePrefix).toBe("WHM");
27
+ });
28
+
29
+ test("returns SerializerResult with files map", () => {
30
+ const chart = new Chart({ apiVersion: "v2", name: "test-app", version: "0.1.0" });
31
+ const entities = makeEntities(["chart", chart]);
32
+ const result = helmSerializer.serialize(entities) as SerializerResult;
33
+
34
+ expect(result.primary).toBeDefined();
35
+ expect(result.files).toBeDefined();
36
+ expect(result.files!["Chart.yaml"]).toBeDefined();
37
+ expect(result.files!["values.yaml"]).toBeDefined();
38
+ expect(result.files![".helmignore"]).toBeDefined();
39
+ expect(result.files!["templates/_helpers.tpl"]).toBeDefined();
40
+ });
41
+
42
+ test("emits Chart.yaml with correct fields", () => {
43
+ const chart = new Chart({
44
+ apiVersion: "v2",
45
+ name: "my-app",
46
+ version: "1.0.0",
47
+ appVersion: "2.0.0",
48
+ description: "My application chart",
49
+ type: "application",
50
+ });
51
+ const entities = makeEntities(["chart", chart]);
52
+ const result = helmSerializer.serialize(entities) as SerializerResult;
53
+ const chartYaml = result.files!["Chart.yaml"];
54
+
55
+ expect(chartYaml).toContain("apiVersion: v2");
56
+ expect(chartYaml).toContain("name: my-app");
57
+ expect(chartYaml).toContain("version: '1.0.0'");
58
+ expect(chartYaml).toContain("appVersion: '2.0.0'");
59
+ expect(chartYaml).toContain("description: My application chart");
60
+ expect(chartYaml).toContain("type: application");
61
+ });
62
+
63
+ test("emits values.yaml from Values entity", () => {
64
+ const chart = new Chart({ name: "test", version: "0.1.0" });
65
+ const vals = new Values({
66
+ replicaCount: 2,
67
+ image: { repository: "nginx", tag: "latest" },
68
+ });
69
+ const entities = makeEntities(["chart", chart], ["values", vals]);
70
+ const result = helmSerializer.serialize(entities) as SerializerResult;
71
+ const valuesYaml = result.files!["values.yaml"];
72
+
73
+ expect(valuesYaml).toContain("replicaCount: 2");
74
+ expect(valuesYaml).toContain("image:");
75
+ expect(valuesYaml).toContain("repository: nginx");
76
+ expect(valuesYaml).toContain("tag: latest");
77
+ });
78
+
79
+ test("generates values.schema.json from Values", () => {
80
+ const chart = new Chart({ name: "test", version: "0.1.0" });
81
+ const vals = new Values({
82
+ replicaCount: 2,
83
+ enabled: true,
84
+ name: "test",
85
+ });
86
+ const entities = makeEntities(["chart", chart], ["values", vals]);
87
+ const result = helmSerializer.serialize(entities) as SerializerResult;
88
+ const schema = JSON.parse(result.files!["values.schema.json"]);
89
+
90
+ expect(schema.$schema).toBe("http://json-schema.org/draft-07/schema#");
91
+ expect(schema.type).toBe("object");
92
+ expect(schema.properties.replicaCount.type).toBe("integer");
93
+ expect(schema.properties.replicaCount.default).toBe(2);
94
+ expect(schema.properties.enabled.type).toBe("boolean");
95
+ expect(schema.properties.enabled.default).toBe(true);
96
+ expect(schema.properties.name.type).toBe("string");
97
+ expect(schema.properties.name.default).toBe("test");
98
+ });
99
+
100
+ test("emits _helpers.tpl with chart name", () => {
101
+ const chart = new Chart({ name: "my-app", version: "0.1.0" });
102
+ const entities = makeEntities(["chart", chart]);
103
+ const result = helmSerializer.serialize(entities) as SerializerResult;
104
+ const helpers = result.files!["templates/_helpers.tpl"];
105
+
106
+ expect(helpers).toContain('define "my-app.name"');
107
+ expect(helpers).toContain('define "my-app.fullname"');
108
+ expect(helpers).toContain('define "my-app.labels"');
109
+ expect(helpers).toContain('define "my-app.selectorLabels"');
110
+ expect(helpers).toContain('define "my-app.chart"');
111
+ });
112
+
113
+ test("emits K8s Deployment as template with Go template expressions", () => {
114
+ const chart = new Chart({ name: "my-app", version: "0.1.0" });
115
+ const vals = new Values({ replicaCount: 1 });
116
+ const deployment = new Deployment({
117
+ metadata: {
118
+ name: include("my-app.fullname"),
119
+ labels: include("my-app.labels"),
120
+ },
121
+ spec: {
122
+ replicas: values.replicaCount,
123
+ template: {
124
+ spec: {
125
+ containers: [{
126
+ name: "app",
127
+ image: printf("%s:%s", values.image.repository, values.image.tag),
128
+ }],
129
+ },
130
+ },
131
+ },
132
+ });
133
+
134
+ const entities = makeEntities(
135
+ ["chart", chart],
136
+ ["values", vals],
137
+ ["webDeployment", deployment],
138
+ );
139
+ const result = helmSerializer.serialize(entities) as SerializerResult;
140
+ const template = result.files!["templates/web-deployment.yaml"];
141
+
142
+ expect(template).toBeDefined();
143
+ expect(template).toContain("apiVersion: apps/v1");
144
+ expect(template).toContain("kind: Deployment");
145
+ expect(template).toContain('{{ include "my-app.fullname" . }}');
146
+ expect(template).toContain("{{ .Values.replicaCount }}");
147
+ expect(template).toContain('{{ printf "%s:%s" .Values.image.repository .Values.image.tag }}');
148
+ });
149
+
150
+ test("emits K8s Service template", () => {
151
+ const chart = new Chart({ name: "test", version: "0.1.0" });
152
+ const svc = new Service({
153
+ metadata: { name: include("test.fullname") },
154
+ spec: {
155
+ type: values.service.type,
156
+ ports: [{
157
+ port: values.service.port,
158
+ targetPort: "http",
159
+ }],
160
+ },
161
+ });
162
+
163
+ const entities = makeEntities(["chart", chart], ["appService", svc]);
164
+ const result = helmSerializer.serialize(entities) as SerializerResult;
165
+ const template = result.files!["templates/app-service.yaml"];
166
+
167
+ expect(template).toBeDefined();
168
+ expect(template).toContain("apiVersion: v1");
169
+ expect(template).toContain("kind: Service");
170
+ expect(template).toContain("{{ .Values.service.type }}");
171
+ expect(template).toContain("{{ .Values.service.port }}");
172
+ });
173
+
174
+ test("emits ConfigMap as specless K8s resource", () => {
175
+ const chart = new Chart({ name: "test", version: "0.1.0" });
176
+ const cm = new ConfigMap({
177
+ metadata: { name: "my-config" },
178
+ data: { key: "value" },
179
+ });
180
+
181
+ const entities = makeEntities(["chart", chart], ["config", cm]);
182
+ const result = helmSerializer.serialize(entities) as SerializerResult;
183
+ const template = result.files!["templates/config.yaml"];
184
+
185
+ expect(template).toBeDefined();
186
+ expect(template).toContain("apiVersion: v1");
187
+ expect(template).toContain("kind: ConfigMap");
188
+ expect(template).toContain("data:");
189
+ expect(template).toContain("key: value");
190
+ // ConfigMap should NOT have spec wrapper
191
+ expect(template).not.toContain("spec:");
192
+ });
193
+
194
+ test("emits NOTES.txt from HelmNotes entity", () => {
195
+ const chart = new Chart({ name: "test", version: "0.1.0" });
196
+ const notes = new HelmNotes({
197
+ content: "Thank you for installing {{ .Chart.Name }}.\nRelease: {{ .Release.Name }}",
198
+ });
199
+
200
+ const entities = makeEntities(["chart", chart], ["notes", notes]);
201
+ const result = helmSerializer.serialize(entities) as SerializerResult;
202
+
203
+ expect(result.files!["templates/NOTES.txt"]).toBe(
204
+ "Thank you for installing {{ .Chart.Name }}.\nRelease: {{ .Release.Name }}",
205
+ );
206
+ });
207
+
208
+ test("defaults Chart.yaml fields when not provided", () => {
209
+ const chart = new Chart({});
210
+ const entities = makeEntities(["chart", chart]);
211
+ const result = helmSerializer.serialize(entities) as SerializerResult;
212
+ const chartYaml = result.files!["Chart.yaml"];
213
+
214
+ expect(chartYaml).toContain("apiVersion: v2");
215
+ expect(chartYaml).toContain("type: application");
216
+ expect(chartYaml).toContain("version:");
217
+ });
218
+
219
+ test("emits empty values.yaml when no Values entity", () => {
220
+ const chart = new Chart({ name: "test", version: "0.1.0" });
221
+ const entities = makeEntities(["chart", chart]);
222
+ const result = helmSerializer.serialize(entities) as SerializerResult;
223
+
224
+ expect(result.files!["values.yaml"]).toBe("{}\n");
225
+ });
226
+
227
+ test("does not emit values.schema.json when no Values entity", () => {
228
+ const chart = new Chart({ name: "test", version: "0.1.0" });
229
+ const entities = makeEntities(["chart", chart]);
230
+ const result = helmSerializer.serialize(entities) as SerializerResult;
231
+
232
+ expect(result.files!["values.schema.json"]).toBeUndefined();
233
+ });
234
+
235
+ test("toYaml intrinsic emits raw expression in template", () => {
236
+ const chart = new Chart({ name: "test", version: "0.1.0" });
237
+ const vals = new Values({ resources: {} });
238
+ const deployment = new Deployment({
239
+ metadata: { name: "test" },
240
+ spec: {
241
+ template: {
242
+ spec: {
243
+ containers: [{
244
+ name: "app",
245
+ resources: toYaml(values.resources, 12),
246
+ }],
247
+ },
248
+ },
249
+ },
250
+ });
251
+
252
+ const entities = makeEntities(
253
+ ["chart", chart],
254
+ ["values", vals],
255
+ ["deployment", deployment],
256
+ );
257
+ const result = helmSerializer.serialize(entities) as SerializerResult;
258
+ const template = result.files!["templates/deployment.yaml"];
259
+
260
+ expect(template).toContain("{{ toYaml .Values.resources | nindent 12 }}");
261
+ });
262
+
263
+ test("Release intrinsic emits in template", () => {
264
+ const chart = new Chart({ name: "test", version: "0.1.0" });
265
+ const cm = new ConfigMap({
266
+ metadata: { name: Release.Name },
267
+ data: { namespace: Release.Namespace },
268
+ });
269
+
270
+ const entities = makeEntities(["chart", chart], ["config", cm]);
271
+ const result = helmSerializer.serialize(entities) as SerializerResult;
272
+ const template = result.files!["templates/config.yaml"];
273
+
274
+ expect(template).toContain("{{ .Release.Name }}");
275
+ expect(template).toContain("{{ .Release.Namespace }}");
276
+ });
277
+
278
+ test("multiple K8s resources emit as separate template files", () => {
279
+ const chart = new Chart({ name: "test", version: "0.1.0" });
280
+ const deploy = new Deployment({
281
+ metadata: { name: "app" },
282
+ spec: { replicas: 1 },
283
+ });
284
+ const svc = new Service({
285
+ metadata: { name: "app" },
286
+ spec: { type: "ClusterIP" },
287
+ });
288
+
289
+ const entities = makeEntities(
290
+ ["chart", chart],
291
+ ["appDeployment", deploy],
292
+ ["appService", svc],
293
+ );
294
+ const result = helmSerializer.serialize(entities) as SerializerResult;
295
+
296
+ expect(result.files!["templates/app-deployment.yaml"]).toBeDefined();
297
+ expect(result.files!["templates/app-service.yaml"]).toBeDefined();
298
+ });
299
+ });
300
+
301
+ // ── Phase 2 tests ─────────────────────────────────────────
302
+
303
+ describe("resource-level If", () => {
304
+ test("wraps entire template in {{- if }} / {{- end }}", () => {
305
+ const chart = new Chart({ name: "test", version: "0.1.0" });
306
+ const ingress = If(
307
+ values.ingress.enabled,
308
+ new Ingress({
309
+ metadata: { name: include("test.fullname") },
310
+ spec: {
311
+ rules: [{ host: values.ingress.host }],
312
+ },
313
+ }),
314
+ );
315
+
316
+ const entities = makeEntities(["chart", chart], ["ingress", ingress as any]);
317
+ const result = helmSerializer.serialize(entities) as SerializerResult;
318
+ const template = result.files!["templates/ingress.yaml"];
319
+
320
+ expect(template).toBeDefined();
321
+ expect(template).toStartWith("{{- if .Values.ingress.enabled }}\n");
322
+ expect(template).toContain("apiVersion: networking.k8s.io/v1");
323
+ expect(template).toContain("kind: Ingress");
324
+ expect(template).toContain('{{ include "test.fullname" . }}');
325
+ expect(template).toContain("{{ .Values.ingress.host }}");
326
+ expect(template.trimEnd()).toEndWith("{{- end }}");
327
+ });
328
+
329
+ test("does not wrap non-conditional resources", () => {
330
+ const chart = new Chart({ name: "test", version: "0.1.0" });
331
+ const svc = new Service({ metadata: { name: "app" }, spec: { type: "ClusterIP" } });
332
+ const entities = makeEntities(["chart", chart], ["svc", svc]);
333
+ const result = helmSerializer.serialize(entities) as SerializerResult;
334
+ const template = result.files!["templates/svc.yaml"];
335
+
336
+ expect(template).not.toContain("{{- if");
337
+ expect(template).not.toContain("{{- end }}");
338
+ });
339
+ });
340
+
341
+ describe("HelmHook serialization", () => {
342
+ test("emits hook annotations on wrapped resource", () => {
343
+ const chart = new Chart({ name: "test", version: "0.1.0" });
344
+ const Job = createResource("K8s::Batch::Job", "k8s", {});
345
+ const hook = new HelmHook({
346
+ hook: "pre-install",
347
+ weight: -5,
348
+ deletePolicy: "before-hook-creation",
349
+ resource: new Job({
350
+ metadata: { name: "db-migrate" },
351
+ spec: {
352
+ template: {
353
+ spec: {
354
+ containers: [{ name: "migrate", image: "migrate:latest" }],
355
+ restartPolicy: "Never",
356
+ },
357
+ },
358
+ },
359
+ }),
360
+ });
361
+
362
+ const entities = makeEntities(["chart", chart], ["dbMigrate", hook]);
363
+ const result = helmSerializer.serialize(entities) as SerializerResult;
364
+ const template = result.files!["templates/db-migrate.yaml"];
365
+
366
+ expect(template).toBeDefined();
367
+ expect(template).toContain("apiVersion: batch/v1");
368
+ expect(template).toContain("kind: Job");
369
+ expect(template).toContain("helm.sh/hook: pre-install");
370
+ expect(template).toContain("helm.sh/hook-weight: -5");
371
+ expect(template).toContain("helm.sh/hook-delete-policy: before-hook-creation");
372
+ });
373
+ });
374
+
375
+ describe("HelmTest serialization", () => {
376
+ test("emits test pod with helm.sh/hook: test annotation", () => {
377
+ const chart = new Chart({ name: "test", version: "0.1.0" });
378
+ const Pod = createResource("K8s::Core::Pod", "k8s", {});
379
+ const testPod = new HelmTest({
380
+ resource: new Pod({
381
+ metadata: { name: "test-connection" },
382
+ spec: {
383
+ containers: [{
384
+ name: "wget",
385
+ image: "busybox",
386
+ command: ["wget", "--spider", "http://test:80"],
387
+ }],
388
+ restartPolicy: "Never",
389
+ },
390
+ }),
391
+ });
392
+
393
+ const entities = makeEntities(["chart", chart], ["testConnection", testPod]);
394
+ const result = helmSerializer.serialize(entities) as SerializerResult;
395
+ const template = result.files!["templates/tests/test-connection.yaml"];
396
+
397
+ expect(template).toBeDefined();
398
+ expect(template).toContain("apiVersion: v1");
399
+ expect(template).toContain("kind: Pod");
400
+ expect(template).toContain("helm.sh/hook: test");
401
+ expect(template).toContain("image: busybox");
402
+ });
403
+ });
404
+
405
+ describe("HelmDependency serialization", () => {
406
+ test("emits dependencies in Chart.yaml", () => {
407
+ const chart = new Chart({ name: "my-app", version: "1.0.0" });
408
+ const redisDep = new HelmDependency({
409
+ name: "redis",
410
+ version: "17.x.x",
411
+ repository: "https://charts.bitnami.com/bitnami",
412
+ condition: "redis.enabled",
413
+ });
414
+ const pgDep = new HelmDependency({
415
+ name: "postgresql",
416
+ version: "12.x.x",
417
+ repository: "https://charts.bitnami.com/bitnami",
418
+ alias: "db",
419
+ });
420
+
421
+ const entities = makeEntities(
422
+ ["chart", chart],
423
+ ["redisDep", redisDep],
424
+ ["pgDep", pgDep],
425
+ );
426
+ const result = helmSerializer.serialize(entities) as SerializerResult;
427
+ const chartYaml = result.files!["Chart.yaml"];
428
+
429
+ expect(chartYaml).toContain("dependencies:");
430
+ expect(chartYaml).toContain("name: redis");
431
+ expect(chartYaml).toContain("version: '17.x.x'");
432
+ expect(chartYaml).toContain("repository: https://charts.bitnami.com/bitnami");
433
+ expect(chartYaml).toContain("condition: redis.enabled");
434
+ expect(chartYaml).toContain("name: postgresql");
435
+ expect(chartYaml).toContain("alias: db");
436
+ });
437
+ });
438
+
439
+ describe("values.schema.json nested types", () => {
440
+ test("infers nested object properties", () => {
441
+ const chart = new Chart({ name: "test", version: "0.1.0" });
442
+ const vals = new Values({
443
+ image: {
444
+ repository: "nginx",
445
+ tag: "latest",
446
+ pullPolicy: "IfNotPresent",
447
+ },
448
+ });
449
+ const entities = makeEntities(["chart", chart], ["values", vals]);
450
+ const result = helmSerializer.serialize(entities) as SerializerResult;
451
+ const schema = JSON.parse(result.files!["values.schema.json"]);
452
+
453
+ expect(schema.properties.image.type).toBe("object");
454
+ expect(schema.properties.image.properties.repository.type).toBe("string");
455
+ expect(schema.properties.image.properties.repository.default).toBe("nginx");
456
+ expect(schema.properties.image.properties.tag.type).toBe("string");
457
+ expect(schema.properties.image.properties.pullPolicy.type).toBe("string");
458
+ });
459
+
460
+ test("infers array types with items", () => {
461
+ const chart = new Chart({ name: "test", version: "0.1.0" });
462
+ const vals = new Values({
463
+ hosts: ["example.com"],
464
+ ports: [80, 443],
465
+ });
466
+ const entities = makeEntities(["chart", chart], ["values", vals]);
467
+ const result = helmSerializer.serialize(entities) as SerializerResult;
468
+ const schema = JSON.parse(result.files!["values.schema.json"]);
469
+
470
+ expect(schema.properties.hosts.type).toBe("array");
471
+ expect(schema.properties.hosts.items.type).toBe("string");
472
+ expect(schema.properties.ports.type).toBe("array");
473
+ expect(schema.properties.ports.items.type).toBe("integer");
474
+ });
475
+
476
+ test("includes required fields for non-empty defaults", () => {
477
+ const chart = new Chart({ name: "test", version: "0.1.0" });
478
+ const vals = new Values({
479
+ image: {
480
+ repository: "nginx",
481
+ tag: "",
482
+ pullPolicy: "IfNotPresent",
483
+ },
484
+ });
485
+ const entities = makeEntities(["chart", chart], ["values", vals]);
486
+ const result = helmSerializer.serialize(entities) as SerializerResult;
487
+ const schema = JSON.parse(result.files!["values.schema.json"]);
488
+
489
+ // tag is empty string, so not required; repository and pullPolicy are
490
+ expect(schema.properties.image.required).toContain("repository");
491
+ expect(schema.properties.image.required).toContain("pullPolicy");
492
+ expect(schema.properties.image.required).not.toContain("tag");
493
+ });
494
+
495
+ test("handles float numbers", () => {
496
+ const chart = new Chart({ name: "test", version: "0.1.0" });
497
+ const vals = new Values({ cpuLimit: 0.5 });
498
+ const entities = makeEntities(["chart", chart], ["values", vals]);
499
+ const result = helmSerializer.serialize(entities) as SerializerResult;
500
+ const schema = JSON.parse(result.files!["values.schema.json"]);
501
+
502
+ expect(schema.properties.cpuLimit.type).toBe("number");
503
+ expect(schema.properties.cpuLimit.default).toBe(0.5);
504
+ });
505
+ });
506
+
507
+ describe("template function expressions in templates", () => {
508
+ test("quote function in template", () => {
509
+ const chart = new Chart({ name: "test", version: "0.1.0" });
510
+ const cm = new ConfigMap({
511
+ metadata: { name: "config" },
512
+ data: { version: quote(values.appVersion) },
513
+ });
514
+
515
+ const entities = makeEntities(["chart", chart], ["config", cm]);
516
+ const result = helmSerializer.serialize(entities) as SerializerResult;
517
+ const template = result.files!["templates/config.yaml"];
518
+
519
+ expect(template).toContain("{{ .Values.appVersion | quote }}");
520
+ });
521
+
522
+ test("helmDefault function in template", () => {
523
+ const chart = new Chart({ name: "test", version: "0.1.0" });
524
+ const deploy = new Deployment({
525
+ metadata: { name: "app" },
526
+ spec: {
527
+ replicas: helmDefault(1, values.replicaCount),
528
+ },
529
+ });
530
+
531
+ const entities = makeEntities(["chart", chart], ["deploy", deploy]);
532
+ const result = helmSerializer.serialize(entities) as SerializerResult;
533
+ const template = result.files!["templates/deploy.yaml"];
534
+
535
+ expect(template).toContain('{{ default 1 .Values.replicaCount }}');
536
+ });
537
+
538
+ test("required function in template", () => {
539
+ const chart = new Chart({ name: "test", version: "0.1.0" });
540
+ const deploy = new Deployment({
541
+ metadata: { name: "app" },
542
+ spec: {
543
+ template: {
544
+ spec: {
545
+ containers: [{
546
+ name: "app",
547
+ image: required("image.tag is required", values.image.tag),
548
+ }],
549
+ },
550
+ },
551
+ },
552
+ });
553
+
554
+ const entities = makeEntities(["chart", chart], ["deploy", deploy]);
555
+ const result = helmSerializer.serialize(entities) as SerializerResult;
556
+ const template = result.files!["templates/deploy.yaml"];
557
+
558
+ expect(template).toContain('{{ required "image.tag is required" .Values.image.tag }}');
559
+ });
560
+
561
+ test("ChartRef in template", () => {
562
+ const chart = new Chart({ name: "test", version: "0.1.0" });
563
+ const cm = new ConfigMap({
564
+ metadata: { name: "meta" },
565
+ data: {
566
+ chartName: ChartRef.Name,
567
+ chartVersion: ChartRef.Version,
568
+ },
569
+ });
570
+
571
+ const entities = makeEntities(["chart", chart], ["meta", cm]);
572
+ const result = helmSerializer.serialize(entities) as SerializerResult;
573
+ const template = result.files!["templates/meta.yaml"];
574
+
575
+ expect(template).toContain("{{ .Chart.Name }}");
576
+ expect(template).toContain("{{ .Chart.Version }}");
577
+ });
578
+ });
579
+
580
+ describe("pipe chaining in values proxy", () => {
581
+ test("values.x.pipe('upper').pipe('quote') emits correct expression", () => {
582
+ const chart = new Chart({ name: "test", version: "0.1.0" });
583
+ const cm = new ConfigMap({
584
+ metadata: { name: "config" },
585
+ data: { env: (values.environment as any).pipe("upper").pipe("quote") },
586
+ });
587
+
588
+ const entities = makeEntities(["chart", chart], ["config", cm]);
589
+ const result = helmSerializer.serialize(entities) as SerializerResult;
590
+ const template = result.files!["templates/config.yaml"];
591
+
592
+ expect(template).toContain("{{ .Values.environment | upper | quote }}");
593
+ });
594
+ });
595
+
596
+ describe("Chart.yaml field ordering", () => {
597
+ test("emits fields in canonical Helm order", () => {
598
+ const chart = new Chart({
599
+ apiVersion: "v2",
600
+ name: "my-app",
601
+ version: "1.0.0",
602
+ kubeVersion: ">=1.20.0",
603
+ description: "My app",
604
+ type: "application",
605
+ keywords: ["web", "app"],
606
+ home: "https://example.com",
607
+ sources: ["https://github.com/example/my-app"],
608
+ icon: "https://example.com/icon.png",
609
+ deprecated: false,
610
+ annotations: { category: "web" },
611
+ condition: "myApp.enabled",
612
+ tags: "frontend",
613
+ appVersion: "2.0.0",
614
+ });
615
+ const entities = makeEntities(["chart", chart]);
616
+ const result = helmSerializer.serialize(entities) as SerializerResult;
617
+ const chartYaml = result.files!["Chart.yaml"];
618
+
619
+ // Verify fields appear and ordering: apiVersion before name before version, etc.
620
+ const lines = chartYaml.split("\n");
621
+ const keyLines = lines.filter((l) => /^\w/.test(l)).map((l) => l.split(":")[0]);
622
+ const idx = (k: string) => keyLines.indexOf(k);
623
+
624
+ expect(idx("apiVersion")).toBeLessThan(idx("name"));
625
+ expect(idx("name")).toBeLessThan(idx("version"));
626
+ expect(idx("version")).toBeLessThan(idx("kubeVersion"));
627
+ expect(idx("kubeVersion")).toBeLessThan(idx("description"));
628
+ expect(idx("description")).toBeLessThan(idx("type"));
629
+ expect(idx("appVersion")).toBeGreaterThan(idx("tags"));
630
+ });
631
+
632
+ test("emits Chart.yaml with kubeVersion, sources, annotations", () => {
633
+ const chart = new Chart({
634
+ name: "my-app",
635
+ version: "1.0.0",
636
+ kubeVersion: ">=1.22.0",
637
+ sources: ["https://github.com/example/my-app"],
638
+ annotations: { "artifacthub.io/license": "Apache-2.0" },
639
+ });
640
+ const entities = makeEntities(["chart", chart]);
641
+ const result = helmSerializer.serialize(entities) as SerializerResult;
642
+ const chartYaml = result.files!["Chart.yaml"];
643
+
644
+ expect(chartYaml).toContain("kubeVersion: >=1.22.0");
645
+ expect(chartYaml).toContain("sources:");
646
+ expect(chartYaml).toContain("- https://github.com/example/my-app");
647
+ expect(chartYaml).toContain("annotations:");
648
+ expect(chartYaml).toContain("artifacthub.io/license: Apache-2.0");
649
+ });
650
+ });
651
+
652
+ describe("HelmMaintainer serialization", () => {
653
+ test("emits maintainers in Chart.yaml", () => {
654
+ const chart = new Chart({ name: "my-app", version: "1.0.0" });
655
+ const m1 = new HelmMaintainer({
656
+ name: "John Doe",
657
+ email: "john@example.com",
658
+ url: "https://example.com/john",
659
+ });
660
+ const m2 = new HelmMaintainer({
661
+ name: "Jane Smith",
662
+ email: "jane@example.com",
663
+ });
664
+
665
+ const entities = makeEntities(
666
+ ["chart", chart],
667
+ ["maintainer1", m1],
668
+ ["maintainer2", m2],
669
+ );
670
+ const result = helmSerializer.serialize(entities) as SerializerResult;
671
+ const chartYaml = result.files!["Chart.yaml"];
672
+
673
+ expect(chartYaml).toContain("maintainers:");
674
+ expect(chartYaml).toContain("name: John Doe");
675
+ expect(chartYaml).toContain("email: john@example.com");
676
+ expect(chartYaml).toContain("url: https://example.com/john");
677
+ expect(chartYaml).toContain("name: Jane Smith");
678
+ expect(chartYaml).toContain("email: jane@example.com");
679
+ });
680
+ });
681
+
682
+ describe("HelmDependency extended fields", () => {
683
+ test("emits enabled and import-values in dependencies", () => {
684
+ const chart = new Chart({ name: "my-app", version: "1.0.0" });
685
+ const dep = new HelmDependency({
686
+ name: "redis",
687
+ version: "17.x.x",
688
+ repository: "https://charts.bitnami.com/bitnami",
689
+ enabled: false,
690
+ importValues: ["data"],
691
+ });
692
+
693
+ const entities = makeEntities(["chart", chart], ["redisDep", dep]);
694
+ const result = helmSerializer.serialize(entities) as SerializerResult;
695
+ const chartYaml = result.files!["Chart.yaml"];
696
+
697
+ expect(chartYaml).toContain("enabled: false");
698
+ expect(chartYaml).toContain("import-values:");
699
+ expect(chartYaml).toContain("- data");
700
+ // Verify the key is "import-values" not "importValues"
701
+ expect(chartYaml).not.toContain("importValues:");
702
+ });
703
+ });
704
+
705
+ describe("ElseIf serialization", () => {
706
+ test("emits else-if chain correctly", () => {
707
+ const chart = new Chart({ name: "test", version: "0.1.0" });
708
+ const cm = new ConfigMap({
709
+ metadata: { name: "config" },
710
+ data: {
711
+ tier: If(values.tier, "gold", ElseIf(values.backup, "silver", "bronze")),
712
+ },
713
+ });
714
+
715
+ const entities = makeEntities(["chart", chart], ["config", cm]);
716
+ const result = helmSerializer.serialize(entities) as SerializerResult;
717
+ const template = result.files!["templates/config.yaml"];
718
+
719
+ expect(template).toContain("{{- if .Values.tier }}");
720
+ expect(template).toContain("{{- else if .Values.backup }}");
721
+ expect(template).toContain("{{- else }}");
722
+ expect(template).toContain("{{- end }}");
723
+ expect(template).toContain("gold");
724
+ expect(template).toContain("silver");
725
+ expect(template).toContain("bronze");
726
+ });
727
+
728
+ test("emits simple else-if without final else", () => {
729
+ const chart = new Chart({ name: "test", version: "0.1.0" });
730
+ const cm = new ConfigMap({
731
+ metadata: { name: "config" },
732
+ data: {
733
+ tier: If(values.a, "one", ElseIf(values.b, "two")),
734
+ },
735
+ });
736
+
737
+ const entities = makeEntities(["chart", chart], ["config", cm]);
738
+ const result = helmSerializer.serialize(entities) as SerializerResult;
739
+ const template = result.files!["templates/config.yaml"];
740
+
741
+ expect(template).toContain("{{- if .Values.a }}");
742
+ expect(template).toContain("{{- else if .Values.b }}");
743
+ expect(template).not.toContain("{{- else }}\n");
744
+ expect(template).toContain("{{- end }}");
745
+ });
746
+ });
747
+
748
+ describe("HelmCRD serialization", () => {
749
+ test("emits CRD content to crds/ directory", () => {
750
+ const chart = new Chart({ name: "test", version: "0.1.0" });
751
+ const crd = new HelmCRD({
752
+ content: "apiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\nmetadata:\n name: foos.example.com\n",
753
+ filename: "foos.yaml",
754
+ });
755
+
756
+ const entities = makeEntities(["chart", chart], ["fooCrd", crd]);
757
+ const result = helmSerializer.serialize(entities) as SerializerResult;
758
+
759
+ expect(result.files!["crds/foos.yaml"]).toBe(
760
+ "apiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\nmetadata:\n name: foos.example.com\n",
761
+ );
762
+ });
763
+
764
+ test("defaults filename from entity name", () => {
765
+ const chart = new Chart({ name: "test", version: "0.1.0" });
766
+ const crd = new HelmCRD({
767
+ content: "apiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\n",
768
+ });
769
+
770
+ const entities = makeEntities(["chart", chart], ["myCustomResource", crd]);
771
+ const result = helmSerializer.serialize(entities) as SerializerResult;
772
+
773
+ expect(result.files!["crds/my-custom-resource.yaml"]).toBeDefined();
774
+ });
775
+ });
776
+
777
+ describe("Capabilities in template", () => {
778
+ test("emits Capabilities.KubeVersion.Version in K8s resource", () => {
779
+ const chart = new Chart({ name: "test", version: "0.1.0" });
780
+ const cm = new ConfigMap({
781
+ metadata: { name: "meta" },
782
+ data: {
783
+ kubeVersion: Capabilities.KubeVersion.Version,
784
+ },
785
+ });
786
+
787
+ const entities = makeEntities(["chart", chart], ["meta", cm]);
788
+ const result = helmSerializer.serialize(entities) as SerializerResult;
789
+ const template = result.files!["templates/meta.yaml"];
790
+
791
+ expect(template).toContain("{{ .Capabilities.KubeVersion.Version }}");
792
+ });
793
+ });
794
+
795
+ describe("helpers", () => {
796
+ test("generateHelpers includes all standard templates", () => {
797
+ const { generateHelpers } = require("./helpers");
798
+ const content = generateHelpers({ chartName: "my-chart" });
799
+
800
+ expect(content).toContain('define "my-chart.name"');
801
+ expect(content).toContain('define "my-chart.fullname"');
802
+ expect(content).toContain('define "my-chart.chart"');
803
+ expect(content).toContain('define "my-chart.labels"');
804
+ expect(content).toContain('define "my-chart.selectorLabels"');
805
+ expect(content).toContain('define "my-chart.serviceAccountName"');
806
+ });
807
+
808
+ test("generateHelpers respects includeServiceAccount=false", () => {
809
+ const { generateHelpers } = require("./helpers");
810
+ const content = generateHelpers({ chartName: "test", includeServiceAccount: false });
811
+
812
+ expect(content).toContain('define "test.name"');
813
+ expect(content).not.toContain("serviceAccountName");
814
+ });
815
+ });
816
+
817
+ describe("fallback GVK resolution", () => {
818
+ test("emits template for unknown K8s group when apiVersion/kind in props", () => {
819
+ const chart = new Chart({ name: "test", version: "0.1.0" });
820
+ const ExternalSecret = createResource("K8s::ExternalSecrets::ExternalSecret", "k8s", {});
821
+ const es = new ExternalSecret({
822
+ apiVersion: "external-secrets.io/v1beta1",
823
+ kind: "ExternalSecret",
824
+ metadata: { name: "my-secret" },
825
+ spec: {
826
+ refreshInterval: "1h",
827
+ secretStoreRef: { name: "vault", kind: "ClusterSecretStore" },
828
+ },
829
+ });
830
+
831
+ const entities = makeEntities(["chart", chart], ["externalSecret", es]);
832
+ const result = helmSerializer.serialize(entities) as SerializerResult;
833
+ const template = result.files!["templates/external-secret.yaml"];
834
+
835
+ expect(template).toBeDefined();
836
+ expect(template).toContain("apiVersion: external-secrets.io/v1beta1");
837
+ expect(template).toContain("kind: ExternalSecret");
838
+ expect(template).toContain("refreshInterval: '1h'");
839
+ });
840
+
841
+ test("returns empty for unknown group without apiVersion/kind in props", () => {
842
+ const chart = new Chart({ name: "test", version: "0.1.0" });
843
+ const Unknown = createResource("K8s::Unknown::Widget", "k8s", {});
844
+ const w = new Unknown({
845
+ metadata: { name: "w" },
846
+ spec: { foo: "bar" },
847
+ });
848
+
849
+ const entities = makeEntities(["chart", chart], ["widget", w]);
850
+ const result = helmSerializer.serialize(entities) as SerializerResult;
851
+
852
+ expect(result.files!["templates/widget.yaml"]).toBeUndefined();
853
+ });
854
+ });
855
+
856
+ describe("enhanced values.schema.json", () => {
857
+ test("adds descriptions to known keys", () => {
858
+ const chart = new Chart({ name: "test", version: "0.1.0" });
859
+ const vals = new Values({
860
+ replicaCount: 3,
861
+ image: { repository: "nginx", tag: "" },
862
+ });
863
+ const entities = makeEntities(["chart", chart], ["values", vals]);
864
+ const result = helmSerializer.serialize(entities) as SerializerResult;
865
+ const schema = JSON.parse(result.files!["values.schema.json"]);
866
+
867
+ expect(schema.properties.replicaCount.description).toBe("Number of pod replicas");
868
+ expect(schema.properties.image.description).toBe("Container image configuration");
869
+ expect(schema.properties.image.properties.repository.description).toBe("Image repository");
870
+ expect(schema.properties.image.properties.tag.description).toBe("Image tag (empty defaults to Chart.appVersion)");
871
+ });
872
+
873
+ test("adds enum values to known string fields", () => {
874
+ const chart = new Chart({ name: "test", version: "0.1.0" });
875
+ const vals = new Values({
876
+ image: { pullPolicy: "IfNotPresent" },
877
+ service: { type: "ClusterIP" },
878
+ });
879
+ const entities = makeEntities(["chart", chart], ["values", vals]);
880
+ const result = helmSerializer.serialize(entities) as SerializerResult;
881
+ const schema = JSON.parse(result.files!["values.schema.json"]);
882
+
883
+ expect(schema.properties.image.properties.pullPolicy.enum).toEqual(["Always", "IfNotPresent", "Never"]);
884
+ expect(schema.properties.service.properties.type.enum).toEqual(["ClusterIP", "NodePort", "LoadBalancer", "ExternalName"]);
885
+ });
886
+
887
+ test("adds numeric constraints to known keys", () => {
888
+ const chart = new Chart({ name: "test", version: "0.1.0" });
889
+ const vals = new Values({
890
+ replicaCount: 1,
891
+ service: { port: 80 },
892
+ autoscaling: {
893
+ minReplicas: 1,
894
+ maxReplicas: 10,
895
+ targetCPUUtilizationPercentage: 80,
896
+ },
897
+ });
898
+ const entities = makeEntities(["chart", chart], ["values", vals]);
899
+ const result = helmSerializer.serialize(entities) as SerializerResult;
900
+ const schema = JSON.parse(result.files!["values.schema.json"]);
901
+
902
+ expect(schema.properties.replicaCount.minimum).toBe(0);
903
+ expect(schema.properties.service.properties.port.minimum).toBe(1);
904
+ expect(schema.properties.service.properties.port.maximum).toBe(65535);
905
+ expect(schema.properties.autoscaling.properties.minReplicas.minimum).toBe(1);
906
+ expect(schema.properties.autoscaling.properties.targetCPUUtilizationPercentage.minimum).toBe(1);
907
+ expect(schema.properties.autoscaling.properties.targetCPUUtilizationPercentage.maximum).toBe(100);
908
+ });
909
+
910
+ test("preserves existing schema behavior", () => {
911
+ const chart = new Chart({ name: "test", version: "0.1.0" });
912
+ const vals = new Values({
913
+ customField: "hello",
914
+ customNumber: 42,
915
+ });
916
+ const entities = makeEntities(["chart", chart], ["values", vals]);
917
+ const result = helmSerializer.serialize(entities) as SerializerResult;
918
+ const schema = JSON.parse(result.files!["values.schema.json"]);
919
+
920
+ // Unknown keys should not get descriptions or enums
921
+ expect(schema.properties.customField.description).toBeUndefined();
922
+ expect(schema.properties.customField.enum).toBeUndefined();
923
+ expect(schema.properties.customNumber.minimum).toBeUndefined();
924
+ // But type and default should still be there
925
+ expect(schema.properties.customField.type).toBe("string");
926
+ expect(schema.properties.customField.default).toBe("hello");
927
+ expect(schema.properties.customNumber.type).toBe("integer");
928
+ expect(schema.properties.customNumber.default).toBe(42);
929
+ });
930
+ });