timonel 2.1.1 → 2.3.0

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.
@@ -1,11 +1,44 @@
1
+ /**
2
+ * @fileoverview Rutter - Main class for building Kubernetes manifests and Helm charts
3
+ * @since 1.0.0
4
+ */
1
5
  import { App, Chart, Testing, ApiObject } from 'cdk8s';
2
6
  import YAML from 'yaml';
3
7
  import { include, helm } from './helm.js';
4
8
  import { HelmChartWriter } from './HelmChartWriter.js';
9
+ import { SecurityUtils } from './security.js';
5
10
  /**
6
- * Rutter builds Kubernetes manifests using cdk8s and writes a Helm chart.
11
+ * Rutter - Main class for building Kubernetes manifests and Helm charts
12
+ *
13
+ * Rutter (maritime pilot) guides the generation of Kubernetes resources
14
+ * using cdk8s and outputs complete Helm charts with proper templating.
15
+ *
16
+ * @class Rutter
17
+ * @since 1.0.0
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * const rutter = new Rutter({
22
+ * meta: { name: 'my-app', version: '1.0.0' },
23
+ * defaultValues: { replicas: 3 }
24
+ * });
25
+ *
26
+ * rutter.addDeployment({
27
+ * name: 'web',
28
+ * image: 'nginx:1.21',
29
+ * replicas: 3
30
+ * });
31
+ *
32
+ * rutter.write('./charts/my-app');
33
+ * ```
7
34
  */
