@intentius/chant-lexicon-k8s 0.0.13 → 0.0.14

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.
@@ -6,6 +6,20 @@ import { AutoscaledService } from "./autoscaled-service";
6
6
  import { WorkerPool } from "./worker-pool";
7
7
  import { NamespaceEnv } from "./namespace-env";
8
8
  import { NodeAgent } from "./node-agent";
9
+ import { BatchJob } from "./batch-job";
10
+ import { SecureIngress } from "./secure-ingress";
11
+ import { ConfiguredApp } from "./configured-app";
12
+ import { SidecarApp } from "./sidecar-app";
13
+ import { MonitoredService } from "./monitored-service";
14
+ import { NetworkIsolatedApp } from "./network-isolated-app";
15
+ import { IrsaServiceAccount } from "./irsa-service-account";
16
+ import { AlbIngress } from "./alb-ingress";
17
+ import { EbsStorageClass } from "./ebs-storage-class";
18
+ import { EfsStorageClass } from "./efs-storage-class";
19
+ import { FluentBitAgent } from "./fluent-bit-agent";
20
+ import { ExternalDnsAgent } from "./external-dns-agent";
21
+ import { AdotCollector } from "./adot-collector";
22
+ import { MetricsServer } from "./metrics-server";
9
23
 
10
24
  // ── WebApp ──────────────────────────────────────────────────────────
11
25
 
@@ -1107,3 +1121,872 @@ describe("NodeAgent", () => {
1107
1121
  expect((result.configMap!.metadata as any).labels["app.kubernetes.io/component"]).toBe("config");
1108
1122
  });
1109
1123
  });
