@intentius/chant-lexicon-k8s 0.0.13 → 0.0.15
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 +20 -15
- package/dist/manifest.json +1 -1
- 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/dist/skills/chant-k8s-eks.md +156 -0
- package/dist/skills/chant-k8s-patterns.md +245 -0
- package/dist/skills/chant-k8s.md +36 -227
- package/package.json +27 -24
- package/src/codegen/docs.ts +5 -5
- package/src/composites/adot-collector.ts +245 -0
- package/src/composites/agic-ingress.ts +149 -0
- package/src/composites/alb-ingress.ts +152 -0
- package/src/composites/autoscaled-service.ts +51 -0
- 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 +221 -0
- package/src/composites/composites.test.ts +1584 -0
- package/src/composites/config-connector-context.ts +62 -0
- package/src/composites/configured-app.ts +224 -0
- package/src/composites/cron-workload.ts +6 -0
- package/src/composites/ebs-storage-class.ts +96 -0
- package/src/composites/efs-storage-class.ts +77 -0
- package/src/composites/external-dns-agent.ts +174 -0
- package/src/composites/filestore-storage-class.ts +79 -0
- package/src/composites/fluent-bit-agent.ts +220 -0
- package/src/composites/gce-pd-storage-class.ts +85 -0
- package/src/composites/gke-gateway.ts +143 -0
- package/src/composites/index.ts +47 -0
- package/src/composites/irsa-service-account.ts +114 -0
- package/src/composites/metrics-server.ts +224 -0
- package/src/composites/monitored-service.ts +221 -0
- package/src/composites/network-isolated-app.ts +202 -0
- package/src/composites/node-agent.ts +6 -0
- package/src/composites/secure-ingress.ts +149 -0
- package/src/composites/security-context.ts +10 -0
- package/src/composites/sidecar-app.ts +207 -0
- package/src/composites/stateful-app.ts +67 -15
- package/src/composites/web-app.ts +104 -35
- package/src/composites/worker-pool.ts +38 -4
- package/src/composites/workload-identity-sa.ts +118 -0
- package/src/composites/workload-identity-service-account.ts +116 -0
- package/src/index.ts +24 -2
- 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/plugin.test.ts +2 -2
- package/src/plugin.ts +556 -242
- package/src/serializer.test.ts +120 -0
- package/src/serializer.ts +16 -4
|
@@ -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";
|
|
@@ -6,6 +7,25 @@ import { AutoscaledService } from "./autoscaled-service";
|
|
|
6
7
|
import { WorkerPool } from "./worker-pool";
|
|
7
8
|
import { NamespaceEnv } from "./namespace-env";
|
|
8
9
|
import { NodeAgent } from "./node-agent";
|
|
10
|
+
import { BatchJob } from "./batch-job";
|
|
11
|
+
import { SecureIngress } from "./secure-ingress";
|
|
12
|
+
import { ConfiguredApp } from "./configured-app";
|
|
13
|
+
import { SidecarApp } from "./sidecar-app";
|
|
14
|
+
import { MonitoredService } from "./monitored-service";
|
|
15
|
+
import { NetworkIsolatedApp } from "./network-isolated-app";
|
|
16
|
+
import { IrsaServiceAccount } from "./irsa-service-account";
|
|
17
|
+
import { AlbIngress } from "./alb-ingress";
|
|
18
|
+
import { EbsStorageClass } from "./ebs-storage-class";
|
|
19
|
+
import { EfsStorageClass } from "./efs-storage-class";
|
|
20
|
+
import { FluentBitAgent } from "./fluent-bit-agent";
|
|
21
|
+
import { ExternalDnsAgent } from "./external-dns-agent";
|
|
22
|
+
import { AdotCollector } from "./adot-collector";
|
|
23
|
+
import { MetricsServer } from "./metrics-server";
|
|
24
|
+
import { WorkloadIdentityServiceAccount } from "./workload-identity-service-account";
|
|
25
|
+
import { GcePdStorageClass } from "./gce-pd-storage-class";
|
|
26
|
+
import { FilestoreStorageClass } from "./filestore-storage-class";
|
|
27
|
+
import { GkeGateway } from "./gke-gateway";
|
|
28
|
+
import { ConfigConnectorContext } from "./config-connector-context";
|
|
9
29
|
|
|
10
30
|
// ── WebApp ──────────────────────────────────────────────────────────
|
|
11
31
|
|
|
@@ -523,6 +543,63 @@ describe("AutoscaledService", () => {
|
|
|
523
543
|
expect(podLabels.team).toBe("platform");
|
|
524
544
|
expect(podLabels["app.kubernetes.io/name"]).toBe("api");
|
|
525
545
|
});
|
|
546
|
+
|
|
547
|
+
test("serviceAccountName wired into pod spec", () => {
|
|
548
|
+
const result = AutoscaledService({ ...minProps, serviceAccountName: "my-sa" });
|
|
549
|
+
const spec = result.deployment.spec as any;
|
|
550
|
+
expect(spec.template.spec.serviceAccountName).toBe("my-sa");
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test("no serviceAccountName by default", () => {
|
|
554
|
+
const result = AutoscaledService(minProps);
|
|
555
|
+
const spec = result.deployment.spec as any;
|
|
556
|
+
expect(spec.template.spec.serviceAccountName).toBeUndefined();
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("volumes and volumeMounts wired into pod spec", () => {
|
|
560
|
+
const result = AutoscaledService({
|
|
561
|
+
...minProps,
|
|
562
|
+
volumes: [{ name: "data", emptyDir: {} }],
|
|
563
|
+
volumeMounts: [{ name: "data", mountPath: "/data" }],
|
|
564
|
+
});
|
|
565
|
+
const spec = result.deployment.spec as any;
|
|
566
|
+
expect(spec.template.spec.volumes).toEqual([{ name: "data", emptyDir: {} }]);
|
|
567
|
+
expect(spec.template.spec.containers[0].volumeMounts).toEqual([{ name: "data", mountPath: "/data" }]);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test("tmpDirs generates emptyDir volumes and mounts", () => {
|
|
571
|
+
const result = AutoscaledService({
|
|
572
|
+
...minProps,
|
|
573
|
+
tmpDirs: ["/tmp", "/var/cache/nginx"],
|
|
574
|
+
});
|
|
575
|
+
const spec = result.deployment.spec as any;
|
|
576
|
+
expect(spec.template.spec.volumes).toEqual([
|
|
577
|
+
{ name: "tmp-0", emptyDir: {} },
|
|
578
|
+
{ name: "tmp-1", emptyDir: {} },
|
|
579
|
+
]);
|
|
580
|
+
expect(spec.template.spec.containers[0].volumeMounts).toEqual([
|
|
581
|
+
{ name: "tmp-0", mountPath: "/tmp" },
|
|
582
|
+
{ name: "tmp-1", mountPath: "/var/cache/nginx" },
|
|
583
|
+
]);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
test("tmpDirs merges with explicit volumes/volumeMounts", () => {
|
|
587
|
+
const result = AutoscaledService({
|
|
588
|
+
...minProps,
|
|
589
|
+
volumes: [{ name: "config", configMap: { name: "app-config" } }],
|
|
590
|
+
volumeMounts: [{ name: "config", mountPath: "/etc/config" }],
|
|
591
|
+
tmpDirs: ["/tmp"],
|
|
592
|
+
});
|
|
593
|
+
const spec = result.deployment.spec as any;
|
|
594
|
+
expect(spec.template.spec.volumes).toEqual([
|
|
595
|
+
{ name: "config", configMap: { name: "app-config" } },
|
|
596
|
+
{ name: "tmp-0", emptyDir: {} },
|
|
597
|
+
]);
|
|
598
|
+
expect(spec.template.spec.containers[0].volumeMounts).toEqual([
|
|
599
|
+
{ name: "config", mountPath: "/etc/config" },
|
|
600
|
+
{ name: "tmp-0", mountPath: "/tmp" },
|
|
601
|
+
]);
|
|
602
|
+
});
|
|
526
603
|
});
|
|
527
604
|
|
|
528
605
|
// ── WorkerPool ─────────────────────────────────────────────────────
|
|
@@ -1107,3 +1184,1510 @@ describe("NodeAgent", () => {
|
|
|
1107
1184
|
expect((result.configMap!.metadata as any).labels["app.kubernetes.io/component"]).toBe("config");
|
|
1108
1185
|
});
|
|
1109
1186
|
});
|
|
1187
|
+
|
|
1188
|
+
// ── Hardening Additions ─────────────────────────────────────────────
|
|
1189
|
+
|
|
1190
|
+
describe("WebApp hardening", () => {
|
|
1191
|
+
test("PDB created when minAvailable set", () => {
|
|
1192
|
+
const result = WebApp({ name: "app", image: "app:1.0", minAvailable: 1 });
|
|
1193
|
+
expect(result.pdb).toBeDefined();
|
|
1194
|
+
const spec = result.pdb!.spec as any;
|
|
1195
|
+
expect(spec.minAvailable).toBe(1);
|
|
1196
|
+
expect(spec.selector.matchLabels["app.kubernetes.io/name"]).toBe("app");
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
test("no PDB by default", () => {
|
|
1200
|
+
const result = WebApp({ name: "app", image: "app:1.0" });
|
|
1201
|
+
expect(result.pdb).toBeUndefined();
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
test("initContainers passed through", () => {
|
|
1205
|
+
const result = WebApp({
|
|
1206
|
+
name: "app",
|
|
1207
|
+
image: "app:1.0",
|
|
1208
|
+
initContainers: [{ name: "migrate", image: "migrate:1.0", command: ["./migrate.sh"] }],
|
|
1209
|
+
});
|
|
1210
|
+
const spec = result.deployment.spec as any;
|
|
1211
|
+
expect(spec.template.spec.initContainers).toHaveLength(1);
|
|
1212
|
+
expect(spec.template.spec.initContainers[0].name).toBe("migrate");
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
test("securityContext passed through", () => {
|
|
1216
|
+
const result = WebApp({
|
|
1217
|
+
name: "app",
|
|
1218
|
+
image: "app:1.0",
|
|
1219
|
+
securityContext: { runAsNonRoot: true, readOnlyRootFilesystem: true },
|
|
1220
|
+
});
|
|
1221
|
+
const spec = result.deployment.spec as any;
|
|
1222
|
+
const container = spec.template.spec.containers[0];
|
|
1223
|
+
expect(container.securityContext.runAsNonRoot).toBe(true);
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
test("securityContext supports PSS restricted fields", () => {
|
|
1227
|
+
const result = WebApp({
|
|
1228
|
+
name: "app",
|
|
1229
|
+
image: "app:1.0",
|
|
1230
|
+
securityContext: {
|
|
1231
|
+
runAsNonRoot: true,
|
|
1232
|
+
readOnlyRootFilesystem: true,
|
|
1233
|
+
allowPrivilegeEscalation: false,
|
|
1234
|
+
capabilities: { drop: ["ALL"] },
|
|
1235
|
+
seccompProfile: { type: "RuntimeDefault" },
|
|
1236
|
+
},
|
|
1237
|
+
});
|
|
1238
|
+
const spec = result.deployment.spec as any;
|
|
1239
|
+
const sc = spec.template.spec.containers[0].securityContext;
|
|
1240
|
+
expect(sc.allowPrivilegeEscalation).toBe(false);
|
|
1241
|
+
expect(sc.capabilities).toEqual({ drop: ["ALL"] });
|
|
1242
|
+
expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
test("terminationGracePeriodSeconds set", () => {
|
|
1246
|
+
const result = WebApp({ name: "app", image: "app:1.0", terminationGracePeriodSeconds: 60 });
|
|
1247
|
+
const spec = result.deployment.spec as any;
|
|
1248
|
+
expect(spec.template.spec.terminationGracePeriodSeconds).toBe(60);
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
test("priorityClassName set", () => {
|
|
1252
|
+
const result = WebApp({ name: "app", image: "app:1.0", priorityClassName: "high-priority" });
|
|
1253
|
+
const spec = result.deployment.spec as any;
|
|
1254
|
+
expect(spec.template.spec.priorityClassName).toBe("high-priority");
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
test("multi-path ingress", () => {
|
|
1258
|
+
const result = WebApp({
|
|
1259
|
+
name: "app",
|
|
1260
|
+
image: "app:1.0",
|
|
1261
|
+
ingressHost: "app.example.com",
|
|
1262
|
+
ingressPaths: [
|
|
1263
|
+
{ path: "/api", serviceName: "api", servicePort: 8080 },
|
|
1264
|
+
{ path: "/web", serviceName: "web", servicePort: 3000 },
|
|
1265
|
+
],
|
|
1266
|
+
});
|
|
1267
|
+
const spec = result.ingress!.spec as any;
|
|
1268
|
+
expect(spec.rules[0].http.paths).toHaveLength(2);
|
|
1269
|
+
expect(spec.rules[0].http.paths[0].path).toBe("/api");
|
|
1270
|
+
expect(spec.rules[0].http.paths[1].backend.service.name).toBe("web");
|
|
1271
|
+
});
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
describe("StatefulApp hardening", () => {
|
|
1275
|
+
test("PDB created when minAvailable set", () => {
|
|
1276
|
+
const result = StatefulApp({ name: "db", image: "postgres:16", minAvailable: 1 });
|
|
1277
|
+
expect(result.pdb).toBeDefined();
|
|
1278
|
+
const spec = result.pdb!.spec as any;
|
|
1279
|
+
expect(spec.minAvailable).toBe(1);
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
test("initContainers passed through", () => {
|
|
1283
|
+
const result = StatefulApp({
|
|
1284
|
+
name: "db",
|
|
1285
|
+
image: "postgres:16",
|
|
1286
|
+
initContainers: [{ name: "init", image: "init:1.0" }],
|
|
1287
|
+
});
|
|
1288
|
+
const spec = result.statefulSet.spec as any;
|
|
1289
|
+
expect(spec.template.spec.initContainers).toHaveLength(1);
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
test("securityContext passed through", () => {
|
|
1293
|
+
const result = StatefulApp({
|
|
1294
|
+
name: "db",
|
|
1295
|
+
image: "postgres:16",
|
|
1296
|
+
securityContext: { runAsNonRoot: true },
|
|
1297
|
+
});
|
|
1298
|
+
const spec = result.statefulSet.spec as any;
|
|
1299
|
+
expect(spec.template.spec.containers[0].securityContext.runAsNonRoot).toBe(true);
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
test("securityContext supports PSS restricted fields", () => {
|
|
1303
|
+
const result = StatefulApp({
|
|
1304
|
+
name: "db",
|
|
1305
|
+
image: "postgres:16",
|
|
1306
|
+
securityContext: {
|
|
1307
|
+
runAsNonRoot: true,
|
|
1308
|
+
allowPrivilegeEscalation: false,
|
|
1309
|
+
capabilities: { drop: ["ALL"] },
|
|
1310
|
+
seccompProfile: { type: "RuntimeDefault" },
|
|
1311
|
+
},
|
|
1312
|
+
});
|
|
1313
|
+
const spec = result.statefulSet.spec as any;
|
|
1314
|
+
const sc = spec.template.spec.containers[0].securityContext;
|
|
1315
|
+
expect(sc.allowPrivilegeEscalation).toBe(false);
|
|
1316
|
+
expect(sc.capabilities).toEqual({ drop: ["ALL"] });
|
|
1317
|
+
expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
|
|
1318
|
+
});
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
describe("WorkerPool hardening", () => {
|
|
1322
|
+
test("PDB created when minAvailable set", () => {
|
|
1323
|
+
const result = WorkerPool({ name: "w", image: "w:1.0", minAvailable: 1 });
|
|
1324
|
+
expect(result.pdb).toBeDefined();
|
|
1325
|
+
const spec = result.pdb!.spec as any;
|
|
1326
|
+
expect(spec.minAvailable).toBe(1);
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
test("securityContext on container", () => {
|
|
1330
|
+
const result = WorkerPool({
|
|
1331
|
+
name: "w",
|
|
1332
|
+
image: "w:1.0",
|
|
1333
|
+
securityContext: { runAsNonRoot: true },
|
|
1334
|
+
});
|
|
1335
|
+
const spec = result.deployment.spec as any;
|
|
1336
|
+
expect(spec.template.spec.containers[0].securityContext.runAsNonRoot).toBe(true);
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
test("securityContext supports PSS restricted fields", () => {
|
|
1340
|
+
const result = WorkerPool({
|
|
1341
|
+
name: "w",
|
|
1342
|
+
image: "w:1.0",
|
|
1343
|
+
securityContext: {
|
|
1344
|
+
runAsNonRoot: true,
|
|
1345
|
+
allowPrivilegeEscalation: false,
|
|
1346
|
+
capabilities: { drop: ["ALL"] },
|
|
1347
|
+
seccompProfile: { type: "RuntimeDefault" },
|
|
1348
|
+
},
|
|
1349
|
+
});
|
|
1350
|
+
const spec = result.deployment.spec as any;
|
|
1351
|
+
const sc = spec.template.spec.containers[0].securityContext;
|
|
1352
|
+
expect(sc.allowPrivilegeEscalation).toBe(false);
|
|
1353
|
+
expect(sc.capabilities).toEqual({ drop: ["ALL"] });
|
|
1354
|
+
expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
|
|
1355
|
+
});
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
describe("AutoscaledService hardening", () => {
|
|
1359
|
+
const minProps = { name: "api", image: "api:1.0", maxReplicas: 10, cpuRequest: "100m", memoryRequest: "128Mi" };
|
|
1360
|
+
|
|
1361
|
+
test("initContainers passed through", () => {
|
|
1362
|
+
const result = AutoscaledService({
|
|
1363
|
+
...minProps,
|
|
1364
|
+
initContainers: [{ name: "migrate", image: "m:1.0" }],
|
|
1365
|
+
});
|
|
1366
|
+
const spec = result.deployment.spec as any;
|
|
1367
|
+
expect(spec.template.spec.initContainers).toHaveLength(1);
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
test("securityContext on container", () => {
|
|
1371
|
+
const result = AutoscaledService({
|
|
1372
|
+
...minProps,
|
|
1373
|
+
securityContext: { runAsNonRoot: true },
|
|
1374
|
+
});
|
|
1375
|
+
const spec = result.deployment.spec as any;
|
|
1376
|
+
expect(spec.template.spec.containers[0].securityContext.runAsNonRoot).toBe(true);
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
test("securityContext supports PSS restricted fields", () => {
|
|
1380
|
+
const result = AutoscaledService({
|
|
1381
|
+
...minProps,
|
|
1382
|
+
securityContext: {
|
|
1383
|
+
runAsNonRoot: true,
|
|
1384
|
+
allowPrivilegeEscalation: false,
|
|
1385
|
+
capabilities: { drop: ["ALL"] },
|
|
1386
|
+
seccompProfile: { type: "RuntimeDefault" },
|
|
1387
|
+
},
|
|
1388
|
+
});
|
|
1389
|
+
const spec = result.deployment.spec as any;
|
|
1390
|
+
const sc = spec.template.spec.containers[0].securityContext;
|
|
1391
|
+
expect(sc.allowPrivilegeEscalation).toBe(false);
|
|
1392
|
+
expect(sc.capabilities).toEqual({ drop: ["ALL"] });
|
|
1393
|
+
expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
test("terminationGracePeriodSeconds set", () => {
|
|
1397
|
+
const result = AutoscaledService({ ...minProps, terminationGracePeriodSeconds: 30 });
|
|
1398
|
+
const spec = result.deployment.spec as any;
|
|
1399
|
+
expect(spec.template.spec.terminationGracePeriodSeconds).toBe(30);
|
|
1400
|
+
});
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
// ── BatchJob ────────────────────────────────────────────────────────
|
|
1404
|
+
|
|
1405
|
+
describe("BatchJob", () => {
|
|
1406
|
+
const minProps = { name: "migrate", image: "migrate:1.0" };
|
|
1407
|
+
|
|
1408
|
+
test("returns job with default RBAC", () => {
|
|
1409
|
+
const result = BatchJob(minProps);
|
|
1410
|
+
expect(result.job).toBeDefined();
|
|
1411
|
+
expect(result.serviceAccount).toBeDefined();
|
|
1412
|
+
expect(result.role).toBeDefined();
|
|
1413
|
+
expect(result.roleBinding).toBeDefined();
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
test("default backoffLimit is 6", () => {
|
|
1417
|
+
const result = BatchJob(minProps);
|
|
1418
|
+
const spec = result.job.spec as any;
|
|
1419
|
+
expect(spec.backoffLimit).toBe(6);
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
test("custom backoffLimit and ttl", () => {
|
|
1423
|
+
const result = BatchJob({ ...minProps, backoffLimit: 3, ttlSecondsAfterFinished: 3600 });
|
|
1424
|
+
const spec = result.job.spec as any;
|
|
1425
|
+
expect(spec.backoffLimit).toBe(3);
|
|
1426
|
+
expect(spec.ttlSecondsAfterFinished).toBe(3600);
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
test("parallelism and completions", () => {
|
|
1430
|
+
const result = BatchJob({ ...minProps, parallelism: 3, completions: 10 });
|
|
1431
|
+
const spec = result.job.spec as any;
|
|
1432
|
+
expect(spec.parallelism).toBe(3);
|
|
1433
|
+
expect(spec.completions).toBe(10);
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
test("rbacRules: [] skips RBAC", () => {
|
|
1437
|
+
const result = BatchJob({ ...minProps, rbacRules: [] });
|
|
1438
|
+
expect(result.serviceAccount).toBeUndefined();
|
|
1439
|
+
expect(result.role).toBeUndefined();
|
|
1440
|
+
expect(result.roleBinding).toBeUndefined();
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
test("command and args passed through", () => {
|
|
1444
|
+
const result = BatchJob({ ...minProps, command: ["python"], args: ["migrate.py"] });
|
|
1445
|
+
const spec = result.job.spec as any;
|
|
1446
|
+
const container = spec.template.spec.containers[0];
|
|
1447
|
+
expect(container.command).toEqual(["python"]);
|
|
1448
|
+
expect(container.args).toEqual(["migrate.py"]);
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
test("namespace propagated", () => {
|
|
1452
|
+
const result = BatchJob({ ...minProps, namespace: "jobs" });
|
|
1453
|
+
expect((result.job.metadata as any).namespace).toBe("jobs");
|
|
1454
|
+
expect((result.serviceAccount!.metadata as any).namespace).toBe("jobs");
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
|
+
test("component labels", () => {
|
|
1458
|
+
const result = BatchJob(minProps);
|
|
1459
|
+
expect((result.job.metadata as any).labels["app.kubernetes.io/component"]).toBe("batch");
|
|
1460
|
+
expect((result.role!.metadata as any).labels["app.kubernetes.io/component"]).toBe("rbac");
|
|
1461
|
+
});
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
// ── SecureIngress ───────────────────────────────────────────────────
|
|
1465
|
+
|
|
1466
|
+
describe("SecureIngress", () => {
|
|
1467
|
+
const minProps = {
|
|
1468
|
+
name: "app-ingress",
|
|
1469
|
+
hosts: [{ hostname: "app.example.com", paths: [{ path: "/", serviceName: "app", servicePort: 80 }] }],
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1472
|
+
test("returns ingress without certificate by default", () => {
|
|
1473
|
+
const result = SecureIngress(minProps);
|
|
1474
|
+
expect(result.ingress).toBeDefined();
|
|
1475
|
+
expect(result.certificate).toBeUndefined();
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
test("creates certificate when clusterIssuer set", () => {
|
|
1479
|
+
const result = SecureIngress({ ...minProps, clusterIssuer: "letsencrypt-prod" });
|
|
1480
|
+
expect(result.certificate).toBeDefined();
|
|
1481
|
+
const certSpec = result.certificate!.spec as any;
|
|
1482
|
+
expect(certSpec.issuerRef.name).toBe("letsencrypt-prod");
|
|
1483
|
+
expect(certSpec.dnsNames).toEqual(["app.example.com"]);
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
test("TLS on ingress when clusterIssuer set", () => {
|
|
1487
|
+
const result = SecureIngress({ ...minProps, clusterIssuer: "letsencrypt-prod" });
|
|
1488
|
+
const spec = result.ingress.spec as any;
|
|
1489
|
+
expect(spec.tls).toBeDefined();
|
|
1490
|
+
expect(spec.tls[0].hosts).toEqual(["app.example.com"]);
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
test("multi-host support", () => {
|
|
1494
|
+
const result = SecureIngress({
|
|
1495
|
+
name: "multi",
|
|
1496
|
+
hosts: [
|
|
1497
|
+
{ hostname: "api.example.com", paths: [{ path: "/", serviceName: "api", servicePort: 80 }] },
|
|
1498
|
+
{ hostname: "admin.example.com", paths: [{ path: "/", serviceName: "admin", servicePort: 80 }] },
|
|
1499
|
+
],
|
|
1500
|
+
clusterIssuer: "letsencrypt-prod",
|
|
1501
|
+
});
|
|
1502
|
+
const spec = result.ingress.spec as any;
|
|
1503
|
+
expect(spec.rules).toHaveLength(2);
|
|
1504
|
+
const certSpec = result.certificate!.spec as any;
|
|
1505
|
+
expect(certSpec.dnsNames).toHaveLength(2);
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
test("multi-path support", () => {
|
|
1509
|
+
const result = SecureIngress({
|
|
1510
|
+
name: "multi-path",
|
|
1511
|
+
hosts: [{
|
|
1512
|
+
hostname: "app.example.com",
|
|
1513
|
+
paths: [
|
|
1514
|
+
{ path: "/api", serviceName: "api", servicePort: 8080 },
|
|
1515
|
+
{ path: "/web", serviceName: "web", servicePort: 3000 },
|
|
1516
|
+
],
|
|
1517
|
+
}],
|
|
1518
|
+
});
|
|
1519
|
+
const spec = result.ingress.spec as any;
|
|
1520
|
+
expect(spec.rules[0].http.paths).toHaveLength(2);
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
test("ingressClassName set", () => {
|
|
1524
|
+
const result = SecureIngress({ ...minProps, ingressClassName: "nginx" });
|
|
1525
|
+
const spec = result.ingress.spec as any;
|
|
1526
|
+
expect(spec.ingressClassName).toBe("nginx");
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
test("cert-manager annotation added", () => {
|
|
1530
|
+
const result = SecureIngress({ ...minProps, clusterIssuer: "letsencrypt-prod" });
|
|
1531
|
+
const meta = result.ingress.metadata as any;
|
|
1532
|
+
expect(meta.annotations["cert-manager.io/cluster-issuer"]).toBe("letsencrypt-prod");
|
|
1533
|
+
});
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
// ── ConfiguredApp ───────────────────────────────────────────────────
|
|
1537
|
+
|
|
1538
|
+
describe("ConfiguredApp", () => {
|
|
1539
|
+
const minProps = { name: "api", image: "api:1.0" };
|
|
1540
|
+
|
|
1541
|
+
test("returns deployment and service", () => {
|
|
1542
|
+
const result = ConfiguredApp(minProps);
|
|
1543
|
+
expect(result.deployment).toBeDefined();
|
|
1544
|
+
expect(result.service).toBeDefined();
|
|
1545
|
+
expect(result.configMap).toBeUndefined();
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
test("creates ConfigMap when configData provided", () => {
|
|
1549
|
+
const result = ConfiguredApp({ ...minProps, configData: { "app.conf": "key=val" }, configMountPath: "/etc/app" });
|
|
1550
|
+
expect(result.configMap).toBeDefined();
|
|
1551
|
+
expect((result.configMap as any).data["app.conf"]).toBe("key=val");
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
test("configMap volume mounted", () => {
|
|
1555
|
+
const result = ConfiguredApp({ ...minProps, configData: { "k": "v" }, configMountPath: "/etc/app" });
|
|
1556
|
+
const spec = result.deployment.spec as any;
|
|
1557
|
+
const container = spec.template.spec.containers[0];
|
|
1558
|
+
expect(container.volumeMounts).toHaveLength(1);
|
|
1559
|
+
expect(container.volumeMounts[0].mountPath).toBe("/etc/app");
|
|
1560
|
+
expect(spec.template.spec.volumes[0].configMap.name).toBe("api-config");
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
test("secret volume mounted", () => {
|
|
1564
|
+
const result = ConfiguredApp({ ...minProps, secretName: "creds", secretMountPath: "/secrets" });
|
|
1565
|
+
const spec = result.deployment.spec as any;
|
|
1566
|
+
const container = spec.template.spec.containers[0];
|
|
1567
|
+
expect(container.volumeMounts[0].mountPath).toBe("/secrets");
|
|
1568
|
+
expect(spec.template.spec.volumes[0].secret.secretName).toBe("creds");
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
test("envFrom with configMapRef and secretRef", () => {
|
|
1572
|
+
const result = ConfiguredApp({
|
|
1573
|
+
...minProps,
|
|
1574
|
+
envFrom: { configMapRef: "my-config", secretRef: "my-secret" },
|
|
1575
|
+
});
|
|
1576
|
+
const spec = result.deployment.spec as any;
|
|
1577
|
+
const container = spec.template.spec.containers[0];
|
|
1578
|
+
expect(container.envFrom).toHaveLength(2);
|
|
1579
|
+
expect(container.envFrom[0].configMapRef.name).toBe("my-config");
|
|
1580
|
+
expect(container.envFrom[1].secretRef.name).toBe("my-secret");
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
test("initContainers supported", () => {
|
|
1584
|
+
const result = ConfiguredApp({
|
|
1585
|
+
...minProps,
|
|
1586
|
+
initContainers: [{ name: "init", image: "init:1.0", command: ["sh"] }],
|
|
1587
|
+
});
|
|
1588
|
+
const spec = result.deployment.spec as any;
|
|
1589
|
+
expect(spec.template.spec.initContainers).toHaveLength(1);
|
|
1590
|
+
});
|
|
1591
|
+
|
|
1592
|
+
test("namespace propagated", () => {
|
|
1593
|
+
const result = ConfiguredApp({ ...minProps, namespace: "prod" });
|
|
1594
|
+
expect((result.deployment.metadata as any).namespace).toBe("prod");
|
|
1595
|
+
expect((result.service.metadata as any).namespace).toBe("prod");
|
|
1596
|
+
});
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
// ── SidecarApp ──────────────────────────────────────────────────────
|
|
1600
|
+
|
|
1601
|
+
describe("SidecarApp", () => {
|
|
1602
|
+
const minProps = {
|
|
1603
|
+
name: "api",
|
|
1604
|
+
image: "api:1.0",
|
|
1605
|
+
sidecars: [{ name: "envoy", image: "envoy:v1.28" }],
|
|
1606
|
+
};
|
|
1607
|
+
|
|
1608
|
+
test("returns deployment and service", () => {
|
|
1609
|
+
const result = SidecarApp(minProps);
|
|
1610
|
+
expect(result.deployment).toBeDefined();
|
|
1611
|
+
expect(result.service).toBeDefined();
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
test("has multiple containers", () => {
|
|
1615
|
+
const result = SidecarApp(minProps);
|
|
1616
|
+
const spec = result.deployment.spec as any;
|
|
1617
|
+
expect(spec.template.spec.containers).toHaveLength(2);
|
|
1618
|
+
expect(spec.template.spec.containers[0].name).toBe("api");
|
|
1619
|
+
expect(spec.template.spec.containers[1].name).toBe("envoy");
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
test("sidecar ports passed through", () => {
|
|
1623
|
+
const result = SidecarApp({
|
|
1624
|
+
...minProps,
|
|
1625
|
+
sidecars: [{ name: "envoy", image: "envoy:v1.28", ports: [{ containerPort: 9901, name: "admin" }] }],
|
|
1626
|
+
});
|
|
1627
|
+
const spec = result.deployment.spec as any;
|
|
1628
|
+
expect(spec.template.spec.containers[1].ports[0].containerPort).toBe(9901);
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
test("initContainers supported", () => {
|
|
1632
|
+
const result = SidecarApp({
|
|
1633
|
+
...minProps,
|
|
1634
|
+
initContainers: [{ name: "migrate", image: "m:1.0", command: ["./migrate.sh"] }],
|
|
1635
|
+
});
|
|
1636
|
+
const spec = result.deployment.spec as any;
|
|
1637
|
+
expect(spec.template.spec.initContainers).toHaveLength(1);
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
test("sharedVolumes creates volumes", () => {
|
|
1641
|
+
const result = SidecarApp({
|
|
1642
|
+
...minProps,
|
|
1643
|
+
sharedVolumes: [{ name: "tmp" }, { name: "config", configMapName: "my-config" }],
|
|
1644
|
+
});
|
|
1645
|
+
const spec = result.deployment.spec as any;
|
|
1646
|
+
expect(spec.template.spec.volumes).toHaveLength(2);
|
|
1647
|
+
expect(spec.template.spec.volumes[0].emptyDir).toBeDefined();
|
|
1648
|
+
expect(spec.template.spec.volumes[1].configMap.name).toBe("my-config");
|
|
1649
|
+
});
|
|
1650
|
+
|
|
1651
|
+
test("common labels on all resources", () => {
|
|
1652
|
+
const result = SidecarApp(minProps);
|
|
1653
|
+
expect((result.deployment.metadata as any).labels["app.kubernetes.io/managed-by"]).toBe("chant");
|
|
1654
|
+
expect((result.service.metadata as any).labels["app.kubernetes.io/managed-by"]).toBe("chant");
|
|
1655
|
+
});
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
// ── MonitoredService ────────────────────────────────────────────────
|
|
1659
|
+
|
|
1660
|
+
describe("MonitoredService", () => {
|
|
1661
|
+
const minProps = { name: "api", image: "api:1.0" };
|
|
1662
|
+
|
|
1663
|
+
test("returns deployment, service, serviceMonitor", () => {
|
|
1664
|
+
const result = MonitoredService(minProps);
|
|
1665
|
+
expect(result.deployment).toBeDefined();
|
|
1666
|
+
expect(result.service).toBeDefined();
|
|
1667
|
+
expect(result.serviceMonitor).toBeDefined();
|
|
1668
|
+
expect(result.prometheusRule).toBeUndefined();
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
test("prometheusRule created when alertRules provided", () => {
|
|
1672
|
+
const result = MonitoredService({
|
|
1673
|
+
...minProps,
|
|
1674
|
+
alertRules: [{ name: "HighError", expr: "rate(errors[5m]) > 0.1", severity: "critical" }],
|
|
1675
|
+
});
|
|
1676
|
+
expect(result.prometheusRule).toBeDefined();
|
|
1677
|
+
const spec = result.prometheusRule!.spec as any;
|
|
1678
|
+
expect(spec.groups[0].rules[0].alert).toBe("HighError");
|
|
1679
|
+
expect(spec.groups[0].rules[0].labels.severity).toBe("critical");
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
test("serviceMonitor has correct selector and endpoint", () => {
|
|
1683
|
+
const result = MonitoredService({ ...minProps, metricsPort: 9090, metricsPath: "/metrics", scrapeInterval: "15s" });
|
|
1684
|
+
const spec = result.serviceMonitor.spec as any;
|
|
1685
|
+
expect(spec.selector.matchLabels["app.kubernetes.io/name"]).toBe("api");
|
|
1686
|
+
expect(spec.endpoints[0].port).toBe("metrics");
|
|
1687
|
+
expect(spec.endpoints[0].path).toBe("/metrics");
|
|
1688
|
+
expect(spec.endpoints[0].interval).toBe("15s");
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
test("separate metrics port on container and service", () => {
|
|
1692
|
+
const result = MonitoredService({ ...minProps, port: 8080, metricsPort: 9090 });
|
|
1693
|
+
const spec = result.deployment.spec as any;
|
|
1694
|
+
const ports = spec.template.spec.containers[0].ports;
|
|
1695
|
+
expect(ports).toHaveLength(2);
|
|
1696
|
+
expect(ports[0].containerPort).toBe(8080);
|
|
1697
|
+
expect(ports[1].containerPort).toBe(9090);
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
test("component labels", () => {
|
|
1701
|
+
const result = MonitoredService({ ...minProps, alertRules: [{ name: "A", expr: "1" }] });
|
|
1702
|
+
expect((result.serviceMonitor.metadata as any).labels["app.kubernetes.io/component"]).toBe("monitoring");
|
|
1703
|
+
expect((result.prometheusRule!.metadata as any).labels["app.kubernetes.io/component"]).toBe("monitoring");
|
|
1704
|
+
});
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
// ── NetworkIsolatedApp ──────────────────────────────────────────────
|
|
1708
|
+
|
|
1709
|
+
describe("NetworkIsolatedApp", () => {
|
|
1710
|
+
const minProps = { name: "api", image: "api:1.0" };
|
|
1711
|
+
|
|
1712
|
+
test("returns deployment, service, networkPolicy", () => {
|
|
1713
|
+
const result = NetworkIsolatedApp(minProps);
|
|
1714
|
+
expect(result.deployment).toBeDefined();
|
|
1715
|
+
expect(result.service).toBeDefined();
|
|
1716
|
+
expect(result.networkPolicy).toBeDefined();
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
test("networkPolicy podSelector matches app", () => {
|
|
1720
|
+
const result = NetworkIsolatedApp(minProps);
|
|
1721
|
+
const spec = result.networkPolicy.spec as any;
|
|
1722
|
+
expect(spec.podSelector.matchLabels["app.kubernetes.io/name"]).toBe("api");
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
test("ingress rules created", () => {
|
|
1726
|
+
const result = NetworkIsolatedApp({
|
|
1727
|
+
...minProps,
|
|
1728
|
+
allowIngressFrom: [{ podSelector: { "app.kubernetes.io/name": "frontend" } }],
|
|
1729
|
+
});
|
|
1730
|
+
const spec = result.networkPolicy.spec as any;
|
|
1731
|
+
expect(spec.policyTypes).toContain("Ingress");
|
|
1732
|
+
expect(spec.ingress[0].from[0].podSelector.matchLabels["app.kubernetes.io/name"]).toBe("frontend");
|
|
1733
|
+
});
|
|
1734
|
+
|
|
1735
|
+
test("egress rules with ports", () => {
|
|
1736
|
+
const result = NetworkIsolatedApp({
|
|
1737
|
+
...minProps,
|
|
1738
|
+
allowEgressTo: [{ podSelector: { "app.kubernetes.io/name": "db" }, ports: [{ port: 5432 }] }],
|
|
1739
|
+
});
|
|
1740
|
+
const spec = result.networkPolicy.spec as any;
|
|
1741
|
+
expect(spec.policyTypes).toContain("Egress");
|
|
1742
|
+
expect(spec.egress[0].ports[0].port).toBe(5432);
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
test("namespace propagated", () => {
|
|
1746
|
+
const result = NetworkIsolatedApp({ ...minProps, namespace: "prod" });
|
|
1747
|
+
expect((result.networkPolicy.metadata as any).namespace).toBe("prod");
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
test("component label on networkPolicy", () => {
|
|
1751
|
+
const result = NetworkIsolatedApp(minProps);
|
|
1752
|
+
expect((result.networkPolicy.metadata as any).labels["app.kubernetes.io/component"]).toBe("network-policy");
|
|
1753
|
+
});
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
// ── IrsaServiceAccount ──────────────────────────────────────────────
|
|
1757
|
+
|
|
1758
|
+
describe("IrsaServiceAccount", () => {
|
|
1759
|
+
const minProps = { name: "app-sa", iamRoleArn: "arn:aws:iam::123456789012:role/app-role" };
|
|
1760
|
+
|
|
1761
|
+
test("returns serviceAccount with IRSA annotation", () => {
|
|
1762
|
+
const result = IrsaServiceAccount(minProps);
|
|
1763
|
+
expect(result.serviceAccount).toBeDefined();
|
|
1764
|
+
const meta = result.serviceAccount.metadata as any;
|
|
1765
|
+
expect(meta.annotations["eks.amazonaws.com/role-arn"]).toBe("arn:aws:iam::123456789012:role/app-role");
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1768
|
+
test("no RBAC by default", () => {
|
|
1769
|
+
const result = IrsaServiceAccount(minProps);
|
|
1770
|
+
expect(result.role).toBeUndefined();
|
|
1771
|
+
expect(result.roleBinding).toBeUndefined();
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
test("RBAC created when rules provided", () => {
|
|
1775
|
+
const result = IrsaServiceAccount({
|
|
1776
|
+
...minProps,
|
|
1777
|
+
rbacRules: [{ apiGroups: [""], resources: ["secrets"], verbs: ["get"] }],
|
|
1778
|
+
});
|
|
1779
|
+
expect(result.role).toBeDefined();
|
|
1780
|
+
expect(result.roleBinding).toBeDefined();
|
|
1781
|
+
const role = result.role as any;
|
|
1782
|
+
expect(role.rules[0].resources).toEqual(["secrets"]);
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
test("namespace propagated", () => {
|
|
1786
|
+
const result = IrsaServiceAccount({ ...minProps, namespace: "prod" });
|
|
1787
|
+
expect((result.serviceAccount.metadata as any).namespace).toBe("prod");
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
test("component labels", () => {
|
|
1791
|
+
const result = IrsaServiceAccount(minProps);
|
|
1792
|
+
expect((result.serviceAccount.metadata as any).labels["app.kubernetes.io/component"]).toBe("service-account");
|
|
1793
|
+
});
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
// ── AlbIngress ──────────────────────────────────────────────────────
|
|
1797
|
+
|
|
1798
|
+
describe("AlbIngress", () => {
|
|
1799
|
+
const minProps = {
|
|
1800
|
+
name: "api-ingress",
|
|
1801
|
+
hosts: [{ hostname: "api.example.com", paths: [{ path: "/", serviceName: "api", servicePort: 80 }] }],
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1804
|
+
test("returns ingress with ALB annotations", () => {
|
|
1805
|
+
const result = AlbIngress(minProps);
|
|
1806
|
+
expect(result.ingress).toBeDefined();
|
|
1807
|
+
const meta = result.ingress.metadata as any;
|
|
1808
|
+
expect(meta.annotations["alb.ingress.kubernetes.io/scheme"]).toBe("internet-facing");
|
|
1809
|
+
expect(meta.annotations["alb.ingress.kubernetes.io/target-type"]).toBe("ip");
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
test("ingressClassName is alb", () => {
|
|
1813
|
+
const result = AlbIngress(minProps);
|
|
1814
|
+
const spec = result.ingress.spec as any;
|
|
1815
|
+
expect(spec.ingressClassName).toBe("alb");
|
|
1816
|
+
});
|
|
1817
|
+
|
|
1818
|
+
test("certificate ARN sets TLS annotations", () => {
|
|
1819
|
+
const result = AlbIngress({ ...minProps, certificateArn: "arn:aws:acm:us-east-1:123:cert/abc" });
|
|
1820
|
+
const meta = result.ingress.metadata as any;
|
|
1821
|
+
expect(meta.annotations["alb.ingress.kubernetes.io/certificate-arn"]).toBe("arn:aws:acm:us-east-1:123:cert/abc");
|
|
1822
|
+
expect(meta.annotations["alb.ingress.kubernetes.io/ssl-redirect"]).toBe("443");
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1825
|
+
test("groupName annotation set", () => {
|
|
1826
|
+
const result = AlbIngress({ ...minProps, groupName: "shared-alb" });
|
|
1827
|
+
const meta = result.ingress.metadata as any;
|
|
1828
|
+
expect(meta.annotations["alb.ingress.kubernetes.io/group.name"]).toBe("shared-alb");
|
|
1829
|
+
});
|
|
1830
|
+
|
|
1831
|
+
test("WAF ACL annotation set", () => {
|
|
1832
|
+
const result = AlbIngress({ ...minProps, wafAclArn: "arn:aws:wafv2:us-east-1:123:regional/webacl/abc" });
|
|
1833
|
+
const meta = result.ingress.metadata as any;
|
|
1834
|
+
expect(meta.annotations["alb.ingress.kubernetes.io/wafv2-acl-arn"]).toBe("arn:aws:wafv2:us-east-1:123:regional/webacl/abc");
|
|
1835
|
+
});
|
|
1836
|
+
|
|
1837
|
+
test("internal scheme", () => {
|
|
1838
|
+
const result = AlbIngress({ ...minProps, scheme: "internal" });
|
|
1839
|
+
const meta = result.ingress.metadata as any;
|
|
1840
|
+
expect(meta.annotations["alb.ingress.kubernetes.io/scheme"]).toBe("internal");
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
test("empty-string certificateArn does NOT set ssl-redirect", () => {
|
|
1844
|
+
const result = AlbIngress({ ...minProps, certificateArn: "" });
|
|
1845
|
+
const meta = result.ingress.metadata as any;
|
|
1846
|
+
expect(meta.annotations["alb.ingress.kubernetes.io/ssl-redirect"]).toBeUndefined();
|
|
1847
|
+
expect(meta.annotations["alb.ingress.kubernetes.io/certificate-arn"]).toBeUndefined();
|
|
1848
|
+
});
|
|
1849
|
+
|
|
1850
|
+
test("explicit sslRedirect: true overrides empty certificateArn", () => {
|
|
1851
|
+
const result = AlbIngress({ ...minProps, certificateArn: "", sslRedirect: true });
|
|
1852
|
+
const meta = result.ingress.metadata as any;
|
|
1853
|
+
expect(meta.annotations["alb.ingress.kubernetes.io/ssl-redirect"]).toBe("443");
|
|
1854
|
+
});
|
|
1855
|
+
|
|
1856
|
+
test("explicit sslRedirect: false suppresses redirect even with cert", () => {
|
|
1857
|
+
const result = AlbIngress({ ...minProps, certificateArn: "arn:aws:acm:us-east-1:123:cert/abc", sslRedirect: false });
|
|
1858
|
+
const meta = result.ingress.metadata as any;
|
|
1859
|
+
expect(meta.annotations["alb.ingress.kubernetes.io/ssl-redirect"]).toBeUndefined();
|
|
1860
|
+
});
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
// ── EbsStorageClass ─────────────────────────────────────────────────
|
|
1864
|
+
|
|
1865
|
+
describe("EbsStorageClass", () => {
|
|
1866
|
+
test("returns storageClass with EBS provisioner", () => {
|
|
1867
|
+
const result = EbsStorageClass({ name: "gp3" });
|
|
1868
|
+
expect(result.storageClass).toBeDefined();
|
|
1869
|
+
expect((result.storageClass as any).provisioner).toBe("ebs.csi.aws.com");
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
test("default type is gp3", () => {
|
|
1873
|
+
const result = EbsStorageClass({ name: "default" });
|
|
1874
|
+
expect((result.storageClass as any).parameters.type).toBe("gp3");
|
|
1875
|
+
});
|
|
1876
|
+
|
|
1877
|
+
test("encryption enabled by default", () => {
|
|
1878
|
+
const result = EbsStorageClass({ name: "enc" });
|
|
1879
|
+
expect((result.storageClass as any).parameters.encrypted).toBe("true");
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
test("custom parameters", () => {
|
|
1883
|
+
const result = EbsStorageClass({ name: "custom", type: "io2", iops: "5000", throughput: "250" });
|
|
1884
|
+
const params = (result.storageClass as any).parameters;
|
|
1885
|
+
expect(params.type).toBe("io2");
|
|
1886
|
+
expect(params.iops).toBe("5000");
|
|
1887
|
+
expect(params.throughput).toBe("250");
|
|
1888
|
+
});
|
|
1889
|
+
|
|
1890
|
+
test("allowVolumeExpansion default true", () => {
|
|
1891
|
+
const result = EbsStorageClass({ name: "exp" });
|
|
1892
|
+
expect((result.storageClass as any).allowVolumeExpansion).toBe(true);
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1895
|
+
test("storageClass is cluster-scoped (no namespace)", () => {
|
|
1896
|
+
const result = EbsStorageClass({ name: "sc" });
|
|
1897
|
+
expect((result.storageClass.metadata as any).namespace).toBeUndefined();
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1900
|
+
test("numeric iops and throughput coerced to strings", () => {
|
|
1901
|
+
const result = EbsStorageClass({ name: "perf", iops: 5000, throughput: 250 });
|
|
1902
|
+
const params = (result.storageClass as any).parameters;
|
|
1903
|
+
expect(params.iops).toBe("5000");
|
|
1904
|
+
expect(params.throughput).toBe("250");
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1907
|
+
test("string iops and throughput passed through", () => {
|
|
1908
|
+
const result = EbsStorageClass({ name: "perf", iops: "3000", throughput: "125" });
|
|
1909
|
+
const params = (result.storageClass as any).parameters;
|
|
1910
|
+
expect(params.iops).toBe("3000");
|
|
1911
|
+
expect(params.throughput).toBe("125");
|
|
1912
|
+
});
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1915
|
+
// ── EfsStorageClass ─────────────────────────────────────────────────
|
|
1916
|
+
|
|
1917
|
+
describe("EfsStorageClass", () => {
|
|
1918
|
+
test("returns storageClass with EFS provisioner", () => {
|
|
1919
|
+
const result = EfsStorageClass({ name: "efs", fileSystemId: "fs-123" });
|
|
1920
|
+
expect((result.storageClass as any).provisioner).toBe("efs.csi.aws.com");
|
|
1921
|
+
});
|
|
1922
|
+
|
|
1923
|
+
test("fileSystemId in parameters", () => {
|
|
1924
|
+
const result = EfsStorageClass({ name: "efs", fileSystemId: "fs-abc" });
|
|
1925
|
+
expect((result.storageClass as any).parameters.fileSystemId).toBe("fs-abc");
|
|
1926
|
+
});
|
|
1927
|
+
|
|
1928
|
+
test("default provisioningMode is efs-ap", () => {
|
|
1929
|
+
const result = EfsStorageClass({ name: "efs", fileSystemId: "fs-123" });
|
|
1930
|
+
expect((result.storageClass as any).parameters.provisioningMode).toBe("efs-ap");
|
|
1931
|
+
});
|
|
1932
|
+
});
|
|
1933
|
+
|
|
1934
|
+
// ── FluentBitAgent ──────────────────────────────────────────────────
|
|
1935
|
+
|
|
1936
|
+
describe("FluentBitAgent", () => {
|
|
1937
|
+
const minProps = { logGroup: "/aws/eks/cluster/containers", region: "us-east-1", clusterName: "cluster" };
|
|
1938
|
+
|
|
1939
|
+
test("returns all 5 resources", () => {
|
|
1940
|
+
const result = FluentBitAgent(minProps);
|
|
1941
|
+
expect(result.daemonSet).toBeDefined();
|
|
1942
|
+
expect(result.serviceAccount).toBeDefined();
|
|
1943
|
+
expect(result.clusterRole).toBeDefined();
|
|
1944
|
+
expect(result.clusterRoleBinding).toBeDefined();
|
|
1945
|
+
expect(result.configMap).toBeDefined();
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1948
|
+
test("default namespace is amazon-cloudwatch", () => {
|
|
1949
|
+
const result = FluentBitAgent(minProps);
|
|
1950
|
+
expect((result.daemonSet.metadata as any).namespace).toBe("amazon-cloudwatch");
|
|
1951
|
+
});
|
|
1952
|
+
|
|
1953
|
+
test("configMap contains fluent-bit config with region", () => {
|
|
1954
|
+
const result = FluentBitAgent(minProps);
|
|
1955
|
+
const data = (result.configMap as any).data;
|
|
1956
|
+
expect(data["fluent-bit.conf"]).toContain("us-east-1");
|
|
1957
|
+
expect(data["fluent-bit.conf"]).toContain("/aws/eks/cluster/containers");
|
|
1958
|
+
});
|
|
1959
|
+
|
|
1960
|
+
test("tolerations for all nodes", () => {
|
|
1961
|
+
const result = FluentBitAgent(minProps);
|
|
1962
|
+
const spec = result.daemonSet.spec as any;
|
|
1963
|
+
expect(spec.template.spec.tolerations).toEqual([{ operator: "Exists" }]);
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1966
|
+
test("clusterRole is cluster-scoped", () => {
|
|
1967
|
+
const result = FluentBitAgent(minProps);
|
|
1968
|
+
expect((result.clusterRole.metadata as any).namespace).toBeUndefined();
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
test("IRSA annotation when iamRoleArn set", () => {
|
|
1972
|
+
const result = FluentBitAgent({ ...minProps, iamRoleArn: "arn:aws:iam::123456789012:role/fb-role" });
|
|
1973
|
+
const meta = result.serviceAccount.metadata as any;
|
|
1974
|
+
expect(meta.annotations["eks.amazonaws.com/role-arn"]).toBe("arn:aws:iam::123456789012:role/fb-role");
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1977
|
+
test("no annotation when iamRoleArn omitted", () => {
|
|
1978
|
+
const result = FluentBitAgent(minProps);
|
|
1979
|
+
const meta = result.serviceAccount.metadata as any;
|
|
1980
|
+
expect(meta.annotations).toBeUndefined();
|
|
1981
|
+
});
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
// ── ExternalDnsAgent ────────────────────────────────────────────────
|
|
1985
|
+
|
|
1986
|
+
describe("ExternalDnsAgent", () => {
|
|
1987
|
+
const minProps = {
|
|
1988
|
+
iamRoleArn: "arn:aws:iam::123456789012:role/external-dns",
|
|
1989
|
+
domainFilters: ["example.com"],
|
|
1990
|
+
};
|
|
1991
|
+
|
|
1992
|
+
test("returns deployment, serviceAccount, clusterRole, clusterRoleBinding", () => {
|
|
1993
|
+
const result = ExternalDnsAgent(minProps);
|
|
1994
|
+
expect(result.deployment).toBeDefined();
|
|
1995
|
+
expect(result.serviceAccount).toBeDefined();
|
|
1996
|
+
expect(result.clusterRole).toBeDefined();
|
|
1997
|
+
expect(result.clusterRoleBinding).toBeDefined();
|
|
1998
|
+
});
|
|
1999
|
+
|
|
2000
|
+
test("IRSA annotation on serviceAccount", () => {
|
|
2001
|
+
const result = ExternalDnsAgent(minProps);
|
|
2002
|
+
const meta = result.serviceAccount.metadata as any;
|
|
2003
|
+
expect(meta.annotations["eks.amazonaws.com/role-arn"]).toBe("arn:aws:iam::123456789012:role/external-dns");
|
|
2004
|
+
});
|
|
2005
|
+
|
|
2006
|
+
test("domain filter in args", () => {
|
|
2007
|
+
const result = ExternalDnsAgent(minProps);
|
|
2008
|
+
const spec = result.deployment.spec as any;
|
|
2009
|
+
const args = spec.template.spec.containers[0].args;
|
|
2010
|
+
expect(args).toContain("--domain-filter=example.com");
|
|
2011
|
+
});
|
|
2012
|
+
|
|
2013
|
+
test("txtOwnerId in args when set", () => {
|
|
2014
|
+
const result = ExternalDnsAgent({ ...minProps, txtOwnerId: "my-cluster" });
|
|
2015
|
+
const spec = result.deployment.spec as any;
|
|
2016
|
+
const args = spec.template.spec.containers[0].args;
|
|
2017
|
+
expect(args).toContain("--txt-owner-id=my-cluster");
|
|
2018
|
+
});
|
|
2019
|
+
|
|
2020
|
+
test("default namespace is kube-system", () => {
|
|
2021
|
+
const result = ExternalDnsAgent(minProps);
|
|
2022
|
+
expect((result.deployment.metadata as any).namespace).toBe("kube-system");
|
|
2023
|
+
});
|
|
2024
|
+
|
|
2025
|
+
test("replicas is 1", () => {
|
|
2026
|
+
const result = ExternalDnsAgent(minProps);
|
|
2027
|
+
const spec = result.deployment.spec as any;
|
|
2028
|
+
expect(spec.replicas).toBe(1);
|
|
2029
|
+
});
|
|
2030
|
+
});
|
|
2031
|
+
|
|
2032
|
+
// ── AdotCollector ───────────────────────────────────────────────────
|
|
2033
|
+
|
|
2034
|
+
describe("AdotCollector", () => {
|
|
2035
|
+
const minProps = { region: "us-east-1", clusterName: "cluster" };
|
|
2036
|
+
|
|
2037
|
+
test("returns all 5 resources", () => {
|
|
2038
|
+
const result = AdotCollector(minProps);
|
|
2039
|
+
expect(result.daemonSet).toBeDefined();
|
|
2040
|
+
expect(result.serviceAccount).toBeDefined();
|
|
2041
|
+
expect(result.clusterRole).toBeDefined();
|
|
2042
|
+
expect(result.clusterRoleBinding).toBeDefined();
|
|
2043
|
+
expect(result.configMap).toBeDefined();
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2046
|
+
test("default namespace is amazon-metrics", () => {
|
|
2047
|
+
const result = AdotCollector(minProps);
|
|
2048
|
+
expect((result.daemonSet.metadata as any).namespace).toBe("amazon-metrics");
|
|
2049
|
+
});
|
|
2050
|
+
|
|
2051
|
+
test("configMap contains ADOT config with region", () => {
|
|
2052
|
+
const result = AdotCollector(minProps);
|
|
2053
|
+
const data = (result.configMap as any).data;
|
|
2054
|
+
expect(data["config.yaml"]).toContain("us-east-1");
|
|
2055
|
+
expect(data["config.yaml"]).toContain("cluster");
|
|
2056
|
+
});
|
|
2057
|
+
|
|
2058
|
+
test("OTLP ports on container", () => {
|
|
2059
|
+
const result = AdotCollector(minProps);
|
|
2060
|
+
const spec = result.daemonSet.spec as any;
|
|
2061
|
+
const ports = spec.template.spec.containers[0].ports;
|
|
2062
|
+
expect(ports).toHaveLength(2);
|
|
2063
|
+
expect(ports[0].containerPort).toBe(4317);
|
|
2064
|
+
expect(ports[1].containerPort).toBe(4318);
|
|
2065
|
+
});
|
|
2066
|
+
|
|
2067
|
+
test("tolerations for all nodes", () => {
|
|
2068
|
+
const result = AdotCollector(minProps);
|
|
2069
|
+
const spec = result.daemonSet.spec as any;
|
|
2070
|
+
expect(spec.template.spec.tolerations).toEqual([{ operator: "Exists" }]);
|
|
2071
|
+
});
|
|
2072
|
+
|
|
2073
|
+
test("custom exporters", () => {
|
|
2074
|
+
const result = AdotCollector({ ...minProps, exporters: ["prometheus"] });
|
|
2075
|
+
const data = (result.configMap as any).data;
|
|
2076
|
+
expect(data["config.yaml"]).toContain("prometheusremotewrite");
|
|
2077
|
+
});
|
|
2078
|
+
|
|
2079
|
+
test("IRSA annotation when iamRoleArn set", () => {
|
|
2080
|
+
const result = AdotCollector({ ...minProps, iamRoleArn: "arn:aws:iam::123456789012:role/adot-role" });
|
|
2081
|
+
const meta = result.serviceAccount.metadata as any;
|
|
2082
|
+
expect(meta.annotations["eks.amazonaws.com/role-arn"]).toBe("arn:aws:iam::123456789012:role/adot-role");
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
test("no annotation when iamRoleArn omitted", () => {
|
|
2086
|
+
const result = AdotCollector(minProps);
|
|
2087
|
+
const meta = result.serviceAccount.metadata as any;
|
|
2088
|
+
expect(meta.annotations).toBeUndefined();
|
|
2089
|
+
});
|
|
2090
|
+
});
|
|
2091
|
+
|
|
2092
|
+
// ── MetricsServer ──────────────────────────────────────────────────
|
|
2093
|
+
|
|
2094
|
+
describe("MetricsServer", () => {
|
|
2095
|
+
test("returns all 8 resources", () => {
|
|
2096
|
+
const result = MetricsServer({});
|
|
2097
|
+
expect(result.deployment).toBeDefined();
|
|
2098
|
+
expect(result.service).toBeDefined();
|
|
2099
|
+
expect(result.serviceAccount).toBeDefined();
|
|
2100
|
+
expect(result.clusterRole).toBeDefined();
|
|
2101
|
+
expect(result.clusterRoleBinding).toBeDefined();
|
|
2102
|
+
expect(result.aggregatedClusterRole).toBeDefined();
|
|
2103
|
+
expect(result.authDelegatorBinding).toBeDefined();
|
|
2104
|
+
expect(result.apiService).toBeDefined();
|
|
2105
|
+
});
|
|
2106
|
+
|
|
2107
|
+
test("default namespace is kube-system", () => {
|
|
2108
|
+
const result = MetricsServer({});
|
|
2109
|
+
expect((result.deployment.metadata as any).namespace).toBe("kube-system");
|
|
2110
|
+
expect((result.service.metadata as any).namespace).toBe("kube-system");
|
|
2111
|
+
expect((result.serviceAccount.metadata as any).namespace).toBe("kube-system");
|
|
2112
|
+
});
|
|
2113
|
+
|
|
2114
|
+
test("service targets port 10250", () => {
|
|
2115
|
+
const result = MetricsServer({});
|
|
2116
|
+
const spec = result.service.spec as any;
|
|
2117
|
+
expect(spec.ports[0].port).toBe(443);
|
|
2118
|
+
expect(spec.ports[0].targetPort).toBe(10250);
|
|
2119
|
+
});
|
|
2120
|
+
|
|
2121
|
+
test("deployment container has correct image and args", () => {
|
|
2122
|
+
const result = MetricsServer({});
|
|
2123
|
+
const spec = result.deployment.spec as any;
|
|
2124
|
+
const container = spec.template.spec.containers[0];
|
|
2125
|
+
expect(container.image).toBe("registry.k8s.io/metrics-server/metrics-server:v0.7.2");
|
|
2126
|
+
expect(container.args).toContain("--secure-port=10250");
|
|
2127
|
+
expect(container.args).toContain("--kubelet-use-node-status-port");
|
|
2128
|
+
expect(container.args).toContain("--metric-resolution=15s");
|
|
2129
|
+
});
|
|
2130
|
+
|
|
2131
|
+
test("clusterRole has nodes/metrics access", () => {
|
|
2132
|
+
const result = MetricsServer({});
|
|
2133
|
+
const rules = result.clusterRole.rules as any[];
|
|
2134
|
+
const nodeMetricsRule = rules.find((r: any) => r.resources?.includes("nodes/metrics"));
|
|
2135
|
+
expect(nodeMetricsRule).toBeDefined();
|
|
2136
|
+
});
|
|
2137
|
+
|
|
2138
|
+
test("apiService references correct service", () => {
|
|
2139
|
+
const result = MetricsServer({});
|
|
2140
|
+
const spec = (result.apiService as any).spec;
|
|
2141
|
+
expect(spec.service.name).toBe("metrics-server");
|
|
2142
|
+
expect(spec.service.namespace).toBe("kube-system");
|
|
2143
|
+
expect(spec.group).toBe("metrics.k8s.io");
|
|
2144
|
+
expect(spec.version).toBe("v1beta1");
|
|
2145
|
+
});
|
|
2146
|
+
|
|
2147
|
+
test("aggregated clusterRole has aggregate labels", () => {
|
|
2148
|
+
const result = MetricsServer({});
|
|
2149
|
+
const labels = (result.aggregatedClusterRole.metadata as any).labels;
|
|
2150
|
+
expect(labels["rbac.authorization.k8s.io/aggregate-to-admin"]).toBe("true");
|
|
2151
|
+
expect(labels["rbac.authorization.k8s.io/aggregate-to-view"]).toBe("true");
|
|
2152
|
+
});
|
|
2153
|
+
|
|
2154
|
+
test("custom image and replicas", () => {
|
|
2155
|
+
const result = MetricsServer({ image: "custom:v1", replicas: 2 });
|
|
2156
|
+
const spec = result.deployment.spec as any;
|
|
2157
|
+
expect(spec.replicas).toBe(2);
|
|
2158
|
+
expect(spec.template.spec.containers[0].image).toBe("custom:v1");
|
|
2159
|
+
});
|
|
2160
|
+
});
|
|
2161
|
+
|
|
2162
|
+
// ── PSS restricted securityContext passthrough tests ─────────────
|
|
2163
|
+
|
|
2164
|
+
const pssSecurityContext = {
|
|
2165
|
+
runAsNonRoot: true,
|
|
2166
|
+
readOnlyRootFilesystem: true,
|
|
2167
|
+
allowPrivilegeEscalation: false,
|
|
2168
|
+
capabilities: { drop: ["ALL"] },
|
|
2169
|
+
seccompProfile: { type: "RuntimeDefault" },
|
|
2170
|
+
};
|
|
2171
|
+
|
|
2172
|
+
describe("BatchJob securityContext", () => {
|
|
2173
|
+
test("securityContext passed through to container", () => {
|
|
2174
|
+
const result = BatchJob({
|
|
2175
|
+
name: "migrate",
|
|
2176
|
+
image: "migrate:1.0",
|
|
2177
|
+
securityContext: pssSecurityContext,
|
|
2178
|
+
});
|
|
2179
|
+
const spec = result.job.spec as any;
|
|
2180
|
+
const sc = spec.template.spec.containers[0].securityContext;
|
|
2181
|
+
expect(sc.runAsNonRoot).toBe(true);
|
|
2182
|
+
expect(sc.allowPrivilegeEscalation).toBe(false);
|
|
2183
|
+
expect(sc.capabilities).toEqual({ drop: ["ALL"] });
|
|
2184
|
+
expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
|
|
2185
|
+
});
|
|
2186
|
+
});
|
|
2187
|
+
|
|
2188
|
+
describe("CronWorkload securityContext", () => {
|
|
2189
|
+
test("securityContext passed through to container", () => {
|
|
2190
|
+
const result = CronWorkload({
|
|
2191
|
+
name: "backup",
|
|
2192
|
+
image: "backup:1.0",
|
|
2193
|
+
schedule: "0 2 * * *",
|
|
2194
|
+
securityContext: pssSecurityContext,
|
|
2195
|
+
});
|
|
2196
|
+
const spec = result.cronJob.spec as any;
|
|
2197
|
+
const sc = spec.jobTemplate.spec.template.spec.containers[0].securityContext;
|
|
2198
|
+
expect(sc.runAsNonRoot).toBe(true);
|
|
2199
|
+
expect(sc.allowPrivilegeEscalation).toBe(false);
|
|
2200
|
+
expect(sc.capabilities).toEqual({ drop: ["ALL"] });
|
|
2201
|
+
expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
|
|
2202
|
+
});
|
|
2203
|
+
});
|
|
2204
|
+
|
|
2205
|
+
describe("NodeAgent securityContext", () => {
|
|
2206
|
+
test("securityContext passed through to container", () => {
|
|
2207
|
+
const result = NodeAgent({
|
|
2208
|
+
name: "agent",
|
|
2209
|
+
image: "agent:1.0",
|
|
2210
|
+
rbacRules: [{ apiGroups: [""], resources: ["pods"], verbs: ["get"] }],
|
|
2211
|
+
securityContext: pssSecurityContext,
|
|
2212
|
+
});
|
|
2213
|
+
const spec = result.daemonSet.spec as any;
|
|
2214
|
+
const sc = spec.template.spec.containers[0].securityContext;
|
|
2215
|
+
expect(sc.runAsNonRoot).toBe(true);
|
|
2216
|
+
expect(sc.allowPrivilegeEscalation).toBe(false);
|
|
2217
|
+
expect(sc.capabilities).toEqual({ drop: ["ALL"] });
|
|
2218
|
+
});
|
|
2219
|
+
});
|
|
2220
|
+
|
|
2221
|
+
describe("ConfiguredApp securityContext", () => {
|
|
2222
|
+
test("securityContext passed through to container", () => {
|
|
2223
|
+
const result = ConfiguredApp({
|
|
2224
|
+
name: "api",
|
|
2225
|
+
image: "api:1.0",
|
|
2226
|
+
securityContext: pssSecurityContext,
|
|
2227
|
+
});
|
|
2228
|
+
const spec = result.deployment.spec as any;
|
|
2229
|
+
const sc = spec.template.spec.containers[0].securityContext;
|
|
2230
|
+
expect(sc.runAsNonRoot).toBe(true);
|
|
2231
|
+
expect(sc.allowPrivilegeEscalation).toBe(false);
|
|
2232
|
+
expect(sc.capabilities).toEqual({ drop: ["ALL"] });
|
|
2233
|
+
expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
|
|
2234
|
+
});
|
|
2235
|
+
});
|
|
2236
|
+
|
|
2237
|
+
describe("SidecarApp securityContext", () => {
|
|
2238
|
+
test("securityContext passed through to primary container", () => {
|
|
2239
|
+
const result = SidecarApp({
|
|
2240
|
+
name: "api",
|
|
2241
|
+
image: "api:1.0",
|
|
2242
|
+
sidecars: [{ name: "envoy", image: "envoy:1.0" }],
|
|
2243
|
+
securityContext: pssSecurityContext,
|
|
2244
|
+
});
|
|
2245
|
+
const spec = result.deployment.spec as any;
|
|
2246
|
+
const sc = spec.template.spec.containers[0].securityContext;
|
|
2247
|
+
expect(sc.runAsNonRoot).toBe(true);
|
|
2248
|
+
expect(sc.allowPrivilegeEscalation).toBe(false);
|
|
2249
|
+
expect(sc.capabilities).toEqual({ drop: ["ALL"] });
|
|
2250
|
+
expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
|
|
2251
|
+
});
|
|
2252
|
+
});
|
|
2253
|
+
|
|
2254
|
+
describe("NetworkIsolatedApp securityContext", () => {
|
|
2255
|
+
test("securityContext passed through to container", () => {
|
|
2256
|
+
const result = NetworkIsolatedApp({
|
|
2257
|
+
name: "api",
|
|
2258
|
+
image: "api:1.0",
|
|
2259
|
+
securityContext: pssSecurityContext,
|
|
2260
|
+
});
|
|
2261
|
+
const spec = result.deployment.spec as any;
|
|
2262
|
+
const sc = spec.template.spec.containers[0].securityContext;
|
|
2263
|
+
expect(sc.runAsNonRoot).toBe(true);
|
|
2264
|
+
expect(sc.allowPrivilegeEscalation).toBe(false);
|
|
2265
|
+
expect(sc.capabilities).toEqual({ drop: ["ALL"] });
|
|
2266
|
+
expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
|
|
2267
|
+
});
|
|
2268
|
+
});
|
|
2269
|
+
|
|
2270
|
+
describe("MonitoredService securityContext", () => {
|
|
2271
|
+
test("securityContext passed through to container", () => {
|
|
2272
|
+
const result = MonitoredService({
|
|
2273
|
+
name: "api",
|
|
2274
|
+
image: "api:1.0",
|
|
2275
|
+
securityContext: pssSecurityContext,
|
|
2276
|
+
});
|
|
2277
|
+
const spec = result.deployment.spec as any;
|
|
2278
|
+
const sc = spec.template.spec.containers[0].securityContext;
|
|
2279
|
+
expect(sc.runAsNonRoot).toBe(true);
|
|
2280
|
+
expect(sc.allowPrivilegeEscalation).toBe(false);
|
|
2281
|
+
expect(sc.capabilities).toEqual({ drop: ["ALL"] });
|
|
2282
|
+
expect(sc.seccompProfile).toEqual({ type: "RuntimeDefault" });
|
|
2283
|
+
});
|
|
2284
|
+
});
|
|
2285
|
+
|
|
2286
|
+
// ── Infrastructure composites: hardcoded security defaults ──────
|
|
2287
|
+
|
|
2288
|
+
describe("ExternalDnsAgent security defaults", () => {
|
|
2289
|
+
test("container has hardcoded PSS-safe securityContext", () => {
|
|
2290
|
+
const result = ExternalDnsAgent({
|
|
2291
|
+
iamRoleArn: "arn:aws:iam::123456789012:role/test",
|
|
2292
|
+
domainFilters: ["example.com"],
|
|
2293
|
+
});
|
|
2294
|
+
const spec = result.deployment.spec as any;
|
|
2295
|
+
const sc = spec.template.spec.containers[0].securityContext;
|
|
2296
|
+
expect(sc.runAsNonRoot).toBe(true);
|
|
2297
|
+
expect(sc.readOnlyRootFilesystem).toBe(true);
|
|
2298
|
+
expect(sc.allowPrivilegeEscalation).toBe(false);
|
|
2299
|
+
});
|
|
2300
|
+
});
|
|
2301
|
+
|
|
2302
|
+
describe("FluentBitAgent security defaults", () => {
|
|
2303
|
+
test("container runs as root (needs host log access) with other PSS hardening", () => {
|
|
2304
|
+
const result = FluentBitAgent({
|
|
2305
|
+
logGroup: "/aws/eks/test/containers",
|
|
2306
|
+
region: "us-east-1",
|
|
2307
|
+
clusterName: "test",
|
|
2308
|
+
});
|
|
2309
|
+
const spec = result.daemonSet.spec as any;
|
|
2310
|
+
const sc = spec.template.spec.containers[0].securityContext;
|
|
2311
|
+
expect(sc.runAsUser).toBe(0);
|
|
2312
|
+
expect(sc.runAsNonRoot).toBeUndefined();
|
|
2313
|
+
expect(sc.readOnlyRootFilesystem).toBe(true);
|
|
2314
|
+
expect(sc.allowPrivilegeEscalation).toBe(false);
|
|
2315
|
+
});
|
|
2316
|
+
});
|
|
2317
|
+
|
|
2318
|
+
describe("AdotCollector security defaults", () => {
|
|
2319
|
+
test("container runs as non-root with explicit UID (ADOT 'aoc' user)", () => {
|
|
2320
|
+
const result = AdotCollector({
|
|
2321
|
+
region: "us-east-1",
|
|
2322
|
+
clusterName: "test",
|
|
2323
|
+
});
|
|
2324
|
+
const spec = result.daemonSet.spec as any;
|
|
2325
|
+
const sc = spec.template.spec.containers[0].securityContext;
|
|
2326
|
+
expect(sc.runAsNonRoot).toBe(true);
|
|
2327
|
+
expect(sc.runAsUser).toBe(10001);
|
|
2328
|
+
expect(sc.readOnlyRootFilesystem).toBe(true);
|
|
2329
|
+
expect(sc.allowPrivilegeEscalation).toBe(false);
|
|
2330
|
+
});
|
|
2331
|
+
});
|
|
2332
|
+
|
|
2333
|
+
// ── Phase 3B: Composite serialization smoke tests ───────────────
|
|
2334
|
+
|
|
2335
|
+
describe("Composite YAML serialization smoke tests", () => {
|
|
2336
|
+
function serializeCompositeProps(props: Record<string, Record<string, unknown>>): string {
|
|
2337
|
+
return Object.values(props)
|
|
2338
|
+
.map((p) => emitYAML(p, 0))
|
|
2339
|
+
.join("\n---\n");
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
test("ExternalDnsAgent serializes to valid YAML", () => {
|
|
2343
|
+
const result = ExternalDnsAgent({
|
|
2344
|
+
iamRoleArn: "arn:aws:iam::123456789012:role/test",
|
|
2345
|
+
domainFilters: ["example.com"],
|
|
2346
|
+
});
|
|
2347
|
+
const yaml = serializeCompositeProps(result as any);
|
|
2348
|
+
expect(yaml).toContain("external-dns");
|
|
2349
|
+
expect(yaml).not.toContain("[object Object]");
|
|
2350
|
+
});
|
|
2351
|
+
|
|
2352
|
+
test("FluentBitAgent serializes multiline config correctly", () => {
|
|
2353
|
+
const result = FluentBitAgent({
|
|
2354
|
+
logGroup: "/aws/eks/test/containers",
|
|
2355
|
+
region: "us-east-1",
|
|
2356
|
+
clusterName: "test",
|
|
2357
|
+
});
|
|
2358
|
+
const yaml = serializeCompositeProps(result as any);
|
|
2359
|
+
// Multiline config should use | block scalar, not flatten
|
|
2360
|
+
expect(yaml).toContain("|");
|
|
2361
|
+
expect(yaml).toContain("[SERVICE]");
|
|
2362
|
+
expect(yaml).toContain("[INPUT]");
|
|
2363
|
+
expect(yaml).not.toContain("\\n");
|
|
2364
|
+
});
|
|
2365
|
+
|
|
2366
|
+
test("AdotCollector serializes multiline config correctly", () => {
|
|
2367
|
+
const result = AdotCollector({
|
|
2368
|
+
region: "us-east-1",
|
|
2369
|
+
clusterName: "test",
|
|
2370
|
+
});
|
|
2371
|
+
const yaml = serializeCompositeProps(result as any);
|
|
2372
|
+
expect(yaml).toContain("|");
|
|
2373
|
+
expect(yaml).toContain("receivers:");
|
|
2374
|
+
expect(yaml).toContain("exporters:");
|
|
2375
|
+
expect(yaml).not.toContain("\\n");
|
|
2376
|
+
});
|
|
2377
|
+
|
|
2378
|
+
test("MetricsServer serializes to valid YAML", () => {
|
|
2379
|
+
const result = MetricsServer({});
|
|
2380
|
+
const yaml = serializeCompositeProps(result as any);
|
|
2381
|
+
expect(yaml).toContain("metrics-server");
|
|
2382
|
+
expect(yaml).not.toContain("[object Object]");
|
|
2383
|
+
});
|
|
2384
|
+
});
|
|
2385
|
+
|
|
2386
|
+
// ── Phase 4A: MetricsServer RBAC completeness ──────────────────
|
|
2387
|
+
|
|
2388
|
+
describe("MetricsServer RBAC completeness", () => {
|
|
2389
|
+
test("clusterRole includes configmaps resource", () => {
|
|
2390
|
+
const result = MetricsServer({});
|
|
2391
|
+
const rules = result.clusterRole.rules as any[];
|
|
2392
|
+
const hasConfigmaps = rules.some((r: any) => r.resources?.includes("configmaps"));
|
|
2393
|
+
expect(hasConfigmaps).toBe(true);
|
|
2394
|
+
});
|
|
2395
|
+
});
|
|
2396
|
+
|
|
2397
|
+
// ── Phase 4B: AdotCollector command vs args ─────────────────────
|
|
2398
|
+
|
|
2399
|
+
describe("AdotCollector command vs args", () => {
|
|
2400
|
+
test("container uses args (not command) for config flag", () => {
|
|
2401
|
+
const result = AdotCollector({
|
|
2402
|
+
region: "us-east-1",
|
|
2403
|
+
clusterName: "test",
|
|
2404
|
+
});
|
|
2405
|
+
const spec = result.daemonSet.spec as any;
|
|
2406
|
+
const container = spec.template.spec.containers[0];
|
|
2407
|
+
// Config flag should be in args, not command
|
|
2408
|
+
expect(container.args).toContain("--config=/etc/adot/config.yaml");
|
|
2409
|
+
expect(container.command).toBeUndefined();
|
|
2410
|
+
});
|
|
2411
|
+
});
|
|
2412
|
+
|
|
2413
|
+
// ── Phase 4C: AdotCollector pipeline exporter separation ────────
|
|
2414
|
+
|
|
2415
|
+
describe("AdotCollector pipeline exporter separation", () => {
|
|
2416
|
+
test("metrics pipeline does NOT include awsxray", () => {
|
|
2417
|
+
const result = AdotCollector({
|
|
2418
|
+
region: "us-east-1",
|
|
2419
|
+
clusterName: "test",
|
|
2420
|
+
exporters: ["cloudwatch", "xray"],
|
|
2421
|
+
});
|
|
2422
|
+
const config = (result.configMap as any).data["config.yaml"] as string;
|
|
2423
|
+
// Extract metrics pipeline exporters line
|
|
2424
|
+
const metricsMatch = config.match(/metrics:\s*\n\s*receivers:.*\n\s*processors:.*\n\s*exporters:\s*\[([^\]]+)\]/);
|
|
2425
|
+
expect(metricsMatch).toBeDefined();
|
|
2426
|
+
const metricsExporters = metricsMatch![1];
|
|
2427
|
+
expect(metricsExporters).not.toContain("awsxray");
|
|
2428
|
+
expect(metricsExporters).toContain("awsemf");
|
|
2429
|
+
});
|
|
2430
|
+
|
|
2431
|
+
test("traces pipeline does NOT include awsemf", () => {
|
|
2432
|
+
const result = AdotCollector({
|
|
2433
|
+
region: "us-east-1",
|
|
2434
|
+
clusterName: "test",
|
|
2435
|
+
exporters: ["cloudwatch", "xray"],
|
|
2436
|
+
});
|
|
2437
|
+
const config = (result.configMap as any).data["config.yaml"] as string;
|
|
2438
|
+
// Extract traces pipeline exporters line
|
|
2439
|
+
const tracesMatch = config.match(/traces:\s*\n\s*receivers:.*\n\s*processors:.*\n\s*exporters:\s*\[([^\]]+)\]/);
|
|
2440
|
+
expect(tracesMatch).toBeDefined();
|
|
2441
|
+
const tracesExporters = tracesMatch![1];
|
|
2442
|
+
expect(tracesExporters).not.toContain("awsemf");
|
|
2443
|
+
expect(tracesExporters).toContain("awsxray");
|
|
2444
|
+
});
|
|
2445
|
+
|
|
2446
|
+
test("cloudwatch-only: traces pipeline falls back to valid default", () => {
|
|
2447
|
+
const result = AdotCollector({
|
|
2448
|
+
region: "us-east-1",
|
|
2449
|
+
clusterName: "test",
|
|
2450
|
+
exporters: ["cloudwatch"],
|
|
2451
|
+
});
|
|
2452
|
+
const config = (result.configMap as any).data["config.yaml"] as string;
|
|
2453
|
+
// Traces pipeline should still have an exporter (fallback to awsxray)
|
|
2454
|
+
const tracesMatch = config.match(/traces:\s*\n\s*receivers:.*\n\s*processors:.*\n\s*exporters:\s*\[([^\]]+)\]/);
|
|
2455
|
+
expect(tracesMatch).toBeDefined();
|
|
2456
|
+
const tracesExporters = tracesMatch![1].trim();
|
|
2457
|
+
expect(tracesExporters.length).toBeGreaterThan(0);
|
|
2458
|
+
});
|
|
2459
|
+
});
|
|
2460
|
+
|
|
2461
|
+
// ── Phase 4D: AdotCollector config parseable as YAML ────────────
|
|
2462
|
+
|
|
2463
|
+
describe("AdotCollector config structure", () => {
|
|
2464
|
+
test("generated config has required top-level sections", () => {
|
|
2465
|
+
const result = AdotCollector({
|
|
2466
|
+
region: "us-east-1",
|
|
2467
|
+
clusterName: "test",
|
|
2468
|
+
});
|
|
2469
|
+
const config = (result.configMap as any).data["config.yaml"] as string;
|
|
2470
|
+
expect(config).toContain("receivers:");
|
|
2471
|
+
expect(config).toContain("exporters:");
|
|
2472
|
+
expect(config).toContain("processors:");
|
|
2473
|
+
expect(config).toContain("service:");
|
|
2474
|
+
expect(config).toContain("pipelines:");
|
|
2475
|
+
expect(config).toContain("metrics:");
|
|
2476
|
+
expect(config).toContain("traces:");
|
|
2477
|
+
});
|
|
2478
|
+
});
|
|
2479
|
+
|
|
2480
|
+
// ── WorkloadIdentityServiceAccount ──────────────────────────────────
|
|
2481
|
+
|
|
2482
|
+
describe("WorkloadIdentityServiceAccount", () => {
|
|
2483
|
+
const minProps = { name: "app-sa", gcpServiceAccountEmail: "sa@my-project.iam.gserviceaccount.com" };
|
|
2484
|
+
|
|
2485
|
+
test("returns serviceAccount with Workload Identity annotation", () => {
|
|
2486
|
+
const result = WorkloadIdentityServiceAccount(minProps);
|
|
2487
|
+
expect(result.serviceAccount).toBeDefined();
|
|
2488
|
+
const meta = result.serviceAccount.metadata as any;
|
|
2489
|
+
expect(meta.annotations["iam.gke.io/gcp-service-account"]).toBe("sa@my-project.iam.gserviceaccount.com");
|
|
2490
|
+
});
|
|
2491
|
+
|
|
2492
|
+
test("no RBAC by default", () => {
|
|
2493
|
+
const result = WorkloadIdentityServiceAccount(minProps);
|
|
2494
|
+
expect(result.role).toBeUndefined();
|
|
2495
|
+
expect(result.roleBinding).toBeUndefined();
|
|
2496
|
+
});
|
|
2497
|
+
|
|
2498
|
+
test("RBAC created when rules provided", () => {
|
|
2499
|
+
const result = WorkloadIdentityServiceAccount({
|
|
2500
|
+
...minProps,
|
|
2501
|
+
rbacRules: [{ apiGroups: [""], resources: ["secrets"], verbs: ["get"] }],
|
|
2502
|
+
});
|
|
2503
|
+
expect(result.role).toBeDefined();
|
|
2504
|
+
expect(result.roleBinding).toBeDefined();
|
|
2505
|
+
const role = result.role as any;
|
|
2506
|
+
expect(role.rules[0].resources).toEqual(["secrets"]);
|
|
2507
|
+
});
|
|
2508
|
+
|
|
2509
|
+
test("namespace propagated", () => {
|
|
2510
|
+
const result = WorkloadIdentityServiceAccount({ ...minProps, namespace: "prod" });
|
|
2511
|
+
expect((result.serviceAccount.metadata as any).namespace).toBe("prod");
|
|
2512
|
+
});
|
|
2513
|
+
|
|
2514
|
+
test("component labels", () => {
|
|
2515
|
+
const result = WorkloadIdentityServiceAccount(minProps);
|
|
2516
|
+
expect((result.serviceAccount.metadata as any).labels["app.kubernetes.io/component"]).toBe("service-account");
|
|
2517
|
+
});
|
|
2518
|
+
});
|
|
2519
|
+
|
|
2520
|
+
// ── GcePdStorageClass ───────────────────────────────────────────────
|
|
2521
|
+
|
|
2522
|
+
describe("GcePdStorageClass", () => {
|
|
2523
|
+
test("returns storageClass with GCE PD provisioner", () => {
|
|
2524
|
+
const result = GcePdStorageClass({ name: "pd-balanced" });
|
|
2525
|
+
expect(result.storageClass).toBeDefined();
|
|
2526
|
+
expect((result.storageClass as any).provisioner).toBe("pd.csi.storage.gke.io");
|
|
2527
|
+
});
|
|
2528
|
+
|
|
2529
|
+
test("default type is pd-balanced", () => {
|
|
2530
|
+
const result = GcePdStorageClass({ name: "default" });
|
|
2531
|
+
expect((result.storageClass as any).parameters.type).toBe("pd-balanced");
|
|
2532
|
+
});
|
|
2533
|
+
|
|
2534
|
+
test("custom type", () => {
|
|
2535
|
+
const result = GcePdStorageClass({ name: "ssd", type: "pd-ssd" });
|
|
2536
|
+
expect((result.storageClass as any).parameters.type).toBe("pd-ssd");
|
|
2537
|
+
});
|
|
2538
|
+
|
|
2539
|
+
test("regional-pd replication type", () => {
|
|
2540
|
+
const result = GcePdStorageClass({ name: "regional", replicationType: "regional-pd" });
|
|
2541
|
+
expect((result.storageClass as any).parameters["replication-type"]).toBe("regional-pd");
|
|
2542
|
+
});
|
|
2543
|
+
|
|
2544
|
+
test("no replication-type param when none", () => {
|
|
2545
|
+
const result = GcePdStorageClass({ name: "default" });
|
|
2546
|
+
expect((result.storageClass as any).parameters["replication-type"]).toBeUndefined();
|
|
2547
|
+
});
|
|
2548
|
+
|
|
2549
|
+
test("allowVolumeExpansion default true", () => {
|
|
2550
|
+
const result = GcePdStorageClass({ name: "exp" });
|
|
2551
|
+
expect((result.storageClass as any).allowVolumeExpansion).toBe(true);
|
|
2552
|
+
});
|
|
2553
|
+
|
|
2554
|
+
test("storageClass is cluster-scoped (no namespace)", () => {
|
|
2555
|
+
const result = GcePdStorageClass({ name: "sc" });
|
|
2556
|
+
expect((result.storageClass.metadata as any).namespace).toBeUndefined();
|
|
2557
|
+
});
|
|
2558
|
+
});
|
|
2559
|
+
|
|
2560
|
+
// ── FilestoreStorageClass ───────────────────────────────────────────
|
|
2561
|
+
|
|
2562
|
+
describe("FilestoreStorageClass", () => {
|
|
2563
|
+
test("returns storageClass with Filestore provisioner", () => {
|
|
2564
|
+
const result = FilestoreStorageClass({ name: "filestore" });
|
|
2565
|
+
expect((result.storageClass as any).provisioner).toBe("filestore.csi.storage.gke.io");
|
|
2566
|
+
});
|
|
2567
|
+
|
|
2568
|
+
test("default tier is standard", () => {
|
|
2569
|
+
const result = FilestoreStorageClass({ name: "fs" });
|
|
2570
|
+
expect((result.storageClass as any).parameters.tier).toBe("standard");
|
|
2571
|
+
});
|
|
2572
|
+
|
|
2573
|
+
test("custom tier", () => {
|
|
2574
|
+
const result = FilestoreStorageClass({ name: "premium-fs", tier: "premium" });
|
|
2575
|
+
expect((result.storageClass as any).parameters.tier).toBe("premium");
|
|
2576
|
+
});
|
|
2577
|
+
|
|
2578
|
+
test("network parameter set when provided", () => {
|
|
2579
|
+
const result = FilestoreStorageClass({ name: "fs", network: "my-vpc" });
|
|
2580
|
+
expect((result.storageClass as any).parameters.network).toBe("my-vpc");
|
|
2581
|
+
});
|
|
2582
|
+
|
|
2583
|
+
test("no network parameter by default", () => {
|
|
2584
|
+
const result = FilestoreStorageClass({ name: "fs" });
|
|
2585
|
+
expect((result.storageClass as any).parameters.network).toBeUndefined();
|
|
2586
|
+
});
|
|
2587
|
+
});
|
|
2588
|
+
|
|
2589
|
+
// ── GkeGateway ──────────────────────────────────────────────────────
|
|
2590
|
+
|
|
2591
|
+
describe("GkeGateway", () => {
|
|
2592
|
+
const minProps = {
|
|
2593
|
+
name: "api-gateway",
|
|
2594
|
+
hosts: [{ hostname: "api.example.com", paths: [{ path: "/", serviceName: "api", servicePort: 80 }] }],
|
|
2595
|
+
};
|
|
2596
|
+
|
|
2597
|
+
test("returns gateway and httpRoute", () => {
|
|
2598
|
+
const result = GkeGateway(minProps);
|
|
2599
|
+
expect(result.gateway).toBeDefined();
|
|
2600
|
+
expect(result.httpRoute).toBeDefined();
|
|
2601
|
+
});
|
|
2602
|
+
|
|
2603
|
+
test("default gatewayClassName", () => {
|
|
2604
|
+
const result = GkeGateway(minProps);
|
|
2605
|
+
const spec = result.gateway.spec as any;
|
|
2606
|
+
expect(spec.gatewayClassName).toBe("gke-l7-global-external-managed");
|
|
2607
|
+
});
|
|
2608
|
+
|
|
2609
|
+
test("custom gatewayClassName", () => {
|
|
2610
|
+
const result = GkeGateway({ ...minProps, gatewayClassName: "gke-l7-rilb" });
|
|
2611
|
+
const spec = result.gateway.spec as any;
|
|
2612
|
+
expect(spec.gatewayClassName).toBe("gke-l7-rilb");
|
|
2613
|
+
});
|
|
2614
|
+
|
|
2615
|
+
test("HTTP listener when no certificate", () => {
|
|
2616
|
+
const result = GkeGateway(minProps);
|
|
2617
|
+
const spec = result.gateway.spec as any;
|
|
2618
|
+
expect(spec.listeners[0].protocol).toBe("HTTP");
|
|
2619
|
+
expect(spec.listeners[0].port).toBe(80);
|
|
2620
|
+
});
|
|
2621
|
+
|
|
2622
|
+
test("HTTPS listener with certificate", () => {
|
|
2623
|
+
const result = GkeGateway({ ...minProps, certificateName: "api-cert" });
|
|
2624
|
+
const spec = result.gateway.spec as any;
|
|
2625
|
+
expect(spec.listeners[0].protocol).toBe("HTTPS");
|
|
2626
|
+
expect(spec.listeners[0].port).toBe(443);
|
|
2627
|
+
expect(spec.listeners[0].tls.certificateRefs[0].name).toBe("api-cert");
|
|
2628
|
+
});
|
|
2629
|
+
|
|
2630
|
+
test("httpRoute references parent gateway", () => {
|
|
2631
|
+
const result = GkeGateway(minProps);
|
|
2632
|
+
const spec = result.httpRoute.spec as any;
|
|
2633
|
+
expect(spec.parentRefs[0].name).toBe("api-gateway");
|
|
2634
|
+
});
|
|
2635
|
+
|
|
2636
|
+
test("httpRoute has hostnames", () => {
|
|
2637
|
+
const result = GkeGateway(minProps);
|
|
2638
|
+
const spec = result.httpRoute.spec as any;
|
|
2639
|
+
expect(spec.hostnames).toEqual(["api.example.com"]);
|
|
2640
|
+
});
|
|
2641
|
+
|
|
2642
|
+
test("httpRoute rules map to backend services", () => {
|
|
2643
|
+
const result = GkeGateway(minProps);
|
|
2644
|
+
const spec = result.httpRoute.spec as any;
|
|
2645
|
+
expect(spec.rules[0].backendRefs[0].name).toBe("api");
|
|
2646
|
+
expect(spec.rules[0].backendRefs[0].port).toBe(80);
|
|
2647
|
+
});
|
|
2648
|
+
|
|
2649
|
+
test("namespace propagated to both resources", () => {
|
|
2650
|
+
const result = GkeGateway({ ...minProps, namespace: "prod" });
|
|
2651
|
+
expect((result.gateway.metadata as any).namespace).toBe("prod");
|
|
2652
|
+
expect((result.httpRoute.metadata as any).namespace).toBe("prod");
|
|
2653
|
+
});
|
|
2654
|
+
});
|
|
2655
|
+
|
|
2656
|
+
// ── ConfigConnectorContext ───────────────────────────────────────────
|
|
2657
|
+
|
|
2658
|
+
describe("ConfigConnectorContext", () => {
|
|
2659
|
+
const minProps = { googleServiceAccountEmail: "cnrm@my-project.iam.gserviceaccount.com" };
|
|
2660
|
+
|
|
2661
|
+
test("returns context with apiVersion and kind", () => {
|
|
2662
|
+
const result = ConfigConnectorContext(minProps);
|
|
2663
|
+
expect(result.context).toBeDefined();
|
|
2664
|
+
expect((result.context as any).apiVersion).toBe("core.cnrm.cloud.google.com/v1beta1");
|
|
2665
|
+
expect((result.context as any).kind).toBe("ConfigConnectorContext");
|
|
2666
|
+
});
|
|
2667
|
+
|
|
2668
|
+
test("googleServiceAccount in spec", () => {
|
|
2669
|
+
const result = ConfigConnectorContext(minProps);
|
|
2670
|
+
const spec = (result.context as any).spec;
|
|
2671
|
+
expect(spec.googleServiceAccount).toBe("cnrm@my-project.iam.gserviceaccount.com");
|
|
2672
|
+
});
|
|
2673
|
+
|
|
2674
|
+
test("default stateIntoSpec is absent", () => {
|
|
2675
|
+
const result = ConfigConnectorContext(minProps);
|
|
2676
|
+
expect((result.context as any).spec.stateIntoSpec).toBe("absent");
|
|
2677
|
+
});
|
|
2678
|
+
|
|
2679
|
+
test("custom stateIntoSpec", () => {
|
|
2680
|
+
const result = ConfigConnectorContext({ ...minProps, stateIntoSpec: "merge" });
|
|
2681
|
+
expect((result.context as any).spec.stateIntoSpec).toBe("merge");
|
|
2682
|
+
});
|
|
2683
|
+
|
|
2684
|
+
test("default namespace is default", () => {
|
|
2685
|
+
const result = ConfigConnectorContext(minProps);
|
|
2686
|
+
expect((result.context as any).metadata.namespace).toBe("default");
|
|
2687
|
+
});
|
|
2688
|
+
|
|
2689
|
+
test("custom namespace", () => {
|
|
2690
|
+
const result = ConfigConnectorContext({ ...minProps, namespace: "config-connector" });
|
|
2691
|
+
expect((result.context as any).metadata.namespace).toBe("config-connector");
|
|
2692
|
+
});
|
|
2693
|
+
});
|