timonel 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +5 -1
- package/SECURITY.md +3 -3
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/{HelmChartWriter.d.ts → helmChartWriter.d.ts} +1 -1
- package/dist/lib/{HelmChartWriter.d.ts.map → helmChartWriter.d.ts.map} +1 -1
- package/dist/lib/{HelmChartWriter.js → helmChartWriter.js} +1 -1
- package/dist/lib/{HelmChartWriter.js.map → helmChartWriter.js.map} +1 -1
- package/dist/lib/resources/autoscaling/autoscalingResources.d.ts +26 -0
- package/dist/lib/resources/autoscaling/autoscalingResources.d.ts.map +1 -0
- package/dist/lib/resources/autoscaling/autoscalingResources.js +35 -0
- package/dist/lib/resources/autoscaling/autoscalingResources.js.map +1 -0
- package/dist/lib/resources/baseResourceProvider.d.ts +47 -0
- package/dist/lib/resources/baseResourceProvider.d.ts.map +1 -0
- package/dist/lib/resources/baseResourceProvider.js +83 -0
- package/dist/lib/resources/baseResourceProvider.js.map +1 -0
- package/dist/lib/resources/cloud/aws/awsResources.d.ts +165 -0
- package/dist/lib/resources/cloud/aws/awsResources.d.ts.map +1 -0
- package/dist/lib/resources/cloud/aws/awsResources.js +187 -0
- package/dist/lib/resources/cloud/aws/awsResources.js.map +1 -0
- package/dist/lib/resources/cloud/azure/azureResources.d.ts +169 -0
- package/dist/lib/resources/cloud/azure/azureResources.d.ts.map +1 -0
- package/dist/lib/resources/cloud/azure/azureResources.js +218 -0
- package/dist/lib/resources/cloud/azure/azureResources.js.map +1 -0
- package/dist/lib/resources/cloud/gcp/gcpResources.d.ts +133 -0
- package/dist/lib/resources/cloud/gcp/gcpResources.d.ts.map +1 -0
- package/dist/lib/resources/cloud/gcp/gcpResources.js +162 -0
- package/dist/lib/resources/cloud/gcp/gcpResources.js.map +1 -0
- package/dist/lib/resources/core/coreResources.d.ts +584 -0
- package/dist/lib/resources/core/coreResources.d.ts.map +1 -0
- package/dist/lib/resources/core/coreResources.js +796 -0
- package/dist/lib/resources/core/coreResources.js.map +1 -0
- package/dist/lib/resources/core/storageResources.d.ts +133 -0
- package/dist/lib/resources/core/storageResources.d.ts.map +1 -0
- package/dist/lib/resources/core/storageResources.js +120 -0
- package/dist/lib/resources/core/storageResources.js.map +1 -0
- package/dist/lib/resources/validation/validationResources.d.ts +26 -0
- package/dist/lib/resources/validation/validationResources.d.ts.map +1 -0
- package/dist/lib/resources/validation/validationResources.js +33 -0
- package/dist/lib/resources/validation/validationResources.js.map +1 -0
- package/dist/lib/rutter.d.ts +554 -0
- package/dist/lib/rutter.d.ts.map +1 -0
- package/dist/lib/rutter.js +665 -0
- package/dist/lib/rutter.js.map +1 -0
- package/dist/lib/security.d.ts +21 -0
- package/dist/lib/security.d.ts.map +1 -1
- package/dist/lib/security.js +21 -0
- package/dist/lib/security.js.map +1 -1
- package/dist/lib/umbrella.d.ts +22 -2
- package/dist/lib/umbrella.d.ts.map +1 -1
- package/dist/lib/umbrella.js +21 -1
- package/dist/lib/umbrella.js.map +1 -1
- package/dist/lib/umbrellaRutter.d.ts +97 -0
- package/dist/lib/umbrellaRutter.d.ts.map +1 -0
- package/dist/lib/{UmbrellaRutter.js → umbrellaRutter.js} +44 -1
- package/dist/lib/umbrellaRutter.js.map +1 -0
- package/package.json +2 -1
- package/dist/lib/Rutter.d.ts +0 -2688
- package/dist/lib/Rutter.d.ts.map +0 -1
- package/dist/lib/Rutter.js +0 -2863
- package/dist/lib/Rutter.js.map +0 -1
- package/dist/lib/UmbrellaRutter.d.ts +0 -34
- package/dist/lib/UmbrellaRutter.d.ts.map +0 -1
- package/dist/lib/UmbrellaRutter.js.map +0 -1
package/dist/lib/Rutter.js
DELETED
|
@@ -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
|