timonel 2.3.0 → 2.5.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +5 -1
  3. package/SECURITY.md +3 -3
  4. package/dist/cli.js +33 -38
  5. package/dist/cli.js.map +1 -1
  6. package/dist/index.d.ts +9 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +10 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/{HelmChartWriter.d.ts → helmChartWriter.d.ts} +1 -1
  11. package/dist/lib/{HelmChartWriter.d.ts.map → helmChartWriter.d.ts.map} +1 -1
  12. package/dist/lib/{HelmChartWriter.js → helmChartWriter.js} +1 -1
  13. package/dist/lib/{HelmChartWriter.js.map → helmChartWriter.js.map} +1 -1
  14. package/dist/lib/resources/autoscaling/autoscalingResources.d.ts +26 -0
  15. package/dist/lib/resources/autoscaling/autoscalingResources.d.ts.map +1 -0
  16. package/dist/lib/resources/autoscaling/autoscalingResources.js +35 -0
  17. package/dist/lib/resources/autoscaling/autoscalingResources.js.map +1 -0
  18. package/dist/lib/resources/baseResourceProvider.d.ts +47 -0
  19. package/dist/lib/resources/baseResourceProvider.d.ts.map +1 -0
  20. package/dist/lib/resources/baseResourceProvider.js +83 -0
  21. package/dist/lib/resources/baseResourceProvider.js.map +1 -0
  22. package/dist/lib/resources/cloud/aws/awsResources.d.ts +165 -0
  23. package/dist/lib/resources/cloud/aws/awsResources.d.ts.map +1 -0
  24. package/dist/lib/resources/cloud/aws/awsResources.js +187 -0
  25. package/dist/lib/resources/cloud/aws/awsResources.js.map +1 -0
  26. package/dist/lib/resources/cloud/azure/azureResources.d.ts +169 -0
  27. package/dist/lib/resources/cloud/azure/azureResources.d.ts.map +1 -0
  28. package/dist/lib/resources/cloud/azure/azureResources.js +218 -0
  29. package/dist/lib/resources/cloud/azure/azureResources.js.map +1 -0
  30. package/dist/lib/resources/cloud/gcp/gcpResources.d.ts +133 -0
  31. package/dist/lib/resources/cloud/gcp/gcpResources.d.ts.map +1 -0
  32. package/dist/lib/resources/cloud/gcp/gcpResources.js +162 -0
  33. package/dist/lib/resources/cloud/gcp/gcpResources.js.map +1 -0
  34. package/dist/lib/resources/core/coreResources.d.ts +766 -0
  35. package/dist/lib/resources/core/coreResources.d.ts.map +1 -0
  36. package/dist/lib/resources/core/coreResources.js +1043 -0
  37. package/dist/lib/resources/core/coreResources.js.map +1 -0
  38. package/dist/lib/resources/core/storageResources.d.ts +133 -0
  39. package/dist/lib/resources/core/storageResources.d.ts.map +1 -0
  40. package/dist/lib/resources/core/storageResources.js +120 -0
  41. package/dist/lib/resources/core/storageResources.js.map +1 -0
  42. package/dist/lib/resources/validation/validationResources.d.ts +26 -0
  43. package/dist/lib/resources/validation/validationResources.d.ts.map +1 -0
  44. package/dist/lib/resources/validation/validationResources.js +33 -0
  45. package/dist/lib/resources/validation/validationResources.js.map +1 -0
  46. package/dist/lib/rutter.d.ts +567 -0
  47. package/dist/lib/rutter.d.ts.map +1 -0
  48. package/dist/lib/rutter.js +694 -0
  49. package/dist/lib/rutter.js.map +1 -0
  50. package/dist/lib/security.d.ts +21 -0
  51. package/dist/lib/security.d.ts.map +1 -1
  52. package/dist/lib/security.js +21 -0
  53. package/dist/lib/security.js.map +1 -1
  54. package/dist/lib/umbrella.d.ts +22 -2
  55. package/dist/lib/umbrella.d.ts.map +1 -1
  56. package/dist/lib/umbrella.js +21 -1
  57. package/dist/lib/umbrella.js.map +1 -1
  58. package/dist/lib/umbrellaRutter.d.ts +97 -0
  59. package/dist/lib/umbrellaRutter.d.ts.map +1 -0
  60. package/dist/lib/{UmbrellaRutter.js → umbrellaRutter.js} +44 -1
  61. package/dist/lib/umbrellaRutter.js.map +1 -0
  62. package/dist/lib/utils/helmHelpers.d.ts +58 -0
  63. package/dist/lib/utils/helmHelpers.d.ts.map +1 -0
  64. package/dist/lib/utils/helmHelpers.js +155 -0
  65. package/dist/lib/utils/helmHelpers.js.map +1 -0
  66. package/dist/lib/utils/podTemplates.d.ts +192 -0
  67. package/dist/lib/utils/podTemplates.d.ts.map +1 -0
  68. package/dist/lib/utils/podTemplates.js +331 -0
  69. package/dist/lib/utils/podTemplates.js.map +1 -0
  70. package/dist/lib/utils/resourceNaming.d.ts +57 -0
  71. package/dist/lib/utils/resourceNaming.d.ts.map +1 -0
  72. package/dist/lib/utils/resourceNaming.js +82 -0
  73. package/dist/lib/utils/resourceNaming.js.map +1 -0
  74. package/package.json +2 -1
  75. package/dist/lib/Rutter.d.ts +0 -2688
  76. package/dist/lib/Rutter.d.ts.map +0 -1
  77. package/dist/lib/Rutter.js +0 -2863
  78. package/dist/lib/Rutter.js.map +0 -1
  79. package/dist/lib/UmbrellaRutter.d.ts +0 -34
  80. package/dist/lib/UmbrellaRutter.d.ts.map +0 -1
  81. package/dist/lib/UmbrellaRutter.js.map +0 -1
