@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.
- package/README.md +22 -0
- package/dist/integrity.json +36 -0
- package/dist/manifest.json +37 -0
- package/dist/meta.json +208 -0
- package/dist/rules/chart-metadata.ts +64 -0
- package/dist/rules/helm-helpers.ts +64 -0
- package/dist/rules/no-hardcoded-image.ts +62 -0
- package/dist/rules/values-no-secrets.ts +82 -0
- package/dist/rules/whm101.ts +46 -0
- package/dist/rules/whm102.ts +33 -0
- package/dist/rules/whm103.ts +59 -0
- package/dist/rules/whm104.ts +35 -0
- package/dist/rules/whm105.ts +30 -0
- package/dist/rules/whm201.ts +36 -0
- package/dist/rules/whm202.ts +50 -0
- package/dist/rules/whm203.ts +39 -0
- package/dist/rules/whm204.ts +60 -0
- package/dist/rules/whm301.ts +41 -0
- package/dist/rules/whm302.ts +40 -0
- package/dist/rules/whm401.ts +57 -0
- package/dist/rules/whm402.ts +45 -0
- package/dist/rules/whm403.ts +45 -0
- package/dist/rules/whm404.ts +36 -0
- package/dist/rules/whm405.ts +53 -0
- package/dist/rules/whm406.ts +34 -0
- package/dist/rules/whm407.ts +83 -0
- package/dist/rules/whm501.ts +103 -0
- package/dist/rules/whm502.ts +94 -0
- package/dist/skills/chant-helm-chart-patterns.md +229 -0
- package/dist/skills/chant-helm-chart-security-patterns.md +192 -0
- package/dist/skills/chant-helm-create-chart.md +211 -0
- package/dist/types/index.d.ts +132 -0
- package/package.json +34 -0
- package/src/codegen/docs-cli.ts +4 -0
- package/src/codegen/docs.ts +483 -0
- package/src/codegen/generate-cli.ts +28 -0
- package/src/codegen/generate.ts +249 -0
- package/src/codegen/naming.ts +38 -0
- package/src/codegen/package.ts +64 -0
- package/src/composites/composites.test.ts +1050 -0
- package/src/composites/helm-batch-job.ts +209 -0
- package/src/composites/helm-crd-lifecycle.ts +184 -0
- package/src/composites/helm-cron-job.ts +177 -0
- package/src/composites/helm-daemon-set.ts +169 -0
- package/src/composites/helm-external-secret.ts +93 -0
- package/src/composites/helm-library.ts +51 -0
- package/src/composites/helm-microservice.ts +331 -0
- package/src/composites/helm-monitored-service.ts +252 -0
- package/src/composites/helm-namespace-env.ts +154 -0
- package/src/composites/helm-secure-ingress.ts +114 -0
- package/src/composites/helm-stateful-service.ts +213 -0
- package/src/composites/helm-web-app.ts +264 -0
- package/src/composites/helm-worker.ts +207 -0
- package/src/composites/index.ts +38 -0
- package/src/coverage.test.ts +21 -0
- package/src/coverage.ts +50 -0
- package/src/generated/index.d.ts +132 -0
- package/src/generated/index.ts +13 -0
- package/src/generated/lexicon-helm.json +208 -0
- package/src/helpers.test.ts +51 -0
- package/src/helpers.ts +100 -0
- package/src/import/generator.ts +285 -0
- package/src/import/import.test.ts +224 -0
- package/src/import/parser.ts +160 -0
- package/src/import/template-stripper.ts +123 -0
- package/src/index.ts +108 -0
- package/src/intrinsics.test.ts +380 -0
- package/src/intrinsics.ts +484 -0
- package/src/lint/post-synth/helm-helpers.ts +64 -0
- package/src/lint/post-synth/post-synth.test.ts +504 -0
- package/src/lint/post-synth/whm101.ts +46 -0
- package/src/lint/post-synth/whm102.ts +33 -0
- package/src/lint/post-synth/whm103.ts +59 -0
- package/src/lint/post-synth/whm104.ts +35 -0
- package/src/lint/post-synth/whm105.ts +30 -0
- package/src/lint/post-synth/whm201.ts +36 -0
- package/src/lint/post-synth/whm202.ts +50 -0
- package/src/lint/post-synth/whm203.ts +39 -0
- package/src/lint/post-synth/whm204.ts +60 -0
- package/src/lint/post-synth/whm301.ts +41 -0
- package/src/lint/post-synth/whm302.ts +40 -0
- package/src/lint/post-synth/whm401.ts +57 -0
- package/src/lint/post-synth/whm402.ts +45 -0
- package/src/lint/post-synth/whm403.ts +45 -0
- package/src/lint/post-synth/whm404.ts +36 -0
- package/src/lint/post-synth/whm405.ts +53 -0
- package/src/lint/post-synth/whm406.ts +34 -0
- package/src/lint/post-synth/whm407.ts +83 -0
- package/src/lint/post-synth/whm501.ts +103 -0
- package/src/lint/post-synth/whm502.ts +94 -0
- package/src/lint/rules/chart-metadata.ts +64 -0
- package/src/lint/rules/lint-rules.test.ts +97 -0
- package/src/lint/rules/no-hardcoded-image.ts +62 -0
- package/src/lint/rules/values-no-secrets.ts +82 -0
- package/src/lsp/completions.test.ts +72 -0
- package/src/lsp/completions.ts +20 -0
- package/src/lsp/hover.test.ts +46 -0
- package/src/lsp/hover.ts +46 -0
- package/src/package-cli.ts +28 -0
- package/src/plugin.test.ts +71 -0
- package/src/plugin.ts +206 -0
- package/src/resources.ts +77 -0
- package/src/serializer.test.ts +930 -0
- package/src/serializer.ts +835 -0
- package/src/skills/chart-patterns.md +229 -0
- package/src/skills/chart-security-patterns.md +192 -0
- package/src/skills/create-chart.md +211 -0
- package/src/validate-cli.ts +21 -0
- package/src/validate.test.ts +37 -0
- package/src/validate.ts +36 -0
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
|
|
3
|
+
import { HelmWebApp } from "./helm-web-app";
|
|
4
|
+
import { HelmStatefulService } from "./helm-stateful-service";
|
|
5
|
+
import { HelmCronJob } from "./helm-cron-job";
|
|
6
|
+
import { HelmMicroservice } from "./helm-microservice";
|
|
7
|
+
import { HelmLibrary } from "./helm-library";
|
|
8
|
+
import { HelmCRDLifecycle } from "./helm-crd-lifecycle";
|
|
9
|
+
import { HelmDaemonSet } from "./helm-daemon-set";
|
|
10
|
+
import { HelmWorker } from "./helm-worker";
|
|
11
|
+
import { HelmExternalSecret } from "./helm-external-secret";
|
|
12
|
+
import { HelmBatchJob } from "./helm-batch-job";
|
|
13
|
+
import { HelmMonitoredService } from "./helm-monitored-service";
|
|
14
|
+
import { HelmSecureIngress } from "./helm-secure-ingress";
|
|
15
|
+
import { HelmNamespaceEnv } from "./helm-namespace-env";
|
|
16
|
+
|
|
17
|
+
function hasIntrinsic(obj: unknown): boolean {
|
|
18
|
+
if (obj && typeof obj === "object" && INTRINSIC_MARKER in (obj as any)) return true;
|
|
19
|
+
if (obj && typeof obj === "object") {
|
|
20
|
+
for (const v of Object.values(obj as Record<string, unknown>)) {
|
|
21
|
+
if (hasIntrinsic(v)) return true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (Array.isArray(obj)) {
|
|
25
|
+
for (const v of obj) {
|
|
26
|
+
if (hasIntrinsic(v)) return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("HelmWebApp", () => {
|
|
33
|
+
test("returns chart, values, deployment, and service", () => {
|
|
34
|
+
const result = HelmWebApp({ name: "my-app" });
|
|
35
|
+
expect(result.chart).toBeDefined();
|
|
36
|
+
expect(result.values).toBeDefined();
|
|
37
|
+
expect(result.deployment).toBeDefined();
|
|
38
|
+
expect(result.service).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("chart has correct metadata", () => {
|
|
42
|
+
const result = HelmWebApp({ name: "web-ui" });
|
|
43
|
+
expect(result.chart.name).toBe("web-ui");
|
|
44
|
+
expect(result.chart.apiVersion).toBe("v2");
|
|
45
|
+
expect(result.chart.type).toBe("application");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("values include default image and service config", () => {
|
|
49
|
+
const result = HelmWebApp({ name: "app" });
|
|
50
|
+
const vals = result.values as any;
|
|
51
|
+
expect(vals.image.repository).toBe("nginx");
|
|
52
|
+
expect(vals.service.port).toBe(80);
|
|
53
|
+
expect(vals.replicaCount).toBe(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("custom props flow through to values", () => {
|
|
57
|
+
const result = HelmWebApp({
|
|
58
|
+
name: "api",
|
|
59
|
+
imageRepository: "myregistry/api",
|
|
60
|
+
port: 3000,
|
|
61
|
+
replicas: 3,
|
|
62
|
+
});
|
|
63
|
+
const vals = result.values as any;
|
|
64
|
+
expect(vals.image.repository).toBe("myregistry/api");
|
|
65
|
+
expect(vals.service.port).toBe(3000);
|
|
66
|
+
expect(vals.replicaCount).toBe(3);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("includes ingress, hpa, and serviceAccount by default", () => {
|
|
70
|
+
const result = HelmWebApp({ name: "app" });
|
|
71
|
+
expect(result.ingress).toBeDefined();
|
|
72
|
+
expect(result.hpa).toBeDefined();
|
|
73
|
+
expect(result.serviceAccount).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("can exclude optional resources", () => {
|
|
77
|
+
const result = HelmWebApp({ name: "app", ingress: false, autoscaling: false, serviceAccount: false });
|
|
78
|
+
expect(result.ingress).toBeUndefined();
|
|
79
|
+
expect(result.hpa).toBeUndefined();
|
|
80
|
+
expect(result.serviceAccount).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("deployment uses Helm intrinsics", () => {
|
|
84
|
+
const result = HelmWebApp({ name: "app" });
|
|
85
|
+
expect(hasIntrinsic(result.deployment)).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("omitted security/scheduling props produce no values change", () => {
|
|
89
|
+
const result = HelmWebApp({ name: "app" });
|
|
90
|
+
const vals = result.values as any;
|
|
91
|
+
expect(vals.podSecurityContext).toBeUndefined();
|
|
92
|
+
expect(vals.securityContext).toBeUndefined();
|
|
93
|
+
expect(vals.nodeSelector).toBeUndefined();
|
|
94
|
+
expect(vals.tolerations).toBeUndefined();
|
|
95
|
+
expect(vals.affinity).toBeUndefined();
|
|
96
|
+
expect(vals.podAnnotations).toBeUndefined();
|
|
97
|
+
expect(vals.livenessProbe).toBeUndefined();
|
|
98
|
+
expect(vals.readinessProbe).toBeUndefined();
|
|
99
|
+
expect(vals.strategy).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("security context props flow through to values", () => {
|
|
103
|
+
const result = HelmWebApp({
|
|
104
|
+
name: "app",
|
|
105
|
+
podSecurityContext: { runAsNonRoot: true },
|
|
106
|
+
securityContext: { readOnlyRootFilesystem: true },
|
|
107
|
+
});
|
|
108
|
+
const vals = result.values as any;
|
|
109
|
+
expect(vals.podSecurityContext).toEqual({ runAsNonRoot: true });
|
|
110
|
+
expect(vals.securityContext).toEqual({ readOnlyRootFilesystem: true });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("scheduling props flow through to values", () => {
|
|
114
|
+
const result = HelmWebApp({
|
|
115
|
+
name: "app",
|
|
116
|
+
nodeSelector: { "kubernetes.io/os": "linux" },
|
|
117
|
+
tolerations: [{ key: "special", operator: "Exists" }],
|
|
118
|
+
affinity: { nodeAffinity: {} },
|
|
119
|
+
});
|
|
120
|
+
const vals = result.values as any;
|
|
121
|
+
expect(vals.nodeSelector).toEqual({ "kubernetes.io/os": "linux" });
|
|
122
|
+
expect(vals.tolerations).toHaveLength(1);
|
|
123
|
+
expect(vals.affinity).toBeDefined();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("probe and strategy props flow through to values", () => {
|
|
127
|
+
const result = HelmWebApp({
|
|
128
|
+
name: "app",
|
|
129
|
+
livenessProbe: { httpGet: { path: "/healthz", port: "http" } },
|
|
130
|
+
readinessProbe: { httpGet: { path: "/readyz", port: "http" } },
|
|
131
|
+
strategy: { type: "RollingUpdate" },
|
|
132
|
+
});
|
|
133
|
+
const vals = result.values as any;
|
|
134
|
+
expect(vals.livenessProbe).toBeDefined();
|
|
135
|
+
expect(vals.readinessProbe).toBeDefined();
|
|
136
|
+
expect(vals.strategy).toBeDefined();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("podAnnotations flows through to values", () => {
|
|
140
|
+
const result = HelmWebApp({
|
|
141
|
+
name: "app",
|
|
142
|
+
podAnnotations: { "prometheus.io/scrape": "true" },
|
|
143
|
+
});
|
|
144
|
+
const vals = result.values as any;
|
|
145
|
+
expect(vals.podAnnotations).toEqual({ "prometheus.io/scrape": "true" });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("scheduling props use With() intrinsics in deployment", () => {
|
|
149
|
+
const result = HelmWebApp({
|
|
150
|
+
name: "app",
|
|
151
|
+
nodeSelector: { "kubernetes.io/os": "linux" },
|
|
152
|
+
});
|
|
153
|
+
const podSpec = (result.deployment as any).spec.template.spec;
|
|
154
|
+
expect(hasIntrinsic(podSpec.nodeSelector)).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("HelmStatefulService", () => {
|
|
159
|
+
test("returns chart, values, statefulSet, and service", () => {
|
|
160
|
+
const result = HelmStatefulService({ name: "db" });
|
|
161
|
+
expect(result.chart).toBeDefined();
|
|
162
|
+
expect(result.values).toBeDefined();
|
|
163
|
+
expect(result.statefulSet).toBeDefined();
|
|
164
|
+
expect(result.service).toBeDefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("chart is marked as application", () => {
|
|
168
|
+
const result = HelmStatefulService({ name: "db" });
|
|
169
|
+
expect(result.chart.type).toBe("application");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("service is headless (clusterIP: None)", () => {
|
|
173
|
+
const result = HelmStatefulService({ name: "db" });
|
|
174
|
+
expect((result.service as any).spec.clusterIP).toBe("None");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("statefulSet has volumeClaimTemplates", () => {
|
|
178
|
+
const result = HelmStatefulService({ name: "db" });
|
|
179
|
+
const spec = (result.statefulSet as any).spec;
|
|
180
|
+
expect(spec.volumeClaimTemplates).toBeDefined();
|
|
181
|
+
expect(spec.volumeClaimTemplates).toHaveLength(1);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("values include persistence config", () => {
|
|
185
|
+
const result = HelmStatefulService({ name: "db", storageSize: "50Gi" });
|
|
186
|
+
const vals = result.values as any;
|
|
187
|
+
expect(vals.persistence.size).toBe("50Gi");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("uses Helm intrinsics", () => {
|
|
191
|
+
const result = HelmStatefulService({ name: "db" });
|
|
192
|
+
expect(hasIntrinsic(result.statefulSet)).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("no serviceAccount by default", () => {
|
|
196
|
+
const result = HelmStatefulService({ name: "db" });
|
|
197
|
+
expect(result.serviceAccount).toBeUndefined();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("serviceAccount can be enabled", () => {
|
|
201
|
+
const result = HelmStatefulService({ name: "db", serviceAccount: true });
|
|
202
|
+
expect(result.serviceAccount).toBeDefined();
|
|
203
|
+
const vals = result.values as any;
|
|
204
|
+
expect(vals.serviceAccount).toBeDefined();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("omitted security/scheduling props produce no change", () => {
|
|
208
|
+
const result = HelmStatefulService({ name: "db" });
|
|
209
|
+
const vals = result.values as any;
|
|
210
|
+
expect(vals.podSecurityContext).toBeUndefined();
|
|
211
|
+
expect(vals.nodeSelector).toBeUndefined();
|
|
212
|
+
expect(vals.livenessProbe).toBeUndefined();
|
|
213
|
+
expect(vals.updateStrategy).toBeUndefined();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("security and scheduling props flow through", () => {
|
|
217
|
+
const result = HelmStatefulService({
|
|
218
|
+
name: "db",
|
|
219
|
+
podSecurityContext: { runAsNonRoot: true },
|
|
220
|
+
nodeSelector: { "kubernetes.io/os": "linux" },
|
|
221
|
+
livenessProbe: { tcpSocket: { port: 5432 } },
|
|
222
|
+
readinessProbe: { tcpSocket: { port: 5432 } },
|
|
223
|
+
updateStrategy: { type: "RollingUpdate" },
|
|
224
|
+
});
|
|
225
|
+
const vals = result.values as any;
|
|
226
|
+
expect(vals.podSecurityContext).toBeDefined();
|
|
227
|
+
expect(vals.nodeSelector).toBeDefined();
|
|
228
|
+
expect(vals.livenessProbe).toBeDefined();
|
|
229
|
+
expect(vals.readinessProbe).toBeDefined();
|
|
230
|
+
expect(vals.updateStrategy).toBeDefined();
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe("HelmCronJob", () => {
|
|
235
|
+
test("returns chart, values, and cronJob", () => {
|
|
236
|
+
const result = HelmCronJob({ name: "cleanup" });
|
|
237
|
+
expect(result.chart).toBeDefined();
|
|
238
|
+
expect(result.values).toBeDefined();
|
|
239
|
+
expect(result.cronJob).toBeDefined();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("chart has correct name", () => {
|
|
243
|
+
const result = HelmCronJob({ name: "backup" });
|
|
244
|
+
expect(result.chart.name).toBe("backup");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("default schedule is hourly", () => {
|
|
248
|
+
const result = HelmCronJob({ name: "job" });
|
|
249
|
+
const vals = result.values as any;
|
|
250
|
+
expect(vals.schedule).toBe("0 * * * *");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("custom schedule flows through", () => {
|
|
254
|
+
const result = HelmCronJob({ name: "nightly", schedule: "0 0 * * *" });
|
|
255
|
+
const vals = result.values as any;
|
|
256
|
+
expect(vals.schedule).toBe("0 0 * * *");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("uses Helm intrinsics", () => {
|
|
260
|
+
const result = HelmCronJob({ name: "job" });
|
|
261
|
+
expect(hasIntrinsic(result.cronJob)).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("omitted props produce no change", () => {
|
|
265
|
+
const result = HelmCronJob({ name: "job" });
|
|
266
|
+
const vals = result.values as any;
|
|
267
|
+
expect(vals.podSecurityContext).toBeUndefined();
|
|
268
|
+
expect(vals.concurrencyPolicy).toBeUndefined();
|
|
269
|
+
expect(vals.backoffLimit).toBeUndefined();
|
|
270
|
+
expect(result.serviceAccount).toBeUndefined();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("job control props flow through", () => {
|
|
274
|
+
const result = HelmCronJob({
|
|
275
|
+
name: "job",
|
|
276
|
+
concurrencyPolicy: "Forbid",
|
|
277
|
+
successfulJobsHistoryLimit: 3,
|
|
278
|
+
failedJobsHistoryLimit: 1,
|
|
279
|
+
backoffLimit: 2,
|
|
280
|
+
});
|
|
281
|
+
const vals = result.values as any;
|
|
282
|
+
expect(vals.concurrencyPolicy).toBe("Forbid");
|
|
283
|
+
expect(vals.successfulJobsHistoryLimit).toBe(3);
|
|
284
|
+
expect(vals.failedJobsHistoryLimit).toBe(1);
|
|
285
|
+
expect(vals.backoffLimit).toBe(2);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("serviceAccount can be enabled", () => {
|
|
289
|
+
const result = HelmCronJob({ name: "job", serviceAccount: true });
|
|
290
|
+
expect(result.serviceAccount).toBeDefined();
|
|
291
|
+
const vals = result.values as any;
|
|
292
|
+
expect(vals.serviceAccount).toBeDefined();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("security and scheduling props flow through", () => {
|
|
296
|
+
const result = HelmCronJob({
|
|
297
|
+
name: "job",
|
|
298
|
+
podSecurityContext: { runAsNonRoot: true },
|
|
299
|
+
securityContext: { readOnlyRootFilesystem: true },
|
|
300
|
+
nodeSelector: { "kubernetes.io/os": "linux" },
|
|
301
|
+
});
|
|
302
|
+
const vals = result.values as any;
|
|
303
|
+
expect(vals.podSecurityContext).toBeDefined();
|
|
304
|
+
expect(vals.securityContext).toBeDefined();
|
|
305
|
+
expect(vals.nodeSelector).toBeDefined();
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe("HelmMicroservice", () => {
|
|
310
|
+
test("returns all core resources", () => {
|
|
311
|
+
const result = HelmMicroservice({ name: "api" });
|
|
312
|
+
expect(result.chart).toBeDefined();
|
|
313
|
+
expect(result.values).toBeDefined();
|
|
314
|
+
expect(result.deployment).toBeDefined();
|
|
315
|
+
expect(result.service).toBeDefined();
|
|
316
|
+
expect(result.serviceAccount).toBeDefined();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("includes optional resources by default", () => {
|
|
320
|
+
const result = HelmMicroservice({ name: "api" });
|
|
321
|
+
expect(result.configMap).toBeDefined();
|
|
322
|
+
expect(result.ingress).toBeDefined();
|
|
323
|
+
expect(result.hpa).toBeDefined();
|
|
324
|
+
expect(result.pdb).toBeDefined();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("can exclude optional resources", () => {
|
|
328
|
+
const result = HelmMicroservice({
|
|
329
|
+
name: "api",
|
|
330
|
+
ingress: false,
|
|
331
|
+
autoscaling: false,
|
|
332
|
+
pdb: false,
|
|
333
|
+
configMap: false,
|
|
334
|
+
});
|
|
335
|
+
expect(result.configMap).toBeUndefined();
|
|
336
|
+
expect(result.ingress).toBeUndefined();
|
|
337
|
+
expect(result.hpa).toBeUndefined();
|
|
338
|
+
expect(result.pdb).toBeUndefined();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("default port is 8080", () => {
|
|
342
|
+
const result = HelmMicroservice({ name: "api" });
|
|
343
|
+
const vals = result.values as any;
|
|
344
|
+
expect(vals.service.port).toBe(8080);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("default replicas is 2", () => {
|
|
348
|
+
const result = HelmMicroservice({ name: "api" });
|
|
349
|
+
const vals = result.values as any;
|
|
350
|
+
expect(vals.replicaCount).toBe(2);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("values include health probes", () => {
|
|
354
|
+
const result = HelmMicroservice({ name: "api" });
|
|
355
|
+
const vals = result.values as any;
|
|
356
|
+
expect(vals.livenessProbe).toBeDefined();
|
|
357
|
+
expect(vals.readinessProbe).toBeDefined();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("values include resource limits and requests", () => {
|
|
361
|
+
const result = HelmMicroservice({ name: "api" });
|
|
362
|
+
const vals = result.values as any;
|
|
363
|
+
expect(vals.resources.limits).toBeDefined();
|
|
364
|
+
expect(vals.resources.requests).toBeDefined();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("uses Helm intrinsics", () => {
|
|
368
|
+
const result = HelmMicroservice({ name: "api" });
|
|
369
|
+
expect(hasIntrinsic(result.deployment)).toBe(true);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("omitted security/scheduling props produce no change", () => {
|
|
373
|
+
const result = HelmMicroservice({ name: "api" });
|
|
374
|
+
const vals = result.values as any;
|
|
375
|
+
expect(vals.podSecurityContext).toBeUndefined();
|
|
376
|
+
expect(vals.securityContext).toBeUndefined();
|
|
377
|
+
expect(vals.nodeSelector).toBeUndefined();
|
|
378
|
+
expect(vals.strategy).toBeUndefined();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("security and scheduling props flow through", () => {
|
|
382
|
+
const result = HelmMicroservice({
|
|
383
|
+
name: "api",
|
|
384
|
+
podSecurityContext: { runAsNonRoot: true },
|
|
385
|
+
securityContext: { readOnlyRootFilesystem: true },
|
|
386
|
+
nodeSelector: { "kubernetes.io/os": "linux" },
|
|
387
|
+
tolerations: [{ key: "special", operator: "Exists" }],
|
|
388
|
+
affinity: { nodeAffinity: {} },
|
|
389
|
+
podAnnotations: { "prometheus.io/scrape": "true" },
|
|
390
|
+
strategy: { type: "RollingUpdate" },
|
|
391
|
+
});
|
|
392
|
+
const vals = result.values as any;
|
|
393
|
+
expect(vals.podSecurityContext).toBeDefined();
|
|
394
|
+
expect(vals.securityContext).toBeDefined();
|
|
395
|
+
expect(vals.nodeSelector).toBeDefined();
|
|
396
|
+
expect(vals.tolerations).toHaveLength(1);
|
|
397
|
+
expect(vals.affinity).toBeDefined();
|
|
398
|
+
expect(vals.podAnnotations).toBeDefined();
|
|
399
|
+
expect(vals.strategy).toBeDefined();
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe("HelmLibrary", () => {
|
|
404
|
+
test("returns chart and helpers", () => {
|
|
405
|
+
const result = HelmLibrary({ name: "common" });
|
|
406
|
+
expect(result.chart).toBeDefined();
|
|
407
|
+
expect(result.helpers).toBeDefined();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("chart type is library", () => {
|
|
411
|
+
const result = HelmLibrary({ name: "common" });
|
|
412
|
+
expect(result.chart.type).toBe("library");
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("default helpers include standard names", () => {
|
|
416
|
+
const result = HelmLibrary({ name: "common" });
|
|
417
|
+
expect(result.helpers).toContain("name");
|
|
418
|
+
expect(result.helpers).toContain("fullname");
|
|
419
|
+
expect(result.helpers).toContain("labels");
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("custom helpers override defaults", () => {
|
|
423
|
+
const result = HelmLibrary({ name: "lib", helpers: ["custom-a", "custom-b"] });
|
|
424
|
+
expect(result.helpers).toEqual(["custom-a", "custom-b"]);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("dependencies are included when provided", () => {
|
|
428
|
+
const result = HelmLibrary({
|
|
429
|
+
name: "common",
|
|
430
|
+
dependencies: [{ name: "base", version: "1.x.x", repository: "https://charts.example.com" }],
|
|
431
|
+
});
|
|
432
|
+
expect(result.chart.dependencies).toHaveLength(1);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("no dependencies by default", () => {
|
|
436
|
+
const result = HelmLibrary({ name: "common" });
|
|
437
|
+
expect(result.chart.dependencies).toBeUndefined();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
describe("HelmCRDLifecycle", () => {
|
|
442
|
+
test("returns all expected resources", () => {
|
|
443
|
+
const result = HelmCRDLifecycle({
|
|
444
|
+
name: "my-operator",
|
|
445
|
+
crdContent: "apiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\n",
|
|
446
|
+
});
|
|
447
|
+
expect(result.chart).toBeDefined();
|
|
448
|
+
expect(result.values).toBeDefined();
|
|
449
|
+
expect(result.crdInstallJob).toBeDefined();
|
|
450
|
+
expect(result.crdConfigMap).toBeDefined();
|
|
451
|
+
expect(result.serviceAccount).toBeDefined();
|
|
452
|
+
expect(result.clusterRole).toBeDefined();
|
|
453
|
+
expect(result.clusterRoleBinding).toBeDefined();
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("Job has hook annotations", () => {
|
|
457
|
+
const result = HelmCRDLifecycle({
|
|
458
|
+
name: "my-operator",
|
|
459
|
+
crdContent: "apiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\n",
|
|
460
|
+
});
|
|
461
|
+
const jobMeta = (result.crdInstallJob as any).metadata;
|
|
462
|
+
expect(jobMeta.annotations["helm.sh/hook"]).toBe("pre-install,pre-upgrade");
|
|
463
|
+
expect(jobMeta.annotations["helm.sh/hook-weight"]).toBe("-5");
|
|
464
|
+
expect(jobMeta.annotations["helm.sh/hook-delete-policy"]).toBe("before-hook-creation");
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("ClusterRole has correct rules", () => {
|
|
468
|
+
const result = HelmCRDLifecycle({
|
|
469
|
+
name: "my-operator",
|
|
470
|
+
crdContent: "crd content",
|
|
471
|
+
});
|
|
472
|
+
const rules = (result.clusterRole as any).rules;
|
|
473
|
+
expect(rules).toHaveLength(1);
|
|
474
|
+
expect(rules[0].apiGroups).toContain("apiextensions.k8s.io");
|
|
475
|
+
expect(rules[0].resources).toContain("customresourcedefinitions");
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test("ConfigMap contains CRD content", () => {
|
|
479
|
+
const crdContent = "apiVersion: apiextensions.k8s.io/v1\nkind: CRD\n";
|
|
480
|
+
const result = HelmCRDLifecycle({
|
|
481
|
+
name: "my-operator",
|
|
482
|
+
crdContent,
|
|
483
|
+
});
|
|
484
|
+
expect((result.crdConfigMap as any).data["crds.yaml"]).toBe(crdContent);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("uses Helm intrinsics", () => {
|
|
488
|
+
const result = HelmCRDLifecycle({
|
|
489
|
+
name: "my-operator",
|
|
490
|
+
crdContent: "crd",
|
|
491
|
+
});
|
|
492
|
+
expect(hasIntrinsic(result.crdInstallJob)).toBe(true);
|
|
493
|
+
expect(hasIntrinsic(result.clusterRoleBinding)).toBe(true);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("custom kubectl image flows through to values", () => {
|
|
497
|
+
const result = HelmCRDLifecycle({
|
|
498
|
+
name: "my-operator",
|
|
499
|
+
crdContent: "crd",
|
|
500
|
+
kubectlImage: "custom/kubectl",
|
|
501
|
+
kubectlTag: "1.28",
|
|
502
|
+
});
|
|
503
|
+
const vals = result.values as any;
|
|
504
|
+
expect(vals.crdLifecycle.kubectl.image).toBe("custom/kubectl");
|
|
505
|
+
expect(vals.crdLifecycle.kubectl.tag).toBe("1.28");
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
describe("HelmDaemonSet", () => {
|
|
510
|
+
test("returns chart, values, and daemonSet", () => {
|
|
511
|
+
const result = HelmDaemonSet({ name: "log-agent" });
|
|
512
|
+
expect(result.chart).toBeDefined();
|
|
513
|
+
expect(result.values).toBeDefined();
|
|
514
|
+
expect(result.daemonSet).toBeDefined();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test("includes serviceAccount by default", () => {
|
|
518
|
+
const result = HelmDaemonSet({ name: "log-agent" });
|
|
519
|
+
expect(result.serviceAccount).toBeDefined();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("can exclude serviceAccount", () => {
|
|
523
|
+
const result = HelmDaemonSet({ name: "log-agent", serviceAccount: false });
|
|
524
|
+
expect(result.serviceAccount).toBeUndefined();
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("DaemonSet has RollingUpdate strategy in values", () => {
|
|
528
|
+
const result = HelmDaemonSet({ name: "log-agent" });
|
|
529
|
+
const vals = result.values as any;
|
|
530
|
+
expect(vals.updateStrategy.type).toBe("RollingUpdate");
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("default image is fluent-bit", () => {
|
|
534
|
+
const result = HelmDaemonSet({ name: "log-agent" });
|
|
535
|
+
const vals = result.values as any;
|
|
536
|
+
expect(vals.image.repository).toBe("fluent/fluent-bit");
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test("custom props flow through", () => {
|
|
540
|
+
const result = HelmDaemonSet({
|
|
541
|
+
name: "metrics",
|
|
542
|
+
imageRepository: "prom/node-exporter",
|
|
543
|
+
imageTag: "v1.6.0",
|
|
544
|
+
port: 9100,
|
|
545
|
+
});
|
|
546
|
+
const vals = result.values as any;
|
|
547
|
+
expect(vals.image.repository).toBe("prom/node-exporter");
|
|
548
|
+
expect(vals.image.tag).toBe("v1.6.0");
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test("uses Helm intrinsics", () => {
|
|
552
|
+
const result = HelmDaemonSet({ name: "agent" });
|
|
553
|
+
expect(hasIntrinsic(result.daemonSet)).toBe(true);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("nodeSelector uses With() intrinsic", () => {
|
|
557
|
+
const result = HelmDaemonSet({ name: "agent" });
|
|
558
|
+
const podSpec = (result.daemonSet as any).spec.template.spec;
|
|
559
|
+
expect(hasIntrinsic(podSpec.nodeSelector)).toBe(true);
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
describe("HelmWorker", () => {
|
|
564
|
+
test("returns chart, values, deployment, and serviceAccount", () => {
|
|
565
|
+
const result = HelmWorker({ name: "job-processor" });
|
|
566
|
+
expect(result.chart).toBeDefined();
|
|
567
|
+
expect(result.values).toBeDefined();
|
|
568
|
+
expect(result.deployment).toBeDefined();
|
|
569
|
+
expect(result.serviceAccount).toBeDefined();
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("no service by default (workers don't serve HTTP)", () => {
|
|
573
|
+
const result = HelmWorker({ name: "job-processor" });
|
|
574
|
+
expect((result as any).service).toBeUndefined();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test("default replicas is 2", () => {
|
|
578
|
+
const result = HelmWorker({ name: "job-processor" });
|
|
579
|
+
const vals = result.values as any;
|
|
580
|
+
expect(vals.replicaCount).toBe(2);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test("includes PDB by default", () => {
|
|
584
|
+
const result = HelmWorker({ name: "job-processor" });
|
|
585
|
+
expect(result.pdb).toBeDefined();
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test("no HPA by default", () => {
|
|
589
|
+
const result = HelmWorker({ name: "job-processor" });
|
|
590
|
+
expect(result.hpa).toBeUndefined();
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test("can enable autoscaling", () => {
|
|
594
|
+
const result = HelmWorker({ name: "job-processor", autoscaling: true });
|
|
595
|
+
expect(result.hpa).toBeDefined();
|
|
596
|
+
const vals = result.values as any;
|
|
597
|
+
expect(vals.autoscaling).toBeDefined();
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test("values include queue config", () => {
|
|
601
|
+
const result = HelmWorker({ name: "job-processor" });
|
|
602
|
+
const vals = result.values as any;
|
|
603
|
+
expect(vals.queue).toBeDefined();
|
|
604
|
+
expect(vals.queue.concurrency).toBe(5);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test("uses exec-based probes", () => {
|
|
608
|
+
const result = HelmWorker({ name: "job-processor" });
|
|
609
|
+
const vals = result.values as any;
|
|
610
|
+
expect(vals.livenessProbe.exec).toBeDefined();
|
|
611
|
+
expect(vals.readinessProbe.exec).toBeDefined();
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test("uses Helm intrinsics", () => {
|
|
615
|
+
const result = HelmWorker({ name: "job-processor" });
|
|
616
|
+
expect(hasIntrinsic(result.deployment)).toBe(true);
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
describe("HelmExternalSecret", () => {
|
|
621
|
+
test("returns chart, values, and externalSecret", () => {
|
|
622
|
+
const result = HelmExternalSecret({
|
|
623
|
+
name: "app-secrets",
|
|
624
|
+
secretStoreName: "vault",
|
|
625
|
+
data: { API_KEY: "secret/data/api-key" },
|
|
626
|
+
});
|
|
627
|
+
expect(result.chart).toBeDefined();
|
|
628
|
+
expect(result.values).toBeDefined();
|
|
629
|
+
expect(result.externalSecret).toBeDefined();
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
test("externalSecret has correct apiVersion and kind", () => {
|
|
633
|
+
const result = HelmExternalSecret({
|
|
634
|
+
name: "app-secrets",
|
|
635
|
+
secretStoreName: "vault",
|
|
636
|
+
data: { API_KEY: "secret/data/api-key" },
|
|
637
|
+
});
|
|
638
|
+
expect((result.externalSecret as any).apiVersion).toBe("external-secrets.io/v1beta1");
|
|
639
|
+
expect((result.externalSecret as any).kind).toBe("ExternalSecret");
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
test("data maps to secretKey/remoteRef format", () => {
|
|
643
|
+
const result = HelmExternalSecret({
|
|
644
|
+
name: "app-secrets",
|
|
645
|
+
secretStoreName: "vault",
|
|
646
|
+
data: {
|
|
647
|
+
DB_PASSWORD: "secret/data/db-password",
|
|
648
|
+
API_KEY: "secret/data/api-key",
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
const spec = (result.externalSecret as any).spec;
|
|
652
|
+
expect(spec.data).toHaveLength(2);
|
|
653
|
+
expect(spec.data[0].secretKey).toBe("DB_PASSWORD");
|
|
654
|
+
expect(spec.data[0].remoteRef.key).toBe("secret/data/db-password");
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test("default secretStoreKind is ClusterSecretStore", () => {
|
|
658
|
+
const result = HelmExternalSecret({
|
|
659
|
+
name: "app-secrets",
|
|
660
|
+
secretStoreName: "vault",
|
|
661
|
+
data: { KEY: "path" },
|
|
662
|
+
});
|
|
663
|
+
const vals = result.values as any;
|
|
664
|
+
expect(vals.externalSecret.secretStore.kind).toBe("ClusterSecretStore");
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
test("custom refreshInterval flows through", () => {
|
|
668
|
+
const result = HelmExternalSecret({
|
|
669
|
+
name: "app-secrets",
|
|
670
|
+
secretStoreName: "vault",
|
|
671
|
+
data: { KEY: "path" },
|
|
672
|
+
refreshInterval: "30m",
|
|
673
|
+
});
|
|
674
|
+
const vals = result.values as any;
|
|
675
|
+
expect(vals.externalSecret.refreshInterval).toBe("30m");
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
test("uses Helm intrinsics", () => {
|
|
679
|
+
const result = HelmExternalSecret({
|
|
680
|
+
name: "app-secrets",
|
|
681
|
+
secretStoreName: "vault",
|
|
682
|
+
data: { KEY: "path" },
|
|
683
|
+
});
|
|
684
|
+
expect(hasIntrinsic(result.externalSecret)).toBe(true);
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// ── New composites ────────────────────────────────────────
|
|
689
|
+
|
|
690
|
+
describe("HelmBatchJob", () => {
|
|
691
|
+
test("returns chart, values, and job", () => {
|
|
692
|
+
const result = HelmBatchJob({ name: "migrate" });
|
|
693
|
+
expect(result.chart).toBeDefined();
|
|
694
|
+
expect(result.values).toBeDefined();
|
|
695
|
+
expect(result.job).toBeDefined();
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
test("chart has correct metadata", () => {
|
|
699
|
+
const result = HelmBatchJob({ name: "migrate" });
|
|
700
|
+
expect(result.chart.name).toBe("migrate");
|
|
701
|
+
expect(result.chart.apiVersion).toBe("v2");
|
|
702
|
+
expect(result.chart.type).toBe("application");
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test("includes serviceAccount by default", () => {
|
|
706
|
+
const result = HelmBatchJob({ name: "migrate" });
|
|
707
|
+
expect(result.serviceAccount).toBeDefined();
|
|
708
|
+
const vals = result.values as any;
|
|
709
|
+
expect(vals.serviceAccount).toBeDefined();
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
test("can exclude serviceAccount", () => {
|
|
713
|
+
const result = HelmBatchJob({ name: "migrate", serviceAccount: false });
|
|
714
|
+
expect(result.serviceAccount).toBeUndefined();
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test("no RBAC by default", () => {
|
|
718
|
+
const result = HelmBatchJob({ name: "migrate" });
|
|
719
|
+
expect(result.role).toBeUndefined();
|
|
720
|
+
expect(result.roleBinding).toBeUndefined();
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
test("RBAC creates Role and RoleBinding when enabled", () => {
|
|
724
|
+
const result = HelmBatchJob({ name: "migrate", rbac: true });
|
|
725
|
+
expect(result.role).toBeDefined();
|
|
726
|
+
expect(result.roleBinding).toBeDefined();
|
|
727
|
+
expect((result.role as any).kind).toBe("Role");
|
|
728
|
+
expect((result.roleBinding as any).kind).toBe("RoleBinding");
|
|
729
|
+
const vals = result.values as any;
|
|
730
|
+
expect(vals.rbac).toBeDefined();
|
|
731
|
+
expect(vals.rbac.rules).toEqual([]);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
test("default job settings", () => {
|
|
735
|
+
const result = HelmBatchJob({ name: "migrate" });
|
|
736
|
+
const vals = result.values as any;
|
|
737
|
+
expect(vals.job.backoffLimit).toBe(6);
|
|
738
|
+
expect(vals.job.completions).toBe(1);
|
|
739
|
+
expect(vals.job.parallelism).toBe(1);
|
|
740
|
+
expect(vals.restartPolicy).toBe("OnFailure");
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
test("custom job settings flow through", () => {
|
|
744
|
+
const result = HelmBatchJob({
|
|
745
|
+
name: "migrate",
|
|
746
|
+
backoffLimit: 3,
|
|
747
|
+
completions: 5,
|
|
748
|
+
parallelism: 2,
|
|
749
|
+
restartPolicy: "Never",
|
|
750
|
+
ttlSecondsAfterFinished: 300,
|
|
751
|
+
});
|
|
752
|
+
const vals = result.values as any;
|
|
753
|
+
expect(vals.job.backoffLimit).toBe(3);
|
|
754
|
+
expect(vals.job.completions).toBe(5);
|
|
755
|
+
expect(vals.job.parallelism).toBe(2);
|
|
756
|
+
expect(vals.restartPolicy).toBe("Never");
|
|
757
|
+
expect(vals.job.ttlSecondsAfterFinished).toBe(300);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
test("default image is busybox", () => {
|
|
761
|
+
const result = HelmBatchJob({ name: "migrate" });
|
|
762
|
+
const vals = result.values as any;
|
|
763
|
+
expect(vals.image.repository).toBe("busybox");
|
|
764
|
+
expect(vals.image.tag).toBe("latest");
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
test("uses Helm intrinsics", () => {
|
|
768
|
+
const result = HelmBatchJob({ name: "migrate" });
|
|
769
|
+
expect(hasIntrinsic(result.job)).toBe(true);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
test("security props flow through", () => {
|
|
773
|
+
const result = HelmBatchJob({
|
|
774
|
+
name: "migrate",
|
|
775
|
+
podSecurityContext: { runAsNonRoot: true },
|
|
776
|
+
securityContext: { readOnlyRootFilesystem: true },
|
|
777
|
+
});
|
|
778
|
+
const vals = result.values as any;
|
|
779
|
+
expect(vals.podSecurityContext).toBeDefined();
|
|
780
|
+
expect(vals.securityContext).toBeDefined();
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
test("scheduling props use With() intrinsic", () => {
|
|
784
|
+
const result = HelmBatchJob({
|
|
785
|
+
name: "migrate",
|
|
786
|
+
nodeSelector: { "kubernetes.io/os": "linux" },
|
|
787
|
+
tolerations: [{ key: "special", operator: "Exists" }],
|
|
788
|
+
});
|
|
789
|
+
const vals = result.values as any;
|
|
790
|
+
expect(vals.nodeSelector).toBeDefined();
|
|
791
|
+
expect(vals.tolerations).toHaveLength(1);
|
|
792
|
+
const podSpec = (result.job as any).spec.template.spec;
|
|
793
|
+
expect(hasIntrinsic(podSpec.nodeSelector)).toBe(true);
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
describe("HelmMonitoredService", () => {
|
|
798
|
+
test("returns chart, values, deployment, service, and serviceMonitor", () => {
|
|
799
|
+
const result = HelmMonitoredService({ name: "api" });
|
|
800
|
+
expect(result.chart).toBeDefined();
|
|
801
|
+
expect(result.values).toBeDefined();
|
|
802
|
+
expect(result.deployment).toBeDefined();
|
|
803
|
+
expect(result.service).toBeDefined();
|
|
804
|
+
expect(result.serviceMonitor).toBeDefined();
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
test("chart has correct metadata", () => {
|
|
808
|
+
const result = HelmMonitoredService({ name: "api" });
|
|
809
|
+
expect(result.chart.name).toBe("api");
|
|
810
|
+
expect(result.chart.type).toBe("application");
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
test("includes serviceAccount by default", () => {
|
|
814
|
+
const result = HelmMonitoredService({ name: "api" });
|
|
815
|
+
expect(result.serviceAccount).toBeDefined();
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
test("can exclude serviceAccount", () => {
|
|
819
|
+
const result = HelmMonitoredService({ name: "api", serviceAccount: false });
|
|
820
|
+
expect(result.serviceAccount).toBeUndefined();
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
test("no PrometheusRule by default", () => {
|
|
824
|
+
const result = HelmMonitoredService({ name: "api" });
|
|
825
|
+
expect(result.prometheusRule).toBeUndefined();
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
test("PrometheusRule created when alertRules enabled", () => {
|
|
829
|
+
const result = HelmMonitoredService({ name: "api", alertRules: true });
|
|
830
|
+
expect(result.prometheusRule).toBeDefined();
|
|
831
|
+
const vals = result.values as any;
|
|
832
|
+
expect(vals.alerting).toBeDefined();
|
|
833
|
+
expect(vals.alerting.rules).toEqual([]);
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
test("default monitoring config", () => {
|
|
837
|
+
const result = HelmMonitoredService({ name: "api" });
|
|
838
|
+
const vals = result.values as any;
|
|
839
|
+
expect(vals.monitoring.enabled).toBe(true);
|
|
840
|
+
expect(vals.monitoring.metricsPort).toBe(9090);
|
|
841
|
+
expect(vals.monitoring.metricsPath).toBe("/metrics");
|
|
842
|
+
expect(vals.monitoring.scrapeInterval).toBe("30s");
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
test("custom monitoring config", () => {
|
|
846
|
+
const result = HelmMonitoredService({
|
|
847
|
+
name: "api",
|
|
848
|
+
metricsPort: 8081,
|
|
849
|
+
metricsPath: "/actuator/prometheus",
|
|
850
|
+
scrapeInterval: "15s",
|
|
851
|
+
});
|
|
852
|
+
const vals = result.values as any;
|
|
853
|
+
expect(vals.monitoring.metricsPort).toBe(8081);
|
|
854
|
+
expect(vals.monitoring.metricsPath).toBe("/actuator/prometheus");
|
|
855
|
+
expect(vals.monitoring.scrapeInterval).toBe("15s");
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
test("service exposes both http and metrics ports", () => {
|
|
859
|
+
const result = HelmMonitoredService({ name: "api" });
|
|
860
|
+
const ports = (result.service as any).spec.ports;
|
|
861
|
+
expect(ports).toHaveLength(2);
|
|
862
|
+
expect(ports[0].name).toBe("http");
|
|
863
|
+
expect(ports[1].name).toBe("metrics");
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
test("container has both http and metrics ports", () => {
|
|
867
|
+
const result = HelmMonitoredService({ name: "api" });
|
|
868
|
+
const container = (result.deployment as any).spec.template.spec.containers[0];
|
|
869
|
+
expect(container.ports).toHaveLength(2);
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
test("serviceMonitor uses Helm intrinsics", () => {
|
|
873
|
+
const result = HelmMonitoredService({ name: "api" });
|
|
874
|
+
expect(hasIntrinsic(result.serviceMonitor)).toBe(true);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
test("uses Helm intrinsics in deployment", () => {
|
|
878
|
+
const result = HelmMonitoredService({ name: "api" });
|
|
879
|
+
expect(hasIntrinsic(result.deployment)).toBe(true);
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
test("security and scheduling props flow through", () => {
|
|
883
|
+
const result = HelmMonitoredService({
|
|
884
|
+
name: "api",
|
|
885
|
+
podSecurityContext: { runAsNonRoot: true },
|
|
886
|
+
nodeSelector: { "kubernetes.io/os": "linux" },
|
|
887
|
+
affinity: { nodeAffinity: {} },
|
|
888
|
+
});
|
|
889
|
+
const vals = result.values as any;
|
|
890
|
+
expect(vals.podSecurityContext).toBeDefined();
|
|
891
|
+
expect(vals.nodeSelector).toBeDefined();
|
|
892
|
+
expect(vals.affinity).toBeDefined();
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
test("default replicas is 2", () => {
|
|
896
|
+
const result = HelmMonitoredService({ name: "api" });
|
|
897
|
+
const vals = result.values as any;
|
|
898
|
+
expect(vals.replicaCount).toBe(2);
|
|
899
|
+
});
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
describe("HelmSecureIngress", () => {
|
|
903
|
+
test("returns chart, values, ingress, and certificate", () => {
|
|
904
|
+
const result = HelmSecureIngress({ name: "web" });
|
|
905
|
+
expect(result.chart).toBeDefined();
|
|
906
|
+
expect(result.values).toBeDefined();
|
|
907
|
+
expect(result.ingress).toBeDefined();
|
|
908
|
+
expect(result.certificate).toBeDefined();
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
test("chart has correct metadata", () => {
|
|
912
|
+
const result = HelmSecureIngress({ name: "web" });
|
|
913
|
+
expect(result.chart.name).toBe("web");
|
|
914
|
+
expect(result.chart.type).toBe("application");
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
test("default values include ingress and certManager config", () => {
|
|
918
|
+
const result = HelmSecureIngress({ name: "web" });
|
|
919
|
+
const vals = result.values as any;
|
|
920
|
+
expect(vals.ingress.enabled).toBe(true);
|
|
921
|
+
expect(vals.ingress.tls.enabled).toBe(true);
|
|
922
|
+
expect(vals.certManager.enabled).toBe(true);
|
|
923
|
+
expect(vals.certManager.clusterIssuer).toBe("letsencrypt-prod");
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
test("custom clusterIssuer flows through", () => {
|
|
927
|
+
const result = HelmSecureIngress({ name: "web", clusterIssuer: "letsencrypt-staging" });
|
|
928
|
+
const vals = result.values as any;
|
|
929
|
+
expect(vals.certManager.clusterIssuer).toBe("letsencrypt-staging");
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
test("custom ingressClassName flows through", () => {
|
|
933
|
+
const result = HelmSecureIngress({ name: "web", ingressClassName: "nginx" });
|
|
934
|
+
const vals = result.values as any;
|
|
935
|
+
expect(vals.ingress.className).toBe("nginx");
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
test("ingress uses Helm intrinsics", () => {
|
|
939
|
+
const result = HelmSecureIngress({ name: "web" });
|
|
940
|
+
expect(hasIntrinsic(result.ingress)).toBe(true);
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
test("certificate uses Helm intrinsics", () => {
|
|
944
|
+
const result = HelmSecureIngress({ name: "web" });
|
|
945
|
+
expect(hasIntrinsic(result.certificate!)).toBe(true);
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
test("ingress uses Range for hosts", () => {
|
|
949
|
+
const result = HelmSecureIngress({ name: "web" });
|
|
950
|
+
expect(hasIntrinsic(result.ingress)).toBe(true);
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
test("default host includes chart name", () => {
|
|
954
|
+
const result = HelmSecureIngress({ name: "web" });
|
|
955
|
+
const vals = result.values as any;
|
|
956
|
+
expect(vals.ingress.hosts[0].host).toBe("web.example.com");
|
|
957
|
+
});
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
describe("HelmNamespaceEnv", () => {
|
|
961
|
+
test("returns chart, values, and namespace", () => {
|
|
962
|
+
const result = HelmNamespaceEnv({ name: "dev" });
|
|
963
|
+
expect(result.chart).toBeDefined();
|
|
964
|
+
expect(result.values).toBeDefined();
|
|
965
|
+
expect(result.namespace).toBeDefined();
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
test("chart has correct metadata", () => {
|
|
969
|
+
const result = HelmNamespaceEnv({ name: "dev" });
|
|
970
|
+
expect(result.chart.name).toBe("dev");
|
|
971
|
+
expect(result.chart.type).toBe("application");
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
test("includes all governance resources by default", () => {
|
|
975
|
+
const result = HelmNamespaceEnv({ name: "dev" });
|
|
976
|
+
expect(result.resourceQuota).toBeDefined();
|
|
977
|
+
expect(result.limitRange).toBeDefined();
|
|
978
|
+
expect(result.networkPolicy).toBeDefined();
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
test("can exclude resourceQuota", () => {
|
|
982
|
+
const result = HelmNamespaceEnv({ name: "dev", resourceQuota: false });
|
|
983
|
+
expect(result.resourceQuota).toBeUndefined();
|
|
984
|
+
const vals = result.values as any;
|
|
985
|
+
expect(vals.resourceQuota).toBeUndefined();
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
test("can exclude limitRange", () => {
|
|
989
|
+
const result = HelmNamespaceEnv({ name: "dev", limitRange: false });
|
|
990
|
+
expect(result.limitRange).toBeUndefined();
|
|
991
|
+
const vals = result.values as any;
|
|
992
|
+
expect(vals.limitRange).toBeUndefined();
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
test("can exclude networkPolicy", () => {
|
|
996
|
+
const result = HelmNamespaceEnv({ name: "dev", networkPolicy: false });
|
|
997
|
+
expect(result.networkPolicy).toBeUndefined();
|
|
998
|
+
const vals = result.values as any;
|
|
999
|
+
expect(vals.networkPolicy).toBeUndefined();
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
test("can exclude all optional resources", () => {
|
|
1003
|
+
const result = HelmNamespaceEnv({
|
|
1004
|
+
name: "dev",
|
|
1005
|
+
resourceQuota: false,
|
|
1006
|
+
limitRange: false,
|
|
1007
|
+
networkPolicy: false,
|
|
1008
|
+
});
|
|
1009
|
+
expect(result.resourceQuota).toBeUndefined();
|
|
1010
|
+
expect(result.limitRange).toBeUndefined();
|
|
1011
|
+
expect(result.networkPolicy).toBeUndefined();
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
test("default resourceQuota values", () => {
|
|
1015
|
+
const result = HelmNamespaceEnv({ name: "dev" });
|
|
1016
|
+
const vals = result.values as any;
|
|
1017
|
+
expect(vals.resourceQuota.enabled).toBe(true);
|
|
1018
|
+
expect(vals.resourceQuota.hard.cpu).toBe("10");
|
|
1019
|
+
expect(vals.resourceQuota.hard.memory).toBe("20Gi");
|
|
1020
|
+
expect(vals.resourceQuota.hard.pods).toBe("50");
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
test("default limitRange values", () => {
|
|
1024
|
+
const result = HelmNamespaceEnv({ name: "dev" });
|
|
1025
|
+
const vals = result.values as any;
|
|
1026
|
+
expect(vals.limitRange.enabled).toBe(true);
|
|
1027
|
+
expect(vals.limitRange.default.cpu).toBe("500m");
|
|
1028
|
+
expect(vals.limitRange.defaultRequest.cpu).toBe("100m");
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
test("default networkPolicy values", () => {
|
|
1032
|
+
const result = HelmNamespaceEnv({ name: "dev" });
|
|
1033
|
+
const vals = result.values as any;
|
|
1034
|
+
expect(vals.networkPolicy.enabled).toBe(true);
|
|
1035
|
+
expect(vals.networkPolicy.denyIngress).toBe(true);
|
|
1036
|
+
expect(vals.networkPolicy.denyEgress).toBe(false);
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
test("namespace uses Helm intrinsics", () => {
|
|
1040
|
+
const result = HelmNamespaceEnv({ name: "dev" });
|
|
1041
|
+
expect(hasIntrinsic(result.namespace)).toBe(true);
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
test("governance resources use If() conditional", () => {
|
|
1045
|
+
const result = HelmNamespaceEnv({ name: "dev" });
|
|
1046
|
+
expect(hasIntrinsic(result.resourceQuota!)).toBe(true);
|
|
1047
|
+
expect(hasIntrinsic(result.limitRange!)).toBe(true);
|
|
1048
|
+
expect(hasIntrinsic(result.networkPolicy!)).toBe(true);
|
|
1049
|
+
});
|
|
1050
|
+
});
|