timonel 0.4.0 → 2.0.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,23 +1,17 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.Rutter = void 0;
7
- const cdk8s_1 = require("cdk8s");
8
- const yaml_1 = __importDefault(require("yaml"));
9
- const helm_1 = require("./helm");
10
- const HelmChartWriter_1 = require("./HelmChartWriter");
1
+ import { App, Chart, Testing, ApiObject } from 'cdk8s';
2
+ import YAML from 'yaml';
3
+ import { include, helm } from './helm.js';
4
+ import { HelmChartWriter } from './HelmChartWriter.js';
11
5
  /**
12
6
  * Rutter builds Kubernetes manifests using cdk8s and writes a Helm chart.
13
7
  */
14
- class Rutter {
8
+ export class Rutter {
15
9
  constructor(props) {
16
10
  this.assets = [];
17
11
  this.valueOverrides = {};
18
12
  this.props = props;
19
- this.app = new cdk8s_1.App();
20
- this.chart = new cdk8s_1.Chart(this.app, props.manifestName ?? props.meta.name);
13
+ this.app = new App();
14
+ this.chart = new Chart(this.app, props.manifestName ?? props.meta.name);
21
15
  }
22
16
  /**
23
17
  * Set dynamic value overrides (from --set flags)
@@ -68,8 +62,8 @@ class Rutter {
68
62
  }
69
63
  addDeployment(spec) {
70
64
  const match = spec.matchLabels ?? {
71
- [Rutter.LABEL_NAME]: (0, helm_1.include)(Rutter.HELPER_NAME),
72
- [Rutter.LABEL_INSTANCE]: helm_1.helm.releaseName,
65
+ [Rutter.LABEL_NAME]: include(Rutter.HELPER_NAME),
66
+ [Rutter.LABEL_INSTANCE]: helm.releaseName,
73
67
  };
74
68
  const envEntries = spec.env
75
69
  ? Object.entries(spec.env).map(([name, value]) => ({ name, value }))
@@ -99,7 +93,7 @@ class Rutter {
99
93
  }))
100
94
  : undefined;
101
95
  const templateLabels = { ...match, ...(spec.podLabels ?? {}) };
102
- const dep = new cdk8s_1.ApiObject(this.chart, spec.name, {
96
+ const dep = new ApiObject(this.chart, spec.name, {
103
97
  apiVersion: 'apps/v1',
104
98
  kind: 'Deployment',
105
99
  metadata: {
@@ -140,7 +134,7 @@ class Rutter {
140
134
  return dep;
141
135
  }
142
136
  addService(spec) {
143
- const svc = new cdk8s_1.ApiObject(this.chart, spec.name, {
137
+ const svc = new ApiObject(this.chart, spec.name, {
144
138
  apiVersion: 'v1',
145
139
  kind: 'Service',
146
140
  metadata: {
@@ -175,8 +169,8 @@ class Rutter {
175
169
  }
176
170
  addReplicaSet(spec) {
177
171
  const match = spec.matchLabels ?? {
178
- [Rutter.LABEL_NAME]: (0, helm_1.include)(Rutter.HELPER_NAME),
179
- [Rutter.LABEL_INSTANCE]: helm_1.helm.releaseName,
172
+ [Rutter.LABEL_NAME]: include(Rutter.HELPER_NAME),
173
+ [Rutter.LABEL_INSTANCE]: helm.releaseName,
180
174
  };
181
175
  const envEntries = spec.env
182
176
  ? Object.entries(spec.env).map(([name, value]) => ({ name, value }))
@@ -206,7 +200,7 @@ class Rutter {
206
200
  }))
207
201
  : undefined;
208
202
  const templateLabels = { ...match, ...(spec.podLabels ?? {}) };
209
- const rs = new cdk8s_1.ApiObject(this.chart, spec.name, {
203
+ const rs = new ApiObject(this.chart, spec.name, {
210
204
  apiVersion: 'apps/v1',
211
205
  kind: 'ReplicaSet',
212
206
  metadata: {
@@ -247,8 +241,8 @@ class Rutter {
247
241
  return rs;
248
242
  }
249
243
  addIngress(spec) {
250
- const ing = new cdk8s_1.ApiObject(this.chart, spec.name, {
251
- apiVersion: 'networking.k8s.io/v1',
244
+ const ing = new ApiObject(this.chart, spec.name, {
245
+ apiVersion: Rutter.NETWORKING_API_VERSION,
252
246
  kind: 'Ingress',
253
247
  metadata: {
254
248
  name: spec.name,
@@ -321,7 +315,7 @@ class Rutter {
321
315
  pvSpec[key] = value;
322
316
  }
323
317
  }
324
- const pv = new cdk8s_1.ApiObject(this.chart, spec.name, {
318
+ const pv = new ApiObject(this.chart, spec.name, {
325
319
  apiVersion: 'v1',
326
320
  kind: 'PersistentVolume',
327
321
  metadata: {
@@ -354,10 +348,10 @@ class Rutter {
354
348
  else if (spec.cloudProvider === 'azure') {
355
349
  optimized.annotations = {
356
350
  ...optimized.annotations,
357
- 'volume.beta.kubernetes.io/storage-provisioner': 'disk.csi.azure.com',
351
+ [Rutter.AZURE_DISK_PROVISIONER_ANNOTATION]: Rutter.AZURE_DISK_CSI_DRIVER,
358
352
  };
359
353
  // Default to Premium_ZRS for multi-zone clusters
360
- if (spec.source.csi?.driver === 'disk.csi.azure.com' && optimized.source.csi) {
354
+ if (spec.source.csi?.driver === Rutter.AZURE_DISK_CSI_DRIVER && optimized.source.csi) {
361
355
  optimized.source.csi.volumeAttributes = {
362
356
  skuName: 'Premium_ZRS',
363
357
  ...spec.source.csi.volumeAttributes,
@@ -378,7 +372,7 @@ class Rutter {
378
372
  addPersistentVolumeClaim(spec) {
379
373
  // Validate access modes compatibility
380
374
  this.validatePVCAccessModes(spec);
381
- const pvc = new cdk8s_1.ApiObject(this.chart, spec.name, {
375
+ const pvc = new ApiObject(this.chart, spec.name, {
382
376
  apiVersion: 'v1',
383
377
  kind: 'PersistentVolumeClaim',
384
378
  metadata: {
@@ -413,7 +407,7 @@ class Rutter {
413
407
  }
414
408
  }
415
409
  addConfigMap(spec) {
416
- const cm = new cdk8s_1.ApiObject(this.chart, spec.name, {
410
+ const cm = new ApiObject(this.chart, spec.name, {
417
411
  apiVersion: 'v1',
418
412
  kind: 'ConfigMap',
419
413
  metadata: {
@@ -429,7 +423,7 @@ class Rutter {
429
423
  return cm;
430
424
  }
431
425
  addSecret(spec) {
432
- const sec = new cdk8s_1.ApiObject(this.chart, spec.name, {
426
+ const sec = new ApiObject(this.chart, spec.name, {
433
427
  apiVersion: 'v1',
434
428
  kind: 'Secret',
435
429
  metadata: {
@@ -447,7 +441,7 @@ class Rutter {
447
441
  }
448
442
  addServiceAccount(spec) {
449
443
  const annotations = this.buildServiceAccountAnnotations(spec);
450
- const sa = new cdk8s_1.ApiObject(this.chart, spec.name, {
444
+ const sa = new ApiObject(this.chart, spec.name, {
451
445
  apiVersion: 'v1',
452
446
  kind: 'ServiceAccount',
453
447
  metadata: {
@@ -515,7 +509,7 @@ class Rutter {
515
509
  },
516
510
  },
517
511
  ];
518
- const hpa = new cdk8s_1.ApiObject(this.chart, spec.name, {
512
+ const hpa = new ApiObject(this.chart, spec.name, {
519
513
  apiVersion: 'autoscaling/v2',
520
514
  kind: 'HorizontalPodAutoscaler',
521
515
  metadata: {
@@ -535,7 +529,7 @@ class Rutter {
535
529
  return hpa;
536
530
  }
537
531
  addVerticalPodAutoscaler(spec) {
538
- const vpa = new cdk8s_1.ApiObject(this.chart, spec.name, {
532
+ const vpa = new ApiObject(this.chart, spec.name, {
539
533
  apiVersion: 'autoscaling.k8s.io/v1',
540
534
  kind: 'VerticalPodAutoscaler',
541
535
  metadata: {
@@ -554,7 +548,7 @@ class Rutter {
554
548
  return vpa;
555
549
  }
556
550
  addPodDisruptionBudget(spec) {
557
- const pdb = new cdk8s_1.ApiObject(this.chart, spec.name, {
551
+ const pdb = new ApiObject(this.chart, spec.name, {
558
552
  apiVersion: 'policy/v1',
559
553
  kind: 'PodDisruptionBudget',
560
554
  metadata: {
@@ -588,8 +582,8 @@ class Rutter {
588
582
  if (spec.throughput && spec.volumeType === DEFAULT_VOLUME_TYPE) {
589
583
  parameters['throughput'] = String(spec.throughput);
590
584
  }
591
- const sc = new cdk8s_1.ApiObject(this.chart, spec.name, {
592
- apiVersion: 'storage.k8s.io/v1',
585
+ const sc = new ApiObject(this.chart, spec.name, {
586
+ apiVersion: Rutter.STORAGE_API_VERSION,
593
587
  kind: 'StorageClass',
594
588
  metadata: {
595
589
  name: spec.name,
@@ -606,7 +600,7 @@ class Rutter {
606
600
  return sc;
607
601
  }
608
602
  addAWSEBSPersistentVolumeClaim(spec) {
609
- const pvc = new cdk8s_1.ApiObject(this.chart, spec.name, {
603
+ const pvc = new ApiObject(this.chart, spec.name, {
610
604
  apiVersion: 'v1',
611
605
  kind: 'PersistentVolumeClaim',
612
606
  metadata: {
@@ -628,8 +622,8 @@ class Rutter {
628
622
  return pvc;
629
623
  }
630
624
  addAWSEFSStorageClass(spec) {
631
- const sc = new cdk8s_1.ApiObject(this.chart, spec.name, {
632
- apiVersion: 'storage.k8s.io/v1',
625
+ const sc = new ApiObject(this.chart, spec.name, {
626
+ apiVersion: Rutter.STORAGE_API_VERSION,
633
627
  kind: 'StorageClass',
634
628
  metadata: {
635
629
  name: spec.name,
@@ -670,7 +664,7 @@ class Rutter {
670
664
  if (Object.keys(volumeAttributes).length > 0) {
671
665
  csiSpec['volumeAttributes'] = volumeAttributes;
672
666
  }
673
- const pv = new cdk8s_1.ApiObject(this.chart, spec.name, {
667
+ const pv = new ApiObject(this.chart, spec.name, {
674
668
  apiVersion: 'v1',
675
669
  kind: 'PersistentVolume',
676
670
  metadata: {
@@ -692,7 +686,7 @@ class Rutter {
692
686
  return pv;
693
687
  }
694
688
  addAWSEFSPersistentVolumeClaim(spec) {
695
- const pvc = new cdk8s_1.ApiObject(this.chart, spec.name, {
689
+ const pvc = new ApiObject(this.chart, spec.name, {
696
690
  apiVersion: 'v1',
697
691
  kind: 'PersistentVolumeClaim',
698
692
  metadata: {
@@ -715,8 +709,8 @@ class Rutter {
715
709
  }
716
710
  addAWSALBIngress(spec) {
717
711
  const annotations = this.buildALBAnnotations(spec);
718
- const ingress = new cdk8s_1.ApiObject(this.chart, spec.name, {
719
- apiVersion: 'networking.k8s.io/v1',
712
+ const ingress = new ApiObject(this.chart, spec.name, {
713
+ apiVersion: Rutter.NETWORKING_API_VERSION,
720
714
  kind: 'Ingress',
721
715
  metadata: {
722
716
  name: spec.name,
@@ -794,12 +788,165 @@ class Rutter {
794
788
  }
795
789
  addTagsAnnotation(annotations, spec) {
796
790
  if (spec.tags && Object.keys(spec.tags).length > 0) {
797
- const tagString = Object.entries(spec.tags)
798
- .map(([key, value]) => `${key}=${value}`)
799
- .join(',');
791
+ const tagString = this.formatCloudResourceTags(spec.tags);
800
792
  annotations['alb.ingress.kubernetes.io/tags'] = tagString;
801
793
  }
802
794
  }
795
+ /**
796
+ * Add Azure Application Gateway Ingress Controller (AGIC) Ingress.
797
+ * Creates an Ingress resource with AGIC-specific annotations for AKS deployments.
798
+ *
799
+ * @param spec - AGIC Ingress specification
800
+ * @returns The created Ingress ApiObject
801
+ *
802
+ * @example
803
+ * ```typescript
804
+ * rutter.addAzureAGICIngress({
805
+ * name: 'app-ingress',
806
+ * rules: [{
807
+ * host: 'app.example.com',
808
+ * paths: [{
809
+ * path: '/',
810
+ * pathType: 'Prefix',
811
+ * backend: { service: { name: 'app-service', port: { number: 80 } } }
812
+ * }]
813
+ * }],
814
+ * sslRedirect: true,
815
+ * backendProtocol: 'https',
816
+ * healthProbePath: '/health'
817
+ * });
818
+ * ```
819
+ */
820
+ addAzureAGICIngress(spec) {
821
+ this.validateAGICHealthProbeParams(spec);
822
+ this.validateAGICSecurityParams(spec);
823
+ this.validateAGICAnnotationConsistency(spec);
824
+ const annotations = this.buildAGICAnnotations(spec);
825
+ const ingress = new ApiObject(this.chart, spec.name, {
826
+ apiVersion: Rutter.NETWORKING_API_VERSION,
827
+ kind: 'Ingress',
828
+ metadata: {
829
+ name: spec.name,
830
+ annotations,
831
+ ...(spec.labels ? { labels: spec.labels } : {}),
832
+ },
833
+ spec: {
834
+ tls: spec.tls,
835
+ rules: spec.rules.map((rule) => ({
836
+ host: rule.host,
837
+ http: {
838
+ paths: rule.paths.map((path) => ({
839
+ path: path.path,
840
+ pathType: path.pathType,
841
+ backend: path.backend,
842
+ })),
843
+ },
844
+ })),
845
+ },
846
+ });
847
+ this.capture(ingress, `${spec.name}-ingress`);
848
+ return ingress;
849
+ }
850
+ buildAGICAnnotations(spec) {
851
+ const annotations = {
852
+ 'kubernetes.io/ingress.class': 'azure/application-gateway',
853
+ ...(spec.annotations ?? {}),
854
+ };
855
+ this.addAGICPathAnnotations(annotations, spec);
856
+ this.addAGICProtocolAnnotations(annotations, spec);
857
+ this.addAGICHealthProbeAnnotations(annotations, spec);
858
+ this.addAGICConnectionAnnotations(annotations, spec);
859
+ this.addAGICSecurityAnnotations(annotations, spec);
860
+ this.addAGICAdvancedAnnotations(annotations, spec);
861
+ return annotations;
862
+ }
863
+ addAGICPathAnnotations(annotations, spec) {
864
+ if (spec.backendPathPrefix) {
865
+ annotations['appgw.ingress.kubernetes.io/backend-path-prefix'] = spec.backendPathPrefix;
866
+ }
867
+ if (spec.backendHostname) {
868
+ annotations['appgw.ingress.kubernetes.io/backend-hostname'] = spec.backendHostname;
869
+ }
870
+ }
871
+ addAGICProtocolAnnotations(annotations, spec) {
872
+ if (spec.backendProtocol) {
873
+ annotations['appgw.ingress.kubernetes.io/backend-protocol'] = spec.backendProtocol;
874
+ }
875
+ if (spec.sslRedirect) {
876
+ annotations['appgw.ingress.kubernetes.io/ssl-redirect'] = 'true';
877
+ }
878
+ if (spec.usePrivateIp) {
879
+ annotations['appgw.ingress.kubernetes.io/use-private-ip'] = 'true';
880
+ }
881
+ if (spec.overrideFrontendPort !== undefined) {
882
+ annotations['appgw.ingress.kubernetes.io/override-frontend-port'] = String(spec.overrideFrontendPort);
883
+ }
884
+ }
885
+ addAGICHealthProbeAnnotations(annotations, spec) {
886
+ if (spec.healthProbeHostname) {
887
+ annotations['appgw.ingress.kubernetes.io/health-probe-hostname'] = spec.healthProbeHostname;
888
+ }
889
+ if (spec.healthProbePort !== undefined) {
890
+ annotations['appgw.ingress.kubernetes.io/health-probe-port'] = String(spec.healthProbePort);
891
+ }
892
+ if (spec.healthProbePath) {
893
+ annotations['appgw.ingress.kubernetes.io/health-probe-path'] = spec.healthProbePath;
894
+ }
895
+ if (spec.healthProbeStatusCodes) {
896
+ annotations['appgw.ingress.kubernetes.io/health-probe-status-codes'] =
897
+ spec.healthProbeStatusCodes;
898
+ }
899
+ if (spec.healthProbeInterval !== undefined) {
900
+ annotations['appgw.ingress.kubernetes.io/health-probe-interval'] = String(spec.healthProbeInterval);
901
+ }
902
+ if (spec.healthProbeTimeout !== undefined) {
903
+ annotations['appgw.ingress.kubernetes.io/health-probe-timeout'] = String(spec.healthProbeTimeout);
904
+ }
905
+ if (spec.healthProbeUnhealthyThreshold !== undefined) {
906
+ annotations['appgw.ingress.kubernetes.io/health-probe-unhealthy-threshold'] = String(spec.healthProbeUnhealthyThreshold);
907
+ }
908
+ }
909
+ addAGICConnectionAnnotations(annotations, spec) {
910
+ if (spec.cookieBasedAffinity) {
911
+ annotations['appgw.ingress.kubernetes.io/cookie-based-affinity'] = 'true';
912
+ }
913
+ if (spec.requestTimeout !== undefined) {
914
+ annotations['appgw.ingress.kubernetes.io/request-timeout'] = String(spec.requestTimeout);
915
+ }
916
+ if (spec.connectionDraining) {
917
+ annotations['appgw.ingress.kubernetes.io/connection-draining'] = 'true';
918
+ }
919
+ if (spec.connectionDrainingTimeout !== undefined) {
920
+ annotations['appgw.ingress.kubernetes.io/connection-draining-timeout'] = String(spec.connectionDrainingTimeout);
921
+ }
922
+ }
923
+ addAGICSecurityAnnotations(annotations, spec) {
924
+ if (spec.appgwSslCertificate) {
925
+ annotations['appgw.ingress.kubernetes.io/appgw-ssl-certificate'] = spec.appgwSslCertificate;
926
+ }
927
+ if (spec.appgwSslProfile) {
928
+ annotations['appgw.ingress.kubernetes.io/appgw-ssl-profile'] = spec.appgwSslProfile;
929
+ }
930
+ if (spec.appgwTrustedRootCertificate && spec.appgwTrustedRootCertificate.length > 0) {
931
+ annotations['appgw.ingress.kubernetes.io/appgw-trusted-root-certificate'] =
932
+ spec.appgwTrustedRootCertificate.join(',');
933
+ }
934
+ if (spec.wafPolicyForPath) {
935
+ annotations['appgw.ingress.kubernetes.io/waf-policy-for-path'] = spec.wafPolicyForPath;
936
+ }
937
+ }
938
+ addAGICAdvancedAnnotations(annotations, spec) {
939
+ if (spec.hostnameExtension && spec.hostnameExtension.length > 0) {
940
+ annotations['appgw.ingress.kubernetes.io/hostname-extension'] =
941
+ spec.hostnameExtension.join(', ');
942
+ }
943
+ if (spec.rewriteRuleSet) {
944
+ annotations['appgw.ingress.kubernetes.io/rewrite-rule-set'] = spec.rewriteRuleSet;
945
+ }
946
+ if (spec.rulePriority !== undefined) {
947
+ annotations['appgw.ingress.kubernetes.io/rule-priority'] = String(spec.rulePriority);
948
+ }
949
+ }
803
950
  addAWSIRSAServiceAccount(spec) {
804
951
  const annotations = {
805
952
  'eks.amazonaws.com/role-arn': spec.roleArn,
@@ -812,7 +959,7 @@ class Rutter {
812
959
  : {}),
813
960
  ...(spec.annotations ?? {}),
814
961
  };
815
- const sa = new cdk8s_1.ApiObject(this.chart, spec.name, {
962
+ const sa = new ApiObject(this.chart, spec.name, {
816
963
  apiVersion: 'v1',
817
964
  kind: 'ServiceAccount',
818
965
  metadata: {
@@ -841,14 +988,14 @@ class Rutter {
841
988
  }
842
989
  return baseObj;
843
990
  });
844
- const objectsYaml = yaml_1.default.stringify(objects, { indent: 2 }).trim();
991
+ const objectsYaml = YAML.stringify(objects, { indent: 2 }).trim();
845
992
  const parameters = {
846
993
  objects: `|\n ${objectsYaml.split('\n').join('\n ')}`,
847
994
  };
848
995
  if (spec.region) {
849
996
  parameters['region'] = spec.region;
850
997
  }
851
- const spc = new cdk8s_1.ApiObject(this.chart, spec.name, {
998
+ const spc = new ApiObject(this.chart, spec.name, {
852
999
  apiVersion: 'secrets-store.csi.x-k8s.io/v1',
853
1000
  kind: 'SecretProviderClass',
854
1001
  metadata: {
@@ -864,6 +1011,386 @@ class Rutter {
864
1011
  this.capture(spc, `${spec.name}-secretproviderclass`);
865
1012
  return spc;
866
1013
  }
1014
+ addAzureDiskStorageClass(spec) {
1015
+ // Validate numeric parameters to prevent runtime errors
1016
+ this.validateAzureDiskNumericParameters(spec);
1017
+ const parameters = this.buildAzureDiskParameters(spec);
1018
+ const sc = new ApiObject(this.chart, spec.name, {
1019
+ apiVersion: Rutter.STORAGE_API_VERSION,
1020
+ kind: 'StorageClass',
1021
+ metadata: {
1022
+ name: spec.name,
1023
+ ...(spec.labels ? { labels: spec.labels } : {}),
1024
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
1025
+ },
1026
+ provisioner: Rutter.AZURE_DISK_CSI_DRIVER,
1027
+ parameters,
1028
+ reclaimPolicy: spec.reclaimPolicy ?? 'Delete',
1029
+ allowVolumeExpansion: spec.allowVolumeExpansion ?? true,
1030
+ volumeBindingMode: spec.volumeBindingMode ?? 'WaitForFirstConsumer',
1031
+ });
1032
+ this.capture(sc, `${spec.name}-storageclass`);
1033
+ return sc;
1034
+ }
1035
+ buildAzureDiskParameters(spec) {
1036
+ const parameters = {
1037
+ skuName: spec.skuName ?? 'StandardSSD_LRS',
1038
+ fsType: spec.fsType ?? 'ext4',
1039
+ // Security best practices: default to private network access and encryption
1040
+ networkAccessPolicy: spec.networkAccessPolicy ?? 'AllowPrivate',
1041
+ };
1042
+ this.addOptionalAzureDiskParameters(parameters, spec);
1043
+ this.addAzureDiskEncryptionParameters(parameters, spec);
1044
+ this.addAzureDiskPerformanceParameters(parameters, spec);
1045
+ return parameters;
1046
+ }
1047
+ addOptionalAzureDiskParameters(parameters, spec) {
1048
+ if (spec.cachingMode) {
1049
+ parameters['cachingMode'] = spec.cachingMode;
1050
+ }
1051
+ if (spec.resourceGroup) {
1052
+ parameters['resourceGroup'] = spec.resourceGroup;
1053
+ }
1054
+ if (spec.logicalSectorSize !== undefined) {
1055
+ parameters['LogicalSectorSize'] = String(spec.logicalSectorSize);
1056
+ }
1057
+ if (spec.tags && Object.keys(spec.tags).length > 0) {
1058
+ const tagString = this.formatCloudResourceTags(spec.tags);
1059
+ parameters['tags'] = tagString;
1060
+ }
1061
+ // networkAccessPolicy is now set as default in buildAzureDiskParameters
1062
+ if (spec.networkAccessPolicy && spec.networkAccessPolicy !== 'AllowPrivate') {
1063
+ parameters['networkAccessPolicy'] = spec.networkAccessPolicy;
1064
+ }
1065
+ if (spec.diskAccessID) {
1066
+ parameters['diskAccessID'] = spec.diskAccessID;
1067
+ }
1068
+ if (spec.maxShares !== undefined) {
1069
+ parameters['maxShares'] = String(spec.maxShares);
1070
+ }
1071
+ }
1072
+ addAzureDiskEncryptionParameters(parameters, spec) {
1073
+ if (spec.diskEncryptionSetID) {
1074
+ parameters['diskEncryptionSetID'] = spec.diskEncryptionSetID;
1075
+ }
1076
+ if (spec.diskEncryptionType) {
1077
+ parameters['diskEncryptionType'] = spec.diskEncryptionType;
1078
+ }
1079
+ }
1080
+ addAzureDiskPerformanceParameters(parameters, spec) {
1081
+ if (spec.diskIOPSReadWrite !== undefined) {
1082
+ parameters['DiskIOPSReadWrite'] = String(spec.diskIOPSReadWrite);
1083
+ }
1084
+ if (spec.diskMBpsReadWrite !== undefined) {
1085
+ parameters['DiskMBpsReadWrite'] = String(spec.diskMBpsReadWrite);
1086
+ }
1087
+ if (spec.writeAcceleratorEnabled !== undefined) {
1088
+ parameters['writeAcceleratorEnabled'] = String(spec.writeAcceleratorEnabled);
1089
+ }
1090
+ if (spec.enableBursting !== undefined) {
1091
+ parameters['enableBursting'] = String(spec.enableBursting);
1092
+ }
1093
+ }
1094
+ validateAzureDiskNumericParameters(spec) {
1095
+ this.validateAzureDiskIOPS(spec);
1096
+ this.validateAzureDiskThroughput(spec);
1097
+ this.validateAzureDiskShares(spec);
1098
+ this.validateAzureDiskSectorSize(spec);
1099
+ }
1100
+ validateAzureDiskIOPS(spec) {
1101
+ if (spec.diskIOPSReadWrite === undefined)
1102
+ return;
1103
+ if (spec.diskIOPSReadWrite < 100) {
1104
+ throw new Error(`Azure Disk ${spec.name}: diskIOPSReadWrite must be at least 100 IOPS`);
1105
+ }
1106
+ if (spec.diskIOPSReadWrite > 400000) {
1107
+ throw new Error(`Azure Disk ${spec.name}: diskIOPSReadWrite cannot exceed 400,000 IOPS`);
1108
+ }
1109
+ }
1110
+ validateAzureDiskThroughput(spec) {
1111
+ if (spec.diskMBpsReadWrite === undefined)
1112
+ return;
1113
+ if (spec.diskMBpsReadWrite < 1) {
1114
+ throw new Error(`Azure Disk ${spec.name}: diskMBpsReadWrite must be at least 1 MB/s`);
1115
+ }
1116
+ if (spec.diskMBpsReadWrite > 10000) {
1117
+ throw new Error(`Azure Disk ${spec.name}: diskMBpsReadWrite cannot exceed 10,000 MB/s`);
1118
+ }
1119
+ }
1120
+ validateAzureDiskShares(spec) {
1121
+ if (spec.maxShares === undefined)
1122
+ return;
1123
+ if (spec.maxShares < 1) {
1124
+ throw new Error(`Azure Disk ${spec.name}: maxShares must be at least 1`);
1125
+ }
1126
+ if (spec.maxShares > 5) {
1127
+ throw new Error(`Azure Disk ${spec.name}: maxShares cannot exceed 5`);
1128
+ }
1129
+ }
1130
+ validateAzureDiskSectorSize(spec) {
1131
+ if (spec.logicalSectorSize === undefined)
1132
+ return;
1133
+ if (spec.logicalSectorSize !== 512 && spec.logicalSectorSize !== 4096) {
1134
+ throw new Error(`Azure Disk ${spec.name}: logicalSectorSize must be either 512 or 4096 bytes`);
1135
+ }
1136
+ }
1137
+ /**
1138
+ * Format cloud resource tags for provider-specific string format.
1139
+ * Provides basic validation and consistent formatting across cloud providers.
1140
+ */
1141
+ formatCloudResourceTags(tags) {
1142
+ this.validateCloudResourceTags(tags);
1143
+ return Object.entries(tags)
1144
+ .map(([key, value]) => `${key}=${value}`)
1145
+ .join(',');
1146
+ }
1147
+ /**
1148
+ * Validate cloud resource tags with basic rules.
1149
+ * Each cloud provider has specific validation, but these are common basics.
1150
+ */
1151
+ validateCloudResourceTags(tags) {
1152
+ for (const [key, value] of Object.entries(tags)) {
1153
+ if (!key || key.trim() === '') {
1154
+ throw new Error('Tag key cannot be empty');
1155
+ }
1156
+ if (value === undefined || value === null) {
1157
+ throw new Error(`Tag value for key '${key}' cannot be null or undefined`);
1158
+ }
1159
+ }
1160
+ }
1161
+ addNetworkPolicy(spec) {
1162
+ // Validate policy types
1163
+ this.validateNetworkPolicyTypes(spec);
1164
+ // Validate CIDR blocks if present
1165
+ this.validateNetworkPolicyCIDRs(spec);
1166
+ const np = new ApiObject(this.chart, spec.name, {
1167
+ apiVersion: Rutter.NETWORKING_API_VERSION,
1168
+ kind: 'NetworkPolicy',
1169
+ metadata: {
1170
+ name: spec.name,
1171
+ ...(spec.labels ? { labels: spec.labels } : {}),
1172
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
1173
+ },
1174
+ spec: {
1175
+ podSelector: spec.podSelector ?? {},
1176
+ policyTypes: spec.policyTypes,
1177
+ ...(spec.ingress ? { ingress: spec.ingress } : {}),
1178
+ ...(spec.egress ? { egress: spec.egress } : {}),
1179
+ },
1180
+ });
1181
+ this.capture(np, `${spec.name}-networkpolicy`);
1182
+ return np;
1183
+ }
1184
+ /**
1185
+ * Create a deny-all ingress NetworkPolicy for the current namespace
1186
+ */
1187
+ addDenyAllIngressNetworkPolicy(name = 'deny-all-ingress') {
1188
+ return this.addNetworkPolicy({
1189
+ name,
1190
+ podSelector: Rutter.EMPTY_POD_SELECTOR,
1191
+ policyTypes: ['Ingress'],
1192
+ });
1193
+ }
1194
+ /**
1195
+ * Create a deny-all egress NetworkPolicy for the current namespace
1196
+ */
1197
+ addDenyAllEgressNetworkPolicy(name = 'deny-all-egress') {
1198
+ return this.addNetworkPolicy({
1199
+ name,
1200
+ podSelector: Rutter.EMPTY_POD_SELECTOR,
1201
+ policyTypes: ['Egress'],
1202
+ });
1203
+ }
1204
+ /**
1205
+ * Create a deny-all NetworkPolicy for both ingress and egress
1206
+ */
1207
+ addDenyAllNetworkPolicy(name = 'deny-all') {
1208
+ return this.addNetworkPolicy({
1209
+ name,
1210
+ podSelector: Rutter.EMPTY_POD_SELECTOR,
1211
+ policyTypes: ['Ingress', 'Egress'],
1212
+ });
1213
+ }
1214
+ /**
1215
+ * Allow traffic from specific pods to target pods on specific ports
1216
+ */
1217
+ addAllowFromPodsNetworkPolicy(spec) {
1218
+ return this.addNetworkPolicy({
1219
+ name: spec.name,
1220
+ podSelector: { matchLabels: spec.targetPodSelector },
1221
+ policyTypes: ['Ingress'],
1222
+ ingress: [
1223
+ {
1224
+ from: [{ podSelector: { matchLabels: spec.sourcePodSelector } }],
1225
+ ...(spec.ports ? { ports: spec.ports } : {}),
1226
+ },
1227
+ ],
1228
+ ...(spec.labels ? { labels: spec.labels } : {}),
1229
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
1230
+ });
1231
+ }
1232
+ /**
1233
+ * Allow traffic from specific namespace to target pods
1234
+ */
1235
+ addAllowFromNamespaceNetworkPolicy(spec) {
1236
+ return this.addNetworkPolicy({
1237
+ name: spec.name,
1238
+ podSelector: { matchLabels: spec.targetPodSelector },
1239
+ policyTypes: ['Ingress'],
1240
+ ingress: [
1241
+ {
1242
+ from: [{ namespaceSelector: { matchLabels: spec.sourceNamespaceSelector } }],
1243
+ ...(spec.ports ? { ports: spec.ports } : {}),
1244
+ },
1245
+ ],
1246
+ ...(spec.labels ? { labels: spec.labels } : {}),
1247
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
1248
+ });
1249
+ }
1250
+ validateNetworkPolicyTypes(spec) {
1251
+ if (!spec.policyTypes || spec.policyTypes.length === 0) {
1252
+ throw new Error(`NetworkPolicy ${spec.name}: policyTypes must be specified`);
1253
+ }
1254
+ this.validatePolicyTypeValues(spec);
1255
+ this.validatePolicyTypeRules(spec);
1256
+ }
1257
+ validatePolicyTypeValues(spec) {
1258
+ const validTypes = ['Ingress', 'Egress'];
1259
+ for (const type of spec.policyTypes) {
1260
+ if (!validTypes.includes(type)) {
1261
+ throw new Error(`NetworkPolicy ${spec.name}: invalid policyType '${type}', must be 'Ingress' or 'Egress'`);
1262
+ }
1263
+ }
1264
+ }
1265
+ validatePolicyTypeRules(spec) {
1266
+ if (spec.policyTypes.includes('Ingress') && spec.ingress === undefined) {
1267
+ console.warn(`NetworkPolicy ${spec.name}: policyType 'Ingress' specified but no ingress rules defined (will deny all ingress)`);
1268
+ }
1269
+ if (spec.policyTypes.includes('Egress') && spec.egress === undefined) {
1270
+ console.warn(`NetworkPolicy ${spec.name}: policyType 'Egress' specified but no egress rules defined (will deny all egress)`);
1271
+ }
1272
+ }
1273
+ validateNetworkPolicyCIDRs(spec) {
1274
+ const validateCIDR = (cidr, context) => {
1275
+ if (!this.isValidCIDR(cidr)) {
1276
+ throw new Error(`NetworkPolicy ${spec.name}: invalid CIDR '${cidr}' in ${context}`);
1277
+ }
1278
+ };
1279
+ this.validateIngressCIDRs(spec, validateCIDR);
1280
+ this.validateEgressCIDRs(spec, validateCIDR);
1281
+ }
1282
+ isValidCIDR(cidr) {
1283
+ // IPv4 CIDR validation
1284
+ const ipv4Parts = cidr.split('/');
1285
+ if (ipv4Parts.length === 2) {
1286
+ const [ip, prefix] = ipv4Parts;
1287
+ if (ip && prefix && this.isValidIPv4(ip) && this.isValidPrefix(prefix, 32)) {
1288
+ return true;
1289
+ }
1290
+ }
1291
+ // IPv6 CIDR validation (basic)
1292
+ if (cidr.includes(':') && ipv4Parts.length === 2) {
1293
+ const [, prefix] = ipv4Parts;
1294
+ if (prefix) {
1295
+ return this.isValidPrefix(prefix, 128);
1296
+ }
1297
+ }
1298
+ return false;
1299
+ }
1300
+ isValidIPv4(ip) {
1301
+ const parts = ip.split('.');
1302
+ if (parts.length !== 4)
1303
+ return false;
1304
+ return parts.every((part) => {
1305
+ const num = parseInt(part, 10);
1306
+ return !isNaN(num) && num >= 0 && num <= 255 && String(num) === part;
1307
+ });
1308
+ }
1309
+ isValidPrefix(prefix, maxPrefix) {
1310
+ const num = parseInt(prefix, 10);
1311
+ return !isNaN(num) && num >= 0 && num <= maxPrefix && String(num) === prefix;
1312
+ }
1313
+ validateIngressCIDRs(spec, validateCIDR) {
1314
+ if (!spec.ingress)
1315
+ return;
1316
+ for (const rule of spec.ingress) {
1317
+ if (rule.from) {
1318
+ for (const peer of rule.from) {
1319
+ this.validatePeerCIDRs(peer, validateCIDR, 'ingress rule');
1320
+ }
1321
+ }
1322
+ }
1323
+ }
1324
+ validateEgressCIDRs(spec, validateCIDR) {
1325
+ if (!spec.egress)
1326
+ return;
1327
+ for (const rule of spec.egress) {
1328
+ if (rule.to) {
1329
+ for (const peer of rule.to) {
1330
+ this.validatePeerCIDRs(peer, validateCIDR, 'egress rule');
1331
+ }
1332
+ }
1333
+ }
1334
+ }
1335
+ validatePeerCIDRs(peer, validateCIDR, context) {
1336
+ if (peer.ipBlock?.cidr) {
1337
+ validateCIDR(peer.ipBlock.cidr, context);
1338
+ if (peer.ipBlock.except) {
1339
+ for (const except of peer.ipBlock.except) {
1340
+ validateCIDR(except, `${context} except block`);
1341
+ }
1342
+ }
1343
+ }
1344
+ }
1345
+ validateAGICHealthProbeParams(spec) {
1346
+ if (spec.healthProbeInterval !== undefined &&
1347
+ (spec.healthProbeInterval < 1 || spec.healthProbeInterval > 86400)) {
1348
+ throw new Error('Health probe interval must be between 1 and 86400 seconds');
1349
+ }
1350
+ if (spec.healthProbeTimeout !== undefined &&
1351
+ (spec.healthProbeTimeout < 1 || spec.healthProbeTimeout > 86400)) {
1352
+ throw new Error('Health probe timeout must be between 1 and 86400 seconds');
1353
+ }
1354
+ if (spec.healthProbeUnhealthyThreshold !== undefined &&
1355
+ (spec.healthProbeUnhealthyThreshold < 1 || spec.healthProbeUnhealthyThreshold > 20)) {
1356
+ throw new Error('Health probe unhealthy threshold must be between 1 and 20');
1357
+ }
1358
+ }
1359
+ validateAGICSecurityParams(spec) {
1360
+ if (spec.wafPolicyForPath &&
1361
+ !/^\/subscriptions\/[^/]+\/resourceGroups\/[^/]+\/providers\/Microsoft\.Network\/applicationGatewayWebApplicationFirewallPolicies\/[^/]+$/.test(spec.wafPolicyForPath)) {
1362
+ throw new Error('Invalid WAF policy resource ID format');
1363
+ }
1364
+ if (spec.hostnameExtension?.some((hostname) => !this.isValidHostname(hostname))) {
1365
+ throw new Error('Invalid hostname format in hostnameExtension');
1366
+ }
1367
+ }
1368
+ isValidHostname(hostname) {
1369
+ // Basic hostname validation without complex regex to prevent ReDoS
1370
+ if (!hostname || hostname.length > 253)
1371
+ return false;
1372
+ if (hostname.startsWith('.') || hostname.endsWith('.'))
1373
+ return false;
1374
+ const labels = hostname.split('.');
1375
+ return labels.every((label) => {
1376
+ if (!label || label.length > 63)
1377
+ return false;
1378
+ if (label.startsWith('-') || label.endsWith('-'))
1379
+ return false;
1380
+ return /^[a-zA-Z0-9-]+$/.test(label);
1381
+ });
1382
+ }
1383
+ validateAGICAnnotationConsistency(spec) {
1384
+ if (spec.sslRedirect && !spec.appgwSslCertificate) {
1385
+ throw new Error('SSL redirect requires an SSL certificate to be specified');
1386
+ }
1387
+ if (spec.backendProtocol === 'https' && !spec.appgwTrustedRootCertificate?.length) {
1388
+ throw new Error('HTTPS backend protocol requires trusted root certificates');
1389
+ }
1390
+ if (spec.overrideFrontendPort === 443 && !spec.appgwSslCertificate) {
1391
+ throw new Error('Port 443 requires an SSL certificate to be specified');
1392
+ }
1393
+ }
867
1394
  /**
868
1395
  * Capture the YAML of an ApiObject or Construct into assets.
869
1396
  */
@@ -898,7 +1425,7 @@ class Rutter {
898
1425
  */
899
1426
  write(outDir) {
900
1427
  // Use cdk8s Testing.synth to obtain manifest objects
901
- const manifestObjs = cdk8s_1.Testing.synth(this.chart);
1428
+ const manifestObjs = Testing.synth(this.chart);
902
1429
  // Enforce common labels best-practice on all rendered objects
903
1430
  const enriched = manifestObjs.map((obj) => {
904
1431
  if (obj && typeof obj === 'object') {
@@ -908,11 +1435,11 @@ class Rutter {
908
1435
  const labels = o.metadata.labels;
909
1436
  const defaults = {
910
1437
  'helm.sh/chart': '{{ .Chart.Name }}-{{ .Chart.Version }}',
911
- 'app.kubernetes.io/name': (0, helm_1.include)(Rutter.HELPER_NAME),
912
- 'app.kubernetes.io/instance': helm_1.helm.releaseName,
913
- 'app.kubernetes.io/version': helm_1.helm.chartVersion,
1438
+ 'app.kubernetes.io/name': include(Rutter.HELPER_NAME),
1439
+ 'app.kubernetes.io/instance': helm.releaseName,
1440
+ 'app.kubernetes.io/version': helm.chartVersion,
914
1441
  'app.kubernetes.io/managed-by': '{{ .Release.Service }}',
915
- 'app.kubernetes.io/part-of': helm_1.helm.chartName,
1442
+ 'app.kubernetes.io/part-of': helm.chartName,
916
1443
  };
917
1444
  for (const [k, v] of Object.entries(defaults)) {
918
1445
  if (!(k in labels)) {
@@ -926,7 +1453,7 @@ class Rutter {
926
1453
  if (this.props.singleManifestFile) {
927
1454
  // Combine all resources into a single manifest file
928
1455
  const combinedYaml = enriched
929
- .map((obj) => yaml_1.default.stringify(obj).trim())
1456
+ .map((obj) => YAML.stringify(obj).trim())
930
1457
  .filter(Boolean)
931
1458
  .join('\n---\n');
932
1459
  this.assets.length = 0;
@@ -937,7 +1464,7 @@ class Rutter {
937
1464
  // Create separate files for each resource (default behavior)
938
1465
  this.assets.length = 0;
939
1466
  enriched.forEach((obj, _index) => {
940
- const yaml = yaml_1.default.stringify(obj).trim();
1467
+ const yaml = YAML.stringify(obj).trim();
941
1468
  if (yaml) {
942
1469
  const resourceName = this.getResourceName(obj);
943
1470
  const manifestId = this.props.manifestName
@@ -994,11 +1521,15 @@ class Rutter {
994
1521
  ...(this.props.notesTpl !== undefined ? { notesTpl: this.props.notesTpl } : {}),
995
1522
  ...(this.props.valuesSchema !== undefined ? { valuesSchema: this.props.valuesSchema } : {}),
996
1523
  };
997
- HelmChartWriter_1.HelmChartWriter.write(options);
1524
+ HelmChartWriter.write(options);
998
1525
  }
999
1526
  }
1000
- exports.Rutter = Rutter;
1001
1527
  Rutter.LABEL_NAME = 'app.kubernetes.io/name';
1002
1528
  Rutter.LABEL_INSTANCE = 'app.kubernetes.io/instance';
1003
1529
  Rutter.HELPER_NAME = 'timonel.name';
1530
+ Rutter.NETWORKING_API_VERSION = 'networking.k8s.io/v1';
1531
+ Rutter.STORAGE_API_VERSION = 'storage.k8s.io/v1';
1532
+ Rutter.AZURE_DISK_CSI_DRIVER = 'disk.csi.azure.com';
1533
+ Rutter.AZURE_DISK_PROVISIONER_ANNOTATION = 'volume.beta.kubernetes.io/storage-provisioner';
1534
+ Rutter.EMPTY_POD_SELECTOR = {};
1004
1535
  //# sourceMappingURL=Rutter.js.map