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.
- package/CHANGELOG.md +47 -18
- package/README.md +134 -9
- package/dist/lib/HelmChartWriter.js +4 -6
- package/dist/lib/HelmChartWriter.js.map +1 -1
- package/dist/lib/Rutter.d.ts +284 -3
- package/dist/lib/Rutter.d.ts.map +1 -1
- package/dist/lib/Rutter.js +609 -7
- package/dist/lib/Rutter.js.map +1 -1
- package/package.json +2 -1
package/dist/lib/Rutter.js
CHANGED
|
@@ -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
|
-
|
|
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 ===
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|