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.
- package/CHANGELOG.md +56 -0
- package/README.md +343 -9
- package/dist/cli.js +32 -72
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -20
- package/dist/index.js.map +1 -1
- package/dist/lib/HelmChartWriter.js +11 -53
- package/dist/lib/HelmChartWriter.js.map +1 -1
- package/dist/lib/Rutter.d.ts +292 -4
- package/dist/lib/Rutter.d.ts.map +1 -1
- package/dist/lib/Rutter.js +587 -56
- package/dist/lib/Rutter.js.map +1 -1
- package/dist/lib/UmbrellaRutter.d.ts +2 -2
- package/dist/lib/UmbrellaRutter.d.ts.map +1 -1
- package/dist/lib/UmbrellaRutter.js +12 -20
- package/dist/lib/UmbrellaRutter.js.map +1 -1
- package/dist/lib/helm.js +11 -24
- package/dist/lib/helm.js.map +1 -1
- package/dist/lib/umbrella.d.ts +2 -2
- package/dist/lib/umbrella.d.ts.map +1 -1
- package/dist/lib/umbrella.js +3 -6
- package/dist/lib/umbrella.js.map +1 -1
- package/package.json +17 -8
package/dist/lib/Rutter.js
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
20
|
-
this.chart = new
|
|
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]:
|
|
72
|
-
[Rutter.LABEL_INSTANCE]:
|
|
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
|
|
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
|
|
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]:
|
|
179
|
-
[Rutter.LABEL_INSTANCE]:
|
|
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
|
|
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
|
|
251
|
-
apiVersion:
|
|
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
|
|
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
|
-
|
|
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 ===
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
592
|
-
apiVersion:
|
|
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
|
|
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
|
|
632
|
-
apiVersion:
|
|
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
|
|
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
|
|
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
|
|
719
|
-
apiVersion:
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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':
|
|
912
|
-
'app.kubernetes.io/instance':
|
|
913
|
-
'app.kubernetes.io/version':
|
|
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':
|
|
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) =>
|
|
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 =
|
|
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
|
-
|
|
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
|