8
35
  export class Rutter {
36
+ /**
37
+ * Creates a new Rutter instance
38
+ *
39
+ * @param {RutterProps} props - Configuration properties
40
+ * @since 1.0.0
41
+ */
9
42
  constructor(props) {
10
43
  this.assets = [];
11
44
  this.valueOverrides = {};
@@ -14,7 +47,21 @@ export class Rutter {
14
47
  this.chart = new Chart(this.app, props.manifestName ?? props.meta.name);
15
48
  }
16
49
  /**
17
- * Set dynamic value overrides (from --set flags)
50
+ * Sets dynamic value overrides for Helm values
51
+ *
52
+ * Used by CLI --set flags to override default values at runtime.
53
+ *
54
+ * @param {Record<string, string>} overrides - Key-value pairs to override
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * rutter.setValues({
59
+ * 'image.tag': 'v2.0.0',
60
+ * 'replicas': '5'
61
+ * });
62
+ * ```
63
+ *
64
+ * @since 1.0.0
18
65
  */
19
66
  setValues(overrides) {
20
67
  this.valueOverrides = { ...this.valueOverrides, ...overrides };
@@ -55,11 +102,32 @@ export class Rutter {
55
102
  try {
56
103
  return JSON.parse(value);
57
104
  }
58
- catch {
105
+ catch (error) {
106
+ // Log parsing error for debugging purposes
107
+ console.debug(`Failed to parse value as JSON: ${value}`, error);
59
108
  // If not valid JSON, treat as string
60
109
  return value;
61
110
  }
62
111
  }
112
+ /**
113
+ * Adds a Kubernetes Deployment to the chart
114
+ *
115
+ * @param {DeploymentSpec} spec - Deployment specification
116
+ * @returns {ApiObject} The created Deployment object
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * rutter.addDeployment({
121
+ * name: 'web-app',
122
+ * image: 'nginx:1.21',
123
+ * replicas: 3,
124
+ * containerPort: 80,
125
+ * env: { NODE_ENV: 'production' }
126
+ * });
127
+ * ```
128
+ *
129
+ * @since 1.0.0
130
+ */
63
131
  addDeployment(spec) {
64
132
  const match = spec.matchLabels ?? {
65
133
  [Rutter.LABEL_NAME]: include(Rutter.HELPER_NAME),
@@ -133,6 +201,23 @@ export class Rutter {
133
201
  this.capture(dep, `${spec.name}-deployment`);
134
202
  return dep;
135
203
  }
204
+ /**
205
+ * Adds a Kubernetes Service to the chart
206
+ *
207
+ * @param {ServiceSpec} spec - Service specification
208
+ * @returns {ApiObject} The created Service object
209
+ *
210
+ * @example
211
+ * ```typescript
212
+ * rutter.addService({
213
+ * name: 'web-service',
214
+ * ports: [{ port: 80, targetPort: 8080 }],
215
+ * type: 'LoadBalancer'
216
+ * });
217
+ * ```
218
+ *
219
+ * @since 1.0.0
220
+ */
136
221
  addService(spec) {
137
222
  const svc = new ApiObject(this.chart, spec.name, {
138
223
  apiVersion: 'v1',
@@ -240,6 +325,24 @@ export class Rutter {
240
325
  this.capture(rs, `${spec.name}-replicaset`);
241
326
  return rs;
242
327
  }
328
+ /**
329
+ * Adds a Kubernetes Job to the chart
330
+ *
331
+ * @param {JobSpec} spec - Job specification
332
+ * @returns {ApiObject} The created Job object
333
+ *
334
+ * @example
335
+ * ```typescript
336
+ * rutter.addJob({
337
+ * name: 'data-migration',
338
+ * image: 'migrate:latest',
339
+ * command: ['./migrate.sh'],
340
+ * backoffLimit: 3
341
+ * });
342
+ * ```
343
+ *
344
+ * @since 2.1.0
345
+ */
243
346
  addJob(spec) {
244
347
  // Validate Job specification
245
348
  this.validateJobSpec(spec);
@@ -262,6 +365,24 @@ export class Rutter {
262
365
  this.capture(job, `${spec.name}-job`);
263
366
  return job;
264
367
  }
368
+ /**
369
+ * Adds a Kubernetes CronJob to the chart
370
+ *
371
+ * @param {CronJobSpec} spec - CronJob specification
372
+ * @returns {ApiObject} The created CronJob object
373
+ *
374
+ * @example
375
+ * ```typescript
376
+ * rutter.addCronJob({
377
+ * name: 'backup-job',
378
+ * schedule: '0 2 * * *',
379
+ * image: 'backup:latest',
380
+ * command: ['./backup.sh']
381
+ * });
382
+ * ```
383
+ *
384
+ * @since 2.1.0
385
+ */
265
386
  addCronJob(spec) {
266
387
  // Validate CronJob specification
267
388
  this.validateCronJobSpec(spec);
@@ -284,6 +405,29 @@ export class Rutter {
284
405
  this.capture(cronJob, `${spec.name}-cronjob`);
285
406
  return cronJob;
286
407
  }
408
+ /**
409
+ * Adds a Kubernetes Ingress to the chart
410
+ *
411
+ * @param {IngressSpec} spec - Ingress specification
412
+ * @returns {ApiObject} The created Ingress object
413
+ *
414
+ * @example
415
+ * ```typescript
416
+ * rutter.addIngress({
417
+ * name: 'web-ingress',
418
+ * rules: [{
419
+ * host: 'example.com',
420
+ * paths: [{
421
+ * path: '/',
422
+ * pathType: 'Prefix',
423
+ * backend: { service: { name: 'web-service', port: { number: 80 } } }
424
+ * }]
425
+ * }]
426
+ * });
427
+ * ```
428
+ *
429
+ * @since 1.0.0
430
+ */
287
431
  addIngress(spec) {
288
432
  const ing = new ApiObject(this.chart, spec.name, {
289
433
  apiVersion: Rutter.NETWORKING_API_VERSION,
@@ -444,12 +588,31 @@ export class Rutter {
444
588
  }
445
589
  // Warn about common misconfigurations
446
590
  if (storageClassName?.includes('azure-disk') && accessModes.includes('ReadWriteMany')) {
447
- console.warn(`PVC ${spec.name}: Azure Disk does not support ReadWriteMany, consider Azure Files`);
591
+ console.warn(SecurityUtils.sanitizeLogMessage(`PVC ${spec.name}: Azure Disk does not support ReadWriteMany, consider Azure Files`));
448
592
  }
449
593
  if (storageClassName?.includes('ebs') && accessModes.includes('ReadWriteMany')) {
450
- console.warn(`PVC ${spec.name}: EBS does not support ReadWriteMany, consider EFS`);
594
+ console.warn(SecurityUtils.sanitizeLogMessage(`PVC ${spec.name}: EBS does not support ReadWriteMany, consider EFS`));
451
595
  }
452
596
  }
597
+ /**
598
+ * Adds a Kubernetes ConfigMap to the chart
599
+ *
600
+ * @param {ConfigMapSpec} spec - ConfigMap specification
601
+ * @returns {ApiObject} The created ConfigMap object
602
+ *
603
+ * @example
604
+ * ```typescript
605
+ * rutter.addConfigMap({
606
+ * name: 'app-config',
607
+ * data: {
608
+ * 'config.yaml': 'key: value',
609
+ * 'app.properties': 'debug=true'
610
+ * }
611
+ * });
612
+ * ```
613
+ *
614
+ * @since 1.0.0
615
+ */
453
616
  addConfigMap(spec) {
454
617
  const cm = new ApiObject(this.chart, spec.name, {
455
618
  apiVersion: 'v1',
@@ -466,6 +629,25 @@ export class Rutter {
466
629
  this.capture(cm, `${spec.name}-configmap`);
467
630
  return cm;
468
631
  }
632
+ /**
633
+ * Adds a Kubernetes Secret to the chart
634
+ *
635
+ * @param {SecretSpec} spec - Secret specification
636
+ * @returns {ApiObject} The created Secret object
637
+ *
638
+ * @example
639
+ * ```typescript
640
+ * rutter.addSecret({
641
+ * name: 'app-secrets',
642
+ * stringData: {
643
+ * 'username': 'admin',
644
+ * 'password': 'secret123'
645
+ * }
646
+ * });
647
+ * ```
648
+ *
649
+ * @since 1.0.0
650
+ */
469
651
  addSecret(spec) {
470
652
  const sec = new ApiObject(this.chart, spec.name, {
471
653
  apiVersion: 'v1',
@@ -483,6 +665,23 @@ export class Rutter {
483
665
  this.capture(sec, `${spec.name}-secret`);
484
666
  return sec;
485
667
  }
668
+ /**
669
+ * Adds a Kubernetes ServiceAccount to the chart
670
+ *
671
+ * @param {ServiceAccountSpec} spec - ServiceAccount specification
672
+ * @returns {ApiObject} The created ServiceAccount object
673
+ *
674
+ * @example
675
+ * ```typescript
676
+ * rutter.addServiceAccount({
677
+ * name: 'app-sa',
678
+ * awsRoleArn: 'arn:aws:iam::123456789012:role/MyRole',
679
+ * automountServiceAccountToken: true
680
+ * });
681
+ * ```
682
+ *
683
+ * @since 1.0.0
684
+ */
486
685
  addServiceAccount(spec) {
487
686
  const annotations = this.buildServiceAccountAnnotations(spec);
488
687
  const sa = new ApiObject(this.chart, spec.name, {
@@ -539,6 +738,28 @@ export class Rutter {
539
738
  annotations['iam.gke.io/gcp-service-account'] = spec.gcpServiceAccountEmail;
540
739
  }
541
740
  }
741
+ /**
742
+ * Adds a Kubernetes HorizontalPodAutoscaler to the chart
743
+ *
744
+ * @param {HorizontalPodAutoscalerSpec} spec - HPA specification
745
+ * @returns {ApiObject} The created HPA object
746
+ *
747
+ * @example
748
+ * ```typescript
749
+ * rutter.addHorizontalPodAutoscaler({
750
+ * name: 'web-hpa',
751
+ * scaleTargetRef: {
752
+ * apiVersion: 'apps/v1',
753
+ * kind: 'Deployment',
754
+ * name: 'web-app'
755
+ * },
756
+ * minReplicas: 2,
757
+ * maxReplicas: 10
758
+ * });
759
+ * ```
760
+ *
761
+ * @since 1.0.0
762
+ */
542
763
  addHorizontalPodAutoscaler(spec) {
543
764
  // Default metrics if none provided (CPU utilization at 80%)
544
765
  const defaultMetrics = [
@@ -705,8 +926,12 @@ export class Rutter {
705
926
  if (spec.subPath) {
706
927
  volumeAttributes['subPath'] = spec.subPath;
707
928
  }
708
- if (Object.keys(volumeAttributes).length > 0) {
709
- csiSpec['volumeAttributes'] = volumeAttributes;
929
+ // Only add volumeAttributes if it has properties (more efficient than Object.keys().length)
930
+ for (const key in volumeAttributes) {
931
+ if (Object.prototype.hasOwnProperty.call(volumeAttributes, key)) {
932
+ csiSpec['volumeAttributes'] = volumeAttributes;
933
+ break;
934
+ }
710
935
  }
711
936
  const pv = new ApiObject(this.chart, spec.name, {
712
937
  apiVersion: 'v1',
@@ -1055,6 +1280,257 @@ export class Rutter {
1055
1280
  this.capture(spc, `${spec.name}-secretproviderclass`);
1056
1281
  return spc;
1057
1282
  }
1283
+ /**
1284
+ * Add Azure Workload Identity ServiceAccount.
1285
+ * Creates a ServiceAccount with Azure Workload Identity annotations for secure authentication.
1286
+ *
1287
+ * @param spec - Azure Workload Identity ServiceAccount specification
1288
+ * @returns The created ServiceAccount ApiObject
1289
+ *
1290
+ * @example
1291
+ * ```typescript
1292
+ * rutter.addAzureWorkloadIdentityServiceAccount({
1293
+ * name: 'workload-identity-sa',
1294
+ * clientId: '12345678-1234-1234-1234-123456789012',
1295
+ * tenantId: '87654321-4321-4321-4321-210987654321',
1296
+ * tokenExpiration: 3600
1297
+ * });
1298
+ * ```
1299
+ */
1300
+ addAzureWorkloadIdentityServiceAccount(spec) {
1301
+ // Validate all inputs first for security
1302
+ if (spec.tokenExpiration !== undefined &&
1303
+ (spec.tokenExpiration < 3600 || spec.tokenExpiration > 86400)) {
1304
+ throw new Error(`Azure Workload Identity ${spec.name}: tokenExpiration must be between 3600 and 86400 seconds`);
1305
+ }
1306
+ if (!spec.clientId?.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) {
1307
+ throw new Error(`Azure Workload Identity ${spec.name}: Invalid client ID format`);
1308
+ }
1309
+ if (!spec.tenantId?.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) {
1310
+ throw new Error(`Azure Workload Identity ${spec.name}: Invalid tenant ID format`);
1311
+ }
1312
+ const annotations = {
1313
+ 'azure.workload.identity/client-id': spec.clientId,
1314
+ 'azure.workload.identity/tenant-id': spec.tenantId,
1315
+ ...(spec.tokenExpiration !== undefined
1316
+ ? {
1317
+ 'azure.workload.identity/service-account-token-expiration': String(spec.tokenExpiration),
1318
+ }
1319
+ : {}),
1320
+ ...(spec.annotations ?? {}),
1321
+ };
1322
+ const sa = new ApiObject(this.chart, spec.name, {
1323
+ apiVersion: 'v1',
1324
+ kind: 'ServiceAccount',
1325
+ metadata: {
1326
+ name: spec.name,
1327
+ annotations,
1328
+ ...(spec.labels ? { labels: spec.labels } : {}),
1329
+ },
1330
+ automountServiceAccountToken: spec.automountServiceAccountToken,
1331
+ imagePullSecrets: spec.imagePullSecrets?.map((n) => ({ name: n })),
1332
+ secrets: spec.secrets?.map((n) => ({ name: n })),
1333
+ });
1334
+ this.capture(sa, `${spec.name}-serviceaccount`);
1335
+ return sa;
1336
+ }
1337
+ /**
1338
+ * Add Azure Key Vault SecretProviderClass.
1339
+ * Creates a SecretProviderClass for mounting secrets from Azure Key Vault using CSI driver.
1340
+ *
1341
+ * @param spec - Azure Key Vault SecretProviderClass specification
1342
+ * @returns The created SecretProviderClass ApiObject
1343
+ *
1344
+ * @example
1345
+ * ```typescript
1346
+ * rutter.addAzureKeyVaultSecretProviderClass({
1347
+ * name: 'app-secrets',
1348
+ * keyVaultName: 'my-keyvault',
1349
+ * tenantId: '87654321-4321-4321-4321-210987654321',
1350
+ * objects: [
1351
+ * { objectName: 'database-password', objectType: 'secret' },
1352
+ * { objectName: 'api-key', objectType: 'secret', objectAlias: 'API_KEY' }
1353
+ * ],
1354
+ * userAssignedIdentityID: '12345678-1234-1234-1234-123456789012'
1355
+ * });
1356
+ * ```
1357
+ */
1358
+ addAzureKeyVaultSecretProviderClass(spec) {
1359
+ // Validate object names for security - prevent path traversal attacks
1360
+ spec.objects.forEach((obj) => {
1361
+ if (obj.objectName.includes('../') || obj.objectName.includes('..\\')) {
1362
+ throw new Error(`Azure Key Vault ${spec.name}: Object name '${obj.objectName}' contains invalid path traversal sequences`);
1363
+ }
1364
+ });
1365
+ const objects = spec.objects.map((obj) => {
1366
+ const baseObj = {
1367
+ objectName: obj.objectName,
1368
+ objectType: obj.objectType,
1369
+ };
1370
+ if (obj.objectAlias) {
1371
+ baseObj['objectAlias'] = obj.objectAlias;
1372
+ }
1373
+ if (obj.objectVersion) {
1374
+ baseObj['objectVersion'] = obj.objectVersion;
1375
+ }
1376
+ return baseObj;
1377
+ });
1378
+ const objectsYaml = YAML.stringify(objects, { indent: 2 }).trim();
1379
+ const parameters = {
1380
+ keyvaultName: spec.keyVaultName,
1381
+ tenantId: spec.tenantId,
1382
+ objects: `|\n ${objectsYaml.split('\n').join('\n ')}`,
1383
+ };
1384
+ if (spec.userAssignedIdentityID) {
1385
+ parameters['userAssignedIdentityID'] = spec.userAssignedIdentityID;
1386
+ }
1387
+ if (spec.cloudName) {
1388
+ parameters['cloudName'] = spec.cloudName;
1389
+ }
1390
+ const spc = new ApiObject(this.chart, spec.name, {
1391
+ apiVersion: 'secrets-store.csi.x-k8s.io/v1',
1392
+ kind: 'SecretProviderClass',
1393
+ metadata: {
1394
+ name: spec.name,
1395
+ ...(spec.labels ? { labels: spec.labels } : {}),
1396
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
1397
+ },
1398
+ spec: {
1399
+ provider: 'azure',
1400
+ parameters,
1401
+ },
1402
+ });
1403
+ this.capture(spc, `${spec.name}-secretproviderclass`);
1404
+ return spc;
1405
+ }
1406
+ /**
1407
+ * Add Azure Files StorageClass.
1408
+ * Creates a StorageClass for dynamic provisioning of Azure Files volumes.
1409
+ *
1410
+ * @param spec - Azure Files StorageClass specification
1411
+ * @returns The created StorageClass ApiObject
1412
+ *
1413
+ * @example
1414
+ * ```typescript
1415
+ * rutter.addAzureFilesStorageClass({
1416
+ * name: 'azure-files-premium',
1417
+ * skuName: 'Premium_LRS',
1418
+ * protocol: 'smb',
1419
+ * allowSharedAccess: true
1420
+ * });
1421
+ * ```
1422
+ */
1423
+ addAzureFilesStorageClass(spec) {
1424
+ // Validate Azure Files parameters for security and compliance
1425
+ this.validateAzureFilesParameters(spec);
1426
+ const parameters = this.buildAzureFilesParameters(spec);
1427
+ const sc = new ApiObject(this.chart, spec.name, {
1428
+ apiVersion: Rutter.STORAGE_API_VERSION,
1429
+ kind: 'StorageClass',
1430
+ metadata: {
1431
+ name: spec.name,
1432
+ ...(spec.labels ? { labels: spec.labels } : {}),
1433
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
1434
+ },
1435
+ provisioner: Rutter.AZURE_FILES_CSI_DRIVER,
1436
+ parameters,
1437
+ reclaimPolicy: spec.reclaimPolicy ?? 'Delete',
1438
+ allowVolumeExpansion: spec.allowVolumeExpansion ?? true,
1439
+ volumeBindingMode: spec.volumeBindingMode ?? 'Immediate',
1440
+ ...(spec.mountOptions ? { mountOptions: spec.mountOptions } : {}),
1441
+ });
1442
+ this.capture(sc, `${spec.name}-storageclass`);
1443
+ return sc;
1444
+ }
1445
+ /**
1446
+ * Add Azure Files PersistentVolume.
1447
+ * Creates a PersistentVolume for static Azure Files volume provisioning.
1448
+ *
1449
+ * @param spec - Azure Files PersistentVolume specification
1450
+ * @returns The created PersistentVolume ApiObject
1451
+ */
1452
+ addAzureFilesPersistentVolume(spec) {
1453
+ const csiSpec = this.buildAzureFilesPVCSISpec(spec);
1454
+ const pv = new ApiObject(this.chart, spec.name, {
1455
+ apiVersion: 'v1',
1456
+ kind: 'PersistentVolume',
1457
+ metadata: {
1458
+ name: spec.name,
1459
+ ...(spec.labels ? { labels: spec.labels } : {}),
1460
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
1461
+ },
1462
+ spec: {
1463
+ capacity: { storage: spec.capacity },
1464
+ accessModes: spec.accessModes ?? ['ReadWriteMany'],
1465
+ persistentVolumeReclaimPolicy: spec.reclaimPolicy ?? 'Retain',
1466
+ storageClassName: spec.storageClassName,
1467
+ ...(spec.mountOptions ? { mountOptions: spec.mountOptions } : {}),
1468
+ csi: csiSpec,
1469
+ },
1470
+ });
1471
+ this.capture(pv, `${spec.name}-pv`);
1472
+ return pv;
1473
+ }
1474
+ /**
1475
+ * Add Azure Files PersistentVolumeClaim.
1476
+ * Creates a PersistentVolumeClaim for Azure Files storage.
1477
+ *
1478
+ * @param spec - Azure Files PersistentVolumeClaim specification
1479
+ * @returns The created PersistentVolumeClaim ApiObject
1480
+ */
1481
+ addAzureFilesPersistentVolumeClaim(spec) {
1482
+ const pvc = new ApiObject(this.chart, spec.name, {
1483
+ apiVersion: 'v1',
1484
+ kind: 'PersistentVolumeClaim',
1485
+ metadata: {
1486
+ name: spec.name,
1487
+ ...(spec.labels ? { labels: spec.labels } : {}),
1488
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
1489
+ },
1490
+ spec: {
1491
+ accessModes: spec.accessModes ?? ['ReadWriteMany'],
1492
+ storageClassName: spec.storageClassName,
1493
+ resources: {
1494
+ requests: { storage: spec.size },
1495
+ },
1496
+ },
1497
+ });
1498
+ this.capture(pvc, `${spec.name}-pvc`);
1499
+ return pvc;
1500
+ }
1501
+ /**
1502
+ * Add Azure Container Registry ServiceAccount.
1503
+ * Creates a ServiceAccount configured for ACR access.
1504
+ *
1505
+ * @param spec - Azure ACR ServiceAccount specification
1506
+ * @returns The created ServiceAccount ApiObject
1507
+ */
1508
+ addAzureACRServiceAccount(spec) {
1509
+ const annotations = {
1510
+ ...(spec.annotations ?? {}),
1511
+ };
1512
+ // Add Workload Identity annotations if provided
1513
+ if (spec.clientId) {
1514
+ annotations['azure.workload.identity/client-id'] = spec.clientId;
1515
+ }
1516
+ if (spec.tenantId) {
1517
+ annotations['azure.workload.identity/tenant-id'] = spec.tenantId;
1518
+ }
1519
+ const sa = new ApiObject(this.chart, spec.name, {
1520
+ apiVersion: 'v1',
1521
+ kind: 'ServiceAccount',
1522
+ metadata: {
1523
+ name: spec.name,
1524
+ ...(Object.keys(annotations).length ? { annotations } : {}),
1525
+ ...(spec.labels ? { labels: spec.labels } : {}),
1526
+ },
1527
+ automountServiceAccountToken: spec.automountServiceAccountToken,
1528
+ imagePullSecrets: spec.imagePullSecrets?.map((n) => ({ name: n })),
1529
+ secrets: spec.secrets?.map((n) => ({ name: n })),
1530
+ });
1531
+ this.capture(sa, `${spec.name}-serviceaccount`);
1532
+ return sa;
1533
+ }
1058
1534
  addAzureDiskStorageClass(spec) {
1059
1535
  // Validate numeric parameters to prevent runtime errors
1060
1536
  this.validateAzureDiskNumericParameters(spec);
@@ -1435,6 +1911,76 @@ export class Rutter {
1435
1911
  throw new Error('Port 443 requires an SSL certificate to be specified');
1436
1912
  }
1437
1913
  }
1914
+ buildAzureFilesParameters(spec) {
1915
+ const parameters = {
1916
+ skuName: spec.skuName ?? 'Standard_LRS',
1917
+ };
1918
+ if (spec.protocol) {
1919
+ parameters['protocol'] = spec.protocol;
1920
+ }
1921
+ if (spec.allowSharedAccess !== undefined) {
1922
+ parameters['allowSharedAccess'] = String(spec.allowSharedAccess);
1923
+ }
1924
+ if (spec.resourceGroup) {
1925
+ parameters['resourceGroup'] = spec.resourceGroup;
1926
+ }
1927
+ if (spec.storageAccount) {
1928
+ parameters['storageAccount'] = spec.storageAccount;
1929
+ }
1930
+ if (spec.location) {
1931
+ parameters['location'] = spec.location;
1932
+ }
1933
+ if (spec.networkEndpointType) {
1934
+ parameters['networkEndpointType'] = spec.networkEndpointType;
1935
+ }
1936
+ // NFS-specific parameters
1937
+ if (spec.protocol === 'nfs' && spec.mountPermissions) {
1938
+ parameters['mountPermissions'] = spec.mountPermissions;
1939
+ }
1940
+ if (spec.protocol === 'nfs' && spec.rootSquashType) {
1941
+ parameters['rootSquashType'] = spec.rootSquashType;
1942
+ }
1943
+ return parameters;
1944
+ }
1945
+ buildAzureFilesPVCSISpec(spec) {
1946
+ // Validate protocol and secret configuration
1947
+ if (spec.protocol === 'nfs' && spec.secretName) {
1948
+ throw new Error(`Azure Files PV ${spec.name}: NFS protocol does not use secrets for authentication`);
1949
+ }
1950
+ const volumeHandle = this.sanitizeVolumeHandle(spec.storageAccount, spec.shareName);
1951
+ const volumeAttributes = {
1952
+ storageAccount: spec.storageAccount,
1953
+ shareName: spec.shareName,
1954
+ };
1955
+ if (spec.resourceGroup) {
1956
+ volumeAttributes['resourceGroup'] = spec.resourceGroup;
1957
+ }
1958
+ if (spec.protocol) {
1959
+ volumeAttributes['protocol'] = spec.protocol;
1960
+ }
1961
+ if (spec.server) {
1962
+ volumeAttributes['server'] = spec.server;
1963
+ }
1964
+ if (spec.folderName) {
1965
+ volumeAttributes['folderName'] = spec.folderName;
1966
+ }
1967
+ if (spec.mountPermissions) {
1968
+ volumeAttributes['mountPermissions'] = spec.mountPermissions;
1969
+ }
1970
+ const csiSpec = {
1971
+ driver: Rutter.AZURE_FILES_CSI_DRIVER,
1972
+ volumeHandle,
1973
+ volumeAttributes,
1974
+ };
1975
+ // Add secret reference for SMB protocol
1976
+ if (spec.protocol !== 'nfs' && spec.secretName) {
1977
+ csiSpec['nodeStageSecretRef'] = {
1978
+ name: spec.secretName,
1979
+ namespace: spec.secretNamespace ?? 'default',
1980
+ };
1981
+ }
1982
+ return csiSpec;
1983
+ }
1438
1984
  buildPodEnvironment(spec) {
1439
1985
  const envEntries = spec.env
1440
1986
  ? Object.entries(spec.env).map(([name, value]) => this.sanitizeEnvVar(name, value))
@@ -1641,6 +2187,37 @@ export class Rutter {
1641
2187
  },
1642
2188
  };
1643
2189
  }
2190
+ /**
2191
+ * Validate Azure Files parameters for security and compliance
2192
+ */
2193
+ validateAzureFilesParameters(spec) {
2194
+ const validSkuNames = [
2195
+ 'Standard_LRS',
2196
+ 'Standard_GRS',
2197
+ 'Standard_RAGRS',
2198
+ 'Standard_ZRS',
2199
+ 'Premium_LRS',
2200
+ 'Premium_ZRS',
2201
+ ];
2202
+ if (spec.skuName && !validSkuNames.includes(spec.skuName)) {
2203
+ throw new Error(`Invalid skuName: ${spec.skuName}. Must be one of: ${validSkuNames.join(', ')}`);
2204
+ }
2205
+ if (spec.protocol === 'nfs' && spec.mountPermissions) {
2206
+ const permRegex = /^[0-7]{3,4}$/;
2207
+ if (!permRegex.test(spec.mountPermissions)) {
2208
+ throw new Error('mountPermissions must be a valid octal permission string (e.g., "0777")');
2209
+ }
2210
+ }
2211
+ }
2212
+ /**
2213
+ * Sanitize volume handle to ensure valid format for CSI driver
2214
+ */
2215
+ sanitizeVolumeHandle(account, share) {
2216
+ // Remove special characters and ensure valid format
2217
+ const sanitizedAccount = account.replace(/[^a-z0-9]/gi, '').toLowerCase();
2218
+ const sanitizedShare = share.replace(/[^a-z0-9-]/gi, '').toLowerCase();
2219
+ return `${sanitizedAccount}#${sanitizedShare}`;
2220
+ }
1644
2221
  /**
1645
2222
  * Capture the YAML of an ApiObject or Construct into assets.
1646
2223
  */
@@ -1665,13 +2242,513 @@ export class Rutter {
1665
2242
  return 'resource';
1666
2243
  }
1667
2244
  /**
1668
- * Add a raw CRD manifest to the chart (written under crds/).
2245
+ * Adds a raw CRD manifest to the chart
2246
+ *
2247
+ * @param {string} yaml - YAML content of the CRD
2248
+ * @param {string} [id='crd'] - Asset identifier
2249
+ *
2250
+ * @example
2251
+ * ```typescript
2252
+ * const crdYaml = `
2253
+ * apiVersion: apiextensions.k8s.io/v1
2254
+ * kind: CustomResourceDefinition
2255
+ * metadata:
2256
+ * name: myresources.example.com
2257
+ * `;
2258
+ * rutter.addCrd(crdYaml, 'myresource-crd');
2259
+ * ```
2260
+ *
2261
+ * @since 1.0.0
1669
2262
  */
1670
2263
  addCrd(yaml, id = 'crd') {
1671
2264
  this.assets.push({ id, yaml, target: 'crds' });
1672
2265
  }
1673
2266
  /**
1674
- * Synthesize the cdk8s app and write a Helm chart to outDir.
2267
+ * Add Karpenter NodePool for intelligent node provisioning.
2268
+ * Creates a NodePool resource that defines compute requirements and lifecycle policies.
2269
+ *
2270
+ * @param spec - Karpenter NodePool specification
2271
+ * @returns The created NodePool ApiObject
2272
+ *
2273
+ * @example
2274
+ * ```typescript
2275
+ * rutter.addKarpenterNodePool({
2276
+ * name: 'general-purpose',
2277
+ * requirements: [
2278
+ * { key: 'eks.amazonaws.com/instance-category', operator: 'In', values: ['c', 'm', 'r'] },
2279
+ * { key: 'kubernetes.io/arch', operator: 'In', values: ['amd64'] }
2280
+ * ],
2281
+ * limits: { cpu: '1000', memory: '1000Gi' },
2282
+ * nodeClassRef: { name: 'default' }
2283
+ * });
2284
+ * ```
2285
+ *
2286
+ * @since 2.3.0
2287
+ */
2288
+ addKarpenterNodePool(spec) {
2289
+ // Validate NodePool specification
2290
+ this.validateKarpenterNodePoolSpec(spec);
2291
+ const nodePool = new ApiObject(this.chart, spec.name, {
2292
+ apiVersion: 'karpenter.sh/v1',
2293
+ kind: 'NodePool',
2294
+ metadata: {
2295
+ name: spec.name,
2296
+ ...(spec.labels ? { labels: spec.labels } : {}),
2297
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
2298
+ },
2299
+ spec: {
2300
+ template: {
2301
+ metadata: {
2302
+ ...(spec.labels ? { labels: spec.labels } : {}),
2303
+ },
2304
+ spec: {
2305
+ nodeClassRef: {
2306
+ group: spec.nodeClassRef.group ?? 'eks.amazonaws.com',
2307
+ kind: spec.nodeClassRef.kind ?? 'NodeClass',
2308
+ name: spec.nodeClassRef.name,
2309
+ },
2310
+ ...(spec.requirements ? { requirements: spec.requirements } : {}),
2311
+ ...(spec.taints ? { taints: spec.taints } : {}),
2312
+ },
2313
+ },
2314
+ ...(spec.limits ? { limits: spec.limits } : {}),
2315
+ ...(spec.disruption ? { disruption: spec.disruption } : {}),
2316
+ },
2317
+ });
2318
+ this.capture(nodePool, `${spec.name}-nodepool`);
2319
+ return nodePool;
2320
+ }
2321
+ /**
2322
+ * Add Karpenter EC2 NodeClass for AWS-specific node configuration.
2323
+ * Creates a NodeClass resource that defines EC2 instance settings and networking.
2324
+ *
2325
+ * @param spec - Karpenter EC2 NodeClass specification
2326
+ * @returns The created NodeClass ApiObject
2327
+ *
2328
+ * @example
2329
+ * ```typescript
2330
+ * rutter.addKarpenterEC2NodeClass({
2331
+ * name: 'default',
2332
+ * amiFamily: 'AL2023',
2333
+ * subnetSelectorTerms: [{ tags: { 'karpenter.sh/discovery': 'my-cluster' } }],
2334
+ * securityGroupSelectorTerms: [{ tags: { 'karpenter.sh/discovery': 'my-cluster' } }],
2335
+ * role: 'KarpenterNodeInstanceProfile'
2336
+ * });
2337
+ * ```
2338
+ *
2339
+ * @since 2.3.0
2340
+ */
2341
+ addKarpenterEC2NodeClass(spec) {
2342
+ // Validate EC2 NodeClass specification
2343
+ this.validateKarpenterEC2NodeClassSpec(spec);
2344
+ const nodeClass = new ApiObject(this.chart, spec.name, {
2345
+ apiVersion: 'eks.amazonaws.com/v1',
2346
+ kind: 'NodeClass',
2347
+ metadata: {
2348
+ name: spec.name,
2349
+ ...(spec.labels ? { labels: spec.labels } : {}),
2350
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
2351
+ },
2352
+ spec: {
2353
+ ...(spec.amiFamily ? { amiFamily: spec.amiFamily } : {}),
2354
+ ...(spec.instanceStorePolicy ? { instanceStorePolicy: spec.instanceStorePolicy } : {}),
2355
+ ...(spec.userData ? { userData: spec.userData } : {}),
2356
+ ...(spec.subnetSelectorTerms ? { subnetSelectorTerms: spec.subnetSelectorTerms } : {}),
2357
+ ...(spec.securityGroupSelectorTerms
2358
+ ? { securityGroupSelectorTerms: spec.securityGroupSelectorTerms }
2359
+ : {}),
2360
+ ...(spec.role ? { role: spec.role } : {}),
2361
+ ...(spec.instanceProfile ? { instanceProfile: spec.instanceProfile } : {}),
2362
+ ...(spec.metadataOptions ? { metadataOptions: spec.metadataOptions } : {}),
2363
+ ...(spec.blockDeviceMappings ? { blockDeviceMappings: spec.blockDeviceMappings } : {}),
2364
+ ...(spec.tags ? { tags: spec.tags } : {}),
2365
+ },
2366
+ });
2367
+ this.capture(nodeClass, `${spec.name}-nodeclass`);
2368
+ return nodeClass;
2369
+ }
2370
+ /**
2371
+ * Validate Karpenter NodePool specification
2372
+ */
2373
+ validateKarpenterNodePoolSpec(spec) {
2374
+ this.validateNodeClassRef(spec);
2375
+ this.validateNodePoolRequirements(spec);
2376
+ this.validateNodePoolLimits(spec);
2377
+ this.validateNodePoolDisruption(spec);
2378
+ }
2379
+ validateNodeClassRef(spec) {
2380
+ if (!spec.nodeClassRef.name) {
2381
+ throw new Error(`Karpenter NodePool ${spec.name}: nodeClassRef.name is required`);
2382
+ }
2383
+ }
2384
+ validateNodePoolRequirements(spec) {
2385
+ if (!spec.requirements)
2386
+ return;
2387
+ const operatorsRequiringValues = ['In', 'NotIn', 'Gt', 'Lt'];
2388
+ for (const req of spec.requirements) {
2389
+ if (operatorsRequiringValues.includes(req.operator) && !req.values?.length) {
2390
+ throw new Error(`Karpenter NodePool ${spec.name}: requirement with operator '${req.operator}' must have values`);
2391
+ }
2392
+ }
2393
+ }
2394
+ validateNodePoolLimits(spec) {
2395
+ if (!spec.limits)
2396
+ return;
2397
+ if (spec.limits.cpu && !this.isValidResourceQuantity(spec.limits.cpu)) {
2398
+ throw new Error(`Karpenter NodePool ${spec.name}: invalid CPU limit format`);
2399
+ }
2400
+ if (spec.limits.memory && !this.isValidResourceQuantity(spec.limits.memory)) {
2401
+ throw new Error(`Karpenter NodePool ${spec.name}: invalid memory limit format`);
2402
+ }
2403
+ }
2404
+ validateNodePoolDisruption(spec) {
2405
+ if (!spec.disruption)
2406
+ return;
2407
+ if (spec.disruption.consolidateAfter &&
2408
+ !this.isValidDuration(spec.disruption.consolidateAfter)) {
2409
+ throw new Error(`Karpenter NodePool ${spec.name}: invalid consolidateAfter duration format`);
2410
+ }
2411
+ if (spec.disruption.expireAfter && !this.isValidDuration(spec.disruption.expireAfter)) {
2412
+ throw new Error(`Karpenter NodePool ${spec.name}: invalid expireAfter duration format`);
2413
+ }
2414
+ }
2415
+ /**
2416
+ * Validate Karpenter EC2 NodeClass specification
2417
+ */
2418
+ validateKarpenterEC2NodeClassSpec(spec) {
2419
+ this.validateNodeClassSelectors(spec);
2420
+ this.validateNodeClassBlockDevices(spec);
2421
+ this.validateNodeClassMetadataOptions(spec);
2422
+ }
2423
+ validateNodeClassSelectors(spec) {
2424
+ if (!spec.subnetSelectorTerms?.length && !spec.securityGroupSelectorTerms?.length) {
2425
+ console.warn(`Karpenter EC2 NodeClass ${spec.name}: Consider specifying subnet and security group selectors for better control`);
2426
+ }
2427
+ }
2428
+ validateNodeClassBlockDevices(spec) {
2429
+ if (!spec.blockDeviceMappings)
2430
+ return;
2431
+ for (const mapping of spec.blockDeviceMappings) {
2432
+ if (mapping.ebs?.volumeSize && !this.isValidStorageSize(mapping.ebs.volumeSize)) {
2433
+ throw new Error(`Karpenter EC2 NodeClass ${spec.name}: invalid volume size format in block device mapping`);
2434
+ }
2435
+ if (mapping.ebs?.iops !== undefined && mapping.ebs.iops < 100) {
2436
+ throw new Error(`Karpenter EC2 NodeClass ${spec.name}: IOPS must be at least 100 for block device mapping`);
2437
+ }
2438
+ }
2439
+ }
2440
+ validateNodeClassMetadataOptions(spec) {
2441
+ if (spec.metadataOptions?.httpPutResponseHopLimit === undefined)
2442
+ return;
2443
+ const hopLimit = spec.metadataOptions.httpPutResponseHopLimit;
2444
+ if (hopLimit < 1 || hopLimit > 64) {
2445
+ throw new Error(`Karpenter EC2 NodeClass ${spec.name}: httpPutResponseHopLimit must be between 1 and 64`);
2446
+ }
2447
+ }
2448
+ /**
2449
+ * Validate Kubernetes resource quantity format (e.g., "100m", "1Gi")
2450
+ */
2451
+ isValidResourceQuantity(quantity) {
2452
+ // Safe validation without regex to prevent ReDoS
2453
+ const validUnits = ['m', 'k', 'M', 'G', 'T', 'P', 'E', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei'];
2454
+ // Find the unit suffix
2455
+ let numberPart = quantity;
2456
+ let hasValidUnit = false;
2457
+ for (const unit of validUnits) {
2458
+ if (quantity.endsWith(unit)) {
2459
+ numberPart = quantity.slice(0, -unit.length);
2460
+ hasValidUnit = true;
2461
+ break;
2462
+ }
2463
+ }
2464
+ // If no unit found, that's also valid (plain number)
2465
+ if (!hasValidUnit && quantity.length > 0) {
2466
+ numberPart = quantity;
2467
+ }
2468
+ // Validate the number part
2469
+ const num = parseFloat(numberPart);
2470
+ return !isNaN(num) && num >= 0 && numberPart === String(num);
2471
+ }
2472
+ /**
2473
+ * Validate duration format (e.g., "30s", "5m", "1h")
2474
+ */
2475
+ isValidDuration(duration) {
2476
+ // Safe validation without regex to prevent ReDoS
2477
+ const validUnits = ['ns', 'us', 'µs', 'ms', 's', 'm', 'h'];
2478
+ // Find the unit suffix
2479
+ let numberPart = duration;
2480
+ let hasValidUnit = false;
2481
+ for (const unit of validUnits) {
2482
+ if (duration.endsWith(unit)) {
2483
+ numberPart = duration.slice(0, -unit.length);
2484
+ hasValidUnit = true;
2485
+ break;
2486
+ }
2487
+ }
2488
+ // Duration must have a unit
2489
+ if (!hasValidUnit)
2490
+ return false;
2491
+ // Validate the number part
2492
+ const num = parseFloat(numberPart);
2493
+ return !isNaN(num) && num >= 0 && numberPart === String(num);
2494
+ }
2495
+ /**
2496
+ * Validate storage size format (e.g., "20", "100Gi")
2497
+ */
2498
+ isValidStorageSize(size) {
2499
+ // Safe validation without regex to prevent ReDoS
2500
+ const validUnits = ['Gi', 'G', 'Ti', 'T'];
2501
+ // Find the unit suffix
2502
+ let numberPart = size;
2503
+ let hasValidUnit = false;
2504
+ for (const unit of validUnits) {
2505
+ if (size.endsWith(unit)) {
2506
+ numberPart = size.slice(0, -unit.length);
2507
+ hasValidUnit = true;
2508
+ break;
2509
+ }
2510
+ }
2511
+ // If no unit found, that's also valid (plain number)
2512
+ if (!hasValidUnit && size.length > 0) {
2513
+ numberPart = size;
2514
+ }
2515
+ // Validate the number part
2516
+ const num = parseFloat(numberPart);
2517
+ return !isNaN(num) && num >= 0 && numberPart === String(num);
2518
+ }
2519
+ /**
2520
+ * Add Karpenter NodeClaim for individual node provisioning.
2521
+ * Creates a NodeClaim resource that represents a request for a single node.
2522
+ *
2523
+ * @param spec - Karpenter NodeClaim specification
2524
+ * @returns The created NodeClaim ApiObject
2525
+ *
2526
+ * @example
2527
+ * ```typescript
2528
+ * rutter.addKarpenterNodeClaim({
2529
+ * name: 'high-memory-node',
2530
+ * requirements: [
2531
+ * { key: 'eks.amazonaws.com/instance-category', operator: 'In', values: ['r'] },
2532
+ * { key: 'eks.amazonaws.com/instance-cpu', operator: 'In', values: ['16', '32'] }
2533
+ * ],
2534
+ * nodeClassRef: { name: 'memory-optimized' },
2535
+ * expireAfter: '24h'
2536
+ * });
2537
+ * ```
2538
+ *
2539
+ * @since 2.3.0
2540
+ */
2541
+ addKarpenterNodeClaim(spec) {
2542
+ // Validate NodeClaim specification
2543
+ this.validateKarpenterNodeClaimSpec(spec);
2544
+ const nodeClaim = new ApiObject(this.chart, spec.name, {
2545
+ apiVersion: 'karpenter.sh/v1',
2546
+ kind: 'NodeClaim',
2547
+ metadata: {
2548
+ name: spec.name,
2549
+ ...(spec.labels ? { labels: spec.labels } : {}),
2550
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
2551
+ },
2552
+ spec: {
2553
+ nodeClassRef: {
2554
+ group: spec.nodeClassRef.group ?? 'eks.amazonaws.com',
2555
+ kind: spec.nodeClassRef.kind ?? 'NodeClass',
2556
+ name: spec.nodeClassRef.name,
2557
+ },
2558
+ ...(spec.requirements ? { requirements: spec.requirements } : {}),
2559
+ ...(spec.taints ? { taints: spec.taints } : {}),
2560
+ ...(spec.startupTaints ? { startupTaints: spec.startupTaints } : {}),
2561
+ ...(spec.expireAfter ? { expireAfter: spec.expireAfter } : {}),
2562
+ ...(spec.terminationGracePeriod
2563
+ ? { terminationGracePeriod: spec.terminationGracePeriod }
2564
+ : {}),
2565
+ },
2566
+ });
2567
+ this.capture(nodeClaim, `${spec.name}-nodeclaim`);
2568
+ return nodeClaim;
2569
+ }
2570
+ /**
2571
+ * Add advanced Karpenter scheduling configuration.
2572
+ * Creates scheduling constraints for fine-grained pod placement control.
2573
+ *
2574
+ * @param spec - Karpenter scheduling specification
2575
+ * @returns Configuration object for use in pod specs
2576
+ *
2577
+ * @example
2578
+ * ```typescript
2579
+ * const schedulingConfig = rutter.addKarpenterScheduling({
2580
+ * name: 'zone-spread-scheduling',
2581
+ * topologySpreadConstraints: [{
2582
+ * maxSkew: 1,
2583
+ * topologyKey: 'topology.kubernetes.io/zone',
2584
+ * whenUnsatisfiable: 'DoNotSchedule',
2585
+ * labelSelector: { matchLabels: { app: 'web' } }
2586
+ * }],
2587
+ * nodeAffinity: {
2588
+ * requiredDuringSchedulingIgnoredDuringExecution: {
2589
+ * nodeSelectorTerms: [{
2590
+ * matchExpressions: [{
2591
+ * key: 'eks.amazonaws.com/instance-category',
2592
+ * operator: 'In',
2593
+ * values: ['c', 'm']
2594
+ * }]
2595
+ * }]
2596
+ * }
2597
+ * }
2598
+ * });
2599
+ * ```
2600
+ *
2601
+ * @since 2.3.0
2602
+ */
2603
+ addKarpenterScheduling(spec) {
2604
+ // Validate scheduling specification
2605
+ this.validateKarpenterSchedulingSpec(spec);
2606
+ // Return scheduling configuration for use in pod specs
2607
+ return {
2608
+ ...(spec.nodeSelector ? { nodeSelector: spec.nodeSelector } : {}),
2609
+ ...(spec.nodeAffinity ? { affinity: { nodeAffinity: spec.nodeAffinity } } : {}),
2610
+ ...(spec.topologySpreadConstraints
2611
+ ? { topologySpreadConstraints: spec.topologySpreadConstraints }
2612
+ : {}),
2613
+ ...(spec.tolerations ? { tolerations: spec.tolerations } : {}),
2614
+ ...(spec.priorityClassName ? { priorityClassName: spec.priorityClassName } : {}),
2615
+ ...(spec.schedulerName ? { schedulerName: spec.schedulerName } : {}),
2616
+ };
2617
+ }
2618
+ /**
2619
+ * Create advanced disruption configuration for NodePools.
2620
+ * Provides fine-grained control over when and how nodes are disrupted.
2621
+ *
2622
+ * @param spec - Advanced disruption specification
2623
+ * @returns Disruption configuration object
2624
+ *
2625
+ * @example
2626
+ * ```typescript
2627
+ * const disruptionConfig = rutter.createKarpenterDisruption({
2628
+ * consolidationPolicy: 'WhenEmptyOrUnderutilized',
2629
+ * consolidateAfter: '30s',
2630
+ * expireAfter: '2160h', // 90 days
2631
+ * budgets: [{
2632
+ * schedule: '0 9 * * mon-fri', // Business hours
2633
+ * duration: '8h',
2634
+ * nodes: '0', // No disruption during business hours
2635
+ * reasons: ['Underutilized', 'Empty']
2636
+ * }]
2637
+ * });
2638
+ * ```
2639
+ *
2640
+ * @since 2.3.0
2641
+ */
2642
+ createKarpenterDisruption(spec) {
2643
+ // Validate disruption specification
2644
+ this.validateKarpenterDisruptionSpec(spec);
2645
+ return {
2646
+ ...(spec.consolidationPolicy ? { consolidationPolicy: spec.consolidationPolicy } : {}),
2647
+ ...(spec.consolidateAfter ? { consolidateAfter: spec.consolidateAfter } : {}),
2648
+ ...(spec.expireAfter ? { expireAfter: spec.expireAfter } : {}),
2649
+ ...(spec.budgets ? { budgets: spec.budgets } : {}),
2650
+ };
2651
+ }
2652
+ /**
2653
+ * Validate Karpenter NodeClaim specification
2654
+ */
2655
+ validateKarpenterNodeClaimSpec(spec) {
2656
+ // Validate NodeClass reference
2657
+ if (!spec.nodeClassRef.name) {
2658
+ throw new Error(`Karpenter NodeClaim ${spec.name}: nodeClassRef.name is required`);
2659
+ }
2660
+ // Validate expiration format
2661
+ if (spec.expireAfter && !this.isValidDuration(spec.expireAfter)) {
2662
+ throw new Error(`Karpenter NodeClaim ${spec.name}: invalid expireAfter duration format`);
2663
+ }
2664
+ // Validate termination grace period
2665
+ if (spec.terminationGracePeriod && !this.isValidDuration(spec.terminationGracePeriod)) {
2666
+ throw new Error(`Karpenter NodeClaim ${spec.name}: invalid terminationGracePeriod duration format`);
2667
+ }
2668
+ // Validate requirements
2669
+ if (spec.requirements) {
2670
+ for (const req of spec.requirements) {
2671
+ if (['In', 'NotIn', 'Gt', 'Lt'].includes(req.operator) && !req.values?.length) {
2672
+ throw new Error(`Karpenter NodeClaim ${spec.name}: requirement with operator '${req.operator}' must have values`);
2673
+ }
2674
+ }
2675
+ }
2676
+ }
2677
+ /**
2678
+ * Validate Karpenter scheduling specification
2679
+ */
2680
+ validateKarpenterSchedulingSpec(spec) {
2681
+ this.validateTopologySpreadConstraints(spec);
2682
+ this.validateSchedulingTolerations(spec);
2683
+ }
2684
+ validateTopologySpreadConstraints(spec) {
2685
+ if (!spec.topologySpreadConstraints)
2686
+ return;
2687
+ for (const constraint of spec.topologySpreadConstraints) {
2688
+ if (constraint.maxSkew < 1) {
2689
+ throw new Error(`Karpenter Scheduling ${spec.name}: maxSkew must be at least 1 in topology spread constraint`);
2690
+ }
2691
+ if (!constraint.topologyKey) {
2692
+ throw new Error(`Karpenter Scheduling ${spec.name}: topologyKey is required in topology spread constraint`);
2693
+ }
2694
+ }
2695
+ }
2696
+ validateSchedulingTolerations(spec) {
2697
+ if (!spec.tolerations)
2698
+ return;
2699
+ for (const toleration of spec.tolerations) {
2700
+ if (toleration.operator === 'Equal' && !toleration.value) {
2701
+ throw new Error(`Karpenter Scheduling ${spec.name}: value is required when operator is 'Equal' in toleration`);
2702
+ }
2703
+ }
2704
+ }
2705
+ /**
2706
+ * Validate Karpenter disruption specification
2707
+ */
2708
+ validateKarpenterDisruptionSpec(spec) {
2709
+ this.validateDisruptionDurations(spec);
2710
+ this.validateDisruptionBudgets(spec);
2711
+ }
2712
+ validateDisruptionDurations(spec) {
2713
+ if (spec.consolidateAfter && !this.isValidDuration(spec.consolidateAfter)) {
2714
+ throw new Error('Invalid consolidateAfter duration format');
2715
+ }
2716
+ if (spec.expireAfter && !this.isValidDuration(spec.expireAfter)) {
2717
+ throw new Error('Invalid expireAfter duration format');
2718
+ }
2719
+ }
2720
+ validateDisruptionBudgets(spec) {
2721
+ if (!spec.budgets)
2722
+ return;
2723
+ for (const budget of spec.budgets) {
2724
+ if (budget.schedule && budget.duration) {
2725
+ // Basic cron validation - should have 5 parts
2726
+ const cronParts = budget.schedule.split(' ');
2727
+ if (cronParts.length !== 5) {
2728
+ throw new Error('Budget schedule must be a valid cron expression with 5 parts');
2729
+ }
2730
+ }
2731
+ if (budget.duration && !this.isValidDuration(budget.duration)) {
2732
+ throw new Error('Invalid budget duration format');
2733
+ }
2734
+ }
2735
+ }
2736
+ /**
2737
+ * Synthesizes the cdk8s app and writes a complete Helm chart
2738
+ *
2739
+ * Generates all Kubernetes manifests, applies Helm templating,
2740
+ * and writes the complete chart structure to the output directory.
2741
+ *
2742
+ * @param {string} outDir - Output directory for the Helm chart
2743
+ * @throws {Error} If synthesis or writing fails
2744
+ *
2745
+ * @example
2746
+ * ```typescript
2747
+ * rutter.write('./charts/my-app');
2748
+ * // Creates: ./charts/my-app/Chart.yaml, values.yaml, templates/, etc.
2749
+ * ```
2750
+ *
2751
+ * @since 1.0.0
1675
2752
  */
1676
2753
  write(outDir) {
1677
2754
  // Use cdk8s Testing.synth to obtain manifest objects
@@ -1781,5 +2858,6 @@ Rutter.NETWORKING_API_VERSION = 'networking.k8s.io/v1';
1781
2858
  Rutter.STORAGE_API_VERSION = 'storage.k8s.io/v1';
1782
2859
  Rutter.AZURE_DISK_CSI_DRIVER = 'disk.csi.azure.com';
1783
2860
  Rutter.AZURE_DISK_PROVISIONER_ANNOTATION = 'volume.beta.kubernetes.io/storage-provisioner';
2861
+ Rutter.AZURE_FILES_CSI_DRIVER = 'file.csi.azure.com';
1784
2862
  Rutter.EMPTY_POD_SELECTOR = {};
1785
2863
  //# sourceMappingURL=Rutter.js.map