timonel 0.3.0 → 1.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.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: {
@@ -336,14 +330,15 @@ class Rutter {
336
330
  }
337
331
  optimizePVForCloud(spec) {
338
332
  const optimized = { ...spec };
333
+ const EBS_CSI_DRIVER = 'ebs.csi.aws.com';
339
334
  // Apply cloud-specific optimizations
340
335
  if (spec.cloudProvider === 'aws') {
341
336
  optimized.annotations = {
342
337
  ...optimized.annotations,
343
- 'volume.beta.kubernetes.io/storage-provisioner': 'ebs.csi.aws.com',
338
+ 'volume.beta.kubernetes.io/storage-provisioner': EBS_CSI_DRIVER,
344
339
  };
345
340
  // Default to gp3 for better performance/cost
346
- if (spec.source.csi?.driver === 'ebs.csi.aws.com' && optimized.source.csi) {
341
+ if (spec.source.csi?.driver === EBS_CSI_DRIVER && optimized.source.csi) {
347
342
  optimized.source.csi.volumeAttributes = {
348
343
  type: 'gp3',
349
344
  ...spec.source.csi.volumeAttributes,
@@ -377,7 +372,7 @@ class Rutter {
377
372
  addPersistentVolumeClaim(spec) {
378
373
  // Validate access modes compatibility
379
374
  this.validatePVCAccessModes(spec);
380
- const pvc = new cdk8s_1.ApiObject(this.chart, spec.name, {
375
+ const pvc = new ApiObject(this.chart, spec.name, {
381
376
  apiVersion: 'v1',
382
377
  kind: 'PersistentVolumeClaim',
383
378
  metadata: {
@@ -412,7 +407,7 @@ class Rutter {
412
407
  }
413
408
  }
414
409
  addConfigMap(spec) {
415
- const cm = new cdk8s_1.ApiObject(this.chart, spec.name, {
410
+ const cm = new ApiObject(this.chart, spec.name, {
416
411
  apiVersion: 'v1',
417
412
  kind: 'ConfigMap',
418
413
  metadata: {
@@ -428,7 +423,7 @@ class Rutter {
428
423
  return cm;
429
424
  }
430
425
  addSecret(spec) {
431
- const sec = new cdk8s_1.ApiObject(this.chart, spec.name, {
426
+ const sec = new ApiObject(this.chart, spec.name, {
432
427
  apiVersion: 'v1',
433
428
  kind: 'Secret',
434
429
  metadata: {
@@ -445,27 +440,378 @@ class Rutter {
445
440
  return sec;
446
441
  }
447
442
  addServiceAccount(spec) {
448
- const ann = {
443
+ const annotations = this.buildServiceAccountAnnotations(spec);
444
+ const sa = new ApiObject(this.chart, spec.name, {
445
+ apiVersion: 'v1',
446
+ kind: 'ServiceAccount',
447
+ metadata: {
448
+ name: spec.name,
449
+ ...(Object.keys(annotations).length ? { annotations } : {}),
450
+ ...(spec.labels ? { labels: spec.labels } : {}),
451
+ },
452
+ automountServiceAccountToken: spec.automountServiceAccountToken,
453
+ imagePullSecrets: spec.imagePullSecrets?.map((n) => ({ name: n })),
454
+ secrets: spec.secrets?.map((n) => ({ name: n })),
455
+ });
456
+ this.capture(sa, `${spec.name}-serviceaccount`);
457
+ return sa;
458
+ }
459
+ buildServiceAccountAnnotations(spec) {
460
+ const annotations = {
449
461
  ...(spec.annotations ?? {}),
450
- ...(spec.awsRoleArn ? { 'eks.amazonaws.com/role-arn': spec.awsRoleArn } : {}),
451
- ...(spec.awsAudience ? { 'eks.amazonaws.com/audience': spec.awsAudience } : {}),
452
- ...(spec.azureClientId ? { 'azure.workload.identity/client-id': spec.azureClientId } : {}),
453
- ...(spec.azureTenantId ? { 'azure.workload.identity/tenant-id': spec.azureTenantId } : {}),
454
- ...(spec.azureServiceAccountTokenExpiration !== undefined
455
- ? {
456
- 'azure.workload.identity/service-account-token-expiration': String(spec.azureServiceAccountTokenExpiration),
457
- }
458
- : {}),
459
- ...(spec.gcpServiceAccountEmail
460
- ? { 'iam.gke.io/gcp-service-account': spec.gcpServiceAccountEmail }
462
+ };
463
+ this.addAWSIRSAAnnotations(annotations, spec);
464
+ this.addAzureWorkloadIdentityAnnotations(annotations, spec);
465
+ this.addGCPWorkloadIdentityAnnotations(annotations, spec);
466
+ return annotations;
467
+ }
468
+ addAWSIRSAAnnotations(annotations, spec) {
469
+ if (spec.awsRoleArn) {
470
+ annotations['eks.amazonaws.com/role-arn'] = spec.awsRoleArn;
471
+ }
472
+ if (spec.awsAudience) {
473
+ annotations['eks.amazonaws.com/audience'] = spec.awsAudience;
474
+ }
475
+ if (spec.awsStsEndpointType) {
476
+ annotations['eks.amazonaws.com/sts-regional-endpoints'] = spec.awsStsEndpointType;
477
+ }
478
+ if (spec.awsTokenExpiration !== undefined) {
479
+ annotations['eks.amazonaws.com/token-expiration'] = String(spec.awsTokenExpiration);
480
+ }
481
+ }
482
+ addAzureWorkloadIdentityAnnotations(annotations, spec) {
483
+ if (spec.azureClientId) {
484
+ annotations['azure.workload.identity/client-id'] = spec.azureClientId;
485
+ }
486
+ if (spec.azureTenantId) {
487
+ annotations['azure.workload.identity/tenant-id'] = spec.azureTenantId;
488
+ }
489
+ if (spec.azureServiceAccountTokenExpiration !== undefined) {
490
+ annotations['azure.workload.identity/service-account-token-expiration'] = String(spec.azureServiceAccountTokenExpiration);
491
+ }
492
+ }
493
+ addGCPWorkloadIdentityAnnotations(annotations, spec) {
494
+ if (spec.gcpServiceAccountEmail) {
495
+ annotations['iam.gke.io/gcp-service-account'] = spec.gcpServiceAccountEmail;
496
+ }
497
+ }
498
+ addHorizontalPodAutoscaler(spec) {
499
+ // Default metrics if none provided (CPU utilization at 80%)
500
+ const defaultMetrics = [
501
+ {
502
+ type: 'Resource',
503
+ resource: {
504
+ name: 'cpu',
505
+ target: {
506
+ type: 'Utilization',
507
+ averageUtilization: 80,
508
+ },
509
+ },
510
+ },
511
+ ];
512
+ const hpa = new ApiObject(this.chart, spec.name, {
513
+ apiVersion: 'autoscaling/v2',
514
+ kind: 'HorizontalPodAutoscaler',
515
+ metadata: {
516
+ name: spec.name,
517
+ ...(spec.labels ? { labels: spec.labels } : {}),
518
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
519
+ },
520
+ spec: {
521
+ scaleTargetRef: spec.scaleTargetRef,
522
+ minReplicas: spec.minReplicas ?? 1,
523
+ maxReplicas: spec.maxReplicas,
524
+ metrics: spec.metrics ?? defaultMetrics,
525
+ ...(spec.behavior ? { behavior: spec.behavior } : {}),
526
+ },
527
+ });
528
+ this.capture(hpa, `${spec.name}-hpa`);
529
+ return hpa;
530
+ }
531
+ addVerticalPodAutoscaler(spec) {
532
+ const vpa = new ApiObject(this.chart, spec.name, {
533
+ apiVersion: 'autoscaling.k8s.io/v1',
534
+ kind: 'VerticalPodAutoscaler',
535
+ metadata: {
536
+ name: spec.name,
537
+ ...(spec.labels ? { labels: spec.labels } : {}),
538
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
539
+ },
540
+ spec: {
541
+ targetRef: spec.targetRef,
542
+ updatePolicy: spec.updatePolicy ?? { updateMode: 'Auto' },
543
+ ...(spec.resourcePolicy ? { resourcePolicy: spec.resourcePolicy } : {}),
544
+ ...(spec.recommenders ? { recommenders: spec.recommenders } : {}),
545
+ },
546
+ });
547
+ this.capture(vpa, `${spec.name}-vpa`);
548
+ return vpa;
549
+ }
550
+ addPodDisruptionBudget(spec) {
551
+ const pdb = new ApiObject(this.chart, spec.name, {
552
+ apiVersion: 'policy/v1',
553
+ kind: 'PodDisruptionBudget',
554
+ metadata: {
555
+ name: spec.name,
556
+ ...(spec.labels ? { labels: spec.labels } : {}),
557
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
558
+ },
559
+ spec: {
560
+ ...(spec.minAvailable !== undefined ? { minAvailable: spec.minAvailable } : {}),
561
+ ...(spec.maxUnavailable !== undefined ? { maxUnavailable: spec.maxUnavailable } : {}),
562
+ selector: spec.selector,
563
+ },
564
+ });
565
+ this.capture(pdb, `${spec.name}-pdb`);
566
+ return pdb;
567
+ }
568
+ addAWSEBSStorageClass(spec) {
569
+ const DEFAULT_VOLUME_TYPE = 'gp3';
570
+ const parameters = {
571
+ type: spec.volumeType ?? DEFAULT_VOLUME_TYPE,
572
+ fsType: spec.fsType ?? 'ext4',
573
+ encrypted: String(spec.encrypted ?? true),
574
+ };
575
+ if (spec.kmsKeyId) {
576
+ parameters['kmsKeyId'] = spec.kmsKeyId;
577
+ }
578
+ const IOPS_SUPPORTED_TYPES = ['gp3', 'io1', 'io2'];
579
+ if (spec.iops && IOPS_SUPPORTED_TYPES.includes(spec.volumeType ?? DEFAULT_VOLUME_TYPE)) {
580
+ parameters['iops'] = String(spec.iops);
581
+ }
582
+ if (spec.throughput && spec.volumeType === DEFAULT_VOLUME_TYPE) {
583
+ parameters['throughput'] = String(spec.throughput);
584
+ }
585
+ const sc = new ApiObject(this.chart, spec.name, {
586
+ apiVersion: 'storage.k8s.io/v1',
587
+ kind: 'StorageClass',
588
+ metadata: {
589
+ name: spec.name,
590
+ ...(spec.labels ? { labels: spec.labels } : {}),
591
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
592
+ },
593
+ provisioner: 'ebs.csi.aws.com',
594
+ parameters,
595
+ reclaimPolicy: spec.reclaimPolicy ?? 'Delete',
596
+ allowVolumeExpansion: spec.allowVolumeExpansion ?? true,
597
+ volumeBindingMode: spec.volumeBindingMode ?? 'WaitForFirstConsumer',
598
+ });
599
+ this.capture(sc, `${spec.name}-storageclass`);
600
+ return sc;
601
+ }
602
+ addAWSEBSPersistentVolumeClaim(spec) {
603
+ const pvc = new ApiObject(this.chart, spec.name, {
604
+ apiVersion: 'v1',
605
+ kind: 'PersistentVolumeClaim',
606
+ metadata: {
607
+ name: spec.name,
608
+ ...(spec.labels ? { labels: spec.labels } : {}),
609
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
610
+ },
611
+ spec: {
612
+ accessModes: spec.accessModes ?? ['ReadWriteOnce'],
613
+ storageClassName: spec.storageClassName,
614
+ resources: {
615
+ requests: {
616
+ storage: spec.size,
617
+ },
618
+ },
619
+ },
620
+ });
621
+ this.capture(pvc, `${spec.name}-pvc`);
622
+ return pvc;
623
+ }
624
+ addAWSEFSStorageClass(spec) {
625
+ const sc = new ApiObject(this.chart, spec.name, {
626
+ apiVersion: 'storage.k8s.io/v1',
627
+ kind: 'StorageClass',
628
+ metadata: {
629
+ name: spec.name,
630
+ ...(spec.labels ? { labels: spec.labels } : {}),
631
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
632
+ },
633
+ provisioner: 'efs.csi.aws.com',
634
+ reclaimPolicy: spec.reclaimPolicy ?? 'Delete',
635
+ volumeBindingMode: spec.volumeBindingMode ?? 'Immediate',
636
+ });
637
+ this.capture(sc, `${spec.name}-storageclass`);
638
+ return sc;
639
+ }
640
+ addAWSEFSPersistentVolume(spec) {
641
+ const csiSpec = {
642
+ driver: 'efs.csi.aws.com',
643
+ volumeHandle: spec.fileSystemId,
644
+ };
645
+ if (spec.accessPoint) {
646
+ csiSpec['volumeHandle'] = `${spec.fileSystemId}::${spec.accessPoint}`;
647
+ }
648
+ const volumeAttributes = {};
649
+ if (spec.directoryPerms) {
650
+ volumeAttributes['directoryPerms'] = spec.directoryPerms;
651
+ }
652
+ if (spec.gidRangeStart !== undefined) {
653
+ volumeAttributes['gidRangeStart'] = String(spec.gidRangeStart);
654
+ }
655
+ if (spec.gidRangeEnd !== undefined) {
656
+ volumeAttributes['gidRangeEnd'] = String(spec.gidRangeEnd);
657
+ }
658
+ if (spec.basePath) {
659
+ volumeAttributes['basePath'] = spec.basePath;
660
+ }
661
+ if (spec.subPath) {
662
+ volumeAttributes['subPath'] = spec.subPath;
663
+ }
664
+ if (Object.keys(volumeAttributes).length > 0) {
665
+ csiSpec['volumeAttributes'] = volumeAttributes;
666
+ }
667
+ const pv = new ApiObject(this.chart, spec.name, {
668
+ apiVersion: 'v1',
669
+ kind: 'PersistentVolume',
670
+ metadata: {
671
+ name: spec.name,
672
+ ...(spec.labels ? { labels: spec.labels } : {}),
673
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
674
+ },
675
+ spec: {
676
+ capacity: {
677
+ storage: spec.capacity ?? '5Gi',
678
+ },
679
+ accessModes: spec.accessModes ?? ['ReadWriteMany'],
680
+ persistentVolumeReclaimPolicy: 'Retain',
681
+ storageClassName: spec.storageClassName,
682
+ csi: csiSpec,
683
+ },
684
+ });
685
+ this.capture(pv, `${spec.name}-pv`);
686
+ return pv;
687
+ }
688
+ addAWSEFSPersistentVolumeClaim(spec) {
689
+ const pvc = new ApiObject(this.chart, spec.name, {
690
+ apiVersion: 'v1',
691
+ kind: 'PersistentVolumeClaim',
692
+ metadata: {
693
+ name: spec.name,
694
+ ...(spec.labels ? { labels: spec.labels } : {}),
695
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
696
+ },
697
+ spec: {
698
+ accessModes: spec.accessModes ?? ['ReadWriteMany'],
699
+ storageClassName: spec.storageClassName,
700
+ resources: {
701
+ requests: {
702
+ storage: spec.size ?? '5Gi',
703
+ },
704
+ },
705
+ },
706
+ });
707
+ this.capture(pvc, `${spec.name}-pvc`);
708
+ return pvc;
709
+ }
710
+ addAWSALBIngress(spec) {
711
+ const annotations = this.buildALBAnnotations(spec);
712
+ const ingress = new ApiObject(this.chart, spec.name, {
713
+ apiVersion: Rutter.NETWORKING_API_VERSION,
714
+ kind: 'Ingress',
715
+ metadata: {
716
+ name: spec.name,
717
+ ...(spec.labels ? { labels: spec.labels } : {}),
718
+ annotations,
719
+ },
720
+ spec: {
721
+ tls: spec.tls,
722
+ rules: spec.rules.map((rule) => ({
723
+ host: rule.host,
724
+ http: {
725
+ paths: rule.paths.map((path) => ({
726
+ path: path.path,
727
+ pathType: path.pathType,
728
+ backend: path.backend,
729
+ })),
730
+ },
731
+ })),
732
+ },
733
+ });
734
+ this.capture(ingress, `${spec.name}-ingress`);
735
+ return ingress;
736
+ }
737
+ buildALBAnnotations(spec) {
738
+ const annotations = {
739
+ 'kubernetes.io/ingress.class': 'alb',
740
+ 'alb.ingress.kubernetes.io/scheme': spec.scheme ?? 'internet-facing',
741
+ 'alb.ingress.kubernetes.io/target-type': spec.targetType ?? 'ip',
742
+ ...(spec.annotations ?? {}),
743
+ };
744
+ this.addOptionalALBAnnotations(annotations, spec);
745
+ this.addHealthCheckAnnotations(annotations, spec);
746
+ this.addTagsAnnotation(annotations, spec);
747
+ return annotations;
748
+ }
749
+ addOptionalALBAnnotations(annotations, spec) {
750
+ if (spec.ipAddressType) {
751
+ annotations['alb.ingress.kubernetes.io/ip-address-type'] = spec.ipAddressType;
752
+ }
753
+ if (spec.groupName) {
754
+ annotations['alb.ingress.kubernetes.io/group.name'] = spec.groupName;
755
+ }
756
+ if (spec.groupOrder !== undefined) {
757
+ annotations['alb.ingress.kubernetes.io/group.order'] = String(spec.groupOrder);
758
+ }
759
+ if (spec.subnets && spec.subnets.length > 0) {
760
+ annotations['alb.ingress.kubernetes.io/subnets'] = spec.subnets.join(',');
761
+ }
762
+ if (spec.securityGroups && spec.securityGroups.length > 0) {
763
+ annotations['alb.ingress.kubernetes.io/security-groups'] = spec.securityGroups.join(',');
764
+ }
765
+ if (spec.certificateArn) {
766
+ annotations['alb.ingress.kubernetes.io/certificate-arn'] = spec.certificateArn;
767
+ }
768
+ if (spec.sslRedirect) {
769
+ annotations['alb.ingress.kubernetes.io/ssl-redirect'] = '443';
770
+ }
771
+ }
772
+ addHealthCheckAnnotations(annotations, spec) {
773
+ if (spec.healthCheckPath) {
774
+ annotations['alb.ingress.kubernetes.io/healthcheck-path'] = spec.healthCheckPath;
775
+ }
776
+ if (spec.healthCheckIntervalSeconds !== undefined) {
777
+ annotations['alb.ingress.kubernetes.io/healthcheck-interval-seconds'] = String(spec.healthCheckIntervalSeconds);
778
+ }
779
+ if (spec.healthCheckTimeoutSeconds !== undefined) {
780
+ annotations['alb.ingress.kubernetes.io/healthcheck-timeout-seconds'] = String(spec.healthCheckTimeoutSeconds);
781
+ }
782
+ if (spec.healthyThresholdCount !== undefined) {
783
+ annotations['alb.ingress.kubernetes.io/healthy-threshold-count'] = String(spec.healthyThresholdCount);
784
+ }
785
+ if (spec.unhealthyThresholdCount !== undefined) {
786
+ annotations['alb.ingress.kubernetes.io/unhealthy-threshold-count'] = String(spec.unhealthyThresholdCount);
787
+ }
788
+ }
789
+ addTagsAnnotation(annotations, spec) {
790
+ if (spec.tags && Object.keys(spec.tags).length > 0) {
791
+ const tagString = Object.entries(spec.tags)
792
+ .map(([key, value]) => `${key}=${value}`)
793
+ .join(',');
794
+ annotations['alb.ingress.kubernetes.io/tags'] = tagString;
795
+ }
796
+ }
797
+ addAWSIRSAServiceAccount(spec) {
798
+ const annotations = {
799
+ 'eks.amazonaws.com/role-arn': spec.roleArn,
800
+ ...(spec.audience ? { 'eks.amazonaws.com/audience': spec.audience } : {}),
801
+ ...(spec.stsEndpointType
802
+ ? { 'eks.amazonaws.com/sts-regional-endpoints': spec.stsEndpointType }
803
+ : { 'eks.amazonaws.com/sts-regional-endpoints': 'regional' }),
804
+ ...(spec.tokenExpiration !== undefined
805
+ ? { 'eks.amazonaws.com/token-expiration': String(spec.tokenExpiration) }
461
806
  : {}),
807
+ ...(spec.annotations ?? {}),
462
808
  };
463
- const sa = new cdk8s_1.ApiObject(this.chart, spec.name, {
809
+ const sa = new ApiObject(this.chart, spec.name, {
464
810
  apiVersion: 'v1',
465
811
  kind: 'ServiceAccount',
466
812
  metadata: {
467
813
  name: spec.name,
468
- ...(Object.keys(ann).length ? { annotations: ann } : {}),
814
+ annotations,
469
815
  ...(spec.labels ? { labels: spec.labels } : {}),
470
816
  },
471
817
  automountServiceAccountToken: spec.automountServiceAccountToken,
@@ -475,6 +821,227 @@ class Rutter {
475
821
  this.capture(sa, `${spec.name}-serviceaccount`);
476
822
  return sa;
477
823
  }
824
+ addAWSSecretProviderClass(spec) {
825
+ const objects = spec.objects.map((obj) => {
826
+ const baseObj = {
827
+ objectName: obj.objectName,
828
+ objectType: obj.objectType,
829
+ };
830
+ if ('objectAlias' in obj && obj.objectAlias) {
831
+ baseObj['objectAlias'] = obj.objectAlias;
832
+ }
833
+ if ('jmesPath' in obj && obj.jmesPath) {
834
+ baseObj['jmesPath'] = obj.jmesPath;
835
+ }
836
+ return baseObj;
837
+ });
838
+ const objectsYaml = YAML.stringify(objects, { indent: 2 }).trim();
839
+ const parameters = {
840
+ objects: `|\n ${objectsYaml.split('\n').join('\n ')}`,
841
+ };
842
+ if (spec.region) {
843
+ parameters['region'] = spec.region;
844
+ }
845
+ const spc = new ApiObject(this.chart, spec.name, {
846
+ apiVersion: 'secrets-store.csi.x-k8s.io/v1',
847
+ kind: 'SecretProviderClass',
848
+ metadata: {
849
+ name: spec.name,
850
+ ...(spec.labels ? { labels: spec.labels } : {}),
851
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
852
+ },
853
+ spec: {
854
+ provider: 'aws',
855
+ parameters,
856
+ },
857
+ });
858
+ this.capture(spc, `${spec.name}-secretproviderclass`);
859
+ return spc;
860
+ }
861
+ addNetworkPolicy(spec) {
862
+ // Validate policy types
863
+ this.validateNetworkPolicyTypes(spec);
864
+ // Validate CIDR blocks if present
865
+ this.validateNetworkPolicyCIDRs(spec);
866
+ const np = new ApiObject(this.chart, spec.name, {
867
+ apiVersion: Rutter.NETWORKING_API_VERSION,
868
+ kind: 'NetworkPolicy',
869
+ metadata: {
870
+ name: spec.name,
871
+ ...(spec.labels ? { labels: spec.labels } : {}),
872
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
873
+ },
874
+ spec: {
875
+ podSelector: spec.podSelector ?? {},
876
+ policyTypes: spec.policyTypes,
877
+ ...(spec.ingress ? { ingress: spec.ingress } : {}),
878
+ ...(spec.egress ? { egress: spec.egress } : {}),
879
+ },
880
+ });
881
+ this.capture(np, `${spec.name}-networkpolicy`);
882
+ return np;
883
+ }
884
+ /**
885
+ * Create a deny-all ingress NetworkPolicy for the current namespace
886
+ */
887
+ addDenyAllIngressNetworkPolicy(name = 'deny-all-ingress') {
888
+ return this.addNetworkPolicy({
889
+ name,
890
+ podSelector: Rutter.EMPTY_POD_SELECTOR,
891
+ policyTypes: ['Ingress'],
892
+ });
893
+ }
894
+ /**
895
+ * Create a deny-all egress NetworkPolicy for the current namespace
896
+ */
897
+ addDenyAllEgressNetworkPolicy(name = 'deny-all-egress') {
898
+ return this.addNetworkPolicy({
899
+ name,
900
+ podSelector: Rutter.EMPTY_POD_SELECTOR,
901
+ policyTypes: ['Egress'],
902
+ });
903
+ }
904
+ /**
905
+ * Create a deny-all NetworkPolicy for both ingress and egress
906
+ */
907
+ addDenyAllNetworkPolicy(name = 'deny-all') {
908
+ return this.addNetworkPolicy({
909
+ name,
910
+ podSelector: Rutter.EMPTY_POD_SELECTOR,
911
+ policyTypes: ['Ingress', 'Egress'],
912
+ });
913
+ }
914
+ /**
915
+ * Allow traffic from specific pods to target pods on specific ports
916
+ */
917
+ addAllowFromPodsNetworkPolicy(spec) {
918
+ return this.addNetworkPolicy({
919
+ name: spec.name,
920
+ podSelector: { matchLabels: spec.targetPodSelector },
921
+ policyTypes: ['Ingress'],
922
+ ingress: [
923
+ {
924
+ from: [{ podSelector: { matchLabels: spec.sourcePodSelector } }],
925
+ ...(spec.ports ? { ports: spec.ports } : {}),
926
+ },
927
+ ],
928
+ ...(spec.labels ? { labels: spec.labels } : {}),
929
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
930
+ });
931
+ }
932
+ /**
933
+ * Allow traffic from specific namespace to target pods
934
+ */
935
+ addAllowFromNamespaceNetworkPolicy(spec) {
936
+ return this.addNetworkPolicy({
937
+ name: spec.name,
938
+ podSelector: { matchLabels: spec.targetPodSelector },
939
+ policyTypes: ['Ingress'],
940
+ ingress: [
941
+ {
942
+ from: [{ namespaceSelector: { matchLabels: spec.sourceNamespaceSelector } }],
943
+ ...(spec.ports ? { ports: spec.ports } : {}),
944
+ },
945
+ ],
946
+ ...(spec.labels ? { labels: spec.labels } : {}),
947
+ ...(spec.annotations ? { annotations: spec.annotations } : {}),
948
+ });
949
+ }
950
+ validateNetworkPolicyTypes(spec) {
951
+ if (!spec.policyTypes || spec.policyTypes.length === 0) {
952
+ throw new Error(`NetworkPolicy ${spec.name}: policyTypes must be specified`);
953
+ }
954
+ this.validatePolicyTypeValues(spec);
955
+ this.validatePolicyTypeRules(spec);
956
+ }
957
+ validatePolicyTypeValues(spec) {
958
+ const validTypes = ['Ingress', 'Egress'];
959
+ for (const type of spec.policyTypes) {
960
+ if (!validTypes.includes(type)) {
961
+ throw new Error(`NetworkPolicy ${spec.name}: invalid policyType '${type}', must be 'Ingress' or 'Egress'`);
962
+ }
963
+ }
964
+ }
965
+ validatePolicyTypeRules(spec) {
966
+ if (spec.policyTypes.includes('Ingress') && spec.ingress === undefined) {
967
+ console.warn(`NetworkPolicy ${spec.name}: policyType 'Ingress' specified but no ingress rules defined (will deny all ingress)`);
968
+ }
969
+ if (spec.policyTypes.includes('Egress') && spec.egress === undefined) {
970
+ console.warn(`NetworkPolicy ${spec.name}: policyType 'Egress' specified but no egress rules defined (will deny all egress)`);
971
+ }
972
+ }
973
+ validateNetworkPolicyCIDRs(spec) {
974
+ const validateCIDR = (cidr, context) => {
975
+ if (!this.isValidCIDR(cidr)) {
976
+ throw new Error(`NetworkPolicy ${spec.name}: invalid CIDR '${cidr}' in ${context}`);
977
+ }
978
+ };
979
+ this.validateIngressCIDRs(spec, validateCIDR);
980
+ this.validateEgressCIDRs(spec, validateCIDR);
981
+ }
982
+ isValidCIDR(cidr) {
983
+ // IPv4 CIDR validation
984
+ const ipv4Parts = cidr.split('/');
985
+ if (ipv4Parts.length === 2) {
986
+ const [ip, prefix] = ipv4Parts;
987
+ if (ip && prefix && this.isValidIPv4(ip) && this.isValidPrefix(prefix, 32)) {
988
+ return true;
989
+ }
990
+ }
991
+ // IPv6 CIDR validation (basic)
992
+ if (cidr.includes(':') && ipv4Parts.length === 2) {
993
+ const [, prefix] = ipv4Parts;
994
+ if (prefix) {
995
+ return this.isValidPrefix(prefix, 128);
996
+ }
997
+ }
998
+ return false;
999
+ }
1000
+ isValidIPv4(ip) {
1001
+ const parts = ip.split('.');
1002
+ if (parts.length !== 4)
1003
+ return false;
1004
+ return parts.every((part) => {
1005
+ const num = parseInt(part, 10);
1006
+ return !isNaN(num) && num >= 0 && num <= 255 && String(num) === part;
1007
+ });
1008
+ }
1009
+ isValidPrefix(prefix, maxPrefix) {
1010
+ const num = parseInt(prefix, 10);
1011
+ return !isNaN(num) && num >= 0 && num <= maxPrefix && String(num) === prefix;
1012
+ }
1013
+ validateIngressCIDRs(spec, validateCIDR) {
1014
+ if (!spec.ingress)
1015
+ return;
1016
+ for (const rule of spec.ingress) {
1017
+ if (rule.from) {
1018
+ for (const peer of rule.from) {
1019
+ this.validatePeerCIDRs(peer, validateCIDR, 'ingress rule');
1020
+ }
1021
+ }
1022
+ }
1023
+ }
1024
+ validateEgressCIDRs(spec, validateCIDR) {
1025
+ if (!spec.egress)
1026
+ return;
1027
+ for (const rule of spec.egress) {
1028
+ if (rule.to) {
1029
+ for (const peer of rule.to) {
1030
+ this.validatePeerCIDRs(peer, validateCIDR, 'egress rule');
1031
+ }
1032
+ }
1033
+ }
1034
+ }
1035
+ validatePeerCIDRs(peer, validateCIDR, context) {
1036
+ if (peer.ipBlock?.cidr) {
1037
+ validateCIDR(peer.ipBlock.cidr, context);
1038
+ if (peer.ipBlock.except) {
1039
+ for (const except of peer.ipBlock.except) {
1040
+ validateCIDR(except, `${context} except block`);
1041
+ }
1042
+ }
1043
+ }
1044
+ }
478
1045
  /**
479
1046
  * Capture the YAML of an ApiObject or Construct into assets.
480
1047
  */
@@ -483,6 +1050,21 @@ class Rutter {
483
1050
  // We defer synth until write(), but we can capture logical ids now
484
1051
  this.assets.push({ id, yaml: `# captured:${id}` });
485
1052
  }
1053
+ /**
1054
+ * Extract a meaningful name from a Kubernetes resource object
1055
+ */
1056
+ getResourceName(obj) {
1057
+ if (obj && typeof obj === 'object') {
1058
+ const resource = obj;
1059
+ if (resource.kind && resource.metadata?.name) {
1060
+ return `${resource.kind.toLowerCase()}-${resource.metadata.name}`;
1061
+ }
1062
+ if (resource.kind) {
1063
+ return resource.kind.toLowerCase();
1064
+ }
1065
+ }
1066
+ return 'resource';
1067
+ }
486
1068
  /**
487
1069
  * Add a raw CRD manifest to the chart (written under crds/).
488
1070
  */
@@ -494,7 +1076,7 @@ class Rutter {
494
1076
  */
495
1077
  write(outDir) {
496
1078
  // Use cdk8s Testing.synth to obtain manifest objects
497
- const manifestObjs = cdk8s_1.Testing.synth(this.chart);
1079
+ const manifestObjs = Testing.synth(this.chart);
498
1080
  // Enforce common labels best-practice on all rendered objects
499
1081
  const enriched = manifestObjs.map((obj) => {
500
1082
  if (obj && typeof obj === 'object') {
@@ -504,11 +1086,11 @@ class Rutter {
504
1086
  const labels = o.metadata.labels;
505
1087
  const defaults = {
506
1088
  'helm.sh/chart': '{{ .Chart.Name }}-{{ .Chart.Version }}',
507
- 'app.kubernetes.io/name': (0, helm_1.include)(Rutter.HELPER_NAME),
508
- 'app.kubernetes.io/instance': helm_1.helm.releaseName,
509
- 'app.kubernetes.io/version': helm_1.helm.chartVersion,
1089
+ 'app.kubernetes.io/name': include(Rutter.HELPER_NAME),
1090
+ 'app.kubernetes.io/instance': helm.releaseName,
1091
+ 'app.kubernetes.io/version': helm.chartVersion,
510
1092
  'app.kubernetes.io/managed-by': '{{ .Release.Service }}',
511
- 'app.kubernetes.io/part-of': helm_1.helm.chartName,
1093
+ 'app.kubernetes.io/part-of': helm.chartName,
512
1094
  };
513
1095
  for (const [k, v] of Object.entries(defaults)) {
514
1096
  if (!(k in labels)) {
@@ -519,13 +1101,30 @@ class Rutter {
519
1101
  }
520
1102
  return obj;
521
1103
  });
522
- const combinedYaml = enriched
523
- .map((obj) => yaml_1.default.stringify(obj).trim())
524
- .filter(Boolean)
525
- .map((y) => `---\n${y}`)
526
- .join('\n');
527
- this.assets.length = 0;
528
- this.assets.push({ id: 'manifests', yaml: combinedYaml });
1104
+ if (this.props.singleManifestFile) {
1105
+ // Combine all resources into a single manifest file
1106
+ const combinedYaml = enriched
1107
+ .map((obj) => YAML.stringify(obj).trim())
1108
+ .filter(Boolean)
1109
+ .join('\n---\n');
1110
+ this.assets.length = 0;
1111
+ const manifestId = this.props.manifestName ?? 'manifests';
1112
+ this.assets.push({ id: manifestId, yaml: combinedYaml, singleFile: true });
1113
+ }
1114
+ else {
1115
+ // Create separate files for each resource (default behavior)
1116
+ this.assets.length = 0;
1117
+ enriched.forEach((obj, _index) => {
1118
+ const yaml = YAML.stringify(obj).trim();
1119
+ if (yaml) {
1120
+ const resourceName = this.getResourceName(obj);
1121
+ const manifestId = this.props.manifestName
1122
+ ? `${this.props.manifestName}-${resourceName}`
1123
+ : resourceName;
1124
+ this.assets.push({ id: manifestId, yaml });
1125
+ }
1126
+ });
1127
+ }
529
1128
  const options = {
530
1129
  outDir,
531
1130
  meta: this.props.meta,
@@ -573,11 +1172,12 @@ class Rutter {
573
1172
  ...(this.props.notesTpl !== undefined ? { notesTpl: this.props.notesTpl } : {}),
574
1173
  ...(this.props.valuesSchema !== undefined ? { valuesSchema: this.props.valuesSchema } : {}),
575
1174
  };
576
- HelmChartWriter_1.HelmChartWriter.write(options);
1175
+ HelmChartWriter.write(options);
577
1176
  }
578
1177
  }
579
- exports.Rutter = Rutter;
580
1178
  Rutter.LABEL_NAME = 'app.kubernetes.io/name';
581
1179
  Rutter.LABEL_INSTANCE = 'app.kubernetes.io/instance';
582
1180
  Rutter.HELPER_NAME = 'timonel.name';
1181
+ Rutter.NETWORKING_API_VERSION = 'networking.k8s.io/v1';
1182
+ Rutter.EMPTY_POD_SELECTOR = {};
583
1183
  //# sourceMappingURL=Rutter.js.map