timonel 2.2.0 → 2.3.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 +10 -0
- package/SECURITY.md +15 -8
- package/dist/cli.js +29 -31
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/HelmChartWriter.d.ts +185 -16
- package/dist/lib/HelmChartWriter.d.ts.map +1 -1
- package/dist/lib/HelmChartWriter.js +239 -45
- package/dist/lib/HelmChartWriter.js.map +1 -1
- package/dist/lib/Rutter.d.ts +1432 -78
- package/dist/lib/Rutter.d.ts.map +1 -1
- package/dist/lib/Rutter.js +734 -9
- package/dist/lib/Rutter.js.map +1 -1
- package/dist/lib/UmbrellaRutter.d.ts.map +1 -1
- package/dist/lib/UmbrellaRutter.js +28 -22
- package/dist/lib/UmbrellaRutter.js.map +1 -1
- package/dist/lib/helm.d.ts +200 -12
- package/dist/lib/helm.d.ts.map +1 -1
- package/dist/lib/helm.js +234 -13
- package/dist/lib/helm.js.map +1 -1
- package/dist/lib/security.d.ts +52 -0
- package/dist/lib/security.d.ts.map +1 -0
- package/dist/lib/security.js +117 -0
- package/dist/lib/security.js.map +1 -0
- package/package.json +1 -1
package/dist/lib/Rutter.js
CHANGED
|
@@ -1,11 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rutter - Main class for building Kubernetes manifests and Helm charts
|
|
3
|
+
* @since 1.0.0
|
|
4
|
+
*/
|
|
1
5
|
import { App, Chart, Testing, ApiObject } from 'cdk8s';
|
|
2
6
|
import YAML from 'yaml';
|
|
3
7
|
import { include, helm } from './helm.js';
|
|
4
8
|
import { HelmChartWriter } from './HelmChartWriter.js';
|
|
9
|
+
import { SecurityUtils } from './security.js';
|
|
5
10
|
/**
|
|
6
|
-
* Rutter
|
|
11
|
+
* Rutter - Main class for building Kubernetes manifests and Helm charts
|
|
12
|
+
*
|
|
13
|
+
* Rutter (maritime pilot) guides the generation of Kubernetes resources
|
|
14
|
+
* using cdk8s and outputs complete Helm charts with proper templating.
|
|
15
|
+
*
|
|
16
|
+
* @class Rutter
|
|
17
|
+
* @since 1.0.0
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const rutter = new Rutter({
|
|
22
|
+
* meta: { name: 'my-app', version: '1.0.0' },
|
|
23
|
+
* defaultValues: { replicas: 3 }
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* rutter.addDeployment({
|
|
27
|
+
* name: 'web',
|
|
28
|
+
* image: 'nginx:1.21',
|
|
29
|
+
* replicas: 3
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* rutter.write('./charts/my-app');
|
|
33
|
+
* ```
|
|
7
34
|
*/
|
|
8
35
|
export class Rutter {
|
|
36
|
+
/**
|
|
37
|
+
* Creates a new Rutter instance
|
|
38
|
+
*
|
|
39
|
+
* @param {RutterProps} props - Configuration properties
|
|
40
|
+
* @since 1.0.0
|
|
41
|
+
*/
|
|
9
42
|
constructor(props) {
|
|
10
43
|
this.assets = [];
|
|
11
44
|
this.valueOverrides = {};
|
|
@@ -14,7 +47,21 @@ export class Rutter {
|
|
|
14
47
|
this.chart = new Chart(this.app, props.manifestName ?? props.meta.name);
|
|
15
48
|
}
|
|
16
49
|
/**
|
|
17
|
-
*
|
|
50
|
+
* Sets dynamic value overrides for Helm values
|
|
51
|
+
*
|
|
52
|
+
* Used by CLI --set flags to override default values at runtime.
|
|
53
|
+
*
|
|
54
|
+
* @param {Record<string, string>} overrides - Key-value pairs to override
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* rutter.setValues({
|
|
59
|
+
* 'image.tag': 'v2.0.0',
|
|
60
|
+
* 'replicas': '5'
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @since 1.0.0
|
|
18
65
|
*/
|
|
19
66
|
setValues(overrides) {
|
|
20
67
|
this.valueOverrides = { ...this.valueOverrides, ...overrides };
|
|
@@ -55,11 +102,32 @@ export class Rutter {
|
|
|
55
102
|
try {
|
|
56
103
|
return JSON.parse(value);
|
|
57
104
|
}
|
|
58
|
-
catch {
|
|
105
|
+
catch (error) {
|
|
106
|
+
// Log parsing error for debugging purposes
|
|
107
|
+
console.debug(`Failed to parse value as JSON: ${value}`, error);
|
|
59
108
|
// If not valid JSON, treat as string
|
|
60
109
|
return value;
|
|
61
110
|
}
|
|
62
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Adds a Kubernetes Deployment to the chart
|
|
114
|
+
*
|
|
115
|
+
* @param {DeploymentSpec} spec - Deployment specification
|
|
116
|
+
* @returns {ApiObject} The created Deployment object
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* rutter.addDeployment({
|
|
121
|
+
* name: 'web-app',
|
|
122
|
+
* image: 'nginx:1.21',
|
|
123
|
+
* replicas: 3,
|
|
124
|
+
* containerPort: 80,
|
|
125
|
+
* env: { NODE_ENV: 'production' }
|
|
126
|
+
* });
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* @since 1.0.0
|
|
130
|
+
*/
|
|
63
131
|
addDeployment(spec) {
|
|
64
132
|
const match = spec.matchLabels ?? {
|
|
65
133
|
[Rutter.LABEL_NAME]: include(Rutter.HELPER_NAME),
|
|
@@ -133,6 +201,23 @@ export class Rutter {
|
|
|
133
201
|
this.capture(dep, `${spec.name}-deployment`);
|
|
134
202
|
return dep;
|
|
135
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Adds a Kubernetes Service to the chart
|
|
206
|
+
*
|
|
207
|
+
* @param {ServiceSpec} spec - Service specification
|
|
208
|
+
* @returns {ApiObject} The created Service object
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```typescript
|
|
212
|
+
* rutter.addService({
|
|
213
|
+
* name: 'web-service',
|
|
214
|
+
* ports: [{ port: 80, targetPort: 8080 }],
|
|
215
|
+
* type: 'LoadBalancer'
|
|
216
|
+
* });
|
|
217
|
+
* ```
|
|
218
|
+
*
|
|
219
|
+
* @since 1.0.0
|
|
220
|
+
*/
|
|
136
221
|
addService(spec) {
|
|
137
222
|
const svc = new ApiObject(this.chart, spec.name, {
|
|
138
223
|
apiVersion: 'v1',
|
|
@@ -240,6 +325,24 @@ export class Rutter {
|
|
|
240
325
|
this.capture(rs, `${spec.name}-replicaset`);
|
|
241
326
|
return rs;
|
|
242
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Adds a Kubernetes Job to the chart
|
|
330
|
+
*
|
|
331
|
+
* @param {JobSpec} spec - Job specification
|
|
332
|
+
* @returns {ApiObject} The created Job object
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```typescript
|
|
336
|
+
* rutter.addJob({
|
|
337
|
+
* name: 'data-migration',
|
|
338
|
+
* image: 'migrate:latest',
|
|
339
|
+
* command: ['./migrate.sh'],
|
|
340
|
+
* backoffLimit: 3
|
|
341
|
+
* });
|
|
342
|
+
* ```
|
|
343
|
+
*
|
|
344
|
+
* @since 2.1.0
|
|
345
|
+
*/
|
|
243
346
|
addJob(spec) {
|
|
244
347
|
// Validate Job specification
|
|
245
348
|
this.validateJobSpec(spec);
|
|
@@ -262,6 +365,24 @@ export class Rutter {
|
|
|
262
365
|
this.capture(job, `${spec.name}-job`);
|
|
263
366
|
return job;
|
|
264
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* Adds a Kubernetes CronJob to the chart
|
|
370
|
+
*
|
|
371
|
+
* @param {CronJobSpec} spec - CronJob specification
|
|
372
|
+
* @returns {ApiObject} The created CronJob object
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* ```typescript
|
|
376
|
+
* rutter.addCronJob({
|
|
377
|
+
* name: 'backup-job',
|
|
378
|
+
* schedule: '0 2 * * *',
|
|
379
|
+
* image: 'backup:latest',
|
|
380
|
+
* command: ['./backup.sh']
|
|
381
|
+
* });
|
|
382
|
+
* ```
|
|
383
|
+
*
|
|
384
|
+
* @since 2.1.0
|
|
385
|
+
*/
|
|
265
386
|
addCronJob(spec) {
|
|
266
387
|
// Validate CronJob specification
|
|
267
388
|
this.validateCronJobSpec(spec);
|
|
@@ -284,6 +405,29 @@ export class Rutter {
|
|
|
284
405
|
this.capture(cronJob, `${spec.name}-cronjob`);
|
|
285
406
|
return cronJob;
|
|
286
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Adds a Kubernetes Ingress to the chart
|
|
410
|
+
*
|
|
411
|
+
* @param {IngressSpec} spec - Ingress specification
|
|
412
|
+
* @returns {ApiObject} The created Ingress object
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* ```typescript
|
|
416
|
+
* rutter.addIngress({
|
|
417
|
+
* name: 'web-ingress',
|
|
418
|
+
* rules: [{
|
|
419
|
+
* host: 'example.com',
|
|
420
|
+
* paths: [{
|
|
421
|
+
* path: '/',
|
|
422
|
+
* pathType: 'Prefix',
|
|
423
|
+
* backend: { service: { name: 'web-service', port: { number: 80 } } }
|
|
424
|
+
* }]
|
|
425
|
+
* }]
|
|
426
|
+
* });
|
|
427
|
+
* ```
|
|
428
|
+
*
|
|
429
|
+
* @since 1.0.0
|
|
430
|
+
*/
|
|
287
431
|
addIngress(spec) {
|
|
288
432
|
const ing = new ApiObject(this.chart, spec.name, {
|
|
289
433
|
apiVersion: Rutter.NETWORKING_API_VERSION,
|
|
@@ -444,12 +588,31 @@ export class Rutter {
|
|
|
444
588
|
}
|
|
445
589
|
// Warn about common misconfigurations
|
|
446
590
|
if (storageClassName?.includes('azure-disk') && accessModes.includes('ReadWriteMany')) {
|
|
447
|
-
console.warn(`PVC ${spec.name}: Azure Disk does not support ReadWriteMany, consider Azure Files`);
|
|
591
|
+
console.warn(SecurityUtils.sanitizeLogMessage(`PVC ${spec.name}: Azure Disk does not support ReadWriteMany, consider Azure Files`));
|
|
448
592
|
}
|
|
449
593
|
if (storageClassName?.includes('ebs') && accessModes.includes('ReadWriteMany')) {
|
|
450
|
-
console.warn(`PVC ${spec.name}: EBS does not support ReadWriteMany, consider EFS`);
|
|
594
|
+
console.warn(SecurityUtils.sanitizeLogMessage(`PVC ${spec.name}: EBS does not support ReadWriteMany, consider EFS`));
|
|
451
595
|
}
|
|
452
596
|
}
|
|
597
|
+
/**
|
|
598
|
+
* Adds a Kubernetes ConfigMap to the chart
|
|
599
|
+
*
|
|
600
|
+
* @param {ConfigMapSpec} spec - ConfigMap specification
|
|
601
|
+
* @returns {ApiObject} The created ConfigMap object
|
|
602
|
+
*
|
|
603
|
+
* @example
|
|
604
|
+
* ```typescript
|
|
605
|
+
* rutter.addConfigMap({
|
|
606
|
+
* name: 'app-config',
|
|
607
|
+
* data: {
|
|
608
|
+
* 'config.yaml': 'key: value',
|
|
609
|
+
* 'app.properties': 'debug=true'
|
|
610
|
+
* }
|
|
611
|
+
* });
|
|
612
|
+
* ```
|
|
613
|
+
*
|
|
614
|
+
* @since 1.0.0
|
|
615
|
+
*/
|
|
453
616
|
addConfigMap(spec) {
|
|
454
617
|
const cm = new ApiObject(this.chart, spec.name, {
|
|
455
618
|
apiVersion: 'v1',
|
|
@@ -466,6 +629,25 @@ export class Rutter {
|
|
|
466
629
|
this.capture(cm, `${spec.name}-configmap`);
|
|
467
630
|
return cm;
|
|
468
631
|
}
|
|
632
|
+
/**
|
|
633
|
+
* Adds a Kubernetes Secret to the chart
|
|
634
|
+
*
|
|
635
|
+
* @param {SecretSpec} spec - Secret specification
|
|
636
|
+
* @returns {ApiObject} The created Secret object
|
|
637
|
+
*
|
|
638
|
+
* @example
|
|
639
|
+
* ```typescript
|
|
640
|
+
* rutter.addSecret({
|
|
641
|
+
* name: 'app-secrets',
|
|
642
|
+
* stringData: {
|
|
643
|
+
* 'username': 'admin',
|
|
644
|
+
* 'password': 'secret123'
|
|
645
|
+
* }
|
|
646
|
+
* });
|
|
647
|
+
* ```
|
|
648
|
+
*
|
|
649
|
+
* @since 1.0.0
|
|
650
|
+
*/
|
|
469
651
|
addSecret(spec) {
|
|
470
652
|
const sec = new ApiObject(this.chart, spec.name, {
|
|
471
653
|
apiVersion: 'v1',
|
|
@@ -483,6 +665,23 @@ export class Rutter {
|
|
|
483
665
|
this.capture(sec, `${spec.name}-secret`);
|
|
484
666
|
return sec;
|
|
485
667
|
}
|
|
668
|
+
/**
|
|
669
|
+
* Adds a Kubernetes ServiceAccount to the chart
|
|
670
|
+
*
|
|
671
|
+
* @param {ServiceAccountSpec} spec - ServiceAccount specification
|
|
672
|
+
* @returns {ApiObject} The created ServiceAccount object
|
|
673
|
+
*
|
|
674
|
+
* @example
|
|
675
|
+
* ```typescript
|
|
676
|
+
* rutter.addServiceAccount({
|
|
677
|
+
* name: 'app-sa',
|
|
678
|
+
* awsRoleArn: 'arn:aws:iam::123456789012:role/MyRole',
|
|
679
|
+
* automountServiceAccountToken: true
|
|
680
|
+
* });
|
|
681
|
+
* ```
|
|
682
|
+
*
|
|
683
|
+
* @since 1.0.0
|
|
684
|
+
*/
|
|
486
685
|
addServiceAccount(spec) {
|
|
487
686
|
const annotations = this.buildServiceAccountAnnotations(spec);
|
|
488
687
|
const sa = new ApiObject(this.chart, spec.name, {
|
|
@@ -539,6 +738,28 @@ export class Rutter {
|
|
|
539
738
|
annotations['iam.gke.io/gcp-service-account'] = spec.gcpServiceAccountEmail;
|
|
540
739
|
}
|
|
541
740
|
}
|
|
741
|
+
/**
|
|
742
|
+
* Adds a Kubernetes HorizontalPodAutoscaler to the chart
|
|
743
|
+
*
|
|
744
|
+
* @param {HorizontalPodAutoscalerSpec} spec - HPA specification
|
|
745
|
+
* @returns {ApiObject} The created HPA object
|
|
746
|
+
*
|
|
747
|
+
* @example
|
|
748
|
+
* ```typescript
|
|
749
|
+
* rutter.addHorizontalPodAutoscaler({
|
|
750
|
+
* name: 'web-hpa',
|
|
751
|
+
* scaleTargetRef: {
|
|
752
|
+
* apiVersion: 'apps/v1',
|
|
753
|
+
* kind: 'Deployment',
|
|
754
|
+
* name: 'web-app'
|
|
755
|
+
* },
|
|
756
|
+
* minReplicas: 2,
|
|
757
|
+
* maxReplicas: 10
|
|
758
|
+
* });
|
|
759
|
+
* ```
|
|
760
|
+
*
|
|
761
|
+
* @since 1.0.0
|
|
762
|
+
*/
|
|
542
763
|
addHorizontalPodAutoscaler(spec) {
|
|
543
764
|
// Default metrics if none provided (CPU utilization at 80%)
|
|
544
765
|
const defaultMetrics = [
|
|
@@ -705,8 +926,12 @@ export class Rutter {
|
|
|
705
926
|
if (spec.subPath) {
|
|
706
927
|
volumeAttributes['subPath'] = spec.subPath;
|
|
707
928
|
}
|
|
708
|
-
if (Object.keys(
|
|
709
|
-
|
|
929
|
+
// Only add volumeAttributes if it has properties (more efficient than Object.keys().length)
|
|
930
|
+
for (const key in volumeAttributes) {
|
|
931
|
+
if (Object.prototype.hasOwnProperty.call(volumeAttributes, key)) {
|
|
932
|
+
csiSpec['volumeAttributes'] = volumeAttributes;
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
710
935
|
}
|
|
711
936
|
const pv = new ApiObject(this.chart, spec.name, {
|
|
712
937
|
apiVersion: 'v1',
|
|
@@ -2017,13 +2242,513 @@ export class Rutter {
|
|
|
2017
2242
|
return 'resource';
|
|
2018
2243
|
}
|
|
2019
2244
|
/**
|
|
2020
|
-
*
|
|
2245
|
+
* Adds a raw CRD manifest to the chart
|
|
2246
|
+
*
|
|
2247
|
+
* @param {string} yaml - YAML content of the CRD
|
|
2248
|
+
* @param {string} [id='crd'] - Asset identifier
|
|
2249
|
+
*
|
|
2250
|
+
* @example
|
|
2251
|
+
* ```typescript
|
|
2252
|
+
* const crdYaml = `
|
|
2253
|
+
* apiVersion: apiextensions.k8s.io/v1
|
|
2254
|
+
* kind: CustomResourceDefinition
|
|
2255
|
+
* metadata:
|
|
2256
|
+
* name: myresources.example.com
|
|
2257
|
+
* `;
|
|
2258
|
+
* rutter.addCrd(crdYaml, 'myresource-crd');
|
|
2259
|
+
* ```
|
|
2260
|
+
*
|
|
2261
|
+
* @since 1.0.0
|
|
2021
2262
|
*/
|
|
2022
2263
|
addCrd(yaml, id = 'crd') {
|
|
2023
2264
|
this.assets.push({ id, yaml, target: 'crds' });
|
|
2024
2265
|
}
|
|
2025
2266
|
/**
|
|
2026
|
-
*
|
|
2267
|
+
* Add Karpenter NodePool for intelligent node provisioning.
|
|
2268
|
+
* Creates a NodePool resource that defines compute requirements and lifecycle policies.
|
|
2269
|
+
*
|
|
2270
|
+
* @param spec - Karpenter NodePool specification
|
|
2271
|
+
* @returns The created NodePool ApiObject
|
|
2272
|
+
*
|
|
2273
|
+
* @example
|
|
2274
|
+
* ```typescript
|
|
2275
|
+
* rutter.addKarpenterNodePool({
|
|
2276
|
+
* name: 'general-purpose',
|
|
2277
|
+
* requirements: [
|
|
2278
|
+
* { key: 'eks.amazonaws.com/instance-category', operator: 'In', values: ['c', 'm', 'r'] },
|
|
2279
|
+
* { key: 'kubernetes.io/arch', operator: 'In', values: ['amd64'] }
|
|
2280
|
+
* ],
|
|
2281
|
+
* limits: { cpu: '1000', memory: '1000Gi' },
|
|
2282
|
+
* nodeClassRef: { name: 'default' }
|
|
2283
|
+
* });
|
|
2284
|
+
* ```
|
|
2285
|
+
*
|
|
2286
|
+
* @since 2.3.0
|
|
2287
|
+
*/
|
|
2288
|
+
addKarpenterNodePool(spec) {
|
|
2289
|
+
// Validate NodePool specification
|
|
2290
|
+
this.validateKarpenterNodePoolSpec(spec);
|
|
2291
|
+
const nodePool = new ApiObject(this.chart, spec.name, {
|
|
2292
|
+
apiVersion: 'karpenter.sh/v1',
|
|
2293
|
+
kind: 'NodePool',
|
|
2294
|
+
metadata: {
|
|
2295
|
+
name: spec.name,
|
|
2296
|
+
...(spec.labels ? { labels: spec.labels } : {}),
|
|
2297
|
+
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
2298
|
+
},
|
|
2299
|
+
spec: {
|
|
2300
|
+
template: {
|
|
2301
|
+
metadata: {
|
|
2302
|
+
...(spec.labels ? { labels: spec.labels } : {}),
|
|
2303
|
+
},
|
|
2304
|
+
spec: {
|
|
2305
|
+
nodeClassRef: {
|
|
2306
|
+
group: spec.nodeClassRef.group ?? 'eks.amazonaws.com',
|
|
2307
|
+
kind: spec.nodeClassRef.kind ?? 'NodeClass',
|
|
2308
|
+
name: spec.nodeClassRef.name,
|
|
2309
|
+
},
|
|
2310
|
+
...(spec.requirements ? { requirements: spec.requirements } : {}),
|
|
2311
|
+
...(spec.taints ? { taints: spec.taints } : {}),
|
|
2312
|
+
},
|
|
2313
|
+
},
|
|
2314
|
+
...(spec.limits ? { limits: spec.limits } : {}),
|
|
2315
|
+
...(spec.disruption ? { disruption: spec.disruption } : {}),
|
|
2316
|
+
},
|
|
2317
|
+
});
|
|
2318
|
+
this.capture(nodePool, `${spec.name}-nodepool`);
|
|
2319
|
+
return nodePool;
|
|
2320
|
+
}
|
|
2321
|
+
/**
|
|
2322
|
+
* Add Karpenter EC2 NodeClass for AWS-specific node configuration.
|
|
2323
|
+
* Creates a NodeClass resource that defines EC2 instance settings and networking.
|
|
2324
|
+
*
|
|
2325
|
+
* @param spec - Karpenter EC2 NodeClass specification
|
|
2326
|
+
* @returns The created NodeClass ApiObject
|
|
2327
|
+
*
|
|
2328
|
+
* @example
|
|
2329
|
+
* ```typescript
|
|
2330
|
+
* rutter.addKarpenterEC2NodeClass({
|
|
2331
|
+
* name: 'default',
|
|
2332
|
+
* amiFamily: 'AL2023',
|
|
2333
|
+
* subnetSelectorTerms: [{ tags: { 'karpenter.sh/discovery': 'my-cluster' } }],
|
|
2334
|
+
* securityGroupSelectorTerms: [{ tags: { 'karpenter.sh/discovery': 'my-cluster' } }],
|
|
2335
|
+
* role: 'KarpenterNodeInstanceProfile'
|
|
2336
|
+
* });
|
|
2337
|
+
* ```
|
|
2338
|
+
*
|
|
2339
|
+
* @since 2.3.0
|
|
2340
|
+
*/
|
|
2341
|
+
addKarpenterEC2NodeClass(spec) {
|
|
2342
|
+
// Validate EC2 NodeClass specification
|
|
2343
|
+
this.validateKarpenterEC2NodeClassSpec(spec);
|
|
2344
|
+
const nodeClass = new ApiObject(this.chart, spec.name, {
|
|
2345
|
+
apiVersion: 'eks.amazonaws.com/v1',
|
|
2346
|
+
kind: 'NodeClass',
|
|
2347
|
+
metadata: {
|
|
2348
|
+
name: spec.name,
|
|
2349
|
+
...(spec.labels ? { labels: spec.labels } : {}),
|
|
2350
|
+
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
2351
|
+
},
|
|
2352
|
+
spec: {
|
|
2353
|
+
...(spec.amiFamily ? { amiFamily: spec.amiFamily } : {}),
|
|
2354
|
+
...(spec.instanceStorePolicy ? { instanceStorePolicy: spec.instanceStorePolicy } : {}),
|
|
2355
|
+
...(spec.userData ? { userData: spec.userData } : {}),
|
|
2356
|
+
...(spec.subnetSelectorTerms ? { subnetSelectorTerms: spec.subnetSelectorTerms } : {}),
|
|
2357
|
+
...(spec.securityGroupSelectorTerms
|
|
2358
|
+
? { securityGroupSelectorTerms: spec.securityGroupSelectorTerms }
|
|
2359
|
+
: {}),
|
|
2360
|
+
...(spec.role ? { role: spec.role } : {}),
|
|
2361
|
+
...(spec.instanceProfile ? { instanceProfile: spec.instanceProfile } : {}),
|
|
2362
|
+
...(spec.metadataOptions ? { metadataOptions: spec.metadataOptions } : {}),
|
|
2363
|
+
...(spec.blockDeviceMappings ? { blockDeviceMappings: spec.blockDeviceMappings } : {}),
|
|
2364
|
+
...(spec.tags ? { tags: spec.tags } : {}),
|
|
2365
|
+
},
|
|
2366
|
+
});
|
|
2367
|
+
this.capture(nodeClass, `${spec.name}-nodeclass`);
|
|
2368
|
+
return nodeClass;
|
|
2369
|
+
}
|
|
2370
|
+
/**
|
|
2371
|
+
* Validate Karpenter NodePool specification
|
|
2372
|
+
*/
|
|
2373
|
+
validateKarpenterNodePoolSpec(spec) {
|
|
2374
|
+
this.validateNodeClassRef(spec);
|
|
2375
|
+
this.validateNodePoolRequirements(spec);
|
|
2376
|
+
this.validateNodePoolLimits(spec);
|
|
2377
|
+
this.validateNodePoolDisruption(spec);
|
|
2378
|
+
}
|
|
2379
|
+
validateNodeClassRef(spec) {
|
|
2380
|
+
if (!spec.nodeClassRef.name) {
|
|
2381
|
+
throw new Error(`Karpenter NodePool ${spec.name}: nodeClassRef.name is required`);
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
validateNodePoolRequirements(spec) {
|
|
2385
|
+
if (!spec.requirements)
|
|
2386
|
+
return;
|
|
2387
|
+
const operatorsRequiringValues = ['In', 'NotIn', 'Gt', 'Lt'];
|
|
2388
|
+
for (const req of spec.requirements) {
|
|
2389
|
+
if (operatorsRequiringValues.includes(req.operator) && !req.values?.length) {
|
|
2390
|
+
throw new Error(`Karpenter NodePool ${spec.name}: requirement with operator '${req.operator}' must have values`);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
validateNodePoolLimits(spec) {
|
|
2395
|
+
if (!spec.limits)
|
|
2396
|
+
return;
|
|
2397
|
+
if (spec.limits.cpu && !this.isValidResourceQuantity(spec.limits.cpu)) {
|
|
2398
|
+
throw new Error(`Karpenter NodePool ${spec.name}: invalid CPU limit format`);
|
|
2399
|
+
}
|
|
2400
|
+
if (spec.limits.memory && !this.isValidResourceQuantity(spec.limits.memory)) {
|
|
2401
|
+
throw new Error(`Karpenter NodePool ${spec.name}: invalid memory limit format`);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
validateNodePoolDisruption(spec) {
|
|
2405
|
+
if (!spec.disruption)
|
|
2406
|
+
return;
|
|
2407
|
+
if (spec.disruption.consolidateAfter &&
|
|
2408
|
+
!this.isValidDuration(spec.disruption.consolidateAfter)) {
|
|
2409
|
+
throw new Error(`Karpenter NodePool ${spec.name}: invalid consolidateAfter duration format`);
|
|
2410
|
+
}
|
|
2411
|
+
if (spec.disruption.expireAfter && !this.isValidDuration(spec.disruption.expireAfter)) {
|
|
2412
|
+
throw new Error(`Karpenter NodePool ${spec.name}: invalid expireAfter duration format`);
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
/**
|
|
2416
|
+
* Validate Karpenter EC2 NodeClass specification
|
|
2417
|
+
*/
|
|
2418
|
+
validateKarpenterEC2NodeClassSpec(spec) {
|
|
2419
|
+
this.validateNodeClassSelectors(spec);
|
|
2420
|
+
this.validateNodeClassBlockDevices(spec);
|
|
2421
|
+
this.validateNodeClassMetadataOptions(spec);
|
|
2422
|
+
}
|
|
2423
|
+
validateNodeClassSelectors(spec) {
|
|
2424
|
+
if (!spec.subnetSelectorTerms?.length && !spec.securityGroupSelectorTerms?.length) {
|
|
2425
|
+
console.warn(`Karpenter EC2 NodeClass ${spec.name}: Consider specifying subnet and security group selectors for better control`);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
validateNodeClassBlockDevices(spec) {
|
|
2429
|
+
if (!spec.blockDeviceMappings)
|
|
2430
|
+
return;
|
|
2431
|
+
for (const mapping of spec.blockDeviceMappings) {
|
|
2432
|
+
if (mapping.ebs?.volumeSize && !this.isValidStorageSize(mapping.ebs.volumeSize)) {
|
|
2433
|
+
throw new Error(`Karpenter EC2 NodeClass ${spec.name}: invalid volume size format in block device mapping`);
|
|
2434
|
+
}
|
|
2435
|
+
if (mapping.ebs?.iops !== undefined && mapping.ebs.iops < 100) {
|
|
2436
|
+
throw new Error(`Karpenter EC2 NodeClass ${spec.name}: IOPS must be at least 100 for block device mapping`);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
validateNodeClassMetadataOptions(spec) {
|
|
2441
|
+
if (spec.metadataOptions?.httpPutResponseHopLimit === undefined)
|
|
2442
|
+
return;
|
|
2443
|
+
const hopLimit = spec.metadataOptions.httpPutResponseHopLimit;
|
|
2444
|
+
if (hopLimit < 1 || hopLimit > 64) {
|
|
2445
|
+
throw new Error(`Karpenter EC2 NodeClass ${spec.name}: httpPutResponseHopLimit must be between 1 and 64`);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
/**
|
|
2449
|
+
* Validate Kubernetes resource quantity format (e.g., "100m", "1Gi")
|
|
2450
|
+
*/
|
|
2451
|
+
isValidResourceQuantity(quantity) {
|
|
2452
|
+
// Safe validation without regex to prevent ReDoS
|
|
2453
|
+
const validUnits = ['m', 'k', 'M', 'G', 'T', 'P', 'E', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei'];
|
|
2454
|
+
// Find the unit suffix
|
|
2455
|
+
let numberPart = quantity;
|
|
2456
|
+
let hasValidUnit = false;
|
|
2457
|
+
for (const unit of validUnits) {
|
|
2458
|
+
if (quantity.endsWith(unit)) {
|
|
2459
|
+
numberPart = quantity.slice(0, -unit.length);
|
|
2460
|
+
hasValidUnit = true;
|
|
2461
|
+
break;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
// If no unit found, that's also valid (plain number)
|
|
2465
|
+
if (!hasValidUnit && quantity.length > 0) {
|
|
2466
|
+
numberPart = quantity;
|
|
2467
|
+
}
|
|
2468
|
+
// Validate the number part
|
|
2469
|
+
const num = parseFloat(numberPart);
|
|
2470
|
+
return !isNaN(num) && num >= 0 && numberPart === String(num);
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* Validate duration format (e.g., "30s", "5m", "1h")
|
|
2474
|
+
*/
|
|
2475
|
+
isValidDuration(duration) {
|
|
2476
|
+
// Safe validation without regex to prevent ReDoS
|
|
2477
|
+
const validUnits = ['ns', 'us', 'µs', 'ms', 's', 'm', 'h'];
|
|
2478
|
+
// Find the unit suffix
|
|
2479
|
+
let numberPart = duration;
|
|
2480
|
+
let hasValidUnit = false;
|
|
2481
|
+
for (const unit of validUnits) {
|
|
2482
|
+
if (duration.endsWith(unit)) {
|
|
2483
|
+
numberPart = duration.slice(0, -unit.length);
|
|
2484
|
+
hasValidUnit = true;
|
|
2485
|
+
break;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
// Duration must have a unit
|
|
2489
|
+
if (!hasValidUnit)
|
|
2490
|
+
return false;
|
|
2491
|
+
// Validate the number part
|
|
2492
|
+
const num = parseFloat(numberPart);
|
|
2493
|
+
return !isNaN(num) && num >= 0 && numberPart === String(num);
|
|
2494
|
+
}
|
|
2495
|
+
/**
|
|
2496
|
+
* Validate storage size format (e.g., "20", "100Gi")
|
|
2497
|
+
*/
|
|
2498
|
+
isValidStorageSize(size) {
|
|
2499
|
+
// Safe validation without regex to prevent ReDoS
|
|
2500
|
+
const validUnits = ['Gi', 'G', 'Ti', 'T'];
|
|
2501
|
+
// Find the unit suffix
|
|
2502
|
+
let numberPart = size;
|
|
2503
|
+
let hasValidUnit = false;
|
|
2504
|
+
for (const unit of validUnits) {
|
|
2505
|
+
if (size.endsWith(unit)) {
|
|
2506
|
+
numberPart = size.slice(0, -unit.length);
|
|
2507
|
+
hasValidUnit = true;
|
|
2508
|
+
break;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
// If no unit found, that's also valid (plain number)
|
|
2512
|
+
if (!hasValidUnit && size.length > 0) {
|
|
2513
|
+
numberPart = size;
|
|
2514
|
+
}
|
|
2515
|
+
// Validate the number part
|
|
2516
|
+
const num = parseFloat(numberPart);
|
|
2517
|
+
return !isNaN(num) && num >= 0 && numberPart === String(num);
|
|
2518
|
+
}
|
|
2519
|
+
/**
|
|
2520
|
+
* Add Karpenter NodeClaim for individual node provisioning.
|
|
2521
|
+
* Creates a NodeClaim resource that represents a request for a single node.
|
|
2522
|
+
*
|
|
2523
|
+
* @param spec - Karpenter NodeClaim specification
|
|
2524
|
+
* @returns The created NodeClaim ApiObject
|
|
2525
|
+
*
|
|
2526
|
+
* @example
|
|
2527
|
+
* ```typescript
|
|
2528
|
+
* rutter.addKarpenterNodeClaim({
|
|
2529
|
+
* name: 'high-memory-node',
|
|
2530
|
+
* requirements: [
|
|
2531
|
+
* { key: 'eks.amazonaws.com/instance-category', operator: 'In', values: ['r'] },
|
|
2532
|
+
* { key: 'eks.amazonaws.com/instance-cpu', operator: 'In', values: ['16', '32'] }
|
|
2533
|
+
* ],
|
|
2534
|
+
* nodeClassRef: { name: 'memory-optimized' },
|
|
2535
|
+
* expireAfter: '24h'
|
|
2536
|
+
* });
|
|
2537
|
+
* ```
|
|
2538
|
+
*
|
|
2539
|
+
* @since 2.3.0
|
|
2540
|
+
*/
|
|
2541
|
+
addKarpenterNodeClaim(spec) {
|
|
2542
|
+
// Validate NodeClaim specification
|
|
2543
|
+
this.validateKarpenterNodeClaimSpec(spec);
|
|
2544
|
+
const nodeClaim = new ApiObject(this.chart, spec.name, {
|
|
2545
|
+
apiVersion: 'karpenter.sh/v1',
|
|
2546
|
+
kind: 'NodeClaim',
|
|
2547
|
+
metadata: {
|
|
2548
|
+
name: spec.name,
|
|
2549
|
+
...(spec.labels ? { labels: spec.labels } : {}),
|
|
2550
|
+
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
2551
|
+
},
|
|
2552
|
+
spec: {
|
|
2553
|
+
nodeClassRef: {
|
|
2554
|
+
group: spec.nodeClassRef.group ?? 'eks.amazonaws.com',
|
|
2555
|
+
kind: spec.nodeClassRef.kind ?? 'NodeClass',
|
|
2556
|
+
name: spec.nodeClassRef.name,
|
|
2557
|
+
},
|
|
2558
|
+
...(spec.requirements ? { requirements: spec.requirements } : {}),
|
|
2559
|
+
...(spec.taints ? { taints: spec.taints } : {}),
|
|
2560
|
+
...(spec.startupTaints ? { startupTaints: spec.startupTaints } : {}),
|
|
2561
|
+
...(spec.expireAfter ? { expireAfter: spec.expireAfter } : {}),
|
|
2562
|
+
...(spec.terminationGracePeriod
|
|
2563
|
+
? { terminationGracePeriod: spec.terminationGracePeriod }
|
|
2564
|
+
: {}),
|
|
2565
|
+
},
|
|
2566
|
+
});
|
|
2567
|
+
this.capture(nodeClaim, `${spec.name}-nodeclaim`);
|
|
2568
|
+
return nodeClaim;
|
|
2569
|
+
}
|
|
2570
|
+
/**
|
|
2571
|
+
* Add advanced Karpenter scheduling configuration.
|
|
2572
|
+
* Creates scheduling constraints for fine-grained pod placement control.
|
|
2573
|
+
*
|
|
2574
|
+
* @param spec - Karpenter scheduling specification
|
|
2575
|
+
* @returns Configuration object for use in pod specs
|
|
2576
|
+
*
|
|
2577
|
+
* @example
|
|
2578
|
+
* ```typescript
|
|
2579
|
+
* const schedulingConfig = rutter.addKarpenterScheduling({
|
|
2580
|
+
* name: 'zone-spread-scheduling',
|
|
2581
|
+
* topologySpreadConstraints: [{
|
|
2582
|
+
* maxSkew: 1,
|
|
2583
|
+
* topologyKey: 'topology.kubernetes.io/zone',
|
|
2584
|
+
* whenUnsatisfiable: 'DoNotSchedule',
|
|
2585
|
+
* labelSelector: { matchLabels: { app: 'web' } }
|
|
2586
|
+
* }],
|
|
2587
|
+
* nodeAffinity: {
|
|
2588
|
+
* requiredDuringSchedulingIgnoredDuringExecution: {
|
|
2589
|
+
* nodeSelectorTerms: [{
|
|
2590
|
+
* matchExpressions: [{
|
|
2591
|
+
* key: 'eks.amazonaws.com/instance-category',
|
|
2592
|
+
* operator: 'In',
|
|
2593
|
+
* values: ['c', 'm']
|
|
2594
|
+
* }]
|
|
2595
|
+
* }]
|
|
2596
|
+
* }
|
|
2597
|
+
* }
|
|
2598
|
+
* });
|
|
2599
|
+
* ```
|
|
2600
|
+
*
|
|
2601
|
+
* @since 2.3.0
|
|
2602
|
+
*/
|
|
2603
|
+
addKarpenterScheduling(spec) {
|
|
2604
|
+
// Validate scheduling specification
|
|
2605
|
+
this.validateKarpenterSchedulingSpec(spec);
|
|
2606
|
+
// Return scheduling configuration for use in pod specs
|
|
2607
|
+
return {
|
|
2608
|
+
...(spec.nodeSelector ? { nodeSelector: spec.nodeSelector } : {}),
|
|
2609
|
+
...(spec.nodeAffinity ? { affinity: { nodeAffinity: spec.nodeAffinity } } : {}),
|
|
2610
|
+
...(spec.topologySpreadConstraints
|
|
2611
|
+
? { topologySpreadConstraints: spec.topologySpreadConstraints }
|
|
2612
|
+
: {}),
|
|
2613
|
+
...(spec.tolerations ? { tolerations: spec.tolerations } : {}),
|
|
2614
|
+
...(spec.priorityClassName ? { priorityClassName: spec.priorityClassName } : {}),
|
|
2615
|
+
...(spec.schedulerName ? { schedulerName: spec.schedulerName } : {}),
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
/**
|
|
2619
|
+
* Create advanced disruption configuration for NodePools.
|
|
2620
|
+
* Provides fine-grained control over when and how nodes are disrupted.
|
|
2621
|
+
*
|
|
2622
|
+
* @param spec - Advanced disruption specification
|
|
2623
|
+
* @returns Disruption configuration object
|
|
2624
|
+
*
|
|
2625
|
+
* @example
|
|
2626
|
+
* ```typescript
|
|
2627
|
+
* const disruptionConfig = rutter.createKarpenterDisruption({
|
|
2628
|
+
* consolidationPolicy: 'WhenEmptyOrUnderutilized',
|
|
2629
|
+
* consolidateAfter: '30s',
|
|
2630
|
+
* expireAfter: '2160h', // 90 days
|
|
2631
|
+
* budgets: [{
|
|
2632
|
+
* schedule: '0 9 * * mon-fri', // Business hours
|
|
2633
|
+
* duration: '8h',
|
|
2634
|
+
* nodes: '0', // No disruption during business hours
|
|
2635
|
+
* reasons: ['Underutilized', 'Empty']
|
|
2636
|
+
* }]
|
|
2637
|
+
* });
|
|
2638
|
+
* ```
|
|
2639
|
+
*
|
|
2640
|
+
* @since 2.3.0
|
|
2641
|
+
*/
|
|
2642
|
+
createKarpenterDisruption(spec) {
|
|
2643
|
+
// Validate disruption specification
|
|
2644
|
+
this.validateKarpenterDisruptionSpec(spec);
|
|
2645
|
+
return {
|
|
2646
|
+
...(spec.consolidationPolicy ? { consolidationPolicy: spec.consolidationPolicy } : {}),
|
|
2647
|
+
...(spec.consolidateAfter ? { consolidateAfter: spec.consolidateAfter } : {}),
|
|
2648
|
+
...(spec.expireAfter ? { expireAfter: spec.expireAfter } : {}),
|
|
2649
|
+
...(spec.budgets ? { budgets: spec.budgets } : {}),
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
/**
|
|
2653
|
+
* Validate Karpenter NodeClaim specification
|
|
2654
|
+
*/
|
|
2655
|
+
validateKarpenterNodeClaimSpec(spec) {
|
|
2656
|
+
// Validate NodeClass reference
|
|
2657
|
+
if (!spec.nodeClassRef.name) {
|
|
2658
|
+
throw new Error(`Karpenter NodeClaim ${spec.name}: nodeClassRef.name is required`);
|
|
2659
|
+
}
|
|
2660
|
+
// Validate expiration format
|
|
2661
|
+
if (spec.expireAfter && !this.isValidDuration(spec.expireAfter)) {
|
|
2662
|
+
throw new Error(`Karpenter NodeClaim ${spec.name}: invalid expireAfter duration format`);
|
|
2663
|
+
}
|
|
2664
|
+
// Validate termination grace period
|
|
2665
|
+
if (spec.terminationGracePeriod && !this.isValidDuration(spec.terminationGracePeriod)) {
|
|
2666
|
+
throw new Error(`Karpenter NodeClaim ${spec.name}: invalid terminationGracePeriod duration format`);
|
|
2667
|
+
}
|
|
2668
|
+
// Validate requirements
|
|
2669
|
+
if (spec.requirements) {
|
|
2670
|
+
for (const req of spec.requirements) {
|
|
2671
|
+
if (['In', 'NotIn', 'Gt', 'Lt'].includes(req.operator) && !req.values?.length) {
|
|
2672
|
+
throw new Error(`Karpenter NodeClaim ${spec.name}: requirement with operator '${req.operator}' must have values`);
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
/**
|
|
2678
|
+
* Validate Karpenter scheduling specification
|
|
2679
|
+
*/
|
|
2680
|
+
validateKarpenterSchedulingSpec(spec) {
|
|
2681
|
+
this.validateTopologySpreadConstraints(spec);
|
|
2682
|
+
this.validateSchedulingTolerations(spec);
|
|
2683
|
+
}
|
|
2684
|
+
validateTopologySpreadConstraints(spec) {
|
|
2685
|
+
if (!spec.topologySpreadConstraints)
|
|
2686
|
+
return;
|
|
2687
|
+
for (const constraint of spec.topologySpreadConstraints) {
|
|
2688
|
+
if (constraint.maxSkew < 1) {
|
|
2689
|
+
throw new Error(`Karpenter Scheduling ${spec.name}: maxSkew must be at least 1 in topology spread constraint`);
|
|
2690
|
+
}
|
|
2691
|
+
if (!constraint.topologyKey) {
|
|
2692
|
+
throw new Error(`Karpenter Scheduling ${spec.name}: topologyKey is required in topology spread constraint`);
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
validateSchedulingTolerations(spec) {
|
|
2697
|
+
if (!spec.tolerations)
|
|
2698
|
+
return;
|
|
2699
|
+
for (const toleration of spec.tolerations) {
|
|
2700
|
+
if (toleration.operator === 'Equal' && !toleration.value) {
|
|
2701
|
+
throw new Error(`Karpenter Scheduling ${spec.name}: value is required when operator is 'Equal' in toleration`);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
/**
|
|
2706
|
+
* Validate Karpenter disruption specification
|
|
2707
|
+
*/
|
|
2708
|
+
validateKarpenterDisruptionSpec(spec) {
|
|
2709
|
+
this.validateDisruptionDurations(spec);
|
|
2710
|
+
this.validateDisruptionBudgets(spec);
|
|
2711
|
+
}
|
|
2712
|
+
validateDisruptionDurations(spec) {
|
|
2713
|
+
if (spec.consolidateAfter && !this.isValidDuration(spec.consolidateAfter)) {
|
|
2714
|
+
throw new Error('Invalid consolidateAfter duration format');
|
|
2715
|
+
}
|
|
2716
|
+
if (spec.expireAfter && !this.isValidDuration(spec.expireAfter)) {
|
|
2717
|
+
throw new Error('Invalid expireAfter duration format');
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
validateDisruptionBudgets(spec) {
|
|
2721
|
+
if (!spec.budgets)
|
|
2722
|
+
return;
|
|
2723
|
+
for (const budget of spec.budgets) {
|
|
2724
|
+
if (budget.schedule && budget.duration) {
|
|
2725
|
+
// Basic cron validation - should have 5 parts
|
|
2726
|
+
const cronParts = budget.schedule.split(' ');
|
|
2727
|
+
if (cronParts.length !== 5) {
|
|
2728
|
+
throw new Error('Budget schedule must be a valid cron expression with 5 parts');
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
if (budget.duration && !this.isValidDuration(budget.duration)) {
|
|
2732
|
+
throw new Error('Invalid budget duration format');
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
/**
|
|
2737
|
+
* Synthesizes the cdk8s app and writes a complete Helm chart
|
|
2738
|
+
*
|
|
2739
|
+
* Generates all Kubernetes manifests, applies Helm templating,
|
|
2740
|
+
* and writes the complete chart structure to the output directory.
|
|
2741
|
+
*
|
|
2742
|
+
* @param {string} outDir - Output directory for the Helm chart
|
|
2743
|
+
* @throws {Error} If synthesis or writing fails
|
|
2744
|
+
*
|
|
2745
|
+
* @example
|
|
2746
|
+
* ```typescript
|
|
2747
|
+
* rutter.write('./charts/my-app');
|
|
2748
|
+
* // Creates: ./charts/my-app/Chart.yaml, values.yaml, templates/, etc.
|
|
2749
|
+
* ```
|
|
2750
|
+
*
|
|
2751
|
+
* @since 1.0.0
|
|
2027
2752
|
*/
|
|
2028
2753
|
write(outDir) {
|
|
2029
2754
|
// Use cdk8s Testing.synth to obtain manifest objects
|