timonel 2.1.1 → 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 +16 -0
- package/README.md +43 -809
- package/SECURITY.md +55 -20
- 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 +1738 -104
- package/dist/lib/Rutter.d.ts.map +1 -1
- package/dist/lib/Rutter.js +1087 -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',
|
|
@@ -1055,6 +1280,257 @@ export class Rutter {
|
|
|
1055
1280
|
this.capture(spc, `${spec.name}-secretproviderclass`);
|
|
1056
1281
|
return spc;
|
|
1057
1282
|
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Add Azure Workload Identity ServiceAccount.
|
|
1285
|
+
* Creates a ServiceAccount with Azure Workload Identity annotations for secure authentication.
|
|
1286
|
+
*
|
|
1287
|
+
* @param spec - Azure Workload Identity ServiceAccount specification
|
|
1288
|
+
* @returns The created ServiceAccount ApiObject
|
|
1289
|
+
*
|
|
1290
|
+
* @example
|
|
1291
|
+
* ```typescript
|
|
1292
|
+
* rutter.addAzureWorkloadIdentityServiceAccount({
|
|
1293
|
+
* name: 'workload-identity-sa',
|
|
1294
|
+
* clientId: '12345678-1234-1234-1234-123456789012',
|
|
1295
|
+
* tenantId: '87654321-4321-4321-4321-210987654321',
|
|
1296
|
+
* tokenExpiration: 3600
|
|
1297
|
+
* });
|
|
1298
|
+
* ```
|
|
1299
|
+
*/
|
|
1300
|
+
addAzureWorkloadIdentityServiceAccount(spec) {
|
|
1301
|
+
// Validate all inputs first for security
|
|
1302
|
+
if (spec.tokenExpiration !== undefined &&
|
|
1303
|
+
(spec.tokenExpiration < 3600 || spec.tokenExpiration > 86400)) {
|
|
1304
|
+
throw new Error(`Azure Workload Identity ${spec.name}: tokenExpiration must be between 3600 and 86400 seconds`);
|
|
1305
|
+
}
|
|
1306
|
+
if (!spec.clientId?.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) {
|
|
1307
|
+
throw new Error(`Azure Workload Identity ${spec.name}: Invalid client ID format`);
|
|
1308
|
+
}
|
|
1309
|
+
if (!spec.tenantId?.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) {
|
|
1310
|
+
throw new Error(`Azure Workload Identity ${spec.name}: Invalid tenant ID format`);
|
|
1311
|
+
}
|
|
1312
|
+
const annotations = {
|
|
1313
|
+
'azure.workload.identity/client-id': spec.clientId,
|
|
1314
|
+
'azure.workload.identity/tenant-id': spec.tenantId,
|
|
1315
|
+
...(spec.tokenExpiration !== undefined
|
|
1316
|
+
? {
|
|
1317
|
+
'azure.workload.identity/service-account-token-expiration': String(spec.tokenExpiration),
|
|
1318
|
+
}
|
|
1319
|
+
: {}),
|
|
1320
|
+
...(spec.annotations ?? {}),
|
|
1321
|
+
};
|
|
1322
|
+
const sa = new ApiObject(this.chart, spec.name, {
|
|
1323
|
+
apiVersion: 'v1',
|
|
1324
|
+
kind: 'ServiceAccount',
|
|
1325
|
+
metadata: {
|
|
1326
|
+
name: spec.name,
|
|
1327
|
+
annotations,
|
|
1328
|
+
...(spec.labels ? { labels: spec.labels } : {}),
|
|
1329
|
+
},
|
|
1330
|
+
automountServiceAccountToken: spec.automountServiceAccountToken,
|
|
1331
|
+
imagePullSecrets: spec.imagePullSecrets?.map((n) => ({ name: n })),
|
|
1332
|
+
secrets: spec.secrets?.map((n) => ({ name: n })),
|
|
1333
|
+
});
|
|
1334
|
+
this.capture(sa, `${spec.name}-serviceaccount`);
|
|
1335
|
+
return sa;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Add Azure Key Vault SecretProviderClass.
|
|
1339
|
+
* Creates a SecretProviderClass for mounting secrets from Azure Key Vault using CSI driver.
|
|
1340
|
+
*
|
|
1341
|
+
* @param spec - Azure Key Vault SecretProviderClass specification
|
|
1342
|
+
* @returns The created SecretProviderClass ApiObject
|
|
1343
|
+
*
|
|
1344
|
+
* @example
|
|
1345
|
+
* ```typescript
|
|
1346
|
+
* rutter.addAzureKeyVaultSecretProviderClass({
|
|
1347
|
+
* name: 'app-secrets',
|
|
1348
|
+
* keyVaultName: 'my-keyvault',
|
|
1349
|
+
* tenantId: '87654321-4321-4321-4321-210987654321',
|
|
1350
|
+
* objects: [
|
|
1351
|
+
* { objectName: 'database-password', objectType: 'secret' },
|
|
1352
|
+
* { objectName: 'api-key', objectType: 'secret', objectAlias: 'API_KEY' }
|
|
1353
|
+
* ],
|
|
1354
|
+
* userAssignedIdentityID: '12345678-1234-1234-1234-123456789012'
|
|
1355
|
+
* });
|
|
1356
|
+
* ```
|
|
1357
|
+
*/
|
|
1358
|
+
addAzureKeyVaultSecretProviderClass(spec) {
|
|
1359
|
+
// Validate object names for security - prevent path traversal attacks
|
|
1360
|
+
spec.objects.forEach((obj) => {
|
|
1361
|
+
if (obj.objectName.includes('../') || obj.objectName.includes('..\\')) {
|
|
1362
|
+
throw new Error(`Azure Key Vault ${spec.name}: Object name '${obj.objectName}' contains invalid path traversal sequences`);
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
const objects = spec.objects.map((obj) => {
|
|
1366
|
+
const baseObj = {
|
|
1367
|
+
objectName: obj.objectName,
|
|
1368
|
+
objectType: obj.objectType,
|
|
1369
|
+
};
|
|
1370
|
+
if (obj.objectAlias) {
|
|
1371
|
+
baseObj['objectAlias'] = obj.objectAlias;
|
|
1372
|
+
}
|
|
1373
|
+
if (obj.objectVersion) {
|
|
1374
|
+
baseObj['objectVersion'] = obj.objectVersion;
|
|
1375
|
+
}
|
|
1376
|
+
return baseObj;
|
|
1377
|
+
});
|
|
1378
|
+
const objectsYaml = YAML.stringify(objects, { indent: 2 }).trim();
|
|
1379
|
+
const parameters = {
|
|
1380
|
+
keyvaultName: spec.keyVaultName,
|
|
1381
|
+
tenantId: spec.tenantId,
|
|
1382
|
+
objects: `|\n ${objectsYaml.split('\n').join('\n ')}`,
|
|
1383
|
+
};
|
|
1384
|
+
if (spec.userAssignedIdentityID) {
|
|
1385
|
+
parameters['userAssignedIdentityID'] = spec.userAssignedIdentityID;
|
|
1386
|
+
}
|
|
1387
|
+
if (spec.cloudName) {
|
|
1388
|
+
parameters['cloudName'] = spec.cloudName;
|
|
1389
|
+
}
|
|
1390
|
+
const spc = new ApiObject(this.chart, spec.name, {
|
|
1391
|
+
apiVersion: 'secrets-store.csi.x-k8s.io/v1',
|
|
1392
|
+
kind: 'SecretProviderClass',
|
|
1393
|
+
metadata: {
|
|
1394
|
+
name: spec.name,
|
|
1395
|
+
...(spec.labels ? { labels: spec.labels } : {}),
|
|
1396
|
+
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
1397
|
+
},
|
|
1398
|
+
spec: {
|
|
1399
|
+
provider: 'azure',
|
|
1400
|
+
parameters,
|
|
1401
|
+
},
|
|
1402
|
+
});
|
|
1403
|
+
this.capture(spc, `${spec.name}-secretproviderclass`);
|
|
1404
|
+
return spc;
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Add Azure Files StorageClass.
|
|
1408
|
+
* Creates a StorageClass for dynamic provisioning of Azure Files volumes.
|
|
1409
|
+
*
|
|
1410
|
+
* @param spec - Azure Files StorageClass specification
|
|
1411
|
+
* @returns The created StorageClass ApiObject
|
|
1412
|
+
*
|
|
1413
|
+
* @example
|
|
1414
|
+
* ```typescript
|
|
1415
|
+
* rutter.addAzureFilesStorageClass({
|
|
1416
|
+
* name: 'azure-files-premium',
|
|
1417
|
+
* skuName: 'Premium_LRS',
|
|
1418
|
+
* protocol: 'smb',
|
|
1419
|
+
* allowSharedAccess: true
|
|
1420
|
+
* });
|
|
1421
|
+
* ```
|
|
1422
|
+
*/
|
|
1423
|
+
addAzureFilesStorageClass(spec) {
|
|
1424
|
+
// Validate Azure Files parameters for security and compliance
|
|
1425
|
+
this.validateAzureFilesParameters(spec);
|
|
1426
|
+
const parameters = this.buildAzureFilesParameters(spec);
|
|
1427
|
+
const sc = new ApiObject(this.chart, spec.name, {
|
|
1428
|
+
apiVersion: Rutter.STORAGE_API_VERSION,
|
|
1429
|
+
kind: 'StorageClass',
|
|
1430
|
+
metadata: {
|
|
1431
|
+
name: spec.name,
|
|
1432
|
+
...(spec.labels ? { labels: spec.labels } : {}),
|
|
1433
|
+
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
1434
|
+
},
|
|
1435
|
+
provisioner: Rutter.AZURE_FILES_CSI_DRIVER,
|
|
1436
|
+
parameters,
|
|
1437
|
+
reclaimPolicy: spec.reclaimPolicy ?? 'Delete',
|
|
1438
|
+
allowVolumeExpansion: spec.allowVolumeExpansion ?? true,
|
|
1439
|
+
volumeBindingMode: spec.volumeBindingMode ?? 'Immediate',
|
|
1440
|
+
...(spec.mountOptions ? { mountOptions: spec.mountOptions } : {}),
|
|
1441
|
+
});
|
|
1442
|
+
this.capture(sc, `${spec.name}-storageclass`);
|
|
1443
|
+
return sc;
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Add Azure Files PersistentVolume.
|
|
1447
|
+
* Creates a PersistentVolume for static Azure Files volume provisioning.
|
|
1448
|
+
*
|
|
1449
|
+
* @param spec - Azure Files PersistentVolume specification
|
|
1450
|
+
* @returns The created PersistentVolume ApiObject
|
|
1451
|
+
*/
|
|
1452
|
+
addAzureFilesPersistentVolume(spec) {
|
|
1453
|
+
const csiSpec = this.buildAzureFilesPVCSISpec(spec);
|
|
1454
|
+
const pv = new ApiObject(this.chart, spec.name, {
|
|
1455
|
+
apiVersion: 'v1',
|
|
1456
|
+
kind: 'PersistentVolume',
|
|
1457
|
+
metadata: {
|
|
1458
|
+
name: spec.name,
|
|
1459
|
+
...(spec.labels ? { labels: spec.labels } : {}),
|
|
1460
|
+
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
1461
|
+
},
|
|
1462
|
+
spec: {
|
|
1463
|
+
capacity: { storage: spec.capacity },
|
|
1464
|
+
accessModes: spec.accessModes ?? ['ReadWriteMany'],
|
|
1465
|
+
persistentVolumeReclaimPolicy: spec.reclaimPolicy ?? 'Retain',
|
|
1466
|
+
storageClassName: spec.storageClassName,
|
|
1467
|
+
...(spec.mountOptions ? { mountOptions: spec.mountOptions } : {}),
|
|
1468
|
+
csi: csiSpec,
|
|
1469
|
+
},
|
|
1470
|
+
});
|
|
1471
|
+
this.capture(pv, `${spec.name}-pv`);
|
|
1472
|
+
return pv;
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Add Azure Files PersistentVolumeClaim.
|
|
1476
|
+
* Creates a PersistentVolumeClaim for Azure Files storage.
|
|
1477
|
+
*
|
|
1478
|
+
* @param spec - Azure Files PersistentVolumeClaim specification
|
|
1479
|
+
* @returns The created PersistentVolumeClaim ApiObject
|
|
1480
|
+
*/
|
|
1481
|
+
addAzureFilesPersistentVolumeClaim(spec) {
|
|
1482
|
+
const pvc = new ApiObject(this.chart, spec.name, {
|
|
1483
|
+
apiVersion: 'v1',
|
|
1484
|
+
kind: 'PersistentVolumeClaim',
|
|
1485
|
+
metadata: {
|
|
1486
|
+
name: spec.name,
|
|
1487
|
+
...(spec.labels ? { labels: spec.labels } : {}),
|
|
1488
|
+
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
1489
|
+
},
|
|
1490
|
+
spec: {
|
|
1491
|
+
accessModes: spec.accessModes ?? ['ReadWriteMany'],
|
|
1492
|
+
storageClassName: spec.storageClassName,
|
|
1493
|
+
resources: {
|
|
1494
|
+
requests: { storage: spec.size },
|
|
1495
|
+
},
|
|
1496
|
+
},
|
|
1497
|
+
});
|
|
1498
|
+
this.capture(pvc, `${spec.name}-pvc`);
|
|
1499
|
+
return pvc;
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Add Azure Container Registry ServiceAccount.
|
|
1503
|
+
* Creates a ServiceAccount configured for ACR access.
|
|
1504
|
+
*
|
|
1505
|
+
* @param spec - Azure ACR ServiceAccount specification
|
|
1506
|
+
* @returns The created ServiceAccount ApiObject
|
|
1507
|
+
*/
|
|
1508
|
+
addAzureACRServiceAccount(spec) {
|
|
1509
|
+
const annotations = {
|
|
1510
|
+
...(spec.annotations ?? {}),
|
|
1511
|
+
};
|
|
1512
|
+
// Add Workload Identity annotations if provided
|
|
1513
|
+
if (spec.clientId) {
|
|
1514
|
+
annotations['azure.workload.identity/client-id'] = spec.clientId;
|
|
1515
|
+
}
|
|
1516
|
+
if (spec.tenantId) {
|
|
1517
|
+
annotations['azure.workload.identity/tenant-id'] = spec.tenantId;
|
|
1518
|
+
}
|
|
1519
|
+
const sa = new ApiObject(this.chart, spec.name, {
|
|
1520
|
+
apiVersion: 'v1',
|
|
1521
|
+
kind: 'ServiceAccount',
|
|
1522
|
+
metadata: {
|
|
1523
|
+
name: spec.name,
|
|
1524
|
+
...(Object.keys(annotations).length ? { annotations } : {}),
|
|
1525
|
+
...(spec.labels ? { labels: spec.labels } : {}),
|
|
1526
|
+
},
|
|
1527
|
+
automountServiceAccountToken: spec.automountServiceAccountToken,
|
|
1528
|
+
imagePullSecrets: spec.imagePullSecrets?.map((n) => ({ name: n })),
|
|
1529
|
+
secrets: spec.secrets?.map((n) => ({ name: n })),
|
|
1530
|
+
});
|
|
1531
|
+
this.capture(sa, `${spec.name}-serviceaccount`);
|
|
1532
|
+
return sa;
|
|
1533
|
+
}
|
|
1058
1534
|
addAzureDiskStorageClass(spec) {
|
|
1059
1535
|
// Validate numeric parameters to prevent runtime errors
|
|
1060
1536
|
this.validateAzureDiskNumericParameters(spec);
|
|
@@ -1435,6 +1911,76 @@ export class Rutter {
|
|
|
1435
1911
|
throw new Error('Port 443 requires an SSL certificate to be specified');
|
|
1436
1912
|
}
|
|
1437
1913
|
}
|
|
1914
|
+
buildAzureFilesParameters(spec) {
|
|
1915
|
+
const parameters = {
|
|
1916
|
+
skuName: spec.skuName ?? 'Standard_LRS',
|
|
1917
|
+
};
|
|
1918
|
+
if (spec.protocol) {
|
|
1919
|
+
parameters['protocol'] = spec.protocol;
|
|
1920
|
+
}
|
|
1921
|
+
if (spec.allowSharedAccess !== undefined) {
|
|
1922
|
+
parameters['allowSharedAccess'] = String(spec.allowSharedAccess);
|
|
1923
|
+
}
|
|
1924
|
+
if (spec.resourceGroup) {
|
|
1925
|
+
parameters['resourceGroup'] = spec.resourceGroup;
|
|
1926
|
+
}
|
|
1927
|
+
if (spec.storageAccount) {
|
|
1928
|
+
parameters['storageAccount'] = spec.storageAccount;
|
|
1929
|
+
}
|
|
1930
|
+
if (spec.location) {
|
|
1931
|
+
parameters['location'] = spec.location;
|
|
1932
|
+
}
|
|
1933
|
+
if (spec.networkEndpointType) {
|
|
1934
|
+
parameters['networkEndpointType'] = spec.networkEndpointType;
|
|
1935
|
+
}
|
|
1936
|
+
// NFS-specific parameters
|
|
1937
|
+
if (spec.protocol === 'nfs' && spec.mountPermissions) {
|
|
1938
|
+
parameters['mountPermissions'] = spec.mountPermissions;
|
|
1939
|
+
}
|
|
1940
|
+
if (spec.protocol === 'nfs' && spec.rootSquashType) {
|
|
1941
|
+
parameters['rootSquashType'] = spec.rootSquashType;
|
|
1942
|
+
}
|
|
1943
|
+
return parameters;
|
|
1944
|
+
}
|
|
1945
|
+
buildAzureFilesPVCSISpec(spec) {
|
|
1946
|
+
// Validate protocol and secret configuration
|
|
1947
|
+
if (spec.protocol === 'nfs' && spec.secretName) {
|
|
1948
|
+
throw new Error(`Azure Files PV ${spec.name}: NFS protocol does not use secrets for authentication`);
|
|
1949
|
+
}
|
|
1950
|
+
const volumeHandle = this.sanitizeVolumeHandle(spec.storageAccount, spec.shareName);
|
|
1951
|
+
const volumeAttributes = {
|
|
1952
|
+
storageAccount: spec.storageAccount,
|
|
1953
|
+
shareName: spec.shareName,
|
|
1954
|
+
};
|
|
1955
|
+
if (spec.resourceGroup) {
|
|
1956
|
+
volumeAttributes['resourceGroup'] = spec.resourceGroup;
|
|
1957
|
+
}
|
|
1958
|
+
if (spec.protocol) {
|
|
1959
|
+
volumeAttributes['protocol'] = spec.protocol;
|
|
1960
|
+
}
|
|
1961
|
+
if (spec.server) {
|
|
1962
|
+
volumeAttributes['server'] = spec.server;
|
|
1963
|
+
}
|
|
1964
|
+
if (spec.folderName) {
|
|
1965
|
+
volumeAttributes['folderName'] = spec.folderName;
|
|
1966
|
+
}
|
|
1967
|
+
if (spec.mountPermissions) {
|
|
1968
|
+
volumeAttributes['mountPermissions'] = spec.mountPermissions;
|
|
1969
|
+
}
|
|
1970
|
+
const csiSpec = {
|
|
1971
|
+
driver: Rutter.AZURE_FILES_CSI_DRIVER,
|
|
1972
|
+
volumeHandle,
|
|
1973
|
+
volumeAttributes,
|
|
1974
|
+
};
|
|
1975
|
+
// Add secret reference for SMB protocol
|
|
1976
|
+
if (spec.protocol !== 'nfs' && spec.secretName) {
|
|
1977
|
+
csiSpec['nodeStageSecretRef'] = {
|
|
1978
|
+
name: spec.secretName,
|
|
1979
|
+
namespace: spec.secretNamespace ?? 'default',
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
return csiSpec;
|
|
1983
|
+
}
|
|
1438
1984
|
buildPodEnvironment(spec) {
|
|
1439
1985
|
const envEntries = spec.env
|
|
1440
1986
|
? Object.entries(spec.env).map(([name, value]) => this.sanitizeEnvVar(name, value))
|
|
@@ -1641,6 +2187,37 @@ export class Rutter {
|
|
|
1641
2187
|
},
|
|
1642
2188
|
};
|
|
1643
2189
|
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Validate Azure Files parameters for security and compliance
|
|
2192
|
+
*/
|
|
2193
|
+
validateAzureFilesParameters(spec) {
|
|
2194
|
+
const validSkuNames = [
|
|
2195
|
+
'Standard_LRS',
|
|
2196
|
+
'Standard_GRS',
|
|
2197
|
+
'Standard_RAGRS',
|
|
2198
|
+
'Standard_ZRS',
|
|
2199
|
+
'Premium_LRS',
|
|
2200
|
+
'Premium_ZRS',
|
|
2201
|
+
];
|
|
2202
|
+
if (spec.skuName && !validSkuNames.includes(spec.skuName)) {
|
|
2203
|
+
throw new Error(`Invalid skuName: ${spec.skuName}. Must be one of: ${validSkuNames.join(', ')}`);
|
|
2204
|
+
}
|
|
2205
|
+
if (spec.protocol === 'nfs' && spec.mountPermissions) {
|
|
2206
|
+
const permRegex = /^[0-7]{3,4}$/;
|
|
2207
|
+
if (!permRegex.test(spec.mountPermissions)) {
|
|
2208
|
+
throw new Error('mountPermissions must be a valid octal permission string (e.g., "0777")');
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Sanitize volume handle to ensure valid format for CSI driver
|
|
2214
|
+
*/
|
|
2215
|
+
sanitizeVolumeHandle(account, share) {
|
|
2216
|
+
// Remove special characters and ensure valid format
|
|
2217
|
+
const sanitizedAccount = account.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
|
2218
|
+
const sanitizedShare = share.replace(/[^a-z0-9-]/gi, '').toLowerCase();
|
|
2219
|
+
return `${sanitizedAccount}#${sanitizedShare}`;
|
|
2220
|
+
}
|
|
1644
2221
|
/**
|
|
1645
2222
|
* Capture the YAML of an ApiObject or Construct into assets.
|
|
1646
2223
|
*/
|
|
@@ -1665,13 +2242,513 @@ export class Rutter {
|
|
|
1665
2242
|
return 'resource';
|
|
1666
2243
|
}
|
|
1667
2244
|
/**
|
|
1668
|
-
*
|
|
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
|
|
1669
2262
|
*/
|
|
1670
2263
|
addCrd(yaml, id = 'crd') {
|
|
1671
2264
|
this.assets.push({ id, yaml, target: 'crds' });
|
|
1672
2265
|
}
|
|
1673
2266
|
/**
|
|
1674
|
-
*
|
|
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
|
|
1675
2752
|
*/
|
|
1676
2753
|
write(outDir) {
|
|
1677
2754
|
// Use cdk8s Testing.synth to obtain manifest objects
|
|
@@ -1781,5 +2858,6 @@ Rutter.NETWORKING_API_VERSION = 'networking.k8s.io/v1';
|
|
|
1781
2858
|
Rutter.STORAGE_API_VERSION = 'storage.k8s.io/v1';
|
|
1782
2859
|
Rutter.AZURE_DISK_CSI_DRIVER = 'disk.csi.azure.com';
|
|
1783
2860
|
Rutter.AZURE_DISK_PROVISIONER_ANNOTATION = 'volume.beta.kubernetes.io/storage-provisioner';
|
|
2861
|
+
Rutter.AZURE_FILES_CSI_DRIVER = 'file.csi.azure.com';
|
|
1784
2862
|
Rutter.EMPTY_POD_SELECTOR = {};
|
|
1785
2863
|
//# sourceMappingURL=Rutter.js.map
|