@intentius/chant-lexicon-k8s 0.0.14 → 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 (63) hide show
  1. package/dist/integrity.json +8 -4
  2. package/dist/manifest.json +1 -1
  3. package/dist/rules/latest-image-tag.ts +121 -0
  4. package/dist/rules/missing-resource-limits.ts +111 -0
  5. package/dist/rules/wk8204.ts +33 -1
  6. package/dist/rules/wk8304.ts +70 -0
  7. package/dist/rules/wk8305.ts +115 -0
  8. package/dist/rules/wk8306.ts +50 -0
  9. package/package.json +27 -24
  10. package/src/codegen/docs.ts +1 -1
  11. package/src/composites/adot-collector.ts +8 -2
  12. package/src/composites/agic-ingress.ts +148 -0
  13. package/src/composites/aks-external-dns-agent.ts +199 -0
  14. package/src/composites/alb-ingress.ts +2 -1
  15. package/src/composites/autoscaled-service.ts +25 -7
  16. package/src/composites/azure-disk-storage-class.ts +82 -0
  17. package/src/composites/azure-file-storage-class.ts +77 -0
  18. package/src/composites/azure-monitor-collector.ts +232 -0
  19. package/src/composites/batch-job.ts +36 -3
  20. package/src/composites/composites.test.ts +1060 -0
  21. package/src/composites/config-connector-context.ts +62 -0
  22. package/src/composites/configured-app.ts +6 -0
  23. package/src/composites/cron-workload.ts +6 -0
  24. package/src/composites/ebs-storage-class.ts +4 -4
  25. package/src/composites/external-dns-agent.ts +6 -0
  26. package/src/composites/filestore-storage-class.ts +79 -0
  27. package/src/composites/fluent-bit-agent.ts +5 -0
  28. package/src/composites/gce-ingress.ts +143 -0
  29. package/src/composites/gce-pd-storage-class.ts +85 -0
  30. package/src/composites/gke-external-dns-agent.ts +175 -0
  31. package/src/composites/gke-fluent-bit-agent.ts +219 -0
  32. package/src/composites/gke-gateway.ts +143 -0
  33. package/src/composites/gke-otel-collector.ts +229 -0
  34. package/src/composites/index.ts +31 -0
  35. package/src/composites/metrics-server.ts +1 -1
  36. package/src/composites/monitored-service.ts +6 -0
  37. package/src/composites/network-isolated-app.ts +6 -0
  38. package/src/composites/node-agent.ts +6 -0
  39. package/src/composites/security-context.ts +10 -0
  40. package/src/composites/sidecar-app.ts +6 -0
  41. package/src/composites/stateful-app.ts +4 -7
  42. package/src/composites/web-app.ts +4 -7
  43. package/src/composites/worker-pool.ts +4 -7
  44. package/src/composites/workload-identity-sa.ts +118 -0
  45. package/src/composites/workload-identity-service-account.ts +116 -0
  46. package/src/index.ts +20 -1
  47. package/src/lint/post-synth/post-synth.test.ts +362 -1
  48. package/src/lint/post-synth/wk8204.ts +33 -1
  49. package/src/lint/post-synth/wk8304.ts +70 -0
  50. package/src/lint/post-synth/wk8305.ts +115 -0
  51. package/src/lint/post-synth/wk8306.ts +50 -0
  52. package/src/lint/rules/latest-image-tag.ts +121 -0
  53. package/src/lint/rules/missing-resource-limits.ts +111 -0
  54. package/src/lint/rules/rules.test.ts +192 -0
  55. package/src/plugin.test.ts +2 -2
  56. package/src/plugin.ts +129 -209
  57. package/src/serializer.test.ts +120 -0
  58. package/src/serializer.ts +16 -4
  59. package/src/skills/chant-k8s-aks.md +146 -0
  60. package/src/skills/chant-k8s-gke.md +191 -0
  61. package/src/skills/kubernetes-patterns.md +183 -0
  62. package/src/skills/kubernetes-security.md +237 -0
  63. /package/{dist → src}/skills/chant-k8s-eks.md +0 -0
@@ -1,4 +1,5 @@
1
1
  import { describe, test, expect, jest } from "bun:test";
2
+ import { emitYAML } from "@intentius/chant/yaml";
2
3
  import { WebApp } from "./web-app";
3
4
  import { StatefulApp } from "./stateful-app";
4
5
  import { CronWorkload } from "./cron-workload";
@@ -14,12 +15,18 @@ import { MonitoredService } from "./monitored-service";
14
15
  import { NetworkIsolatedApp } from "./network-isolated-app";
15
16
  import { IrsaServiceAccount } from "./irsa-service-account";
16
17
  import { AlbIngress } from "./alb-ingress";
18
+ import { GceIngress } from "./gce-ingress";
17
19
  import { EbsStorageClass } from "./ebs-storage-class";
18
20
  import { EfsStorageClass } from "./efs-storage-class";
19
21
  import { FluentBitAgent } from "./fluent-bit-agent";
20
22
  import { ExternalDnsAgent } from "./external-dns-agent";
21
23
  import { AdotCollector } from "./adot-collector";
22
24
  import { MetricsServer } from "./metrics-server";
25
+ import { WorkloadIdentityServiceAccount } from "./workload-identity-service-account";
26
+ import { GcePdStorageClass } from "./gce-pd-storage-class";
27
+ import { FilestoreStorageClass } from "./filestore-storage-class";
28
+ import { GkeGateway } from "./gke-gateway";
29
+ import { ConfigConnectorContext } from "./config-connector-context";
23
30
 
24
31
  // ── WebApp ──────────────────────────────────────────────────────────
25
32
 
@@ -537,6 +544,63 @@ describe("AutoscaledService", () => {
537
544
  expect(podLabels.team).toBe("platform");
538
545
  expect(podLabels["app.kubernetes.io/name"]).toBe("api");
539
546
  });
547
+
548
+ test("serviceAccountName wired into pod spec", () => {
549
+ const result = AutoscaledService({ ...minProps, serviceAccountName: "my-sa" });
550
+ const spec = result.deployment.spec as any;
551
+ expect(spec.template.spec.serviceAccountName).toBe("my-sa");
552
+ });
553
+
554
+ test("no serviceAccountName by default", () => {
555
+ const result = AutoscaledService(minProps);
556
+ const spec = result.deployment.spec as any;
557
+ expect(spec.template.spec.serviceAccountName).toBeUndefined();
558
+ });
559
+
560
+ test("volumes and volumeMounts wired into pod spec", () => {
561
+ const result = AutoscaledService({
562
+ ...minProps,
563
+ volumes: [{ name: "data", emptyDir: {} }],
564
+ volumeMounts: [{ name: "data", mountPath: "/data" }],
565
+ });
566
+ const spec = result.deployment.spec as any;
567
+ expect(spec.template.spec.volumes).toEqual([{ name: "data", emptyDir: {} }]);
568
+ expect(spec.template.spec.containers[0].volumeMounts).toEqual([{ name: "data", mountPath: "/data" }]);
569
+ });
570
+
571
+ test("tmpDirs generates emptyDir volumes and mounts", () => {
572
+ const result = AutoscaledService({
573
+ ...minProps,
574
+ tmpDirs: ["/tmp", "/var/cache/nginx"],
575
+ });
576
+ const spec = result.deployment.spec as any;
577
+ expect(spec.template.spec.volumes).toEqual([
578
+ { name: "tmp-0", emptyDir: {} },
579
+ { name: "tmp-1", emptyDir: {} },
580
+ ]);
581
+ expect(spec.template.spec.containers[0].volumeMounts).toEqual([
582
+ { name: "tmp-0", mountPath: "/tmp" },
583
+ { name: "tmp-1", mountPath: "/var/cache/nginx" },
584
+ ]);
585
+ });
586
+
587
+ test("tmpDirs merges with explicit volumes/volumeMounts", () => {
588
+ const result = AutoscaledService({
589
+ ...minProps,
590
+ volumes: [{ name: "config", configMap: { name: "app-config" } }],
591
+ volumeMounts: [{ name: "config", mountPath: "/etc/config" }],
592
+ tmpDirs: ["/tmp"],
593
+ });
594
+ const spec = result.deployment.spec as any;
595
+ expect(spec.template.spec.volumes).toEqual([
596
+ { name: "config", configMap: { name: "app-config" } },
597
+ { name: "tmp-0", emptyDir: {} },
598
+ ]);
599
+ expect(spec.template.spec.containers[0].volumeMounts).toEqual([
600
+ { name: "config", mountPath: "/etc/config" },
601
+ { name: "tmp-0", mountPath: "/tmp" },
602
+ ]);
603
+ });
540
604
  });
