@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.
- package/dist/integrity.json +8 -4
- package/dist/manifest.json +1 -1
- package/dist/rules/latest-image-tag.ts +121 -0
- package/dist/rules/missing-resource-limits.ts +111 -0
- package/dist/rules/wk8204.ts +33 -1
- package/dist/rules/wk8304.ts +70 -0
- package/dist/rules/wk8305.ts +115 -0
- package/dist/rules/wk8306.ts +50 -0
- package/package.json +27 -24
- package/src/codegen/docs.ts +1 -1
- package/src/composites/adot-collector.ts +8 -2
- package/src/composites/agic-ingress.ts +148 -0
- package/src/composites/aks-external-dns-agent.ts +199 -0
- package/src/composites/alb-ingress.ts +2 -1
- package/src/composites/autoscaled-service.ts +25 -7
- package/src/composites/azure-disk-storage-class.ts +82 -0
- package/src/composites/azure-file-storage-class.ts +77 -0
- package/src/composites/azure-monitor-collector.ts +232 -0
- package/src/composites/batch-job.ts +36 -3
- package/src/composites/composites.test.ts +1060 -0
- package/src/composites/config-connector-context.ts +62 -0
- package/src/composites/configured-app.ts +6 -0
- package/src/composites/cron-workload.ts +6 -0
- package/src/composites/ebs-storage-class.ts +4 -4
- package/src/composites/external-dns-agent.ts +6 -0
- package/src/composites/filestore-storage-class.ts +79 -0
- package/src/composites/fluent-bit-agent.ts +5 -0
- package/src/composites/gce-ingress.ts +143 -0
- package/src/composites/gce-pd-storage-class.ts +85 -0
- package/src/composites/gke-external-dns-agent.ts +175 -0
- package/src/composites/gke-fluent-bit-agent.ts +219 -0
- package/src/composites/gke-gateway.ts +143 -0
- package/src/composites/gke-otel-collector.ts +229 -0
- package/src/composites/index.ts +31 -0
- package/src/composites/metrics-server.ts +1 -1
- package/src/composites/monitored-service.ts +6 -0
- package/src/composites/network-isolated-app.ts +6 -0
- package/src/composites/node-agent.ts +6 -0
- package/src/composites/security-context.ts +10 -0
- package/src/composites/sidecar-app.ts +6 -0
- package/src/composites/stateful-app.ts +4 -7
- package/src/composites/web-app.ts +4 -7
- package/src/composites/worker-pool.ts +4 -7
- package/src/composites/workload-identity-sa.ts +118 -0
- package/src/composites/workload-identity-service-account.ts +116 -0
- package/src/index.ts +20 -1
- package/src/lint/post-synth/post-synth.test.ts +362 -1
- package/src/lint/post-synth/wk8204.ts +33 -1
- package/src/lint/post-synth/wk8304.ts +70 -0
- package/src/lint/post-synth/wk8305.ts +115 -0
- package/src/lint/post-synth/wk8306.ts +50 -0
- package/src/lint/rules/latest-image-tag.ts +121 -0
- package/src/lint/rules/missing-resource-limits.ts +111 -0
- package/src/lint/rules/rules.test.ts +192 -0
- package/src/plugin.test.ts +2 -2
- package/src/plugin.ts +129 -209
- package/src/serializer.test.ts +120 -0
- package/src/serializer.ts +16 -4
- package/src/skills/chant-k8s-aks.md +146 -0
- package/src/skills/chant-k8s-gke.md +191 -0
- package/src/skills/kubernetes-patterns.md +183 -0
- package/src/skills/kubernetes-security.md +237 -0
- /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
|
+
});
|