1124
+
1125
+ // ── Hardening Additions ─────────────────────────────────────────────
1126
+
1127
+ describe("WebApp hardening", () => {
1128
+ test("PDB created when minAvailable set", () => {
1129
+ const result = WebApp({ name: "app", image: "app:1.0", minAvailable: 1 });
1130
+ expect(result.pdb).toBeDefined();
1131
+ const spec = result.pdb!.spec as any;
1132
+ expect(spec.minAvailable).toBe(1);
1133
+ expect(spec.selector.matchLabels["app.kubernetes.io/name"]).toBe("app");
1134
+ });
1135
+
1136
+ test("no PDB by default", () => {
1137
+ const result = WebApp({ name: "app", image: "app:1.0" });
1138
+ expect(result.pdb).toBeUndefined();
1139
+ });
1140
+
1141
+ test("initContainers passed through", () => {
1142
+ const result = WebApp({
1143
+ name: "app",
1144
+ image: "app:1.0",
1145
+ initContainers: [{ name: "migrate", image: "migrate:1.0", command: ["./migrate.sh"] }],
1146
+ });
1147
+ const spec = result.deployment.spec as any;
1148
+ expect(spec.template.spec.initContainers).toHaveLength(1);
1149
+ expect(spec.template.spec.initContainers[0].name).toBe("migrate");
1150
+ });
1151
+
1152
+ test("securityContext passed through", () => {
1153
+ const result = WebApp({
1154
+ name: "app",
1155
+ image: "app:1.0",
1156
+ securityContext: { runAsNonRoot: true, readOnlyRootFilesystem: true },
1157
+ });
1158
+ const spec = result.deployment.spec as any;
1159
+ const container = spec.template.spec.containers[0];
1160
+ expect(container.securityContext.runAsNonRoot).toBe(true);
1161
+ });
1162
+
1163
+ test("terminationGracePeriodSeconds set", () => {
1164
+ const result = WebApp({ name: "app", image: "app:1.0", terminationGracePeriodSeconds: 60 });
1165
+ const spec = result.deployment.spec as any;
1166
+ expect(spec.template.spec.terminationGracePeriodSeconds).toBe(60);
1167
+ });
1168
+
1169
+ test("priorityClassName set", () => {
1170
+ const result = WebApp({ name: "app", image: "app:1.0", priorityClassName: "high-priority" });
1171
+ const spec = result.deployment.spec as any;
1172
+ expect(spec.template.spec.priorityClassName).toBe("high-priority");
1173
+ });
1174
+
1175
+ test("multi-path ingress", () => {
1176
+ const result = WebApp({
1177
+ name: "app",
1178
+ image: "app:1.0",
1179
+ ingressHost: "app.example.com",
1180
+ ingressPaths: [
1181
+ { path: "/api", serviceName: "api", servicePort: 8080 },
1182
+ { path: "/web", serviceName: "web", servicePort: 3000 },
1183
+ ],
1184
+ });
1185
+ const spec = result.ingress!.spec as any;
1186
+ expect(spec.rules[0].http.paths).toHaveLength(2);
1187
+ expect(spec.rules[0].http.paths[0].path).toBe("/api");
1188
+ expect(spec.rules[0].http.paths[1].backend.service.name).toBe("web");
1189
+ });
1190
+ });
1191
+
1192
+ describe("StatefulApp hardening", () => {
1193
+ test("PDB created when minAvailable set", () => {
1194
+ const result = StatefulApp({ name: "db", image: "postgres:16", minAvailable: 1 });
1195
+ expect(result.pdb).toBeDefined();
1196
+ const spec = result.pdb!.spec as any;
1197
+ expect(spec.minAvailable).toBe(1);
1198
+ });
1199
+
1200
+ test("initContainers passed through", () => {
1201
+ const result = StatefulApp({
1202
+ name: "db",
1203
+ image: "postgres:16",
1204
+ initContainers: [{ name: "init", image: "init:1.0" }],
1205
+ });
1206
+ const spec = result.statefulSet.spec as any;
1207
+ expect(spec.template.spec.initContainers).toHaveLength(1);
1208
+ });
1209
+
1210
+ test("securityContext passed through", () => {
1211
+ const result = StatefulApp({
1212
+ name: "db",
1213
+ image: "postgres:16",
1214
+ securityContext: { runAsNonRoot: true },
1215
+ });
1216
+ const spec = result.statefulSet.spec as any;
1217
+ expect(spec.template.spec.containers[0].securityContext.runAsNonRoot).toBe(true);
1218
+ });
1219
+ });
1220
+
1221
+ describe("WorkerPool hardening", () => {
1222
+ test("PDB created when minAvailable set", () => {
1223
+ const result = WorkerPool({ name: "w", image: "w:1.0", minAvailable: 1 });
1224
+ expect(result.pdb).toBeDefined();
1225
+ const spec = result.pdb!.spec as any;
1226
+ expect(spec.minAvailable).toBe(1);
1227
+ });
1228
+
1229
+ test("securityContext on container", () => {
1230
+ const result = WorkerPool({
1231
+ name: "w",
1232
+ image: "w:1.0",
1233
+ securityContext: { runAsNonRoot: true },
1234
+ });
1235
+ const spec = result.deployment.spec as any;
1236
+ expect(spec.template.spec.containers[0].securityContext.runAsNonRoot).toBe(true);
1237
+ });
1238
+ });
1239
+
1240
+ describe("AutoscaledService hardening", () => {
1241
+ const minProps = { name: "api", image: "api:1.0", maxReplicas: 10, cpuRequest: "100m", memoryRequest: "128Mi" };
1242
+
1243
+ test("initContainers passed through", () => {
1244
+ const result = AutoscaledService({
1245
+ ...minProps,
1246
+ initContainers: [{ name: "migrate", image: "m:1.0" }],
1247
+ });
1248
+ const spec = result.deployment.spec as any;
1249
+ expect(spec.template.spec.initContainers).toHaveLength(1);
1250
+ });
1251
+
1252
+ test("securityContext on container", () => {
1253
+ const result = AutoscaledService({
1254
+ ...minProps,
1255
+ securityContext: { runAsNonRoot: true },
1256
+ });
1257
+ const spec = result.deployment.spec as any;
1258
+ expect(spec.template.spec.containers[0].securityContext.runAsNonRoot).toBe(true);
1259
+ });
1260
+
1261
+ test("terminationGracePeriodSeconds set", () => {
1262
+ const result = AutoscaledService({ ...minProps, terminationGracePeriodSeconds: 30 });
1263
+ const spec = result.deployment.spec as any;
1264
+ expect(spec.template.spec.terminationGracePeriodSeconds).toBe(30);
1265
+ });
1266
+ });
1267
+
1268
+ // ── BatchJob ────────────────────────────────────────────────────────
1269
+
1270
+ describe("BatchJob", () => {
1271
+ const minProps = { name: "migrate", image: "migrate:1.0" };
1272
+
1273
+ test("returns job with default RBAC", () => {
1274
+ const result = BatchJob(minProps);
1275
+ expect(result.job).toBeDefined();
1276
+ expect(result.serviceAccount).toBeDefined();
1277
+ expect(result.role).toBeDefined();
1278
+ expect(result.roleBinding).toBeDefined();
1279
+ });
1280
+
1281
+ test("default backoffLimit is 6", () => {
1282
+ const result = BatchJob(minProps);
1283
+ const spec = result.job.spec as any;
1284
+ expect(spec.backoffLimit).toBe(6);
1285
+ });
1286
+
1287
+ test("custom backoffLimit and ttl", () => {
1288
+ const result = BatchJob({ ...minProps, backoffLimit: 3, ttlSecondsAfterFinished: 3600 });
1289
+ const spec = result.job.spec as any;
1290
+ expect(spec.backoffLimit).toBe(3);
1291
+ expect(spec.ttlSecondsAfterFinished).toBe(3600);
1292
+ });
1293
+
1294
+ test("parallelism and completions", () => {
1295
+ const result = BatchJob({ ...minProps, parallelism: 3, completions: 10 });
1296
+ const spec = result.job.spec as any;
1297
+ expect(spec.parallelism).toBe(3);
1298
+ expect(spec.completions).toBe(10);
1299
+ });
1300
+
1301
+ test("rbacRules: [] skips RBAC", () => {
1302
+ const result = BatchJob({ ...minProps, rbacRules: [] });
1303
+ expect(result.serviceAccount).toBeUndefined();
1304
+ expect(result.role).toBeUndefined();
1305
+ expect(result.roleBinding).toBeUndefined();
1306
+ });
1307
+
1308
+ test("command and args passed through", () => {
1309
+ const result = BatchJob({ ...minProps, command: ["python"], args: ["migrate.py"] });
1310
+ const spec = result.job.spec as any;
1311
+ const container = spec.template.spec.containers[0];
1312
+ expect(container.command).toEqual(["python"]);
1313
+ expect(container.args).toEqual(["migrate.py"]);
1314
+ });
1315
+
1316
+ test("namespace propagated", () => {
1317
+ const result = BatchJob({ ...minProps, namespace: "jobs" });
1318
+ expect((result.job.metadata as any).namespace).toBe("jobs");
1319
+ expect((result.serviceAccount!.metadata as any).namespace).toBe("jobs");
1320
+ });
1321
+
1322
+ test("component labels", () => {
1323
+ const result = BatchJob(minProps);
1324
+ expect((result.job.metadata as any).labels["app.kubernetes.io/component"]).toBe("batch");
1325
+ expect((result.role!.metadata as any).labels["app.kubernetes.io/component"]).toBe("rbac");
1326
+ });
1327
+ });
1328
+
1329
+ // ── SecureIngress ───────────────────────────────────────────────────
1330
+
1331
+ describe("SecureIngress", () => {
1332
+ const minProps = {
1333
+ name: "app-ingress",
1334
+ hosts: [{ hostname: "app.example.com", paths: [{ path: "/", serviceName: "app", servicePort: 80 }] }],
1335
+ };
1336
+
1337
+ test("returns ingress without certificate by default", () => {
1338
+ const result = SecureIngress(minProps);
1339
+ expect(result.ingress).toBeDefined();
1340
+ expect(result.certificate).toBeUndefined();
1341
+ });
1342
+
1343
+ test("creates certificate when clusterIssuer set", () => {
1344
+ const result = SecureIngress({ ...minProps, clusterIssuer: "letsencrypt-prod" });
1345
+ expect(result.certificate).toBeDefined();
1346
+ const certSpec = result.certificate!.spec as any;
1347
+ expect(certSpec.issuerRef.name).toBe("letsencrypt-prod");
1348
+ expect(certSpec.dnsNames).toEqual(["app.example.com"]);
1349
+ });
1350
+
1351
+ test("TLS on ingress when clusterIssuer set", () => {
1352
+ const result = SecureIngress({ ...minProps, clusterIssuer: "letsencrypt-prod" });
1353
+ const spec = result.ingress.spec as any;
1354
+ expect(spec.tls).toBeDefined();
1355
+ expect(spec.tls[0].hosts).toEqual(["app.example.com"]);
1356
+ });
1357
+
1358
+ test("multi-host support", () => {
1359
+ const result = SecureIngress({
1360
+ name: "multi",
1361
+ hosts: [
1362
+ { hostname: "api.example.com", paths: [{ path: "/", serviceName: "api", servicePort: 80 }] },
1363
+ { hostname: "admin.example.com", paths: [{ path: "/", serviceName: "admin", servicePort: 80 }] },
1364
+ ],
1365
+ clusterIssuer: "letsencrypt-prod",
1366
+ });
1367
+ const spec = result.ingress.spec as any;
1368
+ expect(spec.rules).toHaveLength(2);
1369
+ const certSpec = result.certificate!.spec as any;
1370
+ expect(certSpec.dnsNames).toHaveLength(2);
1371
+ });
1372
+
1373
+ test("multi-path support", () => {
1374
+ const result = SecureIngress({
1375
+ name: "multi-path",
1376
+ hosts: [{
1377
+ hostname: "app.example.com",
1378
+ paths: [
1379
+ { path: "/api", serviceName: "api", servicePort: 8080 },
1380
+ { path: "/web", serviceName: "web", servicePort: 3000 },
1381
+ ],
1382
+ }],
1383
+ });
1384
+ const spec = result.ingress.spec as any;
1385
+ expect(spec.rules[0].http.paths).toHaveLength(2);
1386
+ });
1387
+
1388
+ test("ingressClassName set", () => {
1389
+ const result = SecureIngress({ ...minProps, ingressClassName: "nginx" });
1390
+ const spec = result.ingress.spec as any;
1391
+ expect(spec.ingressClassName).toBe("nginx");
1392
+ });
1393
+
1394
+ test("cert-manager annotation added", () => {
1395
+ const result = SecureIngress({ ...minProps, clusterIssuer: "letsencrypt-prod" });
1396
+ const meta = result.ingress.metadata as any;
1397
+ expect(meta.annotations["cert-manager.io/cluster-issuer"]).toBe("letsencrypt-prod");
1398
+ });
1399
+ });
1400
+
1401
+ // ── ConfiguredApp ───────────────────────────────────────────────────
1402
+
1403
+ describe("ConfiguredApp", () => {
1404
+ const minProps = { name: "api", image: "api:1.0" };
1405
+
1406
+ test("returns deployment and service", () => {
1407
+ const result = ConfiguredApp(minProps);
1408
+ expect(result.deployment).toBeDefined();
1409
+ expect(result.service).toBeDefined();
1410
+ expect(result.configMap).toBeUndefined();
1411
+ });
1412
+
1413
+ test("creates ConfigMap when configData provided", () => {
1414
+ const result = ConfiguredApp({ ...minProps, configData: { "app.conf": "key=val" }, configMountPath: "/etc/app" });
1415
+ expect(result.configMap).toBeDefined();
1416
+ expect((result.configMap as any).data["app.conf"]).toBe("key=val");
1417
+ });
1418
+
1419
+ test("configMap volume mounted", () => {
1420
+ const result = ConfiguredApp({ ...minProps, configData: { "k": "v" }, configMountPath: "/etc/app" });
1421
+ const spec = result.deployment.spec as any;
1422
+ const container = spec.template.spec.containers[0];
1423
+ expect(container.volumeMounts).toHaveLength(1);
1424
+ expect(container.volumeMounts[0].mountPath).toBe("/etc/app");
1425
+ expect(spec.template.spec.volumes[0].configMap.name).toBe("api-config");
1426
+ });
1427
+
1428
+ test("secret volume mounted", () => {
1429
+ const result = ConfiguredApp({ ...minProps, secretName: "creds", secretMountPath: "/secrets" });
1430
+ const spec = result.deployment.spec as any;
1431
+ const container = spec.template.spec.containers[0];
1432
+ expect(container.volumeMounts[0].mountPath).toBe("/secrets");
1433
+ expect(spec.template.spec.volumes[0].secret.secretName).toBe("creds");
1434
+ });
1435
+
1436
+ test("envFrom with configMapRef and secretRef", () => {
1437
+ const result = ConfiguredApp({
1438
+ ...minProps,
1439
+ envFrom: { configMapRef: "my-config", secretRef: "my-secret" },
1440
+ });
1441
+ const spec = result.deployment.spec as any;
1442
+ const container = spec.template.spec.containers[0];
1443
+ expect(container.envFrom).toHaveLength(2);
1444
+ expect(container.envFrom[0].configMapRef.name).toBe("my-config");
1445
+ expect(container.envFrom[1].secretRef.name).toBe("my-secret");
1446
+ });
1447
+
1448
+ test("initContainers supported", () => {
1449
+ const result = ConfiguredApp({
1450
+ ...minProps,
1451
+ initContainers: [{ name: "init", image: "init:1.0", command: ["sh"] }],
1452
+ });
1453
+ const spec = result.deployment.spec as any;
1454
+ expect(spec.template.spec.initContainers).toHaveLength(1);
1455
+ });
1456
+
1457
+ test("namespace propagated", () => {
1458
+ const result = ConfiguredApp({ ...minProps, namespace: "prod" });
1459
+ expect((result.deployment.metadata as any).namespace).toBe("prod");
1460
+ expect((result.service.metadata as any).namespace).toBe("prod");
1461
+ });
1462
+ });
1463
+
1464
+ // ── SidecarApp ──────────────────────────────────────────────────────
1465
+
1466
+ describe("SidecarApp", () => {
1467
+ const minProps = {
1468
+ name: "api",
1469
+ image: "api:1.0",
1470
+ sidecars: [{ name: "envoy", image: "envoy:v1.28" }],
1471
+ };
1472
+
1473
+ test("returns deployment and service", () => {
1474
+ const result = SidecarApp(minProps);
1475
+ expect(result.deployment).toBeDefined();
1476
+ expect(result.service).toBeDefined();
1477
+ });
1478
+
1479
+ test("has multiple containers", () => {
1480
+ const result = SidecarApp(minProps);
1481
+ const spec = result.deployment.spec as any;
1482
+ expect(spec.template.spec.containers).toHaveLength(2);
1483
+ expect(spec.template.spec.containers[0].name).toBe("api");
1484
+ expect(spec.template.spec.containers[1].name).toBe("envoy");
1485
+ });
1486
+
1487
+ test("sidecar ports passed through", () => {
1488
+ const result = SidecarApp({
1489
+ ...minProps,
1490
+ sidecars: [{ name: "envoy", image: "envoy:v1.28", ports: [{ containerPort: 9901, name: "admin" }] }],
1491
+ });
1492
+ const spec = result.deployment.spec as any;
1493
+ expect(spec.template.spec.containers[1].ports[0].containerPort).toBe(9901);
1494
+ });
1495
+
1496
+ test("initContainers supported", () => {
1497
+ const result = SidecarApp({
1498
+ ...minProps,
1499
+ initContainers: [{ name: "migrate", image: "m:1.0", command: ["./migrate.sh"] }],
1500
+ });
1501
+ const spec = result.deployment.spec as any;
1502
+ expect(spec.template.spec.initContainers).toHaveLength(1);
1503
+ });
1504
+
1505
+ test("sharedVolumes creates volumes", () => {
1506
+ const result = SidecarApp({
1507
+ ...minProps,
1508
+ sharedVolumes: [{ name: "tmp" }, { name: "config", configMapName: "my-config" }],
1509
+ });
1510
+ const spec = result.deployment.spec as any;
1511
+ expect(spec.template.spec.volumes).toHaveLength(2);
1512
+ expect(spec.template.spec.volumes[0].emptyDir).toBeDefined();
1513
+ expect(spec.template.spec.volumes[1].configMap.name).toBe("my-config");
1514
+ });
1515
+
1516
+ test("common labels on all resources", () => {
1517
+ const result = SidecarApp(minProps);
1518
+ expect((result.deployment.metadata as any).labels["app.kubernetes.io/managed-by"]).toBe("chant");
1519
+ expect((result.service.metadata as any).labels["app.kubernetes.io/managed-by"]).toBe("chant");
1520
+ });
1521
+ });
1522
+
1523
+ // ── MonitoredService ────────────────────────────────────────────────
1524
+
1525
+ describe("MonitoredService", () => {
1526
+ const minProps = { name: "api", image: "api:1.0" };
1527
+
1528
+ test("returns deployment, service, serviceMonitor", () => {
1529
+ const result = MonitoredService(minProps);
1530
+ expect(result.deployment).toBeDefined();
1531
+ expect(result.service).toBeDefined();
1532
+ expect(result.serviceMonitor).toBeDefined();
1533
+ expect(result.prometheusRule).toBeUndefined();
1534
+ });
1535
+
1536
+ test("prometheusRule created when alertRules provided", () => {
1537
+ const result = MonitoredService({
1538
+ ...minProps,
1539
+ alertRules: [{ name: "HighError", expr: "rate(errors[5m]) > 0.1", severity: "critical" }],
1540
+ });
1541
+ expect(result.prometheusRule).toBeDefined();
1542
+ const spec = result.prometheusRule!.spec as any;
1543
+ expect(spec.groups[0].rules[0].alert).toBe("HighError");
1544
+ expect(spec.groups[0].rules[0].labels.severity).toBe("critical");
1545
+ });
1546
+
1547
+ test("serviceMonitor has correct selector and endpoint", () => {
1548
+ const result = MonitoredService({ ...minProps, metricsPort: 9090, metricsPath: "/metrics", scrapeInterval: "15s" });
1549
+ const spec = result.serviceMonitor.spec as any;
1550
+ expect(spec.selector.matchLabels["app.kubernetes.io/name"]).toBe("api");
1551
+ expect(spec.endpoints[0].port).toBe("metrics");
1552
+ expect(spec.endpoints[0].path).toBe("/metrics");
1553
+ expect(spec.endpoints[0].interval).toBe("15s");
1554
+ });
1555
+
1556
+ test("separate metrics port on container and service", () => {
1557
+ const result = MonitoredService({ ...minProps, port: 8080, metricsPort: 9090 });
1558
+ const spec = result.deployment.spec as any;
1559
+ const ports = spec.template.spec.containers[0].ports;
1560
+ expect(ports).toHaveLength(2);
1561
+ expect(ports[0].containerPort).toBe(8080);
1562
+ expect(ports[1].containerPort).toBe(9090);
1563
+ });
1564
+
1565
+ test("component labels", () => {
1566
+ const result = MonitoredService({ ...minProps, alertRules: [{ name: "A", expr: "1" }] });
1567
+ expect((result.serviceMonitor.metadata as any).labels["app.kubernetes.io/component"]).toBe("monitoring");
1568
+ expect((result.prometheusRule!.metadata as any).labels["app.kubernetes.io/component"]).toBe("monitoring");
1569
+ });
1570
+ });
1571
+
1572
+ // ── NetworkIsolatedApp ──────────────────────────────────────────────
1573
+
1574
+ describe("NetworkIsolatedApp", () => {
1575
+ const minProps = { name: "api", image: "api:1.0" };
1576
+
1577
+ test("returns deployment, service, networkPolicy", () => {
1578
+ const result = NetworkIsolatedApp(minProps);
1579
+ expect(result.deployment).toBeDefined();
1580
+ expect(result.service).toBeDefined();
1581
+ expect(result.networkPolicy).toBeDefined();
1582
+ });
1583
+
1584
+ test("networkPolicy podSelector matches app", () => {
1585
+ const result = NetworkIsolatedApp(minProps);
1586
+ const spec = result.networkPolicy.spec as any;
1587
+ expect(spec.podSelector.matchLabels["app.kubernetes.io/name"]).toBe("api");
1588
+ });
1589
+
1590
+ test("ingress rules created", () => {
1591
+ const result = NetworkIsolatedApp({
1592
+ ...minProps,
1593
+ allowIngressFrom: [{ podSelector: { "app.kubernetes.io/name": "frontend" } }],
1594
+ });
1595
+ const spec = result.networkPolicy.spec as any;
1596
+ expect(spec.policyTypes).toContain("Ingress");
1597
+ expect(spec.ingress[0].from[0].podSelector.matchLabels["app.kubernetes.io/name"]).toBe("frontend");
1598
+ });
1599
+
1600
+ test("egress rules with ports", () => {
1601
+ const result = NetworkIsolatedApp({
1602
+ ...minProps,
1603
+ allowEgressTo: [{ podSelector: { "app.kubernetes.io/name": "db" }, ports: [{ port: 5432 }] }],
1604
+ });
1605
+ const spec = result.networkPolicy.spec as any;
1606
+ expect(spec.policyTypes).toContain("Egress");
1607
+ expect(spec.egress[0].ports[0].port).toBe(5432);
1608
+ });
1609
+
1610
+ test("namespace propagated", () => {
1611
+ const result = NetworkIsolatedApp({ ...minProps, namespace: "prod" });
1612
+ expect((result.networkPolicy.metadata as any).namespace).toBe("prod");
1613
+ });
1614
+
1615
+ test("component label on networkPolicy", () => {
1616
+ const result = NetworkIsolatedApp(minProps);
1617
+ expect((result.networkPolicy.metadata as any).labels["app.kubernetes.io/component"]).toBe("network-policy");
1618
+ });
1619
+ });
1620
+
1621
+ // ── IrsaServiceAccount ──────────────────────────────────────────────
1622
+
1623
+ describe("IrsaServiceAccount", () => {
1624
+ const minProps = { name: "app-sa", iamRoleArn: "arn:aws:iam::123456789012:role/app-role" };
1625
+
1626
+ test("returns serviceAccount with IRSA annotation", () => {
1627
+ const result = IrsaServiceAccount(minProps);
1628
+ expect(result.serviceAccount).toBeDefined();
1629
+ const meta = result.serviceAccount.metadata as any;
1630
+ expect(meta.annotations["eks.amazonaws.com/role-arn"]).toBe("arn:aws:iam::123456789012:role/app-role");
1631
+ });
1632
+
1633
+ test("no RBAC by default", () => {
1634
+ const result = IrsaServiceAccount(minProps);
1635
+ expect(result.role).toBeUndefined();
1636
+ expect(result.roleBinding).toBeUndefined();
1637
+ });
1638
+
1639
+ test("RBAC created when rules provided", () => {
1640
+ const result = IrsaServiceAccount({
1641
+ ...minProps,
1642
+ rbacRules: [{ apiGroups: [""], resources: ["secrets"], verbs: ["get"] }],
1643
+ });
1644
+ expect(result.role).toBeDefined();
1645
+ expect(result.roleBinding).toBeDefined();
1646
+ const role = result.role as any;
1647
+ expect(role.rules[0].resources).toEqual(["secrets"]);
1648
+ });
1649
+
1650
+ test("namespace propagated", () => {
1651
+ const result = IrsaServiceAccount({ ...minProps, namespace: "prod" });
1652
+ expect((result.serviceAccount.metadata as any).namespace).toBe("prod");
1653
+ });
1654
+
1655
+ test("component labels", () => {
1656
+ const result = IrsaServiceAccount(minProps);
1657
+ expect((result.serviceAccount.metadata as any).labels["app.kubernetes.io/component"]).toBe("service-account");
1658
+ });
1659
+ });
1660
+
1661
+ // ── AlbIngress ──────────────────────────────────────────────────────
1662
+
1663
+ describe("AlbIngress", () => {
1664
+ const minProps = {
1665
+ name: "api-ingress",
1666
+ hosts: [{ hostname: "api.example.com", paths: [{ path: "/", serviceName: "api", servicePort: 80 }] }],
1667
+ };
1668
+
1669
+ test("returns ingress with ALB annotations", () => {
1670
+ const result = AlbIngress(minProps);
1671
+ expect(result.ingress).toBeDefined();
1672
+ const meta = result.ingress.metadata as any;
1673
+ expect(meta.annotations["alb.ingress.kubernetes.io/scheme"]).toBe("internet-facing");
1674
+ expect(meta.annotations["alb.ingress.kubernetes.io/target-type"]).toBe("ip");
1675
+ });
1676
+
1677
+ test("ingressClassName is alb", () => {
1678
+ const result = AlbIngress(minProps);
1679
+ const spec = result.ingress.spec as any;
1680
+ expect(spec.ingressClassName).toBe("alb");
1681
+ });
1682
+
1683
+ test("certificate ARN sets TLS annotations", () => {
1684
+ const result = AlbIngress({ ...minProps, certificateArn: "arn:aws:acm:us-east-1:123:cert/abc" });
1685
+ const meta = result.ingress.metadata as any;
1686
+ expect(meta.annotations["alb.ingress.kubernetes.io/certificate-arn"]).toBe("arn:aws:acm:us-east-1:123:cert/abc");
1687
+ expect(meta.annotations["alb.ingress.kubernetes.io/ssl-redirect"]).toBe("443");
1688
+ });
1689
+
1690
+ test("groupName annotation set", () => {
1691
+ const result = AlbIngress({ ...minProps, groupName: "shared-alb" });
1692
+ const meta = result.ingress.metadata as any;
1693
+ expect(meta.annotations["alb.ingress.kubernetes.io/group.name"]).toBe("shared-alb");
1694
+ });
1695
+
1696
+ test("WAF ACL annotation set", () => {
1697
+ const result = AlbIngress({ ...minProps, wafAclArn: "arn:aws:wafv2:us-east-1:123:regional/webacl/abc" });
1698
+ const meta = result.ingress.metadata as any;
1699
+ expect(meta.annotations["alb.ingress.kubernetes.io/wafv2-acl-arn"]).toBe("arn:aws:wafv2:us-east-1:123:regional/webacl/abc");
1700
+ });
1701
+
1702
+ test("internal scheme", () => {
1703
+ const result = AlbIngress({ ...minProps, scheme: "internal" });
1704
+ const meta = result.ingress.metadata as any;
1705
+ expect(meta.annotations["alb.ingress.kubernetes.io/scheme"]).toBe("internal");
1706
+ });
1707
+ });
1708
+
1709
+ // ── EbsStorageClass ─────────────────────────────────────────────────
1710
+
1711
+ describe("EbsStorageClass", () => {
1712
+ test("returns storageClass with EBS provisioner", () => {
1713
+ const result = EbsStorageClass({ name: "gp3" });
1714
+ expect(result.storageClass).toBeDefined();
1715
+ expect((result.storageClass as any).provisioner).toBe("ebs.csi.aws.com");
1716
+ });
1717
+
1718
+ test("default type is gp3", () => {
1719
+ const result = EbsStorageClass({ name: "default" });
1720
+ expect((result.storageClass as any).parameters.type).toBe("gp3");
1721
+ });
1722
+
1723
+ test("encryption enabled by default", () => {
1724
+ const result = EbsStorageClass({ name: "enc" });
1725
+ expect((result.storageClass as any).parameters.encrypted).toBe("true");
1726
+ });
1727
+
1728
+ test("custom parameters", () => {
1729
+ const result = EbsStorageClass({ name: "custom", type: "io2", iops: "5000", throughput: "250" });
1730
+ const params = (result.storageClass as any).parameters;
1731
+ expect(params.type).toBe("io2");
1732
+ expect(params.iops).toBe("5000");
1733
+ expect(params.throughput).toBe("250");
1734
+ });
1735
+
1736
+ test("allowVolumeExpansion default true", () => {
1737
+ const result = EbsStorageClass({ name: "exp" });
1738
+ expect((result.storageClass as any).allowVolumeExpansion).toBe(true);
1739
+ });
1740
+
1741
+ test("storageClass is cluster-scoped (no namespace)", () => {
1742
+ const result = EbsStorageClass({ name: "sc" });
1743
+ expect((result.storageClass.metadata as any).namespace).toBeUndefined();
1744
+ });
1745
+ });
1746
+
1747
+ // ── EfsStorageClass ─────────────────────────────────────────────────
1748
+
1749
+ describe("EfsStorageClass", () => {
1750
+ test("returns storageClass with EFS provisioner", () => {
1751
+ const result = EfsStorageClass({ name: "efs", fileSystemId: "fs-123" });
1752
+ expect((result.storageClass as any).provisioner).toBe("efs.csi.aws.com");
1753
+ });
1754
+
1755
+ test("fileSystemId in parameters", () => {
1756
+ const result = EfsStorageClass({ name: "efs", fileSystemId: "fs-abc" });
1757
+ expect((result.storageClass as any).parameters.fileSystemId).toBe("fs-abc");
1758
+ });
1759
+
1760
+ test("default provisioningMode is efs-ap", () => {
1761
+ const result = EfsStorageClass({ name: "efs", fileSystemId: "fs-123" });
1762
+ expect((result.storageClass as any).parameters.provisioningMode).toBe("efs-ap");
1763
+ });
1764
+ });
1765
+
1766
+ // ── FluentBitAgent ──────────────────────────────────────────────────
1767
+
1768
+ describe("FluentBitAgent", () => {
1769
+ const minProps = { logGroup: "/aws/eks/cluster/containers", region: "us-east-1", clusterName: "cluster" };
1770
+
1771
+ test("returns all 5 resources", () => {
1772
+ const result = FluentBitAgent(minProps);
1773
+ expect(result.daemonSet).toBeDefined();
1774
+ expect(result.serviceAccount).toBeDefined();
1775
+ expect(result.clusterRole).toBeDefined();
1776
+ expect(result.clusterRoleBinding).toBeDefined();
1777
+ expect(result.configMap).toBeDefined();
1778
+ });
1779
+
1780
+ test("default namespace is amazon-cloudwatch", () => {
1781
+ const result = FluentBitAgent(minProps);
1782
+ expect((result.daemonSet.metadata as any).namespace).toBe("amazon-cloudwatch");
1783
+ });
1784
+
1785
+ test("configMap contains fluent-bit config with region", () => {
1786
+ const result = FluentBitAgent(minProps);
1787
+ const data = (result.configMap as any).data;
1788
+ expect(data["fluent-bit.conf"]).toContain("us-east-1");
1789
+ expect(data["fluent-bit.conf"]).toContain("/aws/eks/cluster/containers");
1790
+ });
1791
+
1792
+ test("tolerations for all nodes", () => {
1793
+ const result = FluentBitAgent(minProps);
1794
+ const spec = result.daemonSet.spec as any;
1795
+ expect(spec.template.spec.tolerations).toEqual([{ operator: "Exists" }]);
1796
+ });
1797
+
1798
+ test("clusterRole is cluster-scoped", () => {
1799
+ const result = FluentBitAgent(minProps);
1800
+ expect((result.clusterRole.metadata as any).namespace).toBeUndefined();
1801
+ });
1802
+
1803
+ test("IRSA annotation when iamRoleArn set", () => {
1804
+ const result = FluentBitAgent({ ...minProps, iamRoleArn: "arn:aws:iam::123456789012:role/fb-role" });
1805
+ const meta = result.serviceAccount.metadata as any;
1806
+ expect(meta.annotations["eks.amazonaws.com/role-arn"]).toBe("arn:aws:iam::123456789012:role/fb-role");
1807
+ });
1808
+
1809
+ test("no annotation when iamRoleArn omitted", () => {
1810
+ const result = FluentBitAgent(minProps);
1811
+ const meta = result.serviceAccount.metadata as any;
1812
+ expect(meta.annotations).toBeUndefined();
1813
+ });
1814
+ });
1815
+
1816
+ // ── ExternalDnsAgent ────────────────────────────────────────────────
1817
+
1818
+ describe("ExternalDnsAgent", () => {
1819
+ const minProps = {
1820
+ iamRoleArn: "arn:aws:iam::123456789012:role/external-dns",
1821
+ domainFilters: ["example.com"],
1822
+ };
1823
+
1824
+ test("returns deployment, serviceAccount, clusterRole, clusterRoleBinding", () => {
1825
+ const result = ExternalDnsAgent(minProps);
1826
+ expect(result.deployment).toBeDefined();
1827
+ expect(result.serviceAccount).toBeDefined();
1828
+ expect(result.clusterRole).toBeDefined();
1829
+ expect(result.clusterRoleBinding).toBeDefined();
1830
+ });
1831
+
1832
+ test("IRSA annotation on serviceAccount", () => {
1833
+ const result = ExternalDnsAgent(minProps);
1834
+ const meta = result.serviceAccount.metadata as any;
1835
+ expect(meta.annotations["eks.amazonaws.com/role-arn"]).toBe("arn:aws:iam::123456789012:role/external-dns");
1836
+ });
1837
+
1838
+ test("domain filter in args", () => {
1839
+ const result = ExternalDnsAgent(minProps);
1840
+ const spec = result.deployment.spec as any;
1841
+ const args = spec.template.spec.containers[0].args;
1842
+ expect(args).toContain("--domain-filter=example.com");
1843
+ });
1844
+
1845
+ test("txtOwnerId in args when set", () => {
1846
+ const result = ExternalDnsAgent({ ...minProps, txtOwnerId: "my-cluster" });
1847
+ const spec = result.deployment.spec as any;
1848
+ const args = spec.template.spec.containers[0].args;
1849
+ expect(args).toContain("--txt-owner-id=my-cluster");
1850
+ });
1851
+
1852
+ test("default namespace is kube-system", () => {
1853
+ const result = ExternalDnsAgent(minProps);
1854
+ expect((result.deployment.metadata as any).namespace).toBe("kube-system");
1855
+ });
1856
+
1857
+ test("replicas is 1", () => {
1858
+ const result = ExternalDnsAgent(minProps);
1859
+ const spec = result.deployment.spec as any;
1860
+ expect(spec.replicas).toBe(1);
1861
+ });
1862
+ });
1863
+
1864
+ // ── AdotCollector ───────────────────────────────────────────────────
1865
+
1866
+ describe("AdotCollector", () => {
1867
+ const minProps = { region: "us-east-1", clusterName: "cluster" };
1868
+
1869
+ test("returns all 5 resources", () => {
1870
+ const result = AdotCollector(minProps);
1871
+ expect(result.daemonSet).toBeDefined();
1872
+ expect(result.serviceAccount).toBeDefined();
1873
+ expect(result.clusterRole).toBeDefined();
1874
+ expect(result.clusterRoleBinding).toBeDefined();
1875
+ expect(result.configMap).toBeDefined();
1876
+ });
1877
+
1878
+ test("default namespace is amazon-metrics", () => {
1879
+ const result = AdotCollector(minProps);
1880
+ expect((result.daemonSet.metadata as any).namespace).toBe("amazon-metrics");
1881
+ });
1882
+
1883
+ test("configMap contains ADOT config with region", () => {
1884
+ const result = AdotCollector(minProps);
1885
+ const data = (result.configMap as any).data;
1886
+ expect(data["config.yaml"]).toContain("us-east-1");
1887
+ expect(data["config.yaml"]).toContain("cluster");
1888
+ });
1889
+
1890
+ test("OTLP ports on container", () => {
1891
+ const result = AdotCollector(minProps);
1892
+ const spec = result.daemonSet.spec as any;
1893
+ const ports = spec.template.spec.containers[0].ports;
1894
+ expect(ports).toHaveLength(2);
1895
+ expect(ports[0].containerPort).toBe(4317);
1896
+ expect(ports[1].containerPort).toBe(4318);
1897
+ });
1898
+
1899
+ test("tolerations for all nodes", () => {
1900
+ const result = AdotCollector(minProps);
1901
+ const spec = result.daemonSet.spec as any;
1902
+ expect(spec.template.spec.tolerations).toEqual([{ operator: "Exists" }]);
1903
+ });
1904
+
1905
+ test("custom exporters", () => {
1906
+ const result = AdotCollector({ ...minProps, exporters: ["prometheus"] });
1907
+ const data = (result.configMap as any).data;
1908
+ expect(data["config.yaml"]).toContain("prometheusremotewrite");
1909
+ });
1910
+
1911
+ test("IRSA annotation when iamRoleArn set", () => {
1912
+ const result = AdotCollector({ ...minProps, iamRoleArn: "arn:aws:iam::123456789012:role/adot-role" });
1913
+ const meta = result.serviceAccount.metadata as any;
1914
+ expect(meta.annotations["eks.amazonaws.com/role-arn"]).toBe("arn:aws:iam::123456789012:role/adot-role");
1915
+ });
1916
+
1917
+ test("no annotation when iamRoleArn omitted", () => {
1918
+ const result = AdotCollector(minProps);
1919
+ const meta = result.serviceAccount.metadata as any;
1920
+ expect(meta.annotations).toBeUndefined();
1921
+ });
1922
+ });
1923
+
1924
+ // ── MetricsServer ──────────────────────────────────────────────────
1925
+
1926
+ describe("MetricsServer", () => {
1927
+ test("returns all 8 resources", () => {
1928
+ const result = MetricsServer({});
1929
+ expect(result.deployment).toBeDefined();
1930
+ expect(result.service).toBeDefined();
1931
+ expect(result.serviceAccount).toBeDefined();
1932
+ expect(result.clusterRole).toBeDefined();
1933
+ expect(result.clusterRoleBinding).toBeDefined();
1934
+ expect(result.aggregatedClusterRole).toBeDefined();
1935
+ expect(result.authDelegatorBinding).toBeDefined();
1936
+ expect(result.apiService).toBeDefined();
1937
+ });
1938
+
1939
+ test("default namespace is kube-system", () => {
1940
+ const result = MetricsServer({});
1941
+ expect((result.deployment.metadata as any).namespace).toBe("kube-system");
1942
+ expect((result.service.metadata as any).namespace).toBe("kube-system");
1943
+ expect((result.serviceAccount.metadata as any).namespace).toBe("kube-system");
1944
+ });
1945
+
1946
+ test("service targets port 10250", () => {
1947
+ const result = MetricsServer({});
1948
+ const spec = result.service.spec as any;
1949
+ expect(spec.ports[0].port).toBe(443);
1950
+ expect(spec.ports[0].targetPort).toBe(10250);
1951
+ });
1952
+
1953
+ test("deployment container has correct image and args", () => {
1954
+ const result = MetricsServer({});
1955
+ const spec = result.deployment.spec as any;
1956
+ const container = spec.template.spec.containers[0];
1957
+ expect(container.image).toBe("registry.k8s.io/metrics-server/metrics-server:v0.7.2");
1958
+ expect(container.args).toContain("--secure-port=10250");
1959
+ expect(container.args).toContain("--kubelet-use-node-status-port");
1960
+ expect(container.args).toContain("--metric-resolution=15s");
1961
+ });
1962
+
1963
+ test("clusterRole has nodes/metrics access", () => {
1964
+ const result = MetricsServer({});
1965
+ const rules = result.clusterRole.rules as any[];
1966
+ const nodeMetricsRule = rules.find((r: any) => r.resources?.includes("nodes/metrics"));
1967
+ expect(nodeMetricsRule).toBeDefined();
1968
+ });
1969
+
1970
+ test("apiService references correct service", () => {
1971
+ const result = MetricsServer({});
1972
+ const spec = (result.apiService as any).spec;
1973
+ expect(spec.service.name).toBe("metrics-server");
1974
+ expect(spec.service.namespace).toBe("kube-system");
1975
+ expect(spec.group).toBe("metrics.k8s.io");
1976
+ expect(spec.version).toBe("v1beta1");
1977
+ });
1978
+
1979
+ test("aggregated clusterRole has aggregate labels", () => {
1980
+ const result = MetricsServer({});
1981
+ const labels = (result.aggregatedClusterRole.metadata as any).labels;
1982
+ expect(labels["rbac.authorization.k8s.io/aggregate-to-admin"]).toBe("true");
1983
+ expect(labels["rbac.authorization.k8s.io/aggregate-to-view"]).toBe("true");
1984
+ });
1985
+
1986
+ test("custom image and replicas", () => {
1987
+ const result = MetricsServer({ image: "custom:v1", replicas: 2 });
1988
+ const spec = result.deployment.spec as any;
1989
+ expect(spec.replicas).toBe(2);
1990
+ expect(spec.template.spec.containers[0].image).toBe("custom:v1");
1991
+ });
1992
+ });