541
605
 
542
606
  // ── WorkerPool ─────────────────────────────────────────────────────
@@ -1160,6 +1224,25 @@ describe("WebApp hardening", () => {
1160
1224
  expect(container.securityContext.runAsNonRoot).toBe(true);
1161
1225
  });
1162
1226
 
1227
+ test("securityContext supports PSS restricted fields", () => {
1228
+ const result = WebApp({
1229
+ name: "app",
1230
+ image: "app:1.0",
1231
+ securityContext: {
1232
+ runAsNonRoot: true,
1233
+ readOnlyRootFilesystem: true,
1234
+ allowPrivilegeEscalation: false,
1235
+ capabilities: { drop: ["ALL"] },
1236
+ seccompProfile: { type: "RuntimeDefault" },
1237
+ },
1238
+ });
1239
+ const spec = result.deployment.spec as any;
1240
+ const sc = spec.template.spec.containers[0].securityContext;
1241
+ expect(sc.allowPrivilegeEscalation).toBe(false);
1242
+ expect(sc.capabilities).toEqual({ drop: ["ALL"] });
1243
+ expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
1244
+ });
1245
+
1163
1246
  test("terminationGracePeriodSeconds set", () => {
1164
1247
  const result = WebApp({ name: "app", image: "app:1.0", terminationGracePeriodSeconds: 60 });
1165
1248
  const spec = result.deployment.spec as any;
@@ -1216,6 +1299,24 @@ describe("StatefulApp hardening", () => {
1216
1299
  const spec = result.statefulSet.spec as any;
1217
1300
  expect(spec.template.spec.containers[0].securityContext.runAsNonRoot).toBe(true);
1218
1301
  });
1302
+
1303
+ test("securityContext supports PSS restricted fields", () => {
1304
+ const result = StatefulApp({
1305
+ name: "db",
1306
+ image: "postgres:16",
1307
+ securityContext: {
1308
+ runAsNonRoot: true,
1309
+ allowPrivilegeEscalation: false,
1310
+ capabilities: { drop: ["ALL"] },
1311
+ seccompProfile: { type: "RuntimeDefault" },
1312
+ },
1313
+ });
1314
+ const spec = result.statefulSet.spec as any;
1315
+ const sc = spec.template.spec.containers[0].securityContext;
1316
+ expect(sc.allowPrivilegeEscalation).toBe(false);
1317
+ expect(sc.capabilities).toEqual({ drop: ["ALL"] });
1318
+ expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
1319
+ });
1219
1320
  });
1220
1321
 
1221
1322
  describe("WorkerPool hardening", () => {
@@ -1235,6 +1336,24 @@ describe("WorkerPool hardening", () => {
1235
1336
  const spec = result.deployment.spec as any;
1236
1337
  expect(spec.template.spec.containers[0].securityContext.runAsNonRoot).toBe(true);
1237
1338
  });
1339
+
1340
+ test("securityContext supports PSS restricted fields", () => {
1341
+ const result = WorkerPool({
1342
+ name: "w",
1343
+ image: "w:1.0",
1344
+ securityContext: {
1345
+ runAsNonRoot: true,
1346
+ allowPrivilegeEscalation: false,
1347
+ capabilities: { drop: ["ALL"] },
1348
+ seccompProfile: { type: "RuntimeDefault" },
1349
+ },
1350
+ });
1351
+ const spec = result.deployment.spec as any;
1352
+ const sc = spec.template.spec.containers[0].securityContext;
1353
+ expect(sc.allowPrivilegeEscalation).toBe(false);
1354
+ expect(sc.capabilities).toEqual({ drop: ["ALL"] });
1355
+ expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
1356
+ });
1238
1357
  });
1239
1358
 