@@ -1,2863 +0,0 @@
1
- /**
2
- * @fileoverview Rutter - Main class for building Kubernetes manifests and Helm charts
3
- * @since 1.0.0
4
- */
5
- import { App, Chart, Testing, ApiObject } from 'cdk8s';
6
- import YAML from 'yaml';
7
- import { include, helm } from './helm.js';
8
- import { HelmChartWriter } from './HelmChartWriter.js';
9
- import { SecurityUtils } from './security.js';
10
- /**
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
- * ```
34
- */
35
- export class Rutter {
36
- /**
37
- * Creates a new Rutter instance
38
- *
39
- * @param {RutterProps} props - Configuration properties
40
- * @since 1.0.0
41
- */
42
- constructor(props) {
43
- this.assets = [];
44
- this.valueOverrides = {};
45
- this.props = props;
46
- this.app = new App();
47
- this.chart = new Chart(this.app, props.manifestName ?? props.meta.name);
48
- }
49
- /**
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
65
- */
66
- setValues(overrides) {
67
- this.valueOverrides = { ...this.valueOverrides, ...overrides };
68
- }
69
- /**
70
- * Apply value overrides to nested object using dot notation
71
- */
72
- applyOverrides(values) {
73
- const result = JSON.parse(JSON.stringify(values)); // Deep clone
74
- for (const [key, value] of Object.entries(this.valueOverrides)) {
75
- this.setNestedValue(result, key, this.parseValue(value));
76
- }
77
- return result;
78
- }
79
- setNestedValue(obj, path, value) {
80
- /* eslint-disable security/detect-object-injection */
81
- const keys = path.split('.').filter(Boolean);
82
- if (keys.length === 0)
83
- return;
84
- let current = obj;
85
- for (let i = 0; i < keys.length - 1; i++) {
86
- const key = keys[i];
87
- if (!key)
88
- continue;
89
- if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
90
- current[key] = {};
91
- }
92
- current = current[key];
93
- }
94
- const finalKey = keys[keys.length - 1];
95
- if (finalKey) {
96
- current[finalKey] = value;
97
- }
98
- /* eslint-enable security/detect-object-injection */
99
- }
100
- parseValue(value) {
101
- // Try to parse as JSON first (for objects, arrays, booleans, numbers)
102
- try {
103
- return JSON.parse(value);
104
- }
105
- catch (error) {
106
- // Log parsing error for debugging purposes
107
- console.debug(`Failed to parse value as JSON: ${value}`, error);
108
- // If not valid JSON, treat as string
109
- return value;
110
- }
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
- */
131
- addDeployment(spec) {
132
- const match = spec.matchLabels ?? {
133
- [Rutter.LABEL_NAME]: include(Rutter.HELPER_NAME),
134
- [Rutter.LABEL_INSTANCE]: helm.releaseName,
135
- };
136
- const envEntries = spec.env
137
- ? Object.entries(spec.env).map(([name, value]) => ({ name, value }))
138
- : undefined;
139
- const envFromEntries = spec.envFrom
140
- ? spec.envFrom.map((s) => ({
141
- configMapRef: s.configMapRef ? { name: s.configMapRef } : undefined,
142
- secretRef: s.secretRef ? { name: s.secretRef } : undefined,
143
- }))
144
- : undefined;
145
- const volumeDefs = spec.volumes
146
- ? spec.volumes.map((v) => ({
147
- name: v.name,
148
- configMap: v.configMap ? { name: v.configMap } : undefined,
149
- secret: v.secret ? { secretName: v.secret } : undefined,
150
- persistentVolumeClaim: v.persistentVolumeClaim
151
- ? { claimName: v.persistentVolumeClaim }
152
- : undefined,
153
- }))
154
- : undefined;
155
- const volumeMountDefs = spec.volumeMounts
156
- ? spec.volumeMounts.map((m) => ({
157
- name: m.name,
158
- mountPath: m.mountPath,
159
- readOnly: m.readOnly,
160
- subPath: m.subPath,
161
- }))
162
- : undefined;
163
- const templateLabels = { ...match, ...(spec.podLabels ?? {}) };
164
- const dep = new ApiObject(this.chart, spec.name, {
165
- apiVersion: 'apps/v1',
166
- kind: 'Deployment',
167
- metadata: {
168
- name: spec.name,
169
- ...(spec.labels ? { labels: spec.labels } : {}),
170
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
171
- },
172
- spec: {
173
- replicas: spec.replicas ?? 1,
174
- selector: { matchLabels: match },
175
- template: {
176
- metadata: {
177
- labels: templateLabels,
178
- ...(spec.podAnnotations ? { annotations: spec.podAnnotations } : {}),
179
- },
180
- spec: {
181
- serviceAccountName: spec.serviceAccountName,
182
- volumes: volumeDefs,
183
- containers: [
184
- {
185
- name: spec.name,
186
- image: spec.image,
187
- imagePullPolicy: spec.imagePullPolicy,
188
- ports: spec.containerPort ? [{ containerPort: spec.containerPort }] : undefined,
189
- env: envEntries,
190
- envFrom: envFromEntries,
191
- volumeMounts: volumeMountDefs,
192
- resources: spec.resources,
193
- livenessProbe: spec.livenessProbe,
194
- readinessProbe: spec.readinessProbe,
195
- },
196
- ],
197
- },
198
- },
199
- },
200
- });
201
- this.capture(dep, `${spec.name}-deployment`);
202
- return dep;
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
- */
221
- addService(spec) {
222
- const svc = new ApiObject(this.chart, spec.name, {
223
- apiVersion: 'v1',
224
- kind: 'Service',
225
- metadata: {
226
- name: spec.name,
227
- ...(spec.labels ? { labels: spec.labels } : {}),
228
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
229
- },
230
- spec: {
231
- type: spec.type ?? 'ClusterIP',
232
- selector: spec.selector,
233
- ports: spec.ports.map((p) => ({
234
- port: p.port,
235
- targetPort: p.targetPort ?? p.port,
236
- protocol: p.protocol ?? 'TCP',
237
- name: p.name,
238
- nodePort: p.nodePort,
239
- })),
240
- clusterIP: spec.clusterIP,
241
- externalName: spec.externalName,
242
- sessionAffinity: spec.sessionAffinity,
243
- loadBalancerIP: spec.loadBalancerIP,
244
- loadBalancerSourceRanges: spec.loadBalancerSourceRanges,
245
- loadBalancerClass: spec.loadBalancerClass,
246
- externalTrafficPolicy: spec.externalTrafficPolicy,
247
- internalTrafficPolicy: spec.internalTrafficPolicy,
248
- ipFamilyPolicy: spec.ipFamilyPolicy,
249
- ipFamilies: spec.ipFamilies,
250
- },
251
- });
252
- this.capture(svc, `${spec.name}-service`);
253
- return svc;
254
- }
255
- addReplicaSet(spec) {
256
- const match = spec.matchLabels ?? {
257
- [Rutter.LABEL_NAME]: include(Rutter.HELPER_NAME),
258
- [Rutter.LABEL_INSTANCE]: helm.releaseName,
259
- };
260
- const envEntries = spec.env
261
- ? Object.entries(spec.env).map(([name, value]) => ({ name, value }))
262
- : undefined;
263
- const envFromEntries = spec.envFrom
264
- ? spec.envFrom.map((s) => ({
265
- configMapRef: s.configMapRef ? { name: s.configMapRef } : undefined,
266
- secretRef: s.secretRef ? { name: s.secretRef } : undefined,
267
- }))
268
- : undefined;
269
- const volumeDefs = spec.volumes
270
- ? spec.volumes.map((v) => ({
271
- name: v.name,
272
- configMap: v.configMap ? { name: v.configMap } : undefined,
273
- secret: v.secret ? { secretName: v.secret } : undefined,
274
- persistentVolumeClaim: v.persistentVolumeClaim
275
- ? { claimName: v.persistentVolumeClaim }
276
- : undefined,
277
- }))
278
- : undefined;
279
- const volumeMountDefs = spec.volumeMounts
280
- ? spec.volumeMounts.map((m) => ({
281
- name: m.name,
282
- mountPath: m.mountPath,
283
- readOnly: m.readOnly,
284
- subPath: m.subPath,
285
- }))
286
- : undefined;
287
- const templateLabels = { ...match, ...(spec.podLabels ?? {}) };
288
- const rs = new ApiObject(this.chart, spec.name, {
289
- apiVersion: 'apps/v1',
290
- kind: 'ReplicaSet',
291
- metadata: {
292
- name: spec.name,
293
- ...(spec.labels ? { labels: spec.labels } : {}),
294
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
295
- },
296
- spec: {
297
- replicas: spec.replicas ?? 1,
298
- selector: { matchLabels: match },
299
- template: {
300
- metadata: {
301
- labels: templateLabels,
302
- ...(spec.podAnnotations ? { annotations: spec.podAnnotations } : {}),
303
- },
304
- spec: {
305
- serviceAccountName: spec.serviceAccountName,
306
- volumes: volumeDefs,
307
- containers: [
308
- {
309
- name: spec.name,
310
- image: spec.image,
311
- imagePullPolicy: spec.imagePullPolicy,
312
- ports: spec.containerPort ? [{ containerPort: spec.containerPort }] : undefined,
313
- env: envEntries,
314
- envFrom: envFromEntries,
315
- volumeMounts: volumeMountDefs,
316
- resources: spec.resources,
317
- livenessProbe: spec.livenessProbe,
318
- readinessProbe: spec.readinessProbe,
319
- },
320
- ],
321
- },
322
- },
323
- },
324
- });
325
- this.capture(rs, `${spec.name}-replicaset`);
326
- return rs;
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
- */
346
- addJob(spec) {
347
- // Validate Job specification
348
- this.validateJobSpec(spec);
349
- const { envEntries, envFromEntries, volumeDefs, volumeMountDefs } = this.buildPodEnvironment(spec);
350
- const jobSpec = this.buildJobSpec(spec);
351
- const podTemplate = this.buildJobPodTemplate(spec, envEntries, envFromEntries, volumeDefs, volumeMountDefs);
352
- const job = new ApiObject(this.chart, spec.name, {
353
- apiVersion: 'batch/v1',
354
- kind: 'Job',
355
- metadata: {
356
- name: spec.name,
357
- ...(spec.labels ? { labels: spec.labels } : {}),
358
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
359
- },
360
- spec: {
361
- ...jobSpec,
362
- template: podTemplate,
363
- },
364
- });
365
- this.capture(job, `${spec.name}-job`);
366
- return job;
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
- */
386
- addCronJob(spec) {
387
- // Validate CronJob specification
388
- this.validateCronJobSpec(spec);
389
- const { envEntries, envFromEntries, volumeDefs, volumeMountDefs } = this.buildPodEnvironment(spec);
390
- const cronJobSpec = this.buildCronJobSpec(spec);
391
- const jobTemplate = this.buildCronJobTemplate(spec, envEntries, envFromEntries, volumeDefs, volumeMountDefs);
392
- const cronJob = new ApiObject(this.chart, spec.name, {
393
- apiVersion: 'batch/v1',
394
- kind: 'CronJob',
395
- metadata: {
396
- name: spec.name,
397
- ...(spec.labels ? { labels: spec.labels } : {}),
398
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
399
- },
400
- spec: {
401
- ...cronJobSpec,
402
- jobTemplate,
403
- },
404
- });
405
- this.capture(cronJob, `${spec.name}-cronjob`);
406
- return cronJob;
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
- */
431
- addIngress(spec) {
432
- const ing = new ApiObject(this.chart, spec.name, {
433
- apiVersion: Rutter.NETWORKING_API_VERSION,
434
- kind: 'Ingress',
435
- metadata: {
436
- name: spec.name,
437
- ...(spec.labels ? { labels: spec.labels } : {}),
438
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
439
- },
440
- spec: {
441
- ingressClassName: spec.ingressClassName,
442
- defaultBackend: spec.defaultBackend,
443
- tls: spec.tls,
444
- rules: spec.rules.map((rule) => ({
445
- host: rule.host,
446
- http: {
447
- paths: rule.paths.map((path) => ({
448
- path: path.path,
449
- pathType: path.pathType,
450
- backend: path.backend,
451
- })),
452
- },
453
- })),
454
- },
455
- });
456
- this.capture(ing, `${spec.name}-ingress`);
457
- return ing;
458
- }
459
- addPersistentVolume(spec) {
460
- // Apply cloud provider optimizations
461
- const optimizedSpec = this.optimizePVForCloud(spec);
462
- const pvSpec = {
463
- capacity: { storage: optimizedSpec.capacity },
464
- accessModes: optimizedSpec.accessModes,
465
- storageClassName: optimizedSpec.storageClassName,
466
- volumeMode: optimizedSpec.volumeMode,
467
- persistentVolumeReclaimPolicy: optimizedSpec.reclaimPolicy,
468
- mountOptions: optimizedSpec.mountOptions,
469
- nodeAffinity: optimizedSpec.nodeAffinity,
470
- claimRef: optimizedSpec.claimRef,
471
- };
472
- // Handle known volume sources with validation
473
- const knownSources = [
474
- 'csi',
475
- 'nfs',
476
- 'awsElasticBlockStore',
477
- 'hostPath',
478
- 'azureDisk',
479
- 'azureFile',
480
- 'gcePersistentDisk',
481
- ];
482
- let sourceCount = 0;
483
- for (const sourceType of knownSources) {
484
- // eslint-disable-next-line security/detect-object-injection -- Safe: sourceType is from known const array
485
- const sourceValue = optimizedSpec.source[sourceType];
486
- if (sourceValue) {
487
- // eslint-disable-next-line security/detect-object-injection -- Safe: sourceType is from known const array
488
- pvSpec[sourceType] = sourceValue;
489
- sourceCount++;
490
- }
491
- }
492
- // Validate single volume source
493
- if (sourceCount === 0) {
494
- throw new Error(`PersistentVolume ${spec.name}: No volume source specified`);
495
- }
496
- if (sourceCount > 1) {
497
- throw new Error(`PersistentVolume ${spec.name}: Multiple volume sources specified, only one allowed`);
498
- }
499
- // Handle custom volume sources
500
- for (const [key, value] of Object.entries(optimizedSpec.source)) {
501
- if (!knownSources.includes(key) && value !== undefined) {
502
- // eslint-disable-next-line security/detect-object-injection -- Safe: controlled object construction
503
- pvSpec[key] = value;
504
- }
505
- }
506
- const pv = new ApiObject(this.chart, spec.name, {
507
- apiVersion: 'v1',
508
- kind: 'PersistentVolume',
509
- metadata: {
510
- name: spec.name,
511
- ...(optimizedSpec.labels ? { labels: optimizedSpec.labels } : {}),
512
- ...(optimizedSpec.annotations ? { annotations: optimizedSpec.annotations } : {}),
513
- },
514
- spec: pvSpec,
515
- });
516
- this.capture(pv, `${spec.name}-pv`);
517
- return pv;
518
- }
519
- optimizePVForCloud(spec) {
520
- const optimized = { ...spec };
521
- const EBS_CSI_DRIVER = 'ebs.csi.aws.com';
522
- // Apply cloud-specific optimizations
523
- if (spec.cloudProvider === 'aws') {
524
- optimized.annotations = {
525
- ...optimized.annotations,
526
- 'volume.beta.kubernetes.io/storage-provisioner': EBS_CSI_DRIVER,
527
- };
528
- // Default to gp3 for better performance/cost
529
- if (spec.source.csi?.driver === EBS_CSI_DRIVER && optimized.source.csi) {
530
- optimized.source.csi.volumeAttributes = {
531
- type: 'gp3',
532
- ...spec.source.csi.volumeAttributes,
533
- };
534
- }
535
- }
536
- else if (spec.cloudProvider === 'azure') {
537
- optimized.annotations = {
538
- ...optimized.annotations,
539
- [Rutter.AZURE_DISK_PROVISIONER_ANNOTATION]: Rutter.AZURE_DISK_CSI_DRIVER,
540
- };
541
- // Default to Premium_ZRS for multi-zone clusters
542
- if (spec.source.csi?.driver === Rutter.AZURE_DISK_CSI_DRIVER && optimized.source.csi) {
543
- optimized.source.csi.volumeAttributes = {
544
- skuName: 'Premium_ZRS',
545
- ...spec.source.csi.volumeAttributes,
546
- };
547
- }
548
- }
549
- else if (spec.cloudProvider === 'gcp') {
550
- optimized.annotations = {
551
- ...optimized.annotations,
552
- 'volume.beta.kubernetes.io/storage-provisioner': 'pd.csi.storage.gke.io',
553
- };
554
- }
555
- // Set sensible defaults
556
- optimized.reclaimPolicy = optimized.reclaimPolicy ?? 'Delete';
557
- optimized.volumeMode = optimized.volumeMode ?? 'Filesystem';
558
- return optimized;
559
- }
560
- addPersistentVolumeClaim(spec) {
561
- // Validate access modes compatibility
562
- this.validatePVCAccessModes(spec);
563
- const pvc = new ApiObject(this.chart, spec.name, {
564
- apiVersion: 'v1',
565
- kind: 'PersistentVolumeClaim',
566
- metadata: {
567
- name: spec.name,
568
- ...(spec.labels ? { labels: spec.labels } : {}),
569
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
570
- },
571
- spec: {
572
- accessModes: spec.accessModes,
573
- resources: spec.resources,
574
- storageClassName: spec.storageClassName,
575
- volumeMode: spec.volumeMode ?? 'Filesystem',
576
- selector: spec.selector,
577
- volumeName: spec.volumeName,
578
- },
579
- });
580
- this.capture(pvc, `${spec.name}-pvc`);
581
- return pvc;
582
- }
583
- validatePVCAccessModes(spec) {
584
- const { accessModes, storageClassName } = spec;
585
- // Validate ReadWriteOncePod (K8s 1.22+)
586
- if (accessModes.includes('ReadWriteOncePod') && accessModes.length > 1) {
587
- throw new Error(`PVC ${spec.name}: ReadWriteOncePod cannot be combined with other access modes`);
588
- }
589
- // Warn about common misconfigurations
590
- if (storageClassName?.includes('azure-disk') && accessModes.includes('ReadWriteMany')) {
591
- console.warn(SecurityUtils.sanitizeLogMessage(`PVC ${spec.name}: Azure Disk does not support ReadWriteMany, consider Azure Files`));
592
- }
593
- if (storageClassName?.includes('ebs') && accessModes.includes('ReadWriteMany')) {
594
- console.warn(SecurityUtils.sanitizeLogMessage(`PVC ${spec.name}: EBS does not support ReadWriteMany, consider EFS`));
595
- }
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
- */
616
- addConfigMap(spec) {
617
- const cm = new ApiObject(this.chart, spec.name, {
618
- apiVersion: 'v1',
619
- kind: 'ConfigMap',
620
- metadata: {
621
- name: spec.name,
622
- ...(spec.labels ? { labels: spec.labels } : {}),
623
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
624
- },
625
- data: spec.data,
626
- binaryData: spec.binaryData,
627
- immutable: spec.immutable,
628
- });
629
- this.capture(cm, `${spec.name}-configmap`);
630
- return cm;
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
- */
651
- addSecret(spec) {
652
- const sec = new ApiObject(this.chart, spec.name, {
653
- apiVersion: 'v1',
654
- kind: 'Secret',
655
- metadata: {
656
- name: spec.name,
657
- ...(spec.labels ? { labels: spec.labels } : {}),
658
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
659
- },
660
- type: spec.type ?? 'Opaque',
661
- stringData: spec.stringData,
662
- data: spec.data,
663
- immutable: spec.immutable,
664
- });
665
- this.capture(sec, `${spec.name}-secret`);
666
- return sec;
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
- */
685
- addServiceAccount(spec) {
686
- const annotations = this.buildServiceAccountAnnotations(spec);
687
- const sa = new ApiObject(this.chart, spec.name, {
688
- apiVersion: 'v1',
689
- kind: 'ServiceAccount',
690
- metadata: {
691
- name: spec.name,
692
- ...(Object.keys(annotations).length ? { annotations } : {}),
693
- ...(spec.labels ? { labels: spec.labels } : {}),
694
- },
695
- automountServiceAccountToken: spec.automountServiceAccountToken,
696
- imagePullSecrets: spec.imagePullSecrets?.map((n) => ({ name: n })),
697
- secrets: spec.secrets?.map((n) => ({ name: n })),
698
- });
699
- this.capture(sa, `${spec.name}-serviceaccount`);
700
- return sa;
701
- }
702
- buildServiceAccountAnnotations(spec) {
703
- const annotations = {
704
- ...(spec.annotations ?? {}),
705
- };
706
- this.addAWSIRSAAnnotations(annotations, spec);
707
- this.addAzureWorkloadIdentityAnnotations(annotations, spec);
708
- this.addGCPWorkloadIdentityAnnotations(annotations, spec);
709
- return annotations;
710
- }
711
- addAWSIRSAAnnotations(annotations, spec) {
712
- if (spec.awsRoleArn) {
713
- annotations['eks.amazonaws.com/role-arn'] = spec.awsRoleArn;
714
- }
715
- if (spec.awsAudience) {
716
- annotations['eks.amazonaws.com/audience'] = spec.awsAudience;
717
- }
718
- if (spec.awsStsEndpointType) {
719
- annotations['eks.amazonaws.com/sts-regional-endpoints'] = spec.awsStsEndpointType;
720
- }
721
- if (spec.awsTokenExpiration !== undefined) {
722
- annotations['eks.amazonaws.com/token-expiration'] = String(spec.awsTokenExpiration);
723
- }
724
- }
725
- addAzureWorkloadIdentityAnnotations(annotations, spec) {
726
- if (spec.azureClientId) {
727
- annotations['azure.workload.identity/client-id'] = spec.azureClientId;
728
- }
729
- if (spec.azureTenantId) {
730
- annotations['azure.workload.identity/tenant-id'] = spec.azureTenantId;
731
- }
732
- if (spec.azureServiceAccountTokenExpiration !== undefined) {
733
- annotations['azure.workload.identity/service-account-token-expiration'] = String(spec.azureServiceAccountTokenExpiration);
734
- }
735
- }
736
- addGCPWorkloadIdentityAnnotations(annotations, spec) {
737
- if (spec.gcpServiceAccountEmail) {
738
- annotations['iam.gke.io/gcp-service-account'] = spec.gcpServiceAccountEmail;
739
- }
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
- */
763
- addHorizontalPodAutoscaler(spec) {
764
- // Default metrics if none provided (CPU utilization at 80%)
765
- const defaultMetrics = [
766
- {
767
- type: 'Resource',
768
- resource: {
769
- name: 'cpu',
770
- target: {
771
- type: 'Utilization',
772
- averageUtilization: 80,
773
- },
774
- },
775
- },
776
- ];
777
- const hpa = new ApiObject(this.chart, spec.name, {
778
- apiVersion: 'autoscaling/v2',
779
- kind: 'HorizontalPodAutoscaler',
780
- metadata: {
781
- name: spec.name,
782
- ...(spec.labels ? { labels: spec.labels } : {}),
783
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
784
- },
785
- spec: {
786
- scaleTargetRef: spec.scaleTargetRef,
787
- minReplicas: spec.minReplicas ?? 1,
788
- maxReplicas: spec.maxReplicas,
789
- metrics: spec.metrics ?? defaultMetrics,
790
- ...(spec.behavior ? { behavior: spec.behavior } : {}),
791
- },
792
- });
793
- this.capture(hpa, `${spec.name}-hpa`);
794
- return hpa;
795
- }
796
- addVerticalPodAutoscaler(spec) {
797
- const vpa = new ApiObject(this.chart, spec.name, {
798
- apiVersion: 'autoscaling.k8s.io/v1',
799
- kind: 'VerticalPodAutoscaler',
800
- metadata: {
801
- name: spec.name,
802
- ...(spec.labels ? { labels: spec.labels } : {}),
803
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
804
- },
805
- spec: {
806
- targetRef: spec.targetRef,
807
- updatePolicy: spec.updatePolicy ?? { updateMode: 'Auto' },
808
- ...(spec.resourcePolicy ? { resourcePolicy: spec.resourcePolicy } : {}),
809
- ...(spec.recommenders ? { recommenders: spec.recommenders } : {}),
810
- },
811
- });
812
- this.capture(vpa, `${spec.name}-vpa`);
813
- return vpa;
814
- }
815
- addPodDisruptionBudget(spec) {
816
- const pdb = new ApiObject(this.chart, spec.name, {
817
- apiVersion: 'policy/v1',
818
- kind: 'PodDisruptionBudget',
819
- metadata: {
820
- name: spec.name,
821
- ...(spec.labels ? { labels: spec.labels } : {}),
822
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
823
- },
824
- spec: {
825
- ...(spec.minAvailable !== undefined ? { minAvailable: spec.minAvailable } : {}),
826
- ...(spec.maxUnavailable !== undefined ? { maxUnavailable: spec.maxUnavailable } : {}),
827
- selector: spec.selector,
828
- },
829
- });
830
- this.capture(pdb, `${spec.name}-pdb`);
831
- return pdb;
832
- }
833
- addAWSEBSStorageClass(spec) {
834
- const DEFAULT_VOLUME_TYPE = 'gp3';
835
- const parameters = {
836
- type: spec.volumeType ?? DEFAULT_VOLUME_TYPE,
837
- fsType: spec.fsType ?? 'ext4',
838
- encrypted: String(spec.encrypted ?? true),
839
- };
840
- if (spec.kmsKeyId) {
841
- parameters['kmsKeyId'] = spec.kmsKeyId;
842
- }
843
- const IOPS_SUPPORTED_TYPES = ['gp3', 'io1', 'io2'];
844
- if (spec.iops && IOPS_SUPPORTED_TYPES.includes(spec.volumeType ?? DEFAULT_VOLUME_TYPE)) {
845
- parameters['iops'] = String(spec.iops);
846
- }
847
- if (spec.throughput && spec.volumeType === DEFAULT_VOLUME_TYPE) {
848
- parameters['throughput'] = String(spec.throughput);
849
- }
850
- const sc = new ApiObject(this.chart, spec.name, {
851
- apiVersion: Rutter.STORAGE_API_VERSION,
852
- kind: 'StorageClass',
853
- metadata: {
854
- name: spec.name,
855
- ...(spec.labels ? { labels: spec.labels } : {}),
856
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
857
- },
858
- provisioner: 'ebs.csi.aws.com',
859
- parameters,
860
- reclaimPolicy: spec.reclaimPolicy ?? 'Delete',
861
- allowVolumeExpansion: spec.allowVolumeExpansion ?? true,
862
- volumeBindingMode: spec.volumeBindingMode ?? 'WaitForFirstConsumer',
863
- });
864
- this.capture(sc, `${spec.name}-storageclass`);
865
- return sc;
866
- }
867
- addAWSEBSPersistentVolumeClaim(spec) {
868
- const pvc = new ApiObject(this.chart, spec.name, {
869
- apiVersion: 'v1',
870
- kind: 'PersistentVolumeClaim',
871
- metadata: {
872
- name: spec.name,
873
- ...(spec.labels ? { labels: spec.labels } : {}),
874
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
875
- },
876
- spec: {
877
- accessModes: spec.accessModes ?? ['ReadWriteOnce'],
878
- storageClassName: spec.storageClassName,
879
- resources: {
880
- requests: {
881
- storage: spec.size,
882
- },
883
- },
884
- },
885
- });
886
- this.capture(pvc, `${spec.name}-pvc`);
887
- return pvc;
888
- }
889
- addAWSEFSStorageClass(spec) {
890
- const sc = new ApiObject(this.chart, spec.name, {
891
- apiVersion: Rutter.STORAGE_API_VERSION,
892
- kind: 'StorageClass',
893
- metadata: {
894
- name: spec.name,
895
- ...(spec.labels ? { labels: spec.labels } : {}),
896
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
897
- },
898
- provisioner: 'efs.csi.aws.com',
899
- reclaimPolicy: spec.reclaimPolicy ?? 'Delete',
900
- volumeBindingMode: spec.volumeBindingMode ?? 'Immediate',
901
- });
902
- this.capture(sc, `${spec.name}-storageclass`);
903
- return sc;
904
- }
905
- addAWSEFSPersistentVolume(spec) {
906
- const csiSpec = {
907
- driver: 'efs.csi.aws.com',
908
- volumeHandle: spec.fileSystemId,
909
- };
910
- if (spec.accessPoint) {
911
- csiSpec['volumeHandle'] = `${spec.fileSystemId}::${spec.accessPoint}`;
912
- }
913
- const volumeAttributes = {};
914
- if (spec.directoryPerms) {
915
- volumeAttributes['directoryPerms'] = spec.directoryPerms;
916
- }
917
- if (spec.gidRangeStart !== undefined) {
918
- volumeAttributes['gidRangeStart'] = String(spec.gidRangeStart);
919
- }
920
- if (spec.gidRangeEnd !== undefined) {
921
- volumeAttributes['gidRangeEnd'] = String(spec.gidRangeEnd);
922
- }
923
- if (spec.basePath) {
924
- volumeAttributes['basePath'] = spec.basePath;
925
- }
926
- if (spec.subPath) {
927
- volumeAttributes['subPath'] = spec.subPath;
928
- }
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
- }
935
- }
936
- const pv = new ApiObject(this.chart, spec.name, {
937
- apiVersion: 'v1',
938
- kind: 'PersistentVolume',
939
- metadata: {
940
- name: spec.name,
941
- ...(spec.labels ? { labels: spec.labels } : {}),
942
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
943
- },
944
- spec: {
945
- capacity: {
946
- storage: spec.capacity ?? '5Gi',
947
- },
948
- accessModes: spec.accessModes ?? ['ReadWriteMany'],
949
- persistentVolumeReclaimPolicy: 'Retain',
950
- storageClassName: spec.storageClassName,
951
- csi: csiSpec,
952
- },
953
- });
954
- this.capture(pv, `${spec.name}-pv`);
955
- return pv;
956
- }
957
- addAWSEFSPersistentVolumeClaim(spec) {
958
- const pvc = new ApiObject(this.chart, spec.name, {
959
- apiVersion: 'v1',
960
- kind: 'PersistentVolumeClaim',
961
- metadata: {
962
- name: spec.name,
963
- ...(spec.labels ? { labels: spec.labels } : {}),
964
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
965
- },
966
- spec: {
967
- accessModes: spec.accessModes ?? ['ReadWriteMany'],
968
- storageClassName: spec.storageClassName,
969
- resources: {
970
- requests: {
971
- storage: spec.size ?? '5Gi',
972
- },
973
- },
974
- },
975
- });
976
- this.capture(pvc, `${spec.name}-pvc`);
977
- return pvc;
978
- }
979
- addAWSALBIngress(spec) {
980
- const annotations = this.buildALBAnnotations(spec);
981
- const ingress = new ApiObject(this.chart, spec.name, {
982
- apiVersion: Rutter.NETWORKING_API_VERSION,
983
- kind: 'Ingress',
984
- metadata: {
985
- name: spec.name,
986
- ...(spec.labels ? { labels: spec.labels } : {}),
987
- annotations,
988
- },
989
- spec: {
990
- tls: spec.tls,
991
- rules: spec.rules.map((rule) => ({
992
- host: rule.host,
993
- http: {
994
- paths: rule.paths.map((path) => ({
995
- path: path.path,
996
- pathType: path.pathType,
997
- backend: path.backend,
998
- })),
999
- },
1000
- })),
1001
- },
1002
- });
1003
- this.capture(ingress, `${spec.name}-ingress`);
1004
- return ingress;
1005
- }
1006
- buildALBAnnotations(spec) {
1007
- const annotations = {
1008
- 'kubernetes.io/ingress.class': 'alb',
1009
- 'alb.ingress.kubernetes.io/scheme': spec.scheme ?? 'internet-facing',
1010
- 'alb.ingress.kubernetes.io/target-type': spec.targetType ?? 'ip',
1011
- ...(spec.annotations ?? {}),
1012
- };
1013
- this.addOptionalALBAnnotations(annotations, spec);
1014
- this.addHealthCheckAnnotations(annotations, spec);
1015
- this.addTagsAnnotation(annotations, spec);
1016
- return annotations;
1017
- }
1018
- addOptionalALBAnnotations(annotations, spec) {
1019
- if (spec.ipAddressType) {
1020
- annotations['alb.ingress.kubernetes.io/ip-address-type'] = spec.ipAddressType;
1021
- }
1022
- if (spec.groupName) {
1023
- annotations['alb.ingress.kubernetes.io/group.name'] = spec.groupName;
1024
- }
1025
- if (spec.groupOrder !== undefined) {
1026
- annotations['alb.ingress.kubernetes.io/group.order'] = String(spec.groupOrder);
1027
- }
1028
- if (spec.subnets && spec.subnets.length > 0) {
1029
- annotations['alb.ingress.kubernetes.io/subnets'] = spec.subnets.join(',');
1030
- }
1031
- if (spec.securityGroups && spec.securityGroups.length > 0) {
1032
- annotations['alb.ingress.kubernetes.io/security-groups'] = spec.securityGroups.join(',');
1033
- }
1034
- if (spec.certificateArn) {
1035
- annotations['alb.ingress.kubernetes.io/certificate-arn'] = spec.certificateArn;
1036
- }
1037
- if (spec.sslRedirect) {
1038
- annotations['alb.ingress.kubernetes.io/ssl-redirect'] = '443';
1039
- }
1040
- }
1041
- addHealthCheckAnnotations(annotations, spec) {
1042
- if (spec.healthCheckPath) {
1043
- annotations['alb.ingress.kubernetes.io/healthcheck-path'] = spec.healthCheckPath;
1044
- }
1045
- if (spec.healthCheckIntervalSeconds !== undefined) {
1046
- annotations['alb.ingress.kubernetes.io/healthcheck-interval-seconds'] = String(spec.healthCheckIntervalSeconds);
1047
- }
1048
- if (spec.healthCheckTimeoutSeconds !== undefined) {
1049
- annotations['alb.ingress.kubernetes.io/healthcheck-timeout-seconds'] = String(spec.healthCheckTimeoutSeconds);
1050
- }
1051
- if (spec.healthyThresholdCount !== undefined) {
1052
- annotations['alb.ingress.kubernetes.io/healthy-threshold-count'] = String(spec.healthyThresholdCount);
1053
- }
1054
- if (spec.unhealthyThresholdCount !== undefined) {
1055
- annotations['alb.ingress.kubernetes.io/unhealthy-threshold-count'] = String(spec.unhealthyThresholdCount);
1056
- }
1057
- }
1058
- addTagsAnnotation(annotations, spec) {
1059
- if (spec.tags && Object.keys(spec.tags).length > 0) {
1060
- const tagString = this.formatCloudResourceTags(spec.tags);
1061
- annotations['alb.ingress.kubernetes.io/tags'] = tagString;
1062
- }
1063
- }
1064
- /**
1065
- * Add Azure Application Gateway Ingress Controller (AGIC) Ingress.
1066
- * Creates an Ingress resource with AGIC-specific annotations for AKS deployments.
1067
- *
1068
- * @param spec - AGIC Ingress specification
1069
- * @returns The created Ingress ApiObject
1070
- *
1071
- * @example
1072
- * ```typescript
1073
- * rutter.addAzureAGICIngress({
1074
- * name: 'app-ingress',
1075
- * rules: [{
1076
- * host: 'app.example.com',
1077
- * paths: [{
1078
- * path: '/',
1079
- * pathType: 'Prefix',
1080
- * backend: { service: { name: 'app-service', port: { number: 80 } } }
1081
- * }]
1082
- * }],
1083
- * sslRedirect: true,
1084
- * backendProtocol: 'https',
1085
- * healthProbePath: '/health'
1086
- * });
1087
- * ```
1088
- */
1089
- addAzureAGICIngress(spec) {
1090
- this.validateAGICHealthProbeParams(spec);
1091
- this.validateAGICSecurityParams(spec);
1092
- this.validateAGICAnnotationConsistency(spec);
1093
- const annotations = this.buildAGICAnnotations(spec);
1094
- const ingress = new ApiObject(this.chart, spec.name, {
1095
- apiVersion: Rutter.NETWORKING_API_VERSION,
1096
- kind: 'Ingress',
1097
- metadata: {
1098
- name: spec.name,
1099
- annotations,
1100
- ...(spec.labels ? { labels: spec.labels } : {}),
1101
- },
1102
- spec: {
1103
- tls: spec.tls,
1104
- rules: spec.rules.map((rule) => ({
1105
- host: rule.host,
1106
- http: {
1107
- paths: rule.paths.map((path) => ({
1108
- path: path.path,
1109
- pathType: path.pathType,
1110
- backend: path.backend,
1111
- })),
1112
- },
1113
- })),
1114
- },
1115
- });
1116
- this.capture(ingress, `${spec.name}-ingress`);
1117
- return ingress;
1118
- }
1119
- buildAGICAnnotations(spec) {
1120
- const annotations = {
1121
- 'kubernetes.io/ingress.class': 'azure/application-gateway',
1122
- ...(spec.annotations ?? {}),
1123
- };
1124
- this.addAGICPathAnnotations(annotations, spec);
1125
- this.addAGICProtocolAnnotations(annotations, spec);
1126
- this.addAGICHealthProbeAnnotations(annotations, spec);
1127
- this.addAGICConnectionAnnotations(annotations, spec);
1128
- this.addAGICSecurityAnnotations(annotations, spec);
1129
- this.addAGICAdvancedAnnotations(annotations, spec);
1130
- return annotations;
1131
- }
1132
- addAGICPathAnnotations(annotations, spec) {
1133
- if (spec.backendPathPrefix) {
1134
- annotations['appgw.ingress.kubernetes.io/backend-path-prefix'] = spec.backendPathPrefix;
1135
- }
1136
- if (spec.backendHostname) {
1137
- annotations['appgw.ingress.kubernetes.io/backend-hostname'] = spec.backendHostname;
1138
- }
1139
- }
1140
- addAGICProtocolAnnotations(annotations, spec) {
1141
- if (spec.backendProtocol) {
1142
- annotations['appgw.ingress.kubernetes.io/backend-protocol'] = spec.backendProtocol;
1143
- }
1144
- if (spec.sslRedirect) {
1145
- annotations['appgw.ingress.kubernetes.io/ssl-redirect'] = 'true';
1146
- }
1147
- if (spec.usePrivateIp) {
1148
- annotations['appgw.ingress.kubernetes.io/use-private-ip'] = 'true';
1149
- }
1150
- if (spec.overrideFrontendPort !== undefined) {
1151
- annotations['appgw.ingress.kubernetes.io/override-frontend-port'] = String(spec.overrideFrontendPort);
1152
- }
1153
- }
1154
- addAGICHealthProbeAnnotations(annotations, spec) {
1155
- if (spec.healthProbeHostname) {
1156
- annotations['appgw.ingress.kubernetes.io/health-probe-hostname'] = spec.healthProbeHostname;
1157
- }
1158
- if (spec.healthProbePort !== undefined) {
1159
- annotations['appgw.ingress.kubernetes.io/health-probe-port'] = String(spec.healthProbePort);
1160
- }
1161
- if (spec.healthProbePath) {
1162
- annotations['appgw.ingress.kubernetes.io/health-probe-path'] = spec.healthProbePath;
1163
- }
1164
- if (spec.healthProbeStatusCodes) {
1165
- annotations['appgw.ingress.kubernetes.io/health-probe-status-codes'] =
1166
- spec.healthProbeStatusCodes;
1167
- }
1168
- if (spec.healthProbeInterval !== undefined) {
1169
- annotations['appgw.ingress.kubernetes.io/health-probe-interval'] = String(spec.healthProbeInterval);
1170
- }
1171
- if (spec.healthProbeTimeout !== undefined) {
1172
- annotations['appgw.ingress.kubernetes.io/health-probe-timeout'] = String(spec.healthProbeTimeout);
1173
- }
1174
- if (spec.healthProbeUnhealthyThreshold !== undefined) {
1175
- annotations['appgw.ingress.kubernetes.io/health-probe-unhealthy-threshold'] = String(spec.healthProbeUnhealthyThreshold);
1176
- }
1177
- }
1178
- addAGICConnectionAnnotations(annotations, spec) {
1179
- if (spec.cookieBasedAffinity) {
1180
- annotations['appgw.ingress.kubernetes.io/cookie-based-affinity'] = 'true';
1181
- }
1182
- if (spec.requestTimeout !== undefined) {
1183
- annotations['appgw.ingress.kubernetes.io/request-timeout'] = String(spec.requestTimeout);
1184
- }
1185
- if (spec.connectionDraining) {
1186
- annotations['appgw.ingress.kubernetes.io/connection-draining'] = 'true';
1187
- }
1188
- if (spec.connectionDrainingTimeout !== undefined) {
1189
- annotations['appgw.ingress.kubernetes.io/connection-draining-timeout'] = String(spec.connectionDrainingTimeout);
1190
- }
1191
- }
1192
- addAGICSecurityAnnotations(annotations, spec) {
1193
- if (spec.appgwSslCertificate) {
1194
- annotations['appgw.ingress.kubernetes.io/appgw-ssl-certificate'] = spec.appgwSslCertificate;
1195
- }
1196
- if (spec.appgwSslProfile) {
1197
- annotations['appgw.ingress.kubernetes.io/appgw-ssl-profile'] = spec.appgwSslProfile;
1198
- }
1199
- if (spec.appgwTrustedRootCertificate && spec.appgwTrustedRootCertificate.length > 0) {
1200
- annotations['appgw.ingress.kubernetes.io/appgw-trusted-root-certificate'] =
1201
- spec.appgwTrustedRootCertificate.join(',');
1202
- }
1203
- if (spec.wafPolicyForPath) {
1204
- annotations['appgw.ingress.kubernetes.io/waf-policy-for-path'] = spec.wafPolicyForPath;
1205
- }
1206
- }
1207
- addAGICAdvancedAnnotations(annotations, spec) {
1208
- if (spec.hostnameExtension && spec.hostnameExtension.length > 0) {
1209
- annotations['appgw.ingress.kubernetes.io/hostname-extension'] =
1210
- spec.hostnameExtension.join(', ');
1211
- }
1212
- if (spec.rewriteRuleSet) {
1213
- annotations['appgw.ingress.kubernetes.io/rewrite-rule-set'] = spec.rewriteRuleSet;
1214
- }
1215
- if (spec.rulePriority !== undefined) {
1216
- annotations['appgw.ingress.kubernetes.io/rule-priority'] = String(spec.rulePriority);
1217
- }
1218
- }
1219
- addAWSIRSAServiceAccount(spec) {
1220
- const annotations = {
1221
- 'eks.amazonaws.com/role-arn': spec.roleArn,
1222
- ...(spec.audience ? { 'eks.amazonaws.com/audience': spec.audience } : {}),
1223
- ...(spec.stsEndpointType
1224
- ? { 'eks.amazonaws.com/sts-regional-endpoints': spec.stsEndpointType }
1225
- : { 'eks.amazonaws.com/sts-regional-endpoints': 'regional' }),
1226
- ...(spec.tokenExpiration !== undefined
1227
- ? { 'eks.amazonaws.com/token-expiration': String(spec.tokenExpiration) }
1228
- : {}),
1229
- ...(spec.annotations ?? {}),
1230
- };
1231
- const sa = new ApiObject(this.chart, spec.name, {
1232
- apiVersion: 'v1',
1233
- kind: 'ServiceAccount',
1234
- metadata: {
1235
- name: spec.name,
1236
- annotations,
1237
- ...(spec.labels ? { labels: spec.labels } : {}),
1238
- },
1239
- automountServiceAccountToken: spec.automountServiceAccountToken,
1240
- imagePullSecrets: spec.imagePullSecrets?.map((n) => ({ name: n })),
1241
- secrets: spec.secrets?.map((n) => ({ name: n })),
1242
- });
1243
- this.capture(sa, `${spec.name}-serviceaccount`);
1244
- return sa;
1245
- }
1246
- addAWSSecretProviderClass(spec) {
1247
- const objects = spec.objects.map((obj) => {
1248
- const baseObj = {
1249
- objectName: obj.objectName,
1250
- objectType: obj.objectType,
1251
- };
1252
- if ('objectAlias' in obj && obj.objectAlias) {
1253
- baseObj['objectAlias'] = obj.objectAlias;
1254
- }
1255
- if ('jmesPath' in obj && obj.jmesPath) {
1256
- baseObj['jmesPath'] = obj.jmesPath;
1257
- }
1258
- return baseObj;
1259
- });
1260
- const objectsYaml = YAML.stringify(objects, { indent: 2 }).trim();
1261
- const parameters = {
1262
- objects: `|\n ${objectsYaml.split('\n').join('\n ')}`,
1263
- };
1264
- if (spec.region) {
1265
- parameters['region'] = spec.region;
1266
- }
1267
- const spc = new ApiObject(this.chart, spec.name, {
1268
- apiVersion: 'secrets-store.csi.x-k8s.io/v1',
1269
- kind: 'SecretProviderClass',
1270
- metadata: {
1271
- name: spec.name,
1272
- ...(spec.labels ? { labels: spec.labels } : {}),
1273
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
1274
- },
1275
- spec: {
1276
- provider: 'aws',
1277
- parameters,
1278
- },
1279
- });
1280
- this.capture(spc, `${spec.name}-secretproviderclass`);
1281
- return spc;
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
- }
1534
- addAzureDiskStorageClass(spec) {
1535
- // Validate numeric parameters to prevent runtime errors
1536
- this.validateAzureDiskNumericParameters(spec);
1537
- const parameters = this.buildAzureDiskParameters(spec);
1538
- const sc = new ApiObject(this.chart, spec.name, {
1539
- apiVersion: Rutter.STORAGE_API_VERSION,
1540
- kind: 'StorageClass',
1541
- metadata: {
1542
- name: spec.name,
1543
- ...(spec.labels ? { labels: spec.labels } : {}),
1544
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
1545
- },
1546
- provisioner: Rutter.AZURE_DISK_CSI_DRIVER,
1547
- parameters,
1548
- reclaimPolicy: spec.reclaimPolicy ?? 'Delete',
1549
- allowVolumeExpansion: spec.allowVolumeExpansion ?? true,
1550
- volumeBindingMode: spec.volumeBindingMode ?? 'WaitForFirstConsumer',
1551
- });
1552
- this.capture(sc, `${spec.name}-storageclass`);
1553
- return sc;
1554
- }
1555
- buildAzureDiskParameters(spec) {
1556
- const parameters = {
1557
- skuName: spec.skuName ?? 'StandardSSD_LRS',
1558
- fsType: spec.fsType ?? 'ext4',
1559
- // Security best practices: default to private network access and encryption
1560
- networkAccessPolicy: spec.networkAccessPolicy ?? 'AllowPrivate',
1561
- };
1562
- this.addOptionalAzureDiskParameters(parameters, spec);
1563
- this.addAzureDiskEncryptionParameters(parameters, spec);
1564
- this.addAzureDiskPerformanceParameters(parameters, spec);
1565
- return parameters;
1566
- }
1567
- addOptionalAzureDiskParameters(parameters, spec) {
1568
- if (spec.cachingMode) {
1569
- parameters['cachingMode'] = spec.cachingMode;
1570
- }
1571
- if (spec.resourceGroup) {
1572
- parameters['resourceGroup'] = spec.resourceGroup;
1573
- }
1574
- if (spec.logicalSectorSize !== undefined) {
1575
- parameters['LogicalSectorSize'] = String(spec.logicalSectorSize);
1576
- }
1577
- if (spec.tags && Object.keys(spec.tags).length > 0) {
1578
- const tagString = this.formatCloudResourceTags(spec.tags);
1579
- parameters['tags'] = tagString;
1580
- }
1581
- // networkAccessPolicy is now set as default in buildAzureDiskParameters
1582
- if (spec.networkAccessPolicy && spec.networkAccessPolicy !== 'AllowPrivate') {
1583
- parameters['networkAccessPolicy'] = spec.networkAccessPolicy;
1584
- }
1585
- if (spec.diskAccessID) {
1586
- parameters['diskAccessID'] = spec.diskAccessID;
1587
- }
1588
- if (spec.maxShares !== undefined) {
1589
- parameters['maxShares'] = String(spec.maxShares);
1590
- }
1591
- }
1592
- addAzureDiskEncryptionParameters(parameters, spec) {
1593
- if (spec.diskEncryptionSetID) {
1594
- parameters['diskEncryptionSetID'] = spec.diskEncryptionSetID;
1595
- }
1596
- if (spec.diskEncryptionType) {
1597
- parameters['diskEncryptionType'] = spec.diskEncryptionType;
1598
- }
1599
- }
1600
- addAzureDiskPerformanceParameters(parameters, spec) {
1601
- if (spec.diskIOPSReadWrite !== undefined) {
1602
- parameters['DiskIOPSReadWrite'] = String(spec.diskIOPSReadWrite);
1603
- }
1604
- if (spec.diskMBpsReadWrite !== undefined) {
1605
- parameters['DiskMBpsReadWrite'] = String(spec.diskMBpsReadWrite);
1606
- }
1607
- if (spec.writeAcceleratorEnabled !== undefined) {
1608
- parameters['writeAcceleratorEnabled'] = String(spec.writeAcceleratorEnabled);
1609
- }
1610
- if (spec.enableBursting !== undefined) {
1611
- parameters['enableBursting'] = String(spec.enableBursting);
1612
- }
1613
- }
1614
- validateAzureDiskNumericParameters(spec) {
1615
- this.validateAzureDiskIOPS(spec);
1616
- this.validateAzureDiskThroughput(spec);
1617
- this.validateAzureDiskShares(spec);
1618
- this.validateAzureDiskSectorSize(spec);
1619
- }
1620
- validateAzureDiskIOPS(spec) {
1621
- if (spec.diskIOPSReadWrite === undefined)
1622
- return;
1623
- if (spec.diskIOPSReadWrite < 100) {
1624
- throw new Error(`Azure Disk ${spec.name}: diskIOPSReadWrite must be at least 100 IOPS`);
1625
- }
1626
- if (spec.diskIOPSReadWrite > 400000) {
1627
- throw new Error(`Azure Disk ${spec.name}: diskIOPSReadWrite cannot exceed 400,000 IOPS`);
1628
- }
1629
- }
1630
- validateAzureDiskThroughput(spec) {
1631
- if (spec.diskMBpsReadWrite === undefined)
1632
- return;
1633
- if (spec.diskMBpsReadWrite < 1) {
1634
- throw new Error(`Azure Disk ${spec.name}: diskMBpsReadWrite must be at least 1 MB/s`);
1635
- }
1636
- if (spec.diskMBpsReadWrite > 10000) {
1637
- throw new Error(`Azure Disk ${spec.name}: diskMBpsReadWrite cannot exceed 10,000 MB/s`);
1638
- }
1639
- }
1640
- validateAzureDiskShares(spec) {
1641
- if (spec.maxShares === undefined)
1642
- return;
1643
- if (spec.maxShares < 1) {
1644
- throw new Error(`Azure Disk ${spec.name}: maxShares must be at least 1`);
1645
- }
1646
- if (spec.maxShares > 5) {
1647
- throw new Error(`Azure Disk ${spec.name}: maxShares cannot exceed 5`);
1648
- }
1649
- }
1650
- validateAzureDiskSectorSize(spec) {
1651
- if (spec.logicalSectorSize === undefined)
1652
- return;
1653
- if (spec.logicalSectorSize !== 512 && spec.logicalSectorSize !== 4096) {
1654
- throw new Error(`Azure Disk ${spec.name}: logicalSectorSize must be either 512 or 4096 bytes`);
1655
- }
1656
- }
1657
- /**
1658
- * Format cloud resource tags for provider-specific string format.
1659
- * Provides basic validation and consistent formatting across cloud providers.
1660
- */
1661
- formatCloudResourceTags(tags) {
1662
- this.validateCloudResourceTags(tags);
1663
- return Object.entries(tags)
1664
- .map(([key, value]) => `${key}=${value}`)
1665
- .join(',');
1666
- }
1667
- /**
1668
- * Validate cloud resource tags with basic rules.
1669
- * Each cloud provider has specific validation, but these are common basics.
1670
- */
1671
- validateCloudResourceTags(tags) {
1672
- for (const [key, value] of Object.entries(tags)) {
1673
- if (!key || key.trim() === '') {
1674
- throw new Error('Tag key cannot be empty');
1675
- }
1676
- if (value === undefined || value === null) {
1677
- throw new Error(`Tag value for key '${key}' cannot be null or undefined`);
1678
- }
1679
- }
1680
- }
1681
- addNetworkPolicy(spec) {
1682
- // Validate policy types
1683
- this.validateNetworkPolicyTypes(spec);
1684
- // Validate CIDR blocks if present
1685
- this.validateNetworkPolicyCIDRs(spec);
1686
- const np = new ApiObject(this.chart, spec.name, {
1687
- apiVersion: Rutter.NETWORKING_API_VERSION,
1688
- kind: 'NetworkPolicy',
1689
- metadata: {
1690
- name: spec.name,
1691
- ...(spec.labels ? { labels: spec.labels } : {}),
1692
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
1693
- },
1694
- spec: {
1695
- podSelector: spec.podSelector ?? {},
1696
- policyTypes: spec.policyTypes,
1697
- ...(spec.ingress ? { ingress: spec.ingress } : {}),
1698
- ...(spec.egress ? { egress: spec.egress } : {}),
1699
- },
1700
- });
1701
- this.capture(np, `${spec.name}-networkpolicy`);
1702
- return np;
1703
- }
1704
- /**
1705
- * Create a deny-all ingress NetworkPolicy for the current namespace
1706
- */
1707
- addDenyAllIngressNetworkPolicy(name = 'deny-all-ingress') {
1708
- return this.addNetworkPolicy({
1709
- name,
1710
- podSelector: Rutter.EMPTY_POD_SELECTOR,
1711
- policyTypes: ['Ingress'],
1712
- });
1713
- }
1714
- /**
1715
- * Create a deny-all egress NetworkPolicy for the current namespace
1716
- */
1717
- addDenyAllEgressNetworkPolicy(name = 'deny-all-egress') {
1718
- return this.addNetworkPolicy({
1719
- name,
1720
- podSelector: Rutter.EMPTY_POD_SELECTOR,
1721
- policyTypes: ['Egress'],
1722
- });
1723
- }
1724
- /**
1725
- * Create a deny-all NetworkPolicy for both ingress and egress
1726
- */
1727
- addDenyAllNetworkPolicy(name = 'deny-all') {
1728
- return this.addNetworkPolicy({
1729
- name,
1730
- podSelector: Rutter.EMPTY_POD_SELECTOR,
1731
- policyTypes: ['Ingress', 'Egress'],
1732
- });
1733
- }
1734
- /**
1735
- * Allow traffic from specific pods to target pods on specific ports
1736
- */
1737
- addAllowFromPodsNetworkPolicy(spec) {
1738
- return this.addNetworkPolicy({
1739
- name: spec.name,
1740
- podSelector: { matchLabels: spec.targetPodSelector },
1741
- policyTypes: ['Ingress'],
1742
- ingress: [
1743
- {
1744
- from: [{ podSelector: { matchLabels: spec.sourcePodSelector } }],
1745
- ...(spec.ports ? { ports: spec.ports } : {}),
1746
- },
1747
- ],
1748
- ...(spec.labels ? { labels: spec.labels } : {}),
1749
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
1750
- });
1751
- }
1752
- /**
1753
- * Allow traffic from specific namespace to target pods
1754
- */
1755
- addAllowFromNamespaceNetworkPolicy(spec) {
1756
- return this.addNetworkPolicy({
1757
- name: spec.name,
1758
- podSelector: { matchLabels: spec.targetPodSelector },
1759
- policyTypes: ['Ingress'],
1760
- ingress: [
1761
- {
1762
- from: [{ namespaceSelector: { matchLabels: spec.sourceNamespaceSelector } }],
1763
- ...(spec.ports ? { ports: spec.ports } : {}),
1764
- },
1765
- ],
1766
- ...(spec.labels ? { labels: spec.labels } : {}),
1767
- ...(spec.annotations ? { annotations: spec.annotations } : {}),
1768
- });
1769
- }
1770
- validateNetworkPolicyTypes(spec) {
1771
- if (!spec.policyTypes || spec.policyTypes.length === 0) {
1772
- throw new Error(`NetworkPolicy ${spec.name}: policyTypes must be specified`);
1773
- }
1774
- this.validatePolicyTypeValues(spec);
1775
- this.validatePolicyTypeRules(spec);
1776
- }
1777
- validatePolicyTypeValues(spec) {
1778
- const validTypes = ['Ingress', 'Egress'];
1779
- for (const type of spec.policyTypes) {
1780
- if (!validTypes.includes(type)) {
1781
- throw new Error(`NetworkPolicy ${spec.name}: invalid policyType '${type}', must be 'Ingress' or 'Egress'`);
1782
- }
1783
- }
1784
- }
1785
- validatePolicyTypeRules(spec) {
1786
- if (spec.policyTypes.includes('Ingress') && spec.ingress === undefined) {
1787
- console.warn(`NetworkPolicy ${spec.name}: policyType 'Ingress' specified but no ingress rules defined (will deny all ingress)`);
1788
- }
1789
- if (spec.policyTypes.includes('Egress') && spec.egress === undefined) {
1790
- console.warn(`NetworkPolicy ${spec.name}: policyType 'Egress' specified but no egress rules defined (will deny all egress)`);
1791
- }
1792
- }
1793
- validateNetworkPolicyCIDRs(spec) {
1794
- const validateCIDR = (cidr, context) => {
1795
- if (!this.isValidCIDR(cidr)) {
1796
- throw new Error(`NetworkPolicy ${spec.name}: invalid CIDR '${cidr}' in ${context}`);
1797
- }
1798
- };
1799
- this.validateIngressCIDRs(spec, validateCIDR);
1800
- this.validateEgressCIDRs(spec, validateCIDR);
1801
- }
1802
- isValidCIDR(cidr) {
1803
- // IPv4 CIDR validation
1804
- const ipv4Parts = cidr.split('/');
1805
- if (ipv4Parts.length === 2) {
1806
- const [ip, prefix] = ipv4Parts;
1807
- if (ip && prefix && this.isValidIPv4(ip) && this.isValidPrefix(prefix, 32)) {
1808
- return true;
1809
- }
1810
- }
1811
- // IPv6 CIDR validation (basic)
1812
- if (cidr.includes(':') && ipv4Parts.length === 2) {
1813
- const [, prefix] = ipv4Parts;
1814
- if (prefix) {
1815
- return this.isValidPrefix(prefix, 128);
1816
- }
1817
- }
1818
- return false;
1819
- }
1820
- isValidIPv4(ip) {
1821
- const parts = ip.split('.');
1822
- if (parts.length !== 4)
1823
- return false;
1824
- return parts.every((part) => {
1825
- const num = parseInt(part, 10);
1826
- return !isNaN(num) && num >= 0 && num <= 255 && String(num) === part;
1827
- });
1828
- }
1829
- isValidPrefix(prefix, maxPrefix) {
1830
- const num = parseInt(prefix, 10);
1831
- return !isNaN(num) && num >= 0 && num <= maxPrefix && String(num) === prefix;
1832
- }
1833
- validateIngressCIDRs(spec, validateCIDR) {
1834
- if (!spec.ingress)
1835
- return;
1836
- for (const rule of spec.ingress) {
1837
- if (rule.from) {
1838
- for (const peer of rule.from) {
1839
- this.validatePeerCIDRs(peer, validateCIDR, 'ingress rule');
1840
- }
1841
- }
1842
- }
1843
- }
1844
- validateEgressCIDRs(spec, validateCIDR) {
1845
- if (!spec.egress)
1846
- return;
1847
- for (const rule of spec.egress) {
1848
- if (rule.to) {
1849
- for (const peer of rule.to) {
1850
- this.validatePeerCIDRs(peer, validateCIDR, 'egress rule');
1851
- }
1852
- }
1853
- }
1854
- }
1855
- validatePeerCIDRs(peer, validateCIDR, context) {
1856
- if (peer.ipBlock?.cidr) {
1857
- validateCIDR(peer.ipBlock.cidr, context);
1858
- if (peer.ipBlock.except) {
1859
- for (const except of peer.ipBlock.except) {
1860
- validateCIDR(except, `${context} except block`);
1861
- }
1862
- }
1863
- }
1864
- }
1865
- validateAGICHealthProbeParams(spec) {
1866
- if (spec.healthProbeInterval !== undefined &&
1867
- (spec.healthProbeInterval < 1 || spec.healthProbeInterval > 86400)) {
1868
- throw new Error('Health probe interval must be between 1 and 86400 seconds');
1869
- }
1870
- if (spec.healthProbeTimeout !== undefined &&
1871
- (spec.healthProbeTimeout < 1 || spec.healthProbeTimeout > 86400)) {
1872
- throw new Error('Health probe timeout must be between 1 and 86400 seconds');
1873
- }
1874
- if (spec.healthProbeUnhealthyThreshold !== undefined &&
1875
- (spec.healthProbeUnhealthyThreshold < 1 || spec.healthProbeUnhealthyThreshold > 20)) {
1876
- throw new Error('Health probe unhealthy threshold must be between 1 and 20');
1877
- }
1878
- }
1879
- validateAGICSecurityParams(spec) {
1880
- if (spec.wafPolicyForPath &&
1881
- !/^\/subscriptions\/[^/]+\/resourceGroups\/[^/]+\/providers\/Microsoft\.Network\/applicationGatewayWebApplicationFirewallPolicies\/[^/]+$/.test(spec.wafPolicyForPath)) {
1882
- throw new Error('Invalid WAF policy resource ID format');
1883
- }
1884
- if (spec.hostnameExtension?.some((hostname) => !this.isValidHostname(hostname))) {
1885
- throw new Error('Invalid hostname format in hostnameExtension');
1886
- }
1887
- }
1888
- isValidHostname(hostname) {
1889
- // Basic hostname validation without complex regex to prevent ReDoS
1890
- if (!hostname || hostname.length > 253)
1891
- return false;
1892
- if (hostname.startsWith('.') || hostname.endsWith('.'))
1893
- return false;
1894
- const labels = hostname.split('.');
1895
- return labels.every((label) => {
1896
- if (!label || label.length > 63)
1897
- return false;
1898
- if (label.startsWith('-') || label.endsWith('-'))
1899
- return false;
1900
- return /^[a-zA-Z0-9-]+$/.test(label);
1901
- });
1902
- }
1903
- validateAGICAnnotationConsistency(spec) {
1904
- if (spec.sslRedirect && !spec.appgwSslCertificate) {
1905
- throw new Error('SSL redirect requires an SSL certificate to be specified');
1906
- }
1907
- if (spec.backendProtocol === 'https' && !spec.appgwTrustedRootCertificate?.length) {
1908
- throw new Error('HTTPS backend protocol requires trusted root certificates');
1909
- }
1910
- if (spec.overrideFrontendPort === 443 && !spec.appgwSslCertificate) {
1911
- throw new Error('Port 443 requires an SSL certificate to be specified');
1912
- }
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
- }
1984
- buildPodEnvironment(spec) {
1985
- const envEntries = spec.env
1986
- ? Object.entries(spec.env).map(([name, value]) => this.sanitizeEnvVar(name, value))
1987
- : undefined;
1988
- const envFromEntries = spec.envFrom
1989
- ? spec.envFrom
1990
- .map((s) => {
1991
- const entry = {};
1992
- if (s.configMapRef) {
1993
- entry.configMapRef = { name: s.configMapRef };
1994
- }
1995
- if (s.secretRef) {
1996
- entry.secretRef = { name: s.secretRef };
1997
- }
1998
- return entry;
1999
- })
2000
- .filter((entry) => entry.configMapRef || entry.secretRef)
2001
- : undefined;
2002
- const volumeDefs = spec.volumes
2003
- ? spec.volumes.map((v) => ({
2004
- name: v.name,
2005
- configMap: v.configMap ? { name: v.configMap } : undefined,
2006
- secret: v.secret ? { secretName: v.secret } : undefined,
2007
- persistentVolumeClaim: v.persistentVolumeClaim
2008
- ? { claimName: v.persistentVolumeClaim }
2009
- : undefined,
2010
- }))
2011
- : undefined;
2012
- const volumeMountDefs = spec.volumeMounts
2013
- ? spec.volumeMounts.map((m) => ({
2014
- name: m.name,
2015
- mountPath: m.mountPath,
2016
- readOnly: m.readOnly,
2017
- subPath: m.subPath,
2018
- }))
2019
- : undefined;
2020
- return { envEntries, envFromEntries, volumeDefs, volumeMountDefs };
2021
- }
2022
- buildJobSpec(spec) {
2023
- return {
2024
- ...(spec.backoffLimit !== undefined ? { backoffLimit: spec.backoffLimit } : {}),
2025
- ...(spec.activeDeadlineSeconds !== undefined
2026
- ? { activeDeadlineSeconds: spec.activeDeadlineSeconds }
2027
- : {}),
2028
- ...(spec.ttlSecondsAfterFinished !== undefined
2029
- ? { ttlSecondsAfterFinished: spec.ttlSecondsAfterFinished }
2030
- : {}),
2031
- ...(spec.completions !== undefined ? { completions: spec.completions } : {}),
2032
- ...(spec.parallelism !== undefined ? { parallelism: spec.parallelism } : {}),
2033
- ...(spec.completionMode !== undefined ? { completionMode: spec.completionMode } : {}),
2034
- ...(spec.suspend !== undefined ? { suspend: spec.suspend } : {}),
2035
- };
2036
- }
2037
- buildJobPodTemplate(spec, envEntries, envFromEntries, volumeDefs, volumeMountDefs) {
2038
- return {
2039
- metadata: {
2040
- ...(spec.podLabels ? { labels: spec.podLabels } : {}),
2041
- ...(spec.podAnnotations ? { annotations: spec.podAnnotations } : {}),
2042
- },
2043
- spec: {
2044
- restartPolicy: spec.restartPolicy ?? 'Never',
2045
- serviceAccountName: spec.serviceAccountName,
2046
- volumes: volumeDefs,
2047
- containers: [
2048
- {
2049
- name: spec.name,
2050
- image: spec.image,
2051
- imagePullPolicy: spec.imagePullPolicy,
2052
- ...(spec.command ? { command: spec.command } : {}),
2053
- ...(spec.args ? { args: spec.args } : {}),
2054
- env: envEntries,
2055
- envFrom: envFromEntries,
2056
- volumeMounts: volumeMountDefs,
2057
- resources: spec.resources ?? this.getDefaultResources(),
2058
- },
2059
- ],
2060
- },
2061
- };
2062
- }
2063
- buildCronJobSpec(spec) {
2064
- return {
2065
- schedule: spec.schedule,
2066
- ...(spec.startingDeadlineSeconds !== undefined
2067
- ? { startingDeadlineSeconds: spec.startingDeadlineSeconds }
2068
- : {}),
2069
- ...(spec.concurrencyPolicy !== undefined
2070
- ? { concurrencyPolicy: spec.concurrencyPolicy }
2071
- : {}),
2072
- ...(spec.suspend !== undefined ? { suspend: spec.suspend } : {}),
2073
- ...(spec.successfulJobsHistoryLimit !== undefined
2074
- ? { successfulJobsHistoryLimit: spec.successfulJobsHistoryLimit }
2075
- : {}),
2076
- ...(spec.failedJobsHistoryLimit !== undefined
2077
- ? { failedJobsHistoryLimit: spec.failedJobsHistoryLimit }
2078
- : {}),
2079
- ...(spec.timeZone !== undefined ? { timeZone: spec.timeZone } : {}),
2080
- };
2081
- }
2082
- buildCronJobTemplate(spec, envEntries, envFromEntries, volumeDefs, volumeMountDefs) {
2083
- return {
2084
- spec: {
2085
- ...(spec.backoffLimit !== undefined ? { backoffLimit: spec.backoffLimit } : {}),
2086
- ...(spec.activeDeadlineSeconds !== undefined
2087
- ? { activeDeadlineSeconds: spec.activeDeadlineSeconds }
2088
- : {}),
2089
- ...(spec.ttlSecondsAfterFinished !== undefined
2090
- ? { ttlSecondsAfterFinished: spec.ttlSecondsAfterFinished }
2091
- : {}),
2092
- ...(spec.completions !== undefined ? { completions: spec.completions } : {}),
2093
- ...(spec.parallelism !== undefined ? { parallelism: spec.parallelism } : {}),
2094
- ...(spec.completionMode !== undefined ? { completionMode: spec.completionMode } : {}),
2095
- template: {
2096
- metadata: {
2097
- ...(spec.podLabels ? { labels: spec.podLabels } : {}),
2098
- ...(spec.podAnnotations ? { annotations: spec.podAnnotations } : {}),
2099
- },
2100
- spec: {
2101
- restartPolicy: spec.restartPolicy ?? 'Never',
2102
- serviceAccountName: spec.serviceAccountName,
2103
- volumes: volumeDefs,
2104
- containers: [
2105
- {
2106
- name: spec.name,
2107
- image: spec.image,
2108
- imagePullPolicy: spec.imagePullPolicy,
2109
- ...(spec.command ? { command: spec.command } : {}),
2110
- ...(spec.args ? { args: spec.args } : {}),
2111
- env: envEntries,
2112
- envFrom: envFromEntries,
2113
- volumeMounts: volumeMountDefs,
2114
- resources: spec.resources ?? this.getDefaultResources(),
2115
- },
2116
- ],
2117
- },
2118
- },
2119
- },
2120
- };
2121
- }
2122
- /**
2123
- * Validate Job specification for security and best practices
2124
- */
2125
- validateJobSpec(spec) {
2126
- if (spec.activeDeadlineSeconds !== undefined && spec.activeDeadlineSeconds <= 0) {
2127
- throw new Error('activeDeadlineSeconds must be a positive integer');
2128
- }
2129
- if (spec.ttlSecondsAfterFinished !== undefined && spec.ttlSecondsAfterFinished < 0) {
2130
- throw new Error('ttlSecondsAfterFinished must be a non-negative integer');
2131
- }
2132
- }
2133
- /**
2134
- * Validate CronJob specification for security and best practices
2135
- */
2136
- validateCronJobSpec(spec) {
2137
- this.validateCronSchedule(spec.schedule);
2138
- if (spec.suspend && spec.startingDeadlineSeconds !== undefined) {
2139
- throw new Error('startingDeadlineSeconds should not be set when job is suspended');
2140
- }
2141
- if (spec.successfulJobsHistoryLimit !== undefined && spec.successfulJobsHistoryLimit < 0) {
2142
- throw new Error('successfulJobsHistoryLimit must be non-negative');
2143
- }
2144
- if (spec.failedJobsHistoryLimit !== undefined && spec.failedJobsHistoryLimit < 0) {
2145
- throw new Error('failedJobsHistoryLimit must be non-negative');
2146
- }
2147
- if (spec.activeDeadlineSeconds !== undefined && spec.activeDeadlineSeconds <= 0) {
2148
- throw new Error('activeDeadlineSeconds must be a positive integer');
2149
- }
2150
- if (spec.ttlSecondsAfterFinished !== undefined && spec.ttlSecondsAfterFinished < 0) {
2151
- throw new Error('ttlSecondsAfterFinished must be a non-negative integer');
2152
- }
2153
- }
2154
- /**
2155
- * Validate cron schedule expression
2156
- */
2157
- validateCronSchedule(schedule) {
2158
- const cronRegex = /^(\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\*\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\*|([0-9]|1[0-9]|2[0-3])|\*\/([0-9]|1[0-9]|2[0-3])) (\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\*\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\*|([1-9]|1[0-2])|\*\/([1-9]|1[0-2])) (\*|([0-6])|\*\/([0-6]))$/;
2159
- if (!cronRegex.test(schedule)) {
2160
- throw new Error('Invalid cron schedule expression');
2161
- }
2162
- }
2163
- /**
2164
- * Sanitize environment variable names and values
2165
- */
2166
- sanitizeEnvVar(name, value) {
2167
- const nameRegex = /^[A-Za-z_][A-Za-z0-9_]*$/;
2168
- if (!nameRegex.test(name)) {
2169
- throw new Error(`Invalid environment variable name: ${name}`);
2170
- }
2171
- // Remove control characters from value (ASCII 0-31 and 127)
2172
- const sanitizedValue = value.replace(/[\cA-\c_\x7F]/g, '');
2173
- return { name, value: sanitizedValue };
2174
- }
2175
- /**
2176
- * Get default resource limits for Jobs and CronJobs
2177
- */
2178
- getDefaultResources() {
2179
- return {
2180
- limits: {
2181
- cpu: '1',
2182
- memory: '512Mi',
2183
- },
2184
- requests: {
2185
- cpu: '100m',
2186
- memory: '128Mi',
2187
- },
2188
- };
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
- }
2221
- /**
2222
- * Capture the YAML of an ApiObject or Construct into assets.
2223
- */
2224
- capture(_obj, id) {
2225
- // Ensure the app can synth
2226
- // We defer synth until write(), but we can capture logical ids now
2227
- this.assets.push({ id, yaml: `# captured:${id}` });
2228
- }
2229
- /**
2230
- * Extract a meaningful name from a Kubernetes resource object
2231
- */
2232
- getResourceName(obj) {
2233
- if (obj && typeof obj === 'object') {
2234
- const resource = obj;
2235
- if (resource.kind && resource.metadata?.name) {
2236
- return `${resource.kind.toLowerCase()}-${resource.metadata.name}`;
2237
- }
2238
- if (resource.kind) {
2239
- return resource.kind.toLowerCase();
2240
- }
2241
- }
2242
- return 'resource';
2243
- }
2244
- /**
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
2262
- */
2263
- addCrd(yaml, id = 'crd') {
2264
- this.assets.push({ id, yaml, target: 'crds' });
2265
- }
2266
- /**
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
2752
- */
2753
- write(outDir) {
2754
- // Use cdk8s Testing.synth to obtain manifest objects
2755
- const manifestObjs = Testing.synth(this.chart);
2756
- // Enforce common labels best-practice on all rendered objects
2757
- const enriched = manifestObjs.map((obj) => {
2758
- if (obj && typeof obj === 'object') {
2759
- const o = obj;
2760
- o.metadata = o.metadata ?? {};
2761
- o.metadata.labels = o.metadata.labels ?? {};
2762
- const labels = o.metadata.labels;
2763
- const defaults = {
2764
- 'helm.sh/chart': '{{ .Chart.Name }}-{{ .Chart.Version }}',
2765
- 'app.kubernetes.io/name': include(Rutter.HELPER_NAME),
2766
- 'app.kubernetes.io/instance': helm.releaseName,
2767
- 'app.kubernetes.io/version': helm.chartVersion,
2768
- 'app.kubernetes.io/managed-by': '{{ .Release.Service }}',
2769
- 'app.kubernetes.io/part-of': helm.chartName,
2770
- };
2771
- for (const [k, v] of Object.entries(defaults)) {
2772
- if (!(k in labels)) {
2773
- // eslint-disable-next-line security/detect-object-injection -- Safe: controlled label assignment
2774
- labels[k] = v;
2775
- }
2776
- }
2777
- }
2778
- return obj;
2779
- });
2780
- if (this.props.singleManifestFile) {
2781
- // Combine all resources into a single manifest file
2782
- const combinedYaml = enriched
2783
- .map((obj) => YAML.stringify(obj).trim())
2784
- .filter(Boolean)
2785
- .join('\n---\n');
2786
- this.assets.length = 0;
2787
- const manifestId = this.props.manifestName ?? 'manifests';
2788
- this.assets.push({ id: manifestId, yaml: combinedYaml, singleFile: true });
2789
- }
2790
- else {
2791
- // Create separate files for each resource (default behavior)
2792
- this.assets.length = 0;
2793
- enriched.forEach((obj, _index) => {
2794
- const yaml = YAML.stringify(obj).trim();
2795
- if (yaml) {
2796
- const resourceName = this.getResourceName(obj);
2797
- const manifestId = this.props.manifestName
2798
- ? `${this.props.manifestName}-${resourceName}`
2799
- : resourceName;
2800
- this.assets.push({ id: manifestId, yaml });
2801
- }
2802
- });
2803
- }
2804
- const options = {
2805
- outDir,
2806
- meta: this.props.meta,
2807
- defaultValues: this.props.defaultValues
2808
- ? this.applyOverrides(this.props.defaultValues)
2809
- : undefined,
2810
- envValues: this.props.envValues,
2811
- assets: this.assets,
2812
- ...(this.props.helpersTpl !== undefined
2813
- ? { helpersTpl: this.props.helpersTpl }
2814
- : {
2815
- helpersTpl: [
2816
- {
2817
- name: 'timonel.name',
2818
- body: '{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}',
2819
- },
2820
- {
2821
- name: 'timonel.fullname',
2822
- body: '{{- if .Values.fullnameOverride -}}\n' +
2823
- '{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}\n' +
2824
- '{{- else -}}\n' +
2825
- '{{- printf "%s-%s" .Chart.Name .Release.Name | trunc 63 | trimSuffix "-" -}}\n' +
2826
- '{{- end -}}',
2827
- },
2828
- {
2829
- name: 'timonel.chart',
2830
- body: '{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}',
2831
- },
2832
- {
2833
- name: 'timonel.labels',
2834
- body: 'helm.sh/chart: {{ include "timonel.chart" . }}\n' +
2835
- 'app.kubernetes.io/name: {{ include "timonel.name" . }}\n' +
2836
- 'app.kubernetes.io/instance: {{ .Release.Name }}\n' +
2837
- 'app.kubernetes.io/version: {{ .Chart.Version }}\n' +
2838
- 'app.kubernetes.io/managed-by: {{ .Release.Service }}\n' +
2839
- 'app.kubernetes.io/part-of: {{ .Chart.Name }}',
2840
- },
2841
- {
2842
- name: 'timonel.selectorLabels',
2843
- body: 'app.kubernetes.io/name: {{ include "timonel.name" . }}\n' +
2844
- 'app.kubernetes.io/instance: {{ .Release.Name }}',
2845
- },
2846
- ],
2847
- }),
2848
- ...(this.props.notesTpl !== undefined ? { notesTpl: this.props.notesTpl } : {}),
2849
- ...(this.props.valuesSchema !== undefined ? { valuesSchema: this.props.valuesSchema } : {}),
2850
- };
2851
- HelmChartWriter.write(options);
2852
- }
2853
- }
2854
- Rutter.LABEL_NAME = 'app.kubernetes.io/name';
2855
- Rutter.LABEL_INSTANCE = 'app.kubernetes.io/instance';
2856
- Rutter.HELPER_NAME = 'timonel.name';
2857
- Rutter.NETWORKING_API_VERSION = 'networking.k8s.io/v1';
2858
- Rutter.STORAGE_API_VERSION = 'storage.k8s.io/v1';
2859
- Rutter.AZURE_DISK_CSI_DRIVER = 'disk.csi.azure.com';
2860
- Rutter.AZURE_DISK_PROVISIONER_ANNOTATION = 'volume.beta.kubernetes.io/storage-provisioner';
2861
- Rutter.AZURE_FILES_CSI_DRIVER = 'file.csi.azure.com';
2862
- Rutter.EMPTY_POD_SELECTOR = {};
2863
- //# sourceMappingURL=Rutter.js.map