timonel 1.0.0 → 2.1.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.
@@ -240,6 +240,50 @@ export class Rutter {
240
240
  this.capture(rs, `${spec.name}-replicaset`);
241
241
  return rs;
242
242
  }
243
+ addJob(spec) {
244
+ // Validate Job specification
245
+ this.validateJobSpec(spec);
246
+ const { envEntries, envFromEntries, volumeDefs, volumeMountDefs } = this.buildPodEnvironment(spec);
247
+ const jobSpec = this.buildJobSpec(spec);
248
+ const podTemplate = this.buildJobPodTemplate(spec, envEntries, envFromEntries, volumeDefs, volumeMountDefs);
249
+ const job = new ApiObject(this.chart, spec.name, {
250
+ apiVersion: 'batch/v1',
251
+ kind: 'Job',
252
+ metadata: {
253
+ name: spec.name,
254
+ ...(spec.labels ? { labels: spec.labels } : {}),
255
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
256
+ },
257
+ spec: {
258
+ ...jobSpec,
259
+ template: podTemplate,
260
+ },
261
+ });
262
+ this.capture(job, `${spec.name}-job`);
263
+ return job;
264
+ }
265
+ addCronJob(spec) {
266
+ // Validate CronJob specification
267
+ this.validateCronJobSpec(spec);
268
+ const { envEntries, envFromEntries, volumeDefs, volumeMountDefs } = this.buildPodEnvironment(spec);
269
+ const cronJobSpec = this.buildCronJobSpec(spec);
270
+ const jobTemplate = this.buildCronJobTemplate(spec, envEntries, envFromEntries, volumeDefs, volumeMountDefs);
271
+ const cronJob = new ApiObject(this.chart, spec.name, {
272
+ apiVersion: 'batch/v1',
273
+ kind: 'CronJob',
274
+ metadata: {
275
+ name: spec.name,
276
+ ...(spec.labels ? { labels: spec.labels } : {}),
277
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
278
+ },
279
+ spec: {
280
+ ...cronJobSpec,
281
+ jobTemplate,
282
+ },
283
+ });
284
+ this.capture(cronJob, `${spec.name}-cronjob`);
285
+ return cronJob;
286
+ }
243
287
  addIngress(spec) {
244
288
  const ing = new ApiObject(this.chart, spec.name, {
245
289
  apiVersion: Rutter.NETWORKING_API_VERSION,
@@ -348,10 +392,10 @@ export class Rutter {
348
392
  else if (spec.cloudProvider === 'azure') {
349
393
  optimized.annotations = {
350
394
  ...optimized.annotations,
351
- 'volume.beta.kubernetes.io/storage-provisioner': 'disk.csi.azure.com',
395
+ [Rutter.AZURE_DISK_PROVISIONER_ANNOTATION]: Rutter.AZURE_DISK_CSI_DRIVER,
352
396
  };
353
397
  // Default to Premium_ZRS for multi-zone clusters
354
- if (spec.source.csi?.driver === 'disk.csi.azure.com' && optimized.source.csi) {
398
+ if (spec.source.csi?.driver === Rutter.AZURE_DISK_CSI_DRIVER && optimized.source.csi) {
355
399
  optimized.source.csi.volumeAttributes = {
356
400
  skuName: 'Premium_ZRS',
357
401
  ...spec.source.csi.volumeAttributes,
@@ -583,7 +627,7 @@ export class Rutter {
583
627
  parameters['throughput'] = String(spec.throughput);
584
628
  }
585
629
  const sc = new ApiObject(this.chart, spec.name, {
586
- apiVersion: 'storage.k8s.io/v1',
630
+ apiVersion: Rutter.STORAGE_API_VERSION,
587
631
  kind: 'StorageClass',
588
632
  metadata: {
589
633
  name: spec.name,
@@ -623,7 +667,7 @@ export class Rutter {
623
667
  }
624
668
  addAWSEFSStorageClass(spec) {
625
669
  const sc = new ApiObject(this.chart, spec.name, {
626
- apiVersion: 'storage.k8s.io/v1',
670
+ apiVersion: Rutter.STORAGE_API_VERSION,
627
671
  kind: 'StorageClass',
628
672
  metadata: {
629
673
  name: spec.name,
@@ -788,12 +832,165 @@ export class Rutter {
788
832
  }
789
833
  addTagsAnnotation(annotations, spec) {
790
834
  if (spec.tags && Object.keys(spec.tags).length > 0) {
791
- const tagString = Object.entries(spec.tags)
792
- .map(([key, value]) => `${key}=${value}`)
793
- .join(',');
835
+ const tagString = this.formatCloudResourceTags(spec.tags);
794
836
  annotations['alb.ingress.kubernetes.io/tags'] = tagString;
795
837
  }
796
838
  }
839
+ /**
840
+ * Add Azure Application Gateway Ingress Controller (AGIC) Ingress.
841
+ * Creates an Ingress resource with AGIC-specific annotations for AKS deployments.
842
+ *
843
+ * @param spec - AGIC Ingress specification
844
+ * @returns The created Ingress ApiObject
845
+ *
846
+ * @example
847
+ * ```typescript
848
+ * rutter.addAzureAGICIngress({
849
+ * name: 'app-ingress',
850
+ * rules: [{
851
+ * host: 'app.example.com',
852
+ * paths: [{
853
+ * path: '/',
854
+ * pathType: 'Prefix',
855
+ * backend: { service: { name: 'app-service', port: { number: 80 } } }
856
+ * }]
857
+ * }],
858
+ * sslRedirect: true,
859
+ * backendProtocol: 'https',
860
+ * healthProbePath: '/health'
861
+ * });
862
+ * ```
863
+ */
864
+ addAzureAGICIngress(spec) {
865
+ this.validateAGICHealthProbeParams(spec);
866
+ this.validateAGICSecurityParams(spec);
867
+ this.validateAGICAnnotationConsistency(spec);
868
+ const annotations = this.buildAGICAnnotations(spec);
869
+ const ingress = new ApiObject(this.chart, spec.name, {
870
+ apiVersion: Rutter.NETWORKING_API_VERSION,
871
+ kind: 'Ingress',
872
+ metadata: {
873
+ name: spec.name,
874
+ annotations,
875
+ ...(spec.labels ? { labels: spec.labels } : {}),
876
+ },
877
+ spec: {
878
+ tls: spec.tls,
879
+ rules: spec.rules.map((rule) => ({
880
+ host: rule.host,
881
+ http: {
882
+ paths: rule.paths.map((path) => ({
883
+ path: path.path,
884
+ pathType: path.pathType,
885
+ backend: path.backend,
886
+ })),
887
+ },
888
+ })),
889
+ },
890
+ });
891
+ this.capture(ingress, `${spec.name}-ingress`);
892
+ return ingress;
893
+ }
894
+ buildAGICAnnotations(spec) {
895
+ const annotations = {
896
+ 'kubernetes.io/ingress.class': 'azure/application-gateway',
897
+ ...(spec.annotations ?? {}),
898
+ };
899
+ this.addAGICPathAnnotations(annotations, spec);
900
+ this.addAGICProtocolAnnotations(annotations, spec);
901
+ this.addAGICHealthProbeAnnotations(annotations, spec);
902
+ this.addAGICConnectionAnnotations(annotations, spec);
903
+ this.addAGICSecurityAnnotations(annotations, spec);
904
+ this.addAGICAdvancedAnnotations(annotations, spec);
905
+ return annotations;
906
+ }
907
+ addAGICPathAnnotations(annotations, spec) {
908
+ if (spec.backendPathPrefix) {
909
+ annotations['appgw.ingress.kubernetes.io/backend-path-prefix'] = spec.backendPathPrefix;
910
+ }
911
+ if (spec.backendHostname) {
912
+ annotations['appgw.ingress.kubernetes.io/backend-hostname'] = spec.backendHostname;
913
+ }
914
+ }
915
+ addAGICProtocolAnnotations(annotations, spec) {
916
+ if (spec.backendProtocol) {
917
+ annotations['appgw.ingress.kubernetes.io/backend-protocol'] = spec.backendProtocol;
918
+ }
919
+ if (spec.sslRedirect) {
920
+ annotations['appgw.ingress.kubernetes.io/ssl-redirect'] = 'true';
921
+ }
922
+ if (spec.usePrivateIp) {
923
+ annotations['appgw.ingress.kubernetes.io/use-private-ip'] = 'true';
924
+ }
925
+ if (spec.overrideFrontendPort !== undefined) {
926
+ annotations['appgw.ingress.kubernetes.io/override-frontend-port'] = String(spec.overrideFrontendPort);
927
+ }
928
+ }
929
+ addAGICHealthProbeAnnotations(annotations, spec) {
930
+ if (spec.healthProbeHostname) {
931
+ annotations['appgw.ingress.kubernetes.io/health-probe-hostname'] = spec.healthProbeHostname;
932
+ }
933
+ if (spec.healthProbePort !== undefined) {
934
+ annotations['appgw.ingress.kubernetes.io/health-probe-port'] = String(spec.healthProbePort);
935
+ }
936
+ if (spec.healthProbePath) {
937
+ annotations['appgw.ingress.kubernetes.io/health-probe-path'] = spec.healthProbePath;
938
+ }
939
+ if (spec.healthProbeStatusCodes) {
940
+ annotations['appgw.ingress.kubernetes.io/health-probe-status-codes'] =
941
+ spec.healthProbeStatusCodes;
942
+ }
943
+ if (spec.healthProbeInterval !== undefined) {
944
+ annotations['appgw.ingress.kubernetes.io/health-probe-interval'] = String(spec.healthProbeInterval);
945
+ }
946
+ if (spec.healthProbeTimeout !== undefined) {
947
+ annotations['appgw.ingress.kubernetes.io/health-probe-timeout'] = String(spec.healthProbeTimeout);
948
+ }
949
+ if (spec.healthProbeUnhealthyThreshold !== undefined) {
950
+ annotations['appgw.ingress.kubernetes.io/health-probe-unhealthy-threshold'] = String(spec.healthProbeUnhealthyThreshold);
951
+ }
952
+ }
953
+ addAGICConnectionAnnotations(annotations, spec) {
954
+ if (spec.cookieBasedAffinity) {
955
+ annotations['appgw.ingress.kubernetes.io/cookie-based-affinity'] = 'true';
956
+ }
957
+ if (spec.requestTimeout !== undefined) {
958
+ annotations['appgw.ingress.kubernetes.io/request-timeout'] = String(spec.requestTimeout);
959
+ }
960
+ if (spec.connectionDraining) {
961
+ annotations['appgw.ingress.kubernetes.io/connection-draining'] = 'true';
962
+ }
963
+ if (spec.connectionDrainingTimeout !== undefined) {
964
+ annotations['appgw.ingress.kubernetes.io/connection-draining-timeout'] = String(spec.connectionDrainingTimeout);
965
+ }
966
+ }
967
+ addAGICSecurityAnnotations(annotations, spec) {
968
+ if (spec.appgwSslCertificate) {
969
+ annotations['appgw.ingress.kubernetes.io/appgw-ssl-certificate'] = spec.appgwSslCertificate;
970
+ }
971
+ if (spec.appgwSslProfile) {
972
+ annotations['appgw.ingress.kubernetes.io/appgw-ssl-profile'] = spec.appgwSslProfile;
973
+ }
974
+ if (spec.appgwTrustedRootCertificate && spec.appgwTrustedRootCertificate.length > 0) {
975
+ annotations['appgw.ingress.kubernetes.io/appgw-trusted-root-certificate'] =
976
+ spec.appgwTrustedRootCertificate.join(',');
977
+ }
978
+ if (spec.wafPolicyForPath) {
979
+ annotations['appgw.ingress.kubernetes.io/waf-policy-for-path'] = spec.wafPolicyForPath;
980
+ }
981
+ }
982
+ addAGICAdvancedAnnotations(annotations, spec) {
983
+ if (spec.hostnameExtension && spec.hostnameExtension.length > 0) {
984
+ annotations['appgw.ingress.kubernetes.io/hostname-extension'] =
985
+ spec.hostnameExtension.join(', ');
986
+ }
987
+ if (spec.rewriteRuleSet) {
988
+ annotations['appgw.ingress.kubernetes.io/rewrite-rule-set'] = spec.rewriteRuleSet;
989
+ }
990
+ if (spec.rulePriority !== undefined) {
991
+ annotations['appgw.ingress.kubernetes.io/rule-priority'] = String(spec.rulePriority);
992
+ }
993
+ }
797
994
  addAWSIRSAServiceAccount(spec) {
798
995
  const annotations = {
799
996
  'eks.amazonaws.com/role-arn': spec.roleArn,
@@ -858,6 +1055,153 @@ export class Rutter {
858
1055
  this.capture(spc, `${spec.name}-secretproviderclass`);
859
1056
  return spc;
860
1057
  }
1058
+ addAzureDiskStorageClass(spec) {
1059
+ // Validate numeric parameters to prevent runtime errors
1060
+ this.validateAzureDiskNumericParameters(spec);
1061
+ const parameters = this.buildAzureDiskParameters(spec);
1062
+ const sc = new ApiObject(this.chart, spec.name, {
1063
+ apiVersion: Rutter.STORAGE_API_VERSION,
1064
+ kind: 'StorageClass',
1065
+ metadata: {
1066
+ name: spec.name,
1067
+ ...(spec.labels ? { labels: spec.labels } : {}),
1068
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
1069
+ },
1070
+ provisioner: Rutter.AZURE_DISK_CSI_DRIVER,
1071
+ parameters,
1072
+ reclaimPolicy: spec.reclaimPolicy ?? 'Delete',
1073
+ allowVolumeExpansion: spec.allowVolumeExpansion ?? true,
1074
+ volumeBindingMode: spec.volumeBindingMode ?? 'WaitForFirstConsumer',
1075
+ });
1076
+ this.capture(sc, `${spec.name}-storageclass`);
1077
+ return sc;
1078
+ }
1079
+ buildAzureDiskParameters(spec) {
1080
+ const parameters = {
1081
+ skuName: spec.skuName ?? 'StandardSSD_LRS',
1082
+ fsType: spec.fsType ?? 'ext4',
1083
+ // Security best practices: default to private network access and encryption
1084
+ networkAccessPolicy: spec.networkAccessPolicy ?? 'AllowPrivate',
1085
+ };
1086
+ this.addOptionalAzureDiskParameters(parameters, spec);
1087
+ this.addAzureDiskEncryptionParameters(parameters, spec);
1088
+ this.addAzureDiskPerformanceParameters(parameters, spec);
1089
+ return parameters;
1090
+ }
1091
+ addOptionalAzureDiskParameters(parameters, spec) {
1092
+ if (spec.cachingMode) {
1093
+ parameters['cachingMode'] = spec.cachingMode;
1094
+ }
1095
+ if (spec.resourceGroup) {
1096
+ parameters['resourceGroup'] = spec.resourceGroup;
1097
+ }
1098
+ if (spec.logicalSectorSize !== undefined) {
1099
+ parameters['LogicalSectorSize'] = String(spec.logicalSectorSize);
1100
+ }
1101
+ if (spec.tags && Object.keys(spec.tags).length > 0) {
1102
+ const tagString = this.formatCloudResourceTags(spec.tags);
1103
+ parameters['tags'] = tagString;
1104
+ }
1105
+ // networkAccessPolicy is now set as default in buildAzureDiskParameters
1106
+ if (spec.networkAccessPolicy && spec.networkAccessPolicy !== 'AllowPrivate') {
1107
+ parameters['networkAccessPolicy'] = spec.networkAccessPolicy;
1108
+ }
1109
+ if (spec.diskAccessID) {
1110
+ parameters['diskAccessID'] = spec.diskAccessID;
1111
+ }
1112
+ if (spec.maxShares !== undefined) {
1113
+ parameters['maxShares'] = String(spec.maxShares);
1114
+ }
1115
+ }
1116
+ addAzureDiskEncryptionParameters(parameters, spec) {
1117
+ if (spec.diskEncryptionSetID) {
1118
+ parameters['diskEncryptionSetID'] = spec.diskEncryptionSetID;
1119
+ }
1120
+ if (spec.diskEncryptionType) {
1121
+ parameters['diskEncryptionType'] = spec.diskEncryptionType;
1122
+ }
1123
+ }
1124
+ addAzureDiskPerformanceParameters(parameters, spec) {
1125
+ if (spec.diskIOPSReadWrite !== undefined) {
1126
+ parameters['DiskIOPSReadWrite'] = String(spec.diskIOPSReadWrite);
1127
+ }
1128
+ if (spec.diskMBpsReadWrite !== undefined) {
1129
+ parameters['DiskMBpsReadWrite'] = String(spec.diskMBpsReadWrite);
1130
+ }
1131
+ if (spec.writeAcceleratorEnabled !== undefined) {
1132
+ parameters['writeAcceleratorEnabled'] = String(spec.writeAcceleratorEnabled);
1133
+ }
1134
+ if (spec.enableBursting !== undefined) {
1135
+ parameters['enableBursting'] = String(spec.enableBursting);
1136
+ }
1137
+ }
1138
+ validateAzureDiskNumericParameters(spec) {
1139
+ this.validateAzureDiskIOPS(spec);
1140
+ this.validateAzureDiskThroughput(spec);
1141
+ this.validateAzureDiskShares(spec);
1142
+ this.validateAzureDiskSectorSize(spec);
1143
+ }
1144
+ validateAzureDiskIOPS(spec) {
1145
+ if (spec.diskIOPSReadWrite === undefined)
1146
+ return;
1147
+ if (spec.diskIOPSReadWrite < 100) {
1148
+ throw new Error(`Azure Disk ${spec.name}: diskIOPSReadWrite must be at least 100 IOPS`);
1149
+ }
1150
+ if (spec.diskIOPSReadWrite > 400000) {
1151
+ throw new Error(`Azure Disk ${spec.name}: diskIOPSReadWrite cannot exceed 400,000 IOPS`);
1152
+ }
1153
+ }
1154
+ validateAzureDiskThroughput(spec) {
1155
+ if (spec.diskMBpsReadWrite === undefined)
1156
+ return;
1157
+ if (spec.diskMBpsReadWrite < 1) {
1158
+ throw new Error(`Azure Disk ${spec.name}: diskMBpsReadWrite must be at least 1 MB/s`);
1159
+ }
1160
+ if (spec.diskMBpsReadWrite > 10000) {
1161
+ throw new Error(`Azure Disk ${spec.name}: diskMBpsReadWrite cannot exceed 10,000 MB/s`);
1162
+ }
1163
+ }
1164
+ validateAzureDiskShares(spec) {
1165
+ if (spec.maxShares === undefined)
1166
+ return;
1167
+ if (spec.maxShares < 1) {
1168
+ throw new Error(`Azure Disk ${spec.name}: maxShares must be at least 1`);
1169
+ }
1170
+ if (spec.maxShares > 5) {
1171
+ throw new Error(`Azure Disk ${spec.name}: maxShares cannot exceed 5`);
1172
+ }
1173
+ }
1174
+ validateAzureDiskSectorSize(spec) {
1175
+ if (spec.logicalSectorSize === undefined)
1176
+ return;
1177
+ if (spec.logicalSectorSize !== 512 && spec.logicalSectorSize !== 4096) {
1178
+ throw new Error(`Azure Disk ${spec.name}: logicalSectorSize must be either 512 or 4096 bytes`);
1179
+ }
1180
+ }
1181
+ /**
1182
+ * Format cloud resource tags for provider-specific string format.
1183
+ * Provides basic validation and consistent formatting across cloud providers.
1184
+ */
1185
+ formatCloudResourceTags(tags) {
1186
+ this.validateCloudResourceTags(tags);
1187
+ return Object.entries(tags)
1188
+ .map(([key, value]) => `${key}=${value}`)
1189
+ .join(',');
1190
+ }
1191
+ /**
1192
+ * Validate cloud resource tags with basic rules.
1193
+ * Each cloud provider has specific validation, but these are common basics.
1194
+ */
1195
+ validateCloudResourceTags(tags) {
1196
+ for (const [key, value] of Object.entries(tags)) {
1197
+ if (!key || key.trim() === '') {
1198
+ throw new Error('Tag key cannot be empty');
1199
+ }
1200
+ if (value === undefined || value === null) {
1201
+ throw new Error(`Tag value for key '${key}' cannot be null or undefined`);
1202
+ }
1203
+ }
1204
+ }
861
1205
  addNetworkPolicy(spec) {
862
1206
  // Validate policy types
863
1207
  this.validateNetworkPolicyTypes(spec);
@@ -1042,6 +1386,261 @@ export class Rutter {
1042
1386
  }
1043
1387
  }
1044
1388
  }
1389
+ validateAGICHealthProbeParams(spec) {
1390
+ if (spec.healthProbeInterval !== undefined &&
1391
+ (spec.healthProbeInterval < 1 || spec.healthProbeInterval > 86400)) {
1392
+ throw new Error('Health probe interval must be between 1 and 86400 seconds');
1393
+ }
1394
+ if (spec.healthProbeTimeout !== undefined &&
1395
+ (spec.healthProbeTimeout < 1 || spec.healthProbeTimeout > 86400)) {
1396
+ throw new Error('Health probe timeout must be between 1 and 86400 seconds');
1397
+ }
1398
+ if (spec.healthProbeUnhealthyThreshold !== undefined &&
1399
+ (spec.healthProbeUnhealthyThreshold < 1 || spec.healthProbeUnhealthyThreshold > 20)) {
1400
+ throw new Error('Health probe unhealthy threshold must be between 1 and 20');
1401
+ }
1402
+ }
1403
+ validateAGICSecurityParams(spec) {
1404
+ if (spec.wafPolicyForPath &&
1405
+ !/^\/subscriptions\/[^/]+\/resourceGroups\/[^/]+\/providers\/Microsoft\.Network\/applicationGatewayWebApplicationFirewallPolicies\/[^/]+$/.test(spec.wafPolicyForPath)) {
1406
+ throw new Error('Invalid WAF policy resource ID format');
1407
+ }
1408
+ if (spec.hostnameExtension?.some((hostname) => !this.isValidHostname(hostname))) {
1409
+ throw new Error('Invalid hostname format in hostnameExtension');
1410
+ }
1411
+ }
1412
+ isValidHostname(hostname) {
1413
+ // Basic hostname validation without complex regex to prevent ReDoS
1414
+ if (!hostname || hostname.length > 253)
1415
+ return false;
1416
+ if (hostname.startsWith('.') || hostname.endsWith('.'))
1417
+ return false;
1418
+ const labels = hostname.split('.');
1419
+ return labels.every((label) => {
1420
+ if (!label || label.length > 63)
1421
+ return false;
1422
+ if (label.startsWith('-') || label.endsWith('-'))
1423
+ return false;
1424
+ return /^[a-zA-Z0-9-]+$/.test(label);
1425
+ });
1426
+ }
1427
+ validateAGICAnnotationConsistency(spec) {
1428
+ if (spec.sslRedirect && !spec.appgwSslCertificate) {
1429
+ throw new Error('SSL redirect requires an SSL certificate to be specified');
1430
+ }
1431
+ if (spec.backendProtocol === 'https' && !spec.appgwTrustedRootCertificate?.length) {
1432
+ throw new Error('HTTPS backend protocol requires trusted root certificates');
1433
+ }
1434
+ if (spec.overrideFrontendPort === 443 && !spec.appgwSslCertificate) {
1435
+ throw new Error('Port 443 requires an SSL certificate to be specified');
1436
+ }
1437
+ }
1438
+ buildPodEnvironment(spec) {
1439
+ const envEntries = spec.env
1440
+ ? Object.entries(spec.env).map(([name, value]) => this.sanitizeEnvVar(name, value))
1441
+ : undefined;
1442
+ const envFromEntries = spec.envFrom
1443
+ ? spec.envFrom
1444
+ .map((s) => {
1445
+ const entry = {};
1446
+ if (s.configMapRef) {
1447
+ entry.configMapRef = { name: s.configMapRef };
1448
+ }
1449
+ if (s.secretRef) {
1450
+ entry.secretRef = { name: s.secretRef };
1451
+ }
1452
+ return entry;
1453
+ })
1454
+ .filter((entry) => entry.configMapRef || entry.secretRef)
1455
+ : undefined;
1456
+ const volumeDefs = spec.volumes
1457
+ ? spec.volumes.map((v) => ({
1458
+ name: v.name,
1459
+ configMap: v.configMap ? { name: v.configMap } : undefined,
1460
+ secret: v.secret ? { secretName: v.secret } : undefined,
1461
+ persistentVolumeClaim: v.persistentVolumeClaim
1462
+ ? { claimName: v.persistentVolumeClaim }
1463
+ : undefined,
1464
+ }))
1465
+ : undefined;
1466
+ const volumeMountDefs = spec.volumeMounts
1467
+ ? spec.volumeMounts.map((m) => ({
1468
+ name: m.name,
1469
+ mountPath: m.mountPath,
1470
+ readOnly: m.readOnly,
1471
+ subPath: m.subPath,
1472
+ }))
1473
+ : undefined;
1474
+ return { envEntries, envFromEntries, volumeDefs, volumeMountDefs };
1475
+ }
1476
+ buildJobSpec(spec) {
1477
+ return {
1478
+ ...(spec.backoffLimit !== undefined ? { backoffLimit: spec.backoffLimit } : {}),
1479
+ ...(spec.activeDeadlineSeconds !== undefined
1480
+ ? { activeDeadlineSeconds: spec.activeDeadlineSeconds }
1481
+ : {}),
1482
+ ...(spec.ttlSecondsAfterFinished !== undefined
1483
+ ? { ttlSecondsAfterFinished: spec.ttlSecondsAfterFinished }
1484
+ : {}),
1485
+ ...(spec.completions !== undefined ? { completions: spec.completions } : {}),
1486
+ ...(spec.parallelism !== undefined ? { parallelism: spec.parallelism } : {}),
1487
+ ...(spec.completionMode !== undefined ? { completionMode: spec.completionMode } : {}),
1488
+ ...(spec.suspend !== undefined ? { suspend: spec.suspend } : {}),
1489
+ };
1490
+ }
1491
+ buildJobPodTemplate(spec, envEntries, envFromEntries, volumeDefs, volumeMountDefs) {
1492
+ return {
1493
+ metadata: {
1494
+ ...(spec.podLabels ? { labels: spec.podLabels } : {}),
1495
+ ...(spec.podAnnotations ? { annotations: spec.podAnnotations } : {}),
1496
+ },
1497
+ spec: {
1498
+ restartPolicy: spec.restartPolicy ?? 'Never',
1499
+ serviceAccountName: spec.serviceAccountName,
1500
+ volumes: volumeDefs,
1501
+ containers: [
1502
+ {
1503
+ name: spec.name,
1504
+ image: spec.image,
1505
+ imagePullPolicy: spec.imagePullPolicy,
1506
+ ...(spec.command ? { command: spec.command } : {}),
1507
+ ...(spec.args ? { args: spec.args } : {}),
1508
+ env: envEntries,
1509
+ envFrom: envFromEntries,
1510
+ volumeMounts: volumeMountDefs,
1511
+ resources: spec.resources ?? this.getDefaultResources(),
1512
+ },
1513
+ ],
1514
+ },
1515
+ };
1516
+ }
1517
+ buildCronJobSpec(spec) {
1518
+ return {
1519
+ schedule: spec.schedule,
1520
+ ...(spec.startingDeadlineSeconds !== undefined
1521
+ ? { startingDeadlineSeconds: spec.startingDeadlineSeconds }
1522
+ : {}),
1523
+ ...(spec.concurrencyPolicy !== undefined
1524
+ ? { concurrencyPolicy: spec.concurrencyPolicy }
1525
+ : {}),
1526
+ ...(spec.suspend !== undefined ? { suspend: spec.suspend } : {}),
1527
+ ...(spec.successfulJobsHistoryLimit !== undefined
1528
+ ? { successfulJobsHistoryLimit: spec.successfulJobsHistoryLimit }
1529
+ : {}),
1530
+ ...(spec.failedJobsHistoryLimit !== undefined
1531
+ ? { failedJobsHistoryLimit: spec.failedJobsHistoryLimit }
1532
+ : {}),
1533
+ ...(spec.timeZone !== undefined ? { timeZone: spec.timeZone } : {}),
1534
+ };
1535
+ }
1536
+ buildCronJobTemplate(spec, envEntries, envFromEntries, volumeDefs, volumeMountDefs) {
1537
+ return {
1538
+ spec: {
1539
+ ...(spec.backoffLimit !== undefined ? { backoffLimit: spec.backoffLimit } : {}),
1540
+ ...(spec.activeDeadlineSeconds !== undefined
1541
+ ? { activeDeadlineSeconds: spec.activeDeadlineSeconds }
1542
+ : {}),
1543
+ ...(spec.ttlSecondsAfterFinished !== undefined
1544
+ ? { ttlSecondsAfterFinished: spec.ttlSecondsAfterFinished }
1545
+ : {}),
1546
+ ...(spec.completions !== undefined ? { completions: spec.completions } : {}),
1547
+ ...(spec.parallelism !== undefined ? { parallelism: spec.parallelism } : {}),
1548
+ ...(spec.completionMode !== undefined ? { completionMode: spec.completionMode } : {}),
1549
+ template: {
1550
+ metadata: {
1551
+ ...(spec.podLabels ? { labels: spec.podLabels } : {}),
1552
+ ...(spec.podAnnotations ? { annotations: spec.podAnnotations } : {}),
1553
+ },
1554
+ spec: {
1555
+ restartPolicy: spec.restartPolicy ?? 'Never',
1556
+ serviceAccountName: spec.serviceAccountName,
1557
+ volumes: volumeDefs,
1558
+ containers: [
1559
+ {
1560
+ name: spec.name,
1561
+ image: spec.image,
1562
+ imagePullPolicy: spec.imagePullPolicy,
1563
+ ...(spec.command ? { command: spec.command } : {}),
1564
+ ...(spec.args ? { args: spec.args } : {}),
1565
+ env: envEntries,
1566
+ envFrom: envFromEntries,
1567
+ volumeMounts: volumeMountDefs,
1568
+ resources: spec.resources ?? this.getDefaultResources(),
1569
+ },
1570
+ ],
1571
+ },
1572
+ },
1573
+ },
1574
+ };
1575
+ }
1576
+ /**
1577
+ * Validate Job specification for security and best practices
1578
+ */
1579
+ validateJobSpec(spec) {
1580
+ if (spec.activeDeadlineSeconds !== undefined && spec.activeDeadlineSeconds <= 0) {
1581
+ throw new Error('activeDeadlineSeconds must be a positive integer');
1582
+ }
1583
+ if (spec.ttlSecondsAfterFinished !== undefined && spec.ttlSecondsAfterFinished < 0) {
1584
+ throw new Error('ttlSecondsAfterFinished must be a non-negative integer');
1585
+ }
1586
+ }
1587
+ /**
1588
+ * Validate CronJob specification for security and best practices
1589
+ */
1590
+ validateCronJobSpec(spec) {
1591
+ this.validateCronSchedule(spec.schedule);
1592
+ if (spec.suspend && spec.startingDeadlineSeconds !== undefined) {
1593
+ throw new Error('startingDeadlineSeconds should not be set when job is suspended');
1594
+ }
1595
+ if (spec.successfulJobsHistoryLimit !== undefined && spec.successfulJobsHistoryLimit < 0) {
1596
+ throw new Error('successfulJobsHistoryLimit must be non-negative');
1597
+ }
1598
+ if (spec.failedJobsHistoryLimit !== undefined && spec.failedJobsHistoryLimit < 0) {
1599
+ throw new Error('failedJobsHistoryLimit must be non-negative');
1600
+ }
1601
+ if (spec.activeDeadlineSeconds !== undefined && spec.activeDeadlineSeconds <= 0) {
1602
+ throw new Error('activeDeadlineSeconds must be a positive integer');
1603
+ }
1604
+ if (spec.ttlSecondsAfterFinished !== undefined && spec.ttlSecondsAfterFinished < 0) {
1605
+ throw new Error('ttlSecondsAfterFinished must be a non-negative integer');
1606
+ }
1607
+ }
1608
+ /**
1609
+ * Validate cron schedule expression
1610
+ */
1611
+ validateCronSchedule(schedule) {
1612
+ const cronRegex = /^(\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\*\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\*|([0-9]|1[0-9]|2[0-3])|\*\/([0-9]|1[0-9]|2[0-3])) (\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\*\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\*|([1-9]|1[0-2])|\*\/([1-9]|1[0-2])) (\*|([0-6])|\*\/([0-6]))$/;
1613
+ if (!cronRegex.test(schedule)) {
1614
+ throw new Error('Invalid cron schedule expression');
1615
+ }
1616
+ }
1617
+ /**
1618
+ * Sanitize environment variable names and values
1619
+ */
1620
+ sanitizeEnvVar(name, value) {
1621
+ const nameRegex = /^[A-Za-z_][A-Za-z0-9_]*$/;
1622
+ if (!nameRegex.test(name)) {
1623
+ throw new Error(`Invalid environment variable name: ${name}`);
1624
+ }
1625
+ // Remove control characters from value (ASCII 0-31 and 127)
1626
+ const sanitizedValue = value.replace(/[\cA-\c_\x7F]/g, '');
1627
+ return { name, value: sanitizedValue };
1628
+ }
1629
+ /**
1630
+ * Get default resource limits for Jobs and CronJobs
1631
+ */
1632
+ getDefaultResources() {
1633
+ return {
1634
+ limits: {
1635
+ cpu: '1',
1636
+ memory: '512Mi',
1637
+ },
1638
+ requests: {
1639
+ cpu: '100m',
1640
+ memory: '128Mi',
1641
+ },
1642
+ };
1643
+ }
1045
1644
  /**
1046
1645
  * Capture the YAML of an ApiObject or Construct into assets.
1047
1646
  */
@@ -1179,5 +1778,8 @@ Rutter.LABEL_NAME = 'app.kubernetes.io/name';
1179
1778
  Rutter.LABEL_INSTANCE = 'app.kubernetes.io/instance';
1180
1779
  Rutter.HELPER_NAME = 'timonel.name';
1181
1780
  Rutter.NETWORKING_API_VERSION = 'networking.k8s.io/v1';
1781
+ Rutter.STORAGE_API_VERSION = 'storage.k8s.io/v1';
1782
+ Rutter.AZURE_DISK_CSI_DRIVER = 'disk.csi.azure.com';
1783
+ Rutter.AZURE_DISK_PROVISIONER_ANNOTATION = 'volume.beta.kubernetes.io/storage-provisioner';
1182
1784
  Rutter.EMPTY_POD_SELECTOR = {};
1183
1785
  //# sourceMappingURL=Rutter.js.map