1240
1359
  describe("AutoscaledService hardening", () => {
@@ -1258,6 +1377,23 @@ describe("AutoscaledService hardening", () => {
1258
1377
  expect(spec.template.spec.containers[0].securityContext.runAsNonRoot).toBe(true);
1259
1378
  });
1260
1379
 
1380
+ test("securityContext supports PSS restricted fields", () => {
1381
+ const result = AutoscaledService({
1382
+ ...minProps,
1383
+ securityContext: {
1384
+ runAsNonRoot: true,
1385
+ allowPrivilegeEscalation: false,
1386
+ capabilities: { drop: ["ALL"] },
1387
+ seccompProfile: { type: "RuntimeDefault" },
1388
+ },
1389
+ });
1390
+ const spec = result.deployment.spec as any;
1391
+ const sc = spec.template.spec.containers[0].securityContext;
1392
+ expect(sc.allowPrivilegeEscalation).toBe(false);
1393
+ expect(sc.capabilities).toEqual({ drop: ["ALL"] });
1394
+ expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
1395
+ });
1396
+
1261
1397
  test("terminationGracePeriodSeconds set", () => {
1262
1398
  const result = AutoscaledService({ ...minProps, terminationGracePeriodSeconds: 30 });
1263
1399
  const spec = result.deployment.spec as any;
@@ -1704,6 +1840,98 @@ describe("AlbIngress", () => {
1704
1840
  const meta = result.ingress.metadata as any;
1705
1841
  expect(meta.annotations["alb.ingress.kubernetes.io/scheme"]).toBe("internal");
1706
1842
  });
1843
+
1844
+ test("empty-string certificateArn does NOT set ssl-redirect", () => {
1845
+ const result = AlbIngress({ ...minProps, certificateArn: "" });
1846
+ const meta = result.ingress.metadata as any;
1847
+ expect(meta.annotations["alb.ingress.kubernetes.io/ssl-redirect"]).toBeUndefined();
1848
+ expect(meta.annotations["alb.ingress.kubernetes.io/certificate-arn"]).toBeUndefined();
1849
+ });
1850
+
1851
+ test("explicit sslRedirect: true overrides empty certificateArn", () => {
1852
+ const result = AlbIngress({ ...minProps, certificateArn: "", sslRedirect: true });
1853
+ const meta = result.ingress.metadata as any;
1854
+ expect(meta.annotations["alb.ingress.kubernetes.io/ssl-redirect"]).toBe("443");
1855
+ });
1856
+
1857
+ test("explicit sslRedirect: false suppresses redirect even with cert", () => {
1858
+ const result = AlbIngress({ ...minProps, certificateArn: "arn:aws:acm:us-east-1:123:cert/abc", sslRedirect: false });
1859
+ const meta = result.ingress.metadata as any;
1860
+ expect(meta.annotations["alb.ingress.kubernetes.io/ssl-redirect"]).toBeUndefined();
1861
+ });
1862
+ });
1863
+
1864
+ // ── GceIngress ──────────────────────────────────────────────────────
1865
+
1866
+ describe("GceIngress", () => {
1867
+ const minProps = {
1868
+ name: "api-ingress",
1869
+ hosts: [{ hostname: "api.example.com", paths: [{ path: "/", serviceName: "api", servicePort: 80 }] }],
1870
+ };
1871
+
1872
+ test("returns ingress with GCE annotations", () => {
1873
+ const result = GceIngress(minProps);
1874
+ expect(result.ingress).toBeDefined();
1875
+ const meta = result.ingress.metadata as any;
1876
+ expect(meta.annotations["kubernetes.io/ingress.class"]).toBe("gce");
1877
+ });
1878
+
1879
+ test("static IP annotation set", () => {
1880
+ const result = GceIngress({ ...minProps, staticIpName: "microservice-ip" });
1881
+ const meta = result.ingress.metadata as any;
1882
+ expect(meta.annotations["kubernetes.io/ingress.global-static-ip-name"]).toBe("microservice-ip");
1883
+ });
1884
+
1885
+ test("managed certificate annotation set", () => {
1886
+ const result = GceIngress({ ...minProps, managedCertificate: "api-cert" });
1887
+ const meta = result.ingress.metadata as any;
1888
+ expect(meta.annotations["networking.gke.io/managed-certificates"]).toBe("api-cert");
1889
+ });
1890
+
1891
+ test("frontendConfig annotation set", () => {
1892
+ const result = GceIngress({ ...minProps, frontendConfig: "my-frontend" });
1893
+ const meta = result.ingress.metadata as any;
1894
+ expect(meta.annotations["networking.gke.io/v1beta1.FrontendConfig"]).toBe("my-frontend");
1895
+ });
1896
+
1897
+ test("managedCertificate sets default FrontendConfig for ssl redirect", () => {
1898
+ const result = GceIngress({ ...minProps, managedCertificate: "api-cert" });
1899
+ const meta = result.ingress.metadata as any;
1900
+ expect(meta.annotations["networking.gke.io/v1beta1.FrontendConfig"]).toBe("api-ingress-frontend-config");
1901
+ });
1902
+
1903
+ test("explicit sslRedirect: false suppresses FrontendConfig even with cert", () => {
1904
+ const result = GceIngress({ ...minProps, managedCertificate: "api-cert", sslRedirect: false });
1905
+ const meta = result.ingress.metadata as any;
1906
+ expect(meta.annotations["networking.gke.io/v1beta1.FrontendConfig"]).toBeUndefined();
1907
+ });
1908
+
1909
+ test("explicit sslRedirect: true sets default FrontendConfig without cert", () => {
1910
+ const result = GceIngress({ ...minProps, sslRedirect: true });
1911
+ const meta = result.ingress.metadata as any;
1912
+ expect(meta.annotations["networking.gke.io/v1beta1.FrontendConfig"]).toBe("api-ingress-frontend-config");
1913
+ });
1914
+
1915
+ test("namespace is set when provided", () => {
1916
+ const result = GceIngress({ ...minProps, namespace: "production" });
1917
+ const meta = result.ingress.metadata as any;
1918
+ expect(meta.namespace).toBe("production");
1919
+ });
1920
+
1921
+ test("host rules are mapped correctly", () => {
1922
+ const result = GceIngress(minProps);
1923
+ const spec = result.ingress.spec as any;
1924
+ expect(spec.rules).toHaveLength(1);
1925
+ expect(spec.rules[0].host).toBe("api.example.com");
1926
+ expect(spec.rules[0].http.paths[0].backend.service.name).toBe("api");
1927
+ });
1928
+
1929
+ test("labels include component: ingress", () => {
1930
+ const result = GceIngress(minProps);
1931
+ const meta = result.ingress.metadata as any;
1932
+ expect(meta.labels["app.kubernetes.io/component"]).toBe("ingress");
1933
+ expect(meta.labels["app.kubernetes.io/managed-by"]).toBe("chant");
1934
+ });
1707
1935
  });
1708
1936
 
1709
1937
  // ── EbsStorageClass ─────────────────────────────────────────────────
@@ -1742,6 +1970,20 @@ describe("EbsStorageClass", () => {
1742
1970
  const result = EbsStorageClass({ name: "sc" });
1743
1971
  expect((result.storageClass.metadata as any).namespace).toBeUndefined();
1744
1972
  });
1973
+
1974
+ test("numeric iops and throughput coerced to strings", () => {
1975
+ const result = EbsStorageClass({ name: "perf", iops: 5000, throughput: 250 });
1976
+ const params = (result.storageClass as any).parameters;
1977
+ expect(params.iops).toBe("5000");
1978
+ expect(params.throughput).toBe("250");
1979
+ });
1980
+
1981
+ test("string iops and throughput passed through", () => {
1982
+ const result = EbsStorageClass({ name: "perf", iops: "3000", throughput: "125" });
1983
+ const params = (result.storageClass as any).parameters;
1984
+ expect(params.iops).toBe("3000");
1985
+ expect(params.throughput).toBe("125");
1986
+ });
1745
1987
  });
1746
1988
 
1747
1989
  // ── EfsStorageClass ─────────────────────────────────────────────────
@@ -1990,3 +2232,821 @@ describe("MetricsServer", () => {
1990
2232
  expect(spec.template.spec.containers[0].image).toBe("custom:v1");
1991
2233
  });
1992
2234
  });
2235
+
2236
+ // ── PSS restricted securityContext passthrough tests ─────────────
2237
+
2238
+ const pssSecurityContext = {
2239
+ runAsNonRoot: true,
2240
+ readOnlyRootFilesystem: true,
2241
+ allowPrivilegeEscalation: false,
2242
+ capabilities: { drop: ["ALL"] },
2243
+ seccompProfile: { type: "RuntimeDefault" },
2244
+ };
2245
+
2246
+ describe("BatchJob securityContext", () => {
2247
+ test("securityContext passed through to container", () => {
2248
+ const result = BatchJob({
2249
+ name: "migrate",
2250
+ image: "migrate:1.0",
2251
+ securityContext: pssSecurityContext,
2252
+ });
2253
+ const spec = result.job.spec as any;
2254
+ const sc = spec.template.spec.containers[0].securityContext;
2255
+ expect(sc.runAsNonRoot).toBe(true);
2256
+ expect(sc.allowPrivilegeEscalation).toBe(false);
2257
+ expect(sc.capabilities).toEqual({ drop: ["ALL"] });
2258
+ expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
2259
+ });
2260
+ });
2261
+
2262
+ describe("CronWorkload securityContext", () => {
2263
+ test("securityContext passed through to container", () => {
2264
+ const result = CronWorkload({
2265
+ name: "backup",
2266
+ image: "backup:1.0",
2267
+ schedule: "0 2 * * *",
2268
+ securityContext: pssSecurityContext,
2269
+ });
2270
+ const spec = result.cronJob.spec as any;
2271
+ const sc = spec.jobTemplate.spec.template.spec.containers[0].securityContext;
2272
+ expect(sc.runAsNonRoot).toBe(true);
2273
+ expect(sc.allowPrivilegeEscalation).toBe(false);
2274
+ expect(sc.capabilities).toEqual({ drop: ["ALL"] });
2275
+ expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
2276
+ });
2277
+ });
2278
+
2279
+ describe("NodeAgent securityContext", () => {
2280
+ test("securityContext passed through to container", () => {
2281
+ const result = NodeAgent({
2282
+ name: "agent",
2283
+ image: "agent:1.0",
2284
+ rbacRules: [{ apiGroups: [""], resources: ["pods"], verbs: ["get"] }],
2285
+ securityContext: pssSecurityContext,
2286
+ });
2287
+ const spec = result.daemonSet.spec as any;
2288
+ const sc = spec.template.spec.containers[0].securityContext;
2289
+ expect(sc.runAsNonRoot).toBe(true);
2290
+ expect(sc.allowPrivilegeEscalation).toBe(false);
2291
+ expect(sc.capabilities).toEqual({ drop: ["ALL"] });
2292
+ });
2293
+ });
2294
+
2295
+ describe("ConfiguredApp securityContext", () => {
2296
+ test("securityContext passed through to container", () => {
2297
+ const result = ConfiguredApp({
2298
+ name: "api",
2299
+ image: "api:1.0",
2300
+ securityContext: pssSecurityContext,
2301
+ });
2302
+ const spec = result.deployment.spec as any;
2303
+ const sc = spec.template.spec.containers[0].securityContext;
2304
+ expect(sc.runAsNonRoot).toBe(true);
2305
+ expect(sc.allowPrivilegeEscalation).toBe(false);
2306
+ expect(sc.capabilities).toEqual({ drop: ["ALL"] });
2307
+ expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
2308
+ });
2309
+ });
2310
+
2311
+ describe("SidecarApp securityContext", () => {
2312
+ test("securityContext passed through to primary container", () => {
2313
+ const result = SidecarApp({
2314
+ name: "api",
2315
+ image: "api:1.0",
2316
+ sidecars: [{ name: "envoy", image: "envoy:1.0" }],
2317
+ securityContext: pssSecurityContext,
2318
+ });
2319
+ const spec = result.deployment.spec as any;
2320
+ const sc = spec.template.spec.containers[0].securityContext;
2321
+ expect(sc.runAsNonRoot).toBe(true);
2322
+ expect(sc.allowPrivilegeEscalation).toBe(false);
2323
+ expect(sc.capabilities).toEqual({ drop: ["ALL"] });
2324
+ expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
2325
+ });
2326
+ });
2327
+
2328
+ describe("NetworkIsolatedApp securityContext", () => {
2329
+ test("securityContext passed through to container", () => {
2330
+ const result = NetworkIsolatedApp({
2331
+ name: "api",
2332
+ image: "api:1.0",
2333
+ securityContext: pssSecurityContext,
2334
+ });
2335
+ const spec = result.deployment.spec as any;
2336
+ const sc = spec.template.spec.containers[0].securityContext;
2337
+ expect(sc.runAsNonRoot).toBe(true);
2338
+ expect(sc.allowPrivilegeEscalation).toBe(false);
2339
+ expect(sc.capabilities).toEqual({ drop: ["ALL"] });
2340
+ expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
2341
+ });
2342
+ });
2343
+
2344
+ describe("MonitoredService securityContext", () => {
2345
+ test("securityContext passed through to container", () => {
2346
+ const result = MonitoredService({
2347
+ name: "api",
2348
+ image: "api:1.0",
2349
+ securityContext: pssSecurityContext,
2350
+ });
2351
+ const spec = result.deployment.spec as any;
2352
+ const sc = spec.template.spec.containers[0].securityContext;
2353
+ expect(sc.runAsNonRoot).toBe(true);
2354
+ expect(sc.allowPrivilegeEscalation).toBe(false);
2355
+ expect(sc.capabilities).toEqual({ drop: ["ALL"] });
2356
+ expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
2357
+ });
2358
+ });
2359
+
2360
+ // ── Infrastructure composites: hardcoded security defaults ──────
2361
+
2362
+ describe("ExternalDnsAgent security defaults", () => {
2363
+ test("container has hardcoded PSS-safe securityContext", () => {
2364
+ const result = ExternalDnsAgent({
2365
+ iamRoleArn: "arn:aws:iam::123456789012:role/test",
2366
+ domainFilters: ["example.com"],
2367
+ });
2368
+ const spec = result.deployment.spec as any;
2369
+ const sc = spec.template.spec.containers[0].securityContext;
2370
+ expect(sc.runAsNonRoot).toBe(true);
2371
+ expect(sc.readOnlyRootFilesystem).toBe(true);
2372
+ expect(sc.allowPrivilegeEscalation).toBe(false);
2373
+ });
2374
+ });
2375
+
2376
+ describe("FluentBitAgent security defaults", () => {
2377
+ test("container runs as root (needs host log access) with other PSS hardening", () => {
2378
+ const result = FluentBitAgent({
2379
+ logGroup: "/aws/eks/test/containers",
2380
+ region: "us-east-1",
2381
+ clusterName: "test",
2382
+ });
2383
+ const spec = result.daemonSet.spec as any;
2384
+ const sc = spec.template.spec.containers[0].securityContext;
2385
+ expect(sc.runAsUser).toBe(0);
2386
+ expect(sc.runAsNonRoot).toBeUndefined();
2387
+ expect(sc.readOnlyRootFilesystem).toBe(true);
2388
+ expect(sc.allowPrivilegeEscalation).toBe(false);
2389
+ });
2390
+ });
2391
+
2392
+ describe("AdotCollector security defaults", () => {
2393
+ test("container runs as non-root with explicit UID (ADOT 'aoc' user)", () => {
2394
+ const result = AdotCollector({
2395
+ region: "us-east-1",
2396
+ clusterName: "test",
2397
+ });
2398
+ const spec = result.daemonSet.spec as any;
2399
+ const sc = spec.template.spec.containers[0].securityContext;
2400
+ expect(sc.runAsNonRoot).toBe(true);
2401
+ expect(sc.runAsUser).toBe(10001);
2402
+ expect(sc.readOnlyRootFilesystem).toBe(true);
2403
+ expect(sc.allowPrivilegeEscalation).toBe(false);
2404
+ });
2405
+ });
2406
+
2407
+ // ── Phase 3B: Composite serialization smoke tests ───────────────
2408
+
2409
+ describe("Composite YAML serialization smoke tests", () => {
2410
+ function serializeCompositeProps(props: Record<string, Record<string, unknown>>): string {
2411
+ return Object.values(props)
2412
+ .map((p) => emitYAML(p, 0))
2413
+ .join("\n---\n");
2414
+ }
2415
+
2416
+ test("ExternalDnsAgent serializes to valid YAML", () => {
2417
+ const result = ExternalDnsAgent({
2418
+ iamRoleArn: "arn:aws:iam::123456789012:role/test",
2419
+ domainFilters: ["example.com"],
2420
+ });
2421
+ const yaml = serializeCompositeProps(result as any);
2422
+ expect(yaml).toContain("external-dns");
2423
+ expect(yaml).not.toContain("[object Object]");
2424
+ });
2425
+
2426
+ test("FluentBitAgent serializes multiline config correctly", () => {
2427
+ const result = FluentBitAgent({
2428
+ logGroup: "/aws/eks/test/containers",
2429
+ region: "us-east-1",
2430
+ clusterName: "test",
2431
+ });
2432
+ const yaml = serializeCompositeProps(result as any);
2433
+ // Multiline config should use | block scalar, not flatten
2434
+ expect(yaml).toContain("|");
2435
+ expect(yaml).toContain("[SERVICE]");
2436
+ expect(yaml).toContain("[INPUT]");
2437
+ expect(yaml).not.toContain("\\n");
2438
+ });
2439
+
2440
+ test("AdotCollector serializes multiline config correctly", () => {
2441
+ const result = AdotCollector({
2442
+ region: "us-east-1",
2443
+ clusterName: "test",
2444
+ });
2445
+ const yaml = serializeCompositeProps(result as any);
2446
+ expect(yaml).toContain("|");
2447
+ expect(yaml).toContain("receivers:");
2448
+ expect(yaml).toContain("exporters:");
2449
+ expect(yaml).not.toContain("\\n");
2450
+ });
2451
+
2452
+ test("MetricsServer serializes to valid YAML", () => {
2453
+ const result = MetricsServer({});
2454
+ const yaml = serializeCompositeProps(result as any);
2455
+ expect(yaml).toContain("metrics-server");
2456
+ expect(yaml).not.toContain("[object Object]");
2457
+ });
2458
+ });
2459
+
2460
+ // ── Phase 4A: MetricsServer RBAC completeness ──────────────────
2461
+
2462
+ describe("MetricsServer RBAC completeness", () => {
2463
+ test("clusterRole includes configmaps resource", () => {
2464
+ const result = MetricsServer({});
2465
+ const rules = result.clusterRole.rules as any[];
2466
+ const hasConfigmaps = rules.some((r: any) => r.resources?.includes("configmaps"));
2467
+ expect(hasConfigmaps).toBe(true);
2468
+ });
2469
+ });
2470
+
2471
+ // ── Phase 4B: AdotCollector command vs args ─────────────────────
2472
+
2473
+ describe("AdotCollector command vs args", () => {
2474
+ test("container uses args (not command) for config flag", () => {
2475
+ const result = AdotCollector({
2476
+ region: "us-east-1",
2477
+ clusterName: "test",
2478
+ });
2479
+ const spec = result.daemonSet.spec as any;
2480
+ const container = spec.template.spec.containers[0];
2481
+ // Config flag should be in args, not command
2482
+ expect(container.args).toContain("--config=/etc/adot/config.yaml");
2483
+ expect(container.command).toBeUndefined();
2484
+ });
2485
+ });
2486
+
2487
+ // ── Phase 4C: AdotCollector pipeline exporter separation ────────
2488
+
2489
+ describe("AdotCollector pipeline exporter separation", () => {
2490
+ test("metrics pipeline does NOT include awsxray", () => {
2491
+ const result = AdotCollector({
2492
+ region: "us-east-1",
2493
+ clusterName: "test",
2494
+ exporters: ["cloudwatch", "xray"],
2495
+ });
2496
+ const config = (result.configMap as any).data["config.yaml"] as string;
2497
+ // Extract metrics pipeline exporters line
2498
+ const metricsMatch = config.match(/metrics:\s*\n\s*receivers:.*\n\s*processors:.*\n\s*exporters:\s*\[([^\]]+)\]/);
2499
+ expect(metricsMatch).toBeDefined();
2500
+ const metricsExporters = metricsMatch![1];
2501
+ expect(metricsExporters).not.toContain("awsxray");
2502
+ expect(metricsExporters).toContain("awsemf");
2503
+ });
2504
+
2505
+ test("traces pipeline does NOT include awsemf", () => {
2506
+ const result = AdotCollector({
2507
+ region: "us-east-1",
2508
+ clusterName: "test",
2509
+ exporters: ["cloudwatch", "xray"],
2510
+ });
2511
+ const config = (result.configMap as any).data["config.yaml"] as string;
2512
+ // Extract traces pipeline exporters line
2513
+ const tracesMatch = config.match(/traces:\s*\n\s*receivers:.*\n\s*processors:.*\n\s*exporters:\s*\[([^\]]+)\]/);
2514
+ expect(tracesMatch).toBeDefined();
2515
+ const tracesExporters = tracesMatch![1];
2516
+ expect(tracesExporters).not.toContain("awsemf");
2517
+ expect(tracesExporters).toContain("awsxray");
2518
+ });
2519
+
2520
+ test("cloudwatch-only: traces pipeline falls back to valid default", () => {
2521
+ const result = AdotCollector({
2522
+ region: "us-east-1",
2523
+ clusterName: "test",
2524
+ exporters: ["cloudwatch"],
2525
+ });
2526
+ const config = (result.configMap as any).data["config.yaml"] as string;
2527
+ // Traces pipeline should still have an exporter (fallback to awsxray)
2528
+ const tracesMatch = config.match(/traces:\s*\n\s*receivers:.*\n\s*processors:.*\n\s*exporters:\s*\[([^\]]+)\]/);
2529
+ expect(tracesMatch).toBeDefined();
2530
+ const tracesExporters = tracesMatch![1].trim();
2531
+ expect(tracesExporters.length).toBeGreaterThan(0);
2532
+ });
2533
+ });
2534
+
2535
+ // ── Phase 4D: AdotCollector config parseable as YAML ────────────
2536
+
2537
+ describe("AdotCollector config structure", () => {
2538
+ test("generated config has required top-level sections", () => {
2539
+ const result = AdotCollector({
2540
+ region: "us-east-1",
2541
+ clusterName: "test",
2542
+ });
2543
+ const config = (result.configMap as any).data["config.yaml"] as string;
2544
+ expect(config).toContain("receivers:");
2545
+ expect(config).toContain("exporters:");
2546
+ expect(config).toContain("processors:");
2547
+ expect(config).toContain("service:");
2548
+ expect(config).toContain("pipelines:");
2549
+ expect(config).toContain("metrics:");
2550
+ expect(config).toContain("traces:");
2551
+ });
2552
+ });
2553
+
2554
+ // ── WorkloadIdentityServiceAccount ──────────────────────────────────
2555
+
2556
+ describe("WorkloadIdentityServiceAccount", () => {
2557
+ const minProps = { name: "app-sa", gcpServiceAccountEmail: "sa@my-project.iam.gserviceaccount.com" };
2558
+
2559
+ test("returns serviceAccount with Workload Identity annotation", () => {
2560
+ const result = WorkloadIdentityServiceAccount(minProps);
2561
+ expect(result.serviceAccount).toBeDefined();
2562
+ const meta = result.serviceAccount.metadata as any;
2563
+ expect(meta.annotations["iam.gke.io/gcp-service-account"]).toBe("sa@my-project.iam.gserviceaccount.com");
2564
+ });
2565
+
2566
+ test("no RBAC by default", () => {
2567
+ const result = WorkloadIdentityServiceAccount(minProps);
2568
+ expect(result.role).toBeUndefined();
2569
+ expect(result.roleBinding).toBeUndefined();
2570
+ });
2571
+
2572
+ test("RBAC created when rules provided", () => {
2573
+ const result = WorkloadIdentityServiceAccount({
2574
+ ...minProps,
2575
+ rbacRules: [{ apiGroups: [""], resources: ["secrets"], verbs: ["get"] }],
2576
+ });
2577
+ expect(result.role).toBeDefined();
2578
+ expect(result.roleBinding).toBeDefined();
2579
+ const role = result.role as any;
2580
+ expect(role.rules[0].resources).toEqual(["secrets"]);
2581
+ });
2582
+
2583
+ test("namespace propagated", () => {
2584
+ const result = WorkloadIdentityServiceAccount({ ...minProps, namespace: "prod" });
2585
+ expect((result.serviceAccount.metadata as any).namespace).toBe("prod");
2586
+ });
2587
+
2588
+ test("component labels", () => {
2589
+ const result = WorkloadIdentityServiceAccount(minProps);
2590
+ expect((result.serviceAccount.metadata as any).labels["app.kubernetes.io/component"]).toBe("service-account");
2591
+ });
2592
+ });
2593
+
2594
+ // ── GcePdStorageClass ───────────────────────────────────────────────
2595
+
2596
+ describe("GcePdStorageClass", () => {
2597
+ test("returns storageClass with GCE PD provisioner", () => {
2598
+ const result = GcePdStorageClass({ name: "pd-balanced" });
2599
+ expect(result.storageClass).toBeDefined();
2600
+ expect((result.storageClass as any).provisioner).toBe("pd.csi.storage.gke.io");
2601
+ });
2602
+
2603
+ test("default type is pd-balanced", () => {
2604
+ const result = GcePdStorageClass({ name: "default" });
2605
+ expect((result.storageClass as any).parameters.type).toBe("pd-balanced");
2606
+ });
2607
+
2608
+ test("custom type", () => {
2609
+ const result = GcePdStorageClass({ name: "ssd", type: "pd-ssd" });
2610
+ expect((result.storageClass as any).parameters.type).toBe("pd-ssd");
2611
+ });
2612
+
2613
+ test("regional-pd replication type", () => {
2614
+ const result = GcePdStorageClass({ name: "regional", replicationType: "regional-pd" });
2615
+ expect((result.storageClass as any).parameters["replication-type"]).toBe("regional-pd");
2616
+ });
2617
+
2618
+ test("no replication-type param when none", () => {
2619
+ const result = GcePdStorageClass({ name: "default" });
2620
+ expect((result.storageClass as any).parameters["replication-type"]).toBeUndefined();
2621
+ });
2622
+
2623
+ test("allowVolumeExpansion default true", () => {
2624
+ const result = GcePdStorageClass({ name: "exp" });
2625
+ expect((result.storageClass as any).allowVolumeExpansion).toBe(true);
2626
+ });
2627
+
2628
+ test("storageClass is cluster-scoped (no namespace)", () => {
2629
+ const result = GcePdStorageClass({ name: "sc" });
2630
+ expect((result.storageClass.metadata as any).namespace).toBeUndefined();
2631
+ });
2632
+ });
2633
+
2634
+ // ── FilestoreStorageClass ───────────────────────────────────────────
2635
+
2636
+ describe("FilestoreStorageClass", () => {
2637
+ test("returns storageClass with Filestore provisioner", () => {
2638
+ const result = FilestoreStorageClass({ name: "filestore" });
2639
+ expect((result.storageClass as any).provisioner).toBe("filestore.csi.storage.gke.io");
2640
+ });
2641
+
2642
+ test("default tier is standard", () => {
2643
+ const result = FilestoreStorageClass({ name: "fs" });
2644
+ expect((result.storageClass as any).parameters.tier).toBe("standard");
2645
+ });
2646
+
2647
+ test("custom tier", () => {
2648
+ const result = FilestoreStorageClass({ name: "premium-fs", tier: "premium" });
2649
+ expect((result.storageClass as any).parameters.tier).toBe("premium");
2650
+ });
2651
+
2652
+ test("network parameter set when provided", () => {
2653
+ const result = FilestoreStorageClass({ name: "fs", network: "my-vpc" });
2654
+ expect((result.storageClass as any).parameters.network).toBe("my-vpc");
2655
+ });
2656
+
2657
+ test("no network parameter by default", () => {
2658
+ const result = FilestoreStorageClass({ name: "fs" });
2659
+ expect((result.storageClass as any).parameters.network).toBeUndefined();
2660
+ });
2661
+ });
2662
+
2663
+ // ── GkeGateway ──────────────────────────────────────────────────────
2664
+
2665
+ describe("GkeGateway", () => {
2666
+ const minProps = {
2667
+ name: "api-gateway",
2668
+ hosts: [{ hostname: "api.example.com", paths: [{ path: "/", serviceName: "api", servicePort: 80 }] }],
2669
+ };
2670
+
2671
+ test("returns gateway and httpRoute", () => {
2672
+ const result = GkeGateway(minProps);
2673
+ expect(result.gateway).toBeDefined();
2674
+ expect(result.httpRoute).toBeDefined();
2675
+ });
2676
+
2677
+ test("default gatewayClassName", () => {
2678
+ const result = GkeGateway(minProps);
2679
+ const spec = result.gateway.spec as any;
2680
+ expect(spec.gatewayClassName).toBe("gke-l7-global-external-managed");
2681
+ });
2682
+
2683
+ test("custom gatewayClassName", () => {
2684
+ const result = GkeGateway({ ...minProps, gatewayClassName: "gke-l7-rilb" });
2685
+ const spec = result.gateway.spec as any;
2686
+ expect(spec.gatewayClassName).toBe("gke-l7-rilb");
2687
+ });
2688
+
2689
+ test("HTTP listener when no certificate", () => {
2690
+ const result = GkeGateway(minProps);
2691
+ const spec = result.gateway.spec as any;
2692
+ expect(spec.listeners[0].protocol).toBe("HTTP");
2693
+ expect(spec.listeners[0].port).toBe(80);
2694
+ });
2695
+
2696
+ test("HTTPS listener with certificate", () => {
2697
+ const result = GkeGateway({ ...minProps, certificateName: "api-cert" });
2698
+ const spec = result.gateway.spec as any;
2699
+ expect(spec.listeners[0].protocol).toBe("HTTPS");
2700
+ expect(spec.listeners[0].port).toBe(443);
2701
+ expect(spec.listeners[0].tls.certificateRefs[0].name).toBe("api-cert");
2702
+ });
2703
+
2704
+ test("httpRoute references parent gateway", () => {
2705
+ const result = GkeGateway(minProps);
2706
+ const spec = result.httpRoute.spec as any;
2707
+ expect(spec.parentRefs[0].name).toBe("api-gateway");
2708
+ });
2709
+
2710
+ test("httpRoute has hostnames", () => {
2711
+ const result = GkeGateway(minProps);
2712
+ const spec = result.httpRoute.spec as any;
2713
+ expect(spec.hostnames).toEqual(["api.example.com"]);
2714
+ });
2715
+
2716
+ test("httpRoute rules map to backend services", () => {
2717
+ const result = GkeGateway(minProps);
2718
+ const spec = result.httpRoute.spec as any;
2719
+ expect(spec.rules[0].backendRefs[0].name).toBe("api");
2720
+ expect(spec.rules[0].backendRefs[0].port).toBe(80);
2721
+ });
2722
+
2723
+ test("namespace propagated to both resources", () => {
2724
+ const result = GkeGateway({ ...minProps, namespace: "prod" });
2725
+ expect((result.gateway.metadata as any).namespace).toBe("prod");
2726
+ expect((result.httpRoute.metadata as any).namespace).toBe("prod");
2727
+ });
2728
+ });
2729
+
2730
+ // ── ConfigConnectorContext ───────────────────────────────────────────
2731
+
2732
+ describe("ConfigConnectorContext", () => {
2733
+ const minProps = { googleServiceAccountEmail: "cnrm@my-project.iam.gserviceaccount.com" };
2734
+
2735
+ test("returns context with apiVersion and kind", () => {
2736
+ const result = ConfigConnectorContext(minProps);
2737
+ expect(result.context).toBeDefined();
2738
+ expect((result.context as any).apiVersion).toBe("core.cnrm.cloud.google.com/v1beta1");
2739
+ expect((result.context as any).kind).toBe("ConfigConnectorContext");
2740
+ });
2741
+
2742
+ test("googleServiceAccount in spec", () => {
2743
+ const result = ConfigConnectorContext(minProps);
2744
+ const spec = (result.context as any).spec;
2745
+ expect(spec.googleServiceAccount).toBe("cnrm@my-project.iam.gserviceaccount.com");
2746
+ });
2747
+
2748
+ test("default stateIntoSpec is absent", () => {
2749
+ const result = ConfigConnectorContext(minProps);
2750
+ expect((result.context as any).spec.stateIntoSpec).toBe("absent");
2751
+ });
2752
+
2753
+ test("custom stateIntoSpec", () => {
2754
+ const result = ConfigConnectorContext({ ...minProps, stateIntoSpec: "merge" });
2755
+ expect((result.context as any).spec.stateIntoSpec).toBe("merge");
2756
+ });
2757
+
2758
+ test("default namespace is default", () => {
2759
+ const result = ConfigConnectorContext(minProps);
2760
+ expect((result.context as any).metadata.namespace).toBe("default");
2761
+ });
2762
+
2763
+ test("custom namespace", () => {
2764
+ const result = ConfigConnectorContext({ ...minProps, namespace: "config-connector" });
2765
+ expect((result.context as any).metadata.namespace).toBe("config-connector");
2766
+ });
2767
+ });
2768
+
2769
+ // ── GkeFluentBitAgent ────────────────────────────────────────────────
2770
+
2771
+ describe("GkeFluentBitAgent", () => {
2772
+ const { GkeFluentBitAgent } = require("./gke-fluent-bit-agent");
2773
+
2774
+ const minProps = {
2775
+ clusterName: "test-cluster",
2776
+ projectId: "test-project",
2777
+ gcpServiceAccountEmail: "fluent-bit@test-project.iam.gserviceaccount.com",
2778
+ };
2779
+
2780
+ test("returns all expected resources", () => {
2781
+ const result = GkeFluentBitAgent(minProps);
2782
+ expect(result.daemonSet).toBeDefined();
2783
+ expect(result.serviceAccount).toBeDefined();
2784
+ expect(result.clusterRole).toBeDefined();
2785
+ expect(result.clusterRoleBinding).toBeDefined();
2786
+ expect(result.configMap).toBeDefined();
2787
+ });
2788
+
2789
+ test("SA has GKE Workload Identity annotation", () => {
2790
+ const result = GkeFluentBitAgent(minProps);
2791
+ const meta = result.serviceAccount.metadata as any;
2792
+ expect(meta.annotations["iam.gke.io/gcp-service-account"]).toBe(
2793
+ "fluent-bit@test-project.iam.gserviceaccount.com",
2794
+ );
2795
+ });
2796
+
2797
+ test("SA has no annotation when gcpServiceAccountEmail omitted", () => {
2798
+ const result = GkeFluentBitAgent({
2799
+ clusterName: "test-cluster",
2800
+ projectId: "test-project",
2801
+ });
2802
+ const meta = result.serviceAccount.metadata as any;
2803
+ expect(meta.annotations).toBeUndefined();
2804
+ });
2805
+
2806
+ test("configMap uses stackdriver output", () => {
2807
+ const result = GkeFluentBitAgent(minProps);
2808
+ const data = result.configMap.data as any;
2809
+ expect(data["fluent-bit.conf"]).toContain("stackdriver");
2810
+ expect(data["fluent-bit.conf"]).toContain("test-cluster");
2811
+ });
2812
+
2813
+ test("default namespace is gke-logging", () => {
2814
+ const result = GkeFluentBitAgent(minProps);
2815
+ expect((result.daemonSet.metadata as any).namespace).toBe("gke-logging");
2816
+ expect((result.serviceAccount.metadata as any).namespace).toBe("gke-logging");
2817
+ });
2818
+
2819
+ test("common labels on all resources", () => {
2820
+ const result = GkeFluentBitAgent(minProps);
2821
+ for (const resource of [result.daemonSet, result.serviceAccount, result.clusterRole, result.clusterRoleBinding, result.configMap]) {
2822
+ expect((resource.metadata as any).labels["app.kubernetes.io/managed-by"]).toBe("chant");
2823
+ }
2824
+ });
2825
+
2826
+ test("DaemonSet mounts host log directory", () => {
2827
+ const result = GkeFluentBitAgent(minProps);
2828
+ const spec = (result.daemonSet as any).spec.template.spec;
2829
+ const varlog = spec.volumes.find((v: any) => v.name === "varlog");
2830
+ expect(varlog.hostPath.path).toBe("/var/log");
2831
+ });
2832
+
2833
+ test("container runs as root for log access", () => {
2834
+ const result = GkeFluentBitAgent(minProps);
2835
+ const container = (result.daemonSet as any).spec.template.spec.containers[0];
2836
+ expect(container.securityContext.runAsUser).toBe(0);
2837
+ expect(container.securityContext.readOnlyRootFilesystem).toBe(true);
2838
+ });
2839
+ });
2840
+
2841
+ // ── GkeOtelCollector ─────────────────────────────────────────────────
2842
+
2843
+ describe("GkeOtelCollector", () => {
2844
+ const { GkeOtelCollector } = require("./gke-otel-collector");
2845
+
2846
+ const minProps = {
2847
+ clusterName: "test-cluster",
2848
+ projectId: "test-project",
2849
+ gcpServiceAccountEmail: "otel@test-project.iam.gserviceaccount.com",
2850
+ };
2851
+
2852
+ test("returns all expected resources", () => {
2853
+ const result = GkeOtelCollector(minProps);
2854
+ expect(result.daemonSet).toBeDefined();
2855
+ expect(result.serviceAccount).toBeDefined();
2856
+ expect(result.clusterRole).toBeDefined();
2857
+ expect(result.clusterRoleBinding).toBeDefined();
2858
+ expect(result.configMap).toBeDefined();
2859
+ });
2860
+
2861
+ test("SA has GKE Workload Identity annotation", () => {
2862
+ const result = GkeOtelCollector(minProps);
2863
+ const meta = result.serviceAccount.metadata as any;
2864
+ expect(meta.annotations["iam.gke.io/gcp-service-account"]).toBe(
2865
+ "otel@test-project.iam.gserviceaccount.com",
2866
+ );
2867
+ });
2868
+
2869
+ test("SA has no annotation when gcpServiceAccountEmail omitted", () => {
2870
+ const result = GkeOtelCollector({
2871
+ clusterName: "test-cluster",
2872
+ projectId: "test-project",
2873
+ });
2874
+ const meta = result.serviceAccount.metadata as any;
2875
+ expect(meta.annotations).toBeUndefined();
2876
+ });
2877
+
2878
+ test("configMap uses googlecloud exporter", () => {
2879
+ const result = GkeOtelCollector(minProps);
2880
+ const data = result.configMap.data as any;
2881
+ expect(data["config.yaml"]).toContain("googlecloud");
2882
+ expect(data["config.yaml"]).toContain("test-project");
2883
+ });
2884
+
2885
+ test("default namespace is gke-monitoring", () => {
2886
+ const result = GkeOtelCollector(minProps);
2887
+ expect((result.daemonSet.metadata as any).namespace).toBe("gke-monitoring");
2888
+ });
2889
+
2890
+ test("container exposes OTLP ports", () => {
2891
+ const result = GkeOtelCollector(minProps);
2892
+ const container = (result.daemonSet as any).spec.template.spec.containers[0];
2893
+ const ports = container.ports.map((p: any) => p.containerPort);
2894
+ expect(ports).toContain(4317);
2895
+ expect(ports).toContain(4318);
2896
+ });
2897
+
2898
+ test("container runs as non-root", () => {
2899
+ const result = GkeOtelCollector(minProps);
2900
+ const container = (result.daemonSet as any).spec.template.spec.containers[0];
2901
+ expect(container.securityContext.runAsNonRoot).toBe(true);
2902
+ expect(container.securityContext.runAsUser).toBe(10001);
2903
+ });
2904
+
2905
+ test("common labels on all resources", () => {
2906
+ const result = GkeOtelCollector(minProps);
2907
+ for (const resource of [result.daemonSet, result.serviceAccount, result.clusterRole, result.clusterRoleBinding, result.configMap]) {
2908
+ expect((resource.metadata as any).labels["app.kubernetes.io/managed-by"]).toBe("chant");
2909
+ }
2910
+ });
2911
+ });
2912
+
2913
+ // ── GkeExternalDnsAgent ──────────────────────────────────────────────
2914
+
2915
+ describe("GkeExternalDnsAgent", () => {
2916
+ const { GkeExternalDnsAgent } = require("./gke-external-dns-agent");
2917
+
2918
+ const minProps = {
2919
+ gcpServiceAccountEmail: "external-dns@test-project.iam.gserviceaccount.com",
2920
+ gcpProjectId: "test-project",
2921
+ domainFilters: ["example.com"],
2922
+ };
2923
+
2924
+ test("returns all expected resources", () => {
2925
+ const result = GkeExternalDnsAgent(minProps);
2926
+ expect(result.deployment).toBeDefined();
2927
+ expect(result.serviceAccount).toBeDefined();
2928
+ expect(result.clusterRole).toBeDefined();
2929
+ expect(result.clusterRoleBinding).toBeDefined();
2930
+ });
2931
+
2932
+ test("SA has GKE Workload Identity annotation", () => {
2933
+ const result = GkeExternalDnsAgent(minProps);
2934
+ const meta = result.serviceAccount.metadata as any;
2935
+ expect(meta.annotations["iam.gke.io/gcp-service-account"]).toBe(
2936
+ "external-dns@test-project.iam.gserviceaccount.com",
2937
+ );
2938
+ });
2939
+
2940
+ test("uses google provider", () => {
2941
+ const result = GkeExternalDnsAgent(minProps);
2942
+ const container = (result.deployment as any).spec.template.spec.containers[0];
2943
+ expect(container.args).toContain("--provider=google");
2944
+ });
2945
+
2946
+ test("passes google-project arg", () => {
2947
+ const result = GkeExternalDnsAgent(minProps);
2948
+ const container = (result.deployment as any).spec.template.spec.containers[0];
2949
+ expect(container.args).toContain("--google-project=test-project");
2950
+ });
2951
+
2952
+ test("domain filters are applied", () => {
2953
+ const result = GkeExternalDnsAgent(minProps);
2954
+ const container = (result.deployment as any).spec.template.spec.containers[0];
2955
+ expect(container.args).toContain("--domain-filter=example.com");
2956
+ });
2957
+
2958
+ test("txtOwnerId is applied when set", () => {
2959
+ const result = GkeExternalDnsAgent({ ...minProps, txtOwnerId: "my-cluster" });
2960
+ const container = (result.deployment as any).spec.template.spec.containers[0];
2961
+ expect(container.args).toContain("--txt-owner-id=my-cluster");
2962
+ });
2963
+
2964
+ test("default namespace is kube-system", () => {
2965
+ const result = GkeExternalDnsAgent(minProps);
2966
+ expect((result.deployment.metadata as any).namespace).toBe("kube-system");
2967
+ });
2968
+
2969
+ test("container runs as non-root", () => {
2970
+ const result = GkeExternalDnsAgent(minProps);
2971
+ const container = (result.deployment as any).spec.template.spec.containers[0];
2972
+ expect(container.securityContext.runAsNonRoot).toBe(true);
2973
+ expect(container.securityContext.runAsUser).toBe(65534);
2974
+ });
2975
+ });
2976
+
2977
+ // ── AksExternalDnsAgent ──────────────────────────────────────────────
2978
+
2979
+ describe("AksExternalDnsAgent", () => {
2980
+ const { AksExternalDnsAgent } = require("./aks-external-dns-agent");
2981
+
2982
+ const minProps = {
2983
+ clientId: "00000000-0000-0000-0000-000000000000",
2984
+ resourceGroup: "test-rg",
2985
+ subscriptionId: "11111111-1111-1111-1111-111111111111",
2986
+ tenantId: "22222222-2222-2222-2222-222222222222",
2987
+ domainFilters: ["example.com"],
2988
+ };
2989
+
2990
+ test("returns all expected resources", () => {
2991
+ const result = AksExternalDnsAgent(minProps);
2992
+ expect(result.deployment).toBeDefined();
2993
+ expect(result.serviceAccount).toBeDefined();
2994
+ expect(result.clusterRole).toBeDefined();
2995
+ expect(result.clusterRoleBinding).toBeDefined();
2996
+ });
2997
+
2998
+ test("SA has AKS Workload Identity annotation and label", () => {
2999
+ const result = AksExternalDnsAgent(minProps);
3000
+ const meta = result.serviceAccount.metadata as any;
3001
+ expect(meta.annotations["azure.workload.identity/client-id"]).toBe(
3002
+ "00000000-0000-0000-0000-000000000000",
3003
+ );
3004
+ expect(meta.labels["azure.workload.identity/use"]).toBe("true");
3005
+ });
3006
+
3007
+ test("uses azure provider", () => {
3008
+ const result = AksExternalDnsAgent(minProps);
3009
+ const container = (result.deployment as any).spec.template.spec.containers[0];
3010
+ expect(container.args).toContain("--provider=azure");
3011
+ });
3012
+
3013
+ test("passes azure resource group and subscription", () => {
3014
+ const result = AksExternalDnsAgent(minProps);
3015
+ const container = (result.deployment as any).spec.template.spec.containers[0];
3016
+ expect(container.args).toContain("--azure-resource-group=test-rg");
3017
+ expect(container.args).toContain("--azure-subscription-id=11111111-1111-1111-1111-111111111111");
3018
+ });
3019
+
3020
+ test("container has Azure env vars", () => {
3021
+ const result = AksExternalDnsAgent(minProps);
3022
+ const container = (result.deployment as any).spec.template.spec.containers[0];
3023
+ const envMap = Object.fromEntries(container.env.map((e: any) => [e.name, e.value]));
3024
+ expect(envMap.AZURE_TENANT_ID).toBe("22222222-2222-2222-2222-222222222222");
3025
+ expect(envMap.AZURE_SUBSCRIPTION_ID).toBe("11111111-1111-1111-1111-111111111111");
3026
+ expect(envMap.AZURE_RESOURCE_GROUP).toBe("test-rg");
3027
+ });
3028
+
3029
+ test("pod labels include workload identity use label", () => {
3030
+ const result = AksExternalDnsAgent(minProps);
3031
+ const podLabels = (result.deployment as any).spec.template.metadata.labels;
3032
+ expect(podLabels["azure.workload.identity/use"]).toBe("true");
3033
+ });
3034
+
3035
+ test("domain filters are applied", () => {
3036
+ const result = AksExternalDnsAgent(minProps);
3037
+ const container = (result.deployment as any).spec.template.spec.containers[0];
3038
+ expect(container.args).toContain("--domain-filter=example.com");
3039
+ });
3040
+
3041
+ test("default namespace is kube-system", () => {
3042
+ const result = AksExternalDnsAgent(minProps);
3043
+ expect((result.deployment.metadata as any).namespace).toBe("kube-system");
3044
+ });
3045
+
3046
+ test("container runs as non-root", () => {
3047
+ const result = AksExternalDnsAgent(minProps);
3048
+ const container = (result.deployment as any).spec.template.spec.containers[0];
3049
+ expect(container.securityContext.runAsNonRoot).toBe(true);
3050
+ expect(container.securityContext.runAsUser).toBe(65534);
3051
+ });
3052
+ });