timonel 2.2.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 +16 -0
- package/README.md +5 -1
- package/SECURITY.md +15 -8
- package/dist/cli.js +29 -31
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/helm.d.ts +200 -12
- package/dist/lib/helm.d.ts.map +1 -1
- package/dist/lib/helm.js +234 -13
- package/dist/lib/helm.js.map +1 -1
- package/dist/lib/helmChartWriter.d.ts +238 -0
- package/dist/lib/helmChartWriter.d.ts.map +1 -0
- package/dist/lib/helmChartWriter.js +318 -0
- package/dist/lib/helmChartWriter.js.map +1 -0
- 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 +73 -0
- package/dist/lib/security.d.ts.map +1 -0
- package/dist/lib/security.js +138 -0
- package/dist/lib/security.js.map +1 -0
- 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} +72 -23
- package/dist/lib/umbrellaRutter.js.map +1 -0
- package/package.json +2 -1
- package/dist/lib/HelmChartWriter.d.ts +0 -69
- package/dist/lib/HelmChartWriter.d.ts.map +0 -1
- package/dist/lib/HelmChartWriter.js +0 -124
- package/dist/lib/HelmChartWriter.js.map +0 -1
- package/dist/lib/Rutter.d.ts +0 -1334
- package/dist/lib/Rutter.d.ts.map +0 -1
- package/dist/lib/Rutter.js +0 -2138
- 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,2138 +0,0 @@
|
|
|
1
|
-
import { App, Chart, Testing, ApiObject } from 'cdk8s';
|
|
2
|
-
import YAML from 'yaml';
|
|
3
|
-
import { include, helm } from './helm.js';
|
|
4
|
-
import { HelmChartWriter } from './HelmChartWriter.js';
|
|
5
|
-
/**
|
|
6
|
-
* Rutter builds Kubernetes manifests using cdk8s and writes a Helm chart.
|
|
7
|
-
*/
|
|
8
|
-
export class Rutter {
|
|
9
|
-
constructor(props) {
|
|
10
|
-
this.assets = [];
|
|
11
|
-
this.valueOverrides = {};
|
|
12
|
-
this.props = props;
|
|
13
|
-
this.app = new App();
|
|
14
|
-
this.chart = new Chart(this.app, props.manifestName ?? props.meta.name);
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Set dynamic value overrides (from --set flags)
|
|
18
|
-
*/
|
|
19
|
-
setValues(overrides) {
|
|
20
|
-
this.valueOverrides = { ...this.valueOverrides, ...overrides };
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Apply value overrides to nested object using dot notation
|
|
24
|
-
*/
|
|
25
|
-
applyOverrides(values) {
|
|
26
|
-
const result = JSON.parse(JSON.stringify(values)); // Deep clone
|
|
27
|
-
for (const [key, value] of Object.entries(this.valueOverrides)) {
|
|
28
|
-
this.setNestedValue(result, key, this.parseValue(value));
|
|
29
|
-
}
|
|
30
|
-
return result;
|
|
31
|
-
}
|
|
32
|
-
setNestedValue(obj, path, value) {
|
|
33
|
-
/* eslint-disable security/detect-object-injection */
|
|
34
|
-
const keys = path.split('.').filter(Boolean);
|
|
35
|
-
if (keys.length === 0)
|
|
36
|
-
return;
|
|
37
|
-
let current = obj;
|
|
38
|
-
for (let i = 0; i < keys.length - 1; i++) {
|
|
39
|
-
const key = keys[i];
|
|
40
|
-
if (!key)
|
|
41
|
-
continue;
|
|
42
|
-
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
|
|
43
|
-
current[key] = {};
|
|
44
|
-
}
|
|
45
|
-
current = current[key];
|
|
46
|
-
}
|
|
47
|
-
const finalKey = keys[keys.length - 1];
|
|
48
|
-
if (finalKey) {
|
|
49
|
-
current[finalKey] = value;
|
|
50
|
-
}
|
|
51
|
-
/* eslint-enable security/detect-object-injection */
|
|
52
|
-
}
|
|
53
|
-
parseValue(value) {
|
|
54
|
-
// Try to parse as JSON first (for objects, arrays, booleans, numbers)
|
|
55
|
-
try {
|
|
56
|
-
return JSON.parse(value);
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
// If not valid JSON, treat as string
|
|
60
|
-
return value;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
addDeployment(spec) {
|
|
64
|
-
const match = spec.matchLabels ?? {
|
|
65
|
-
[Rutter.LABEL_NAME]: include(Rutter.HELPER_NAME),
|
|
66
|
-
[Rutter.LABEL_INSTANCE]: helm.releaseName,
|
|
67
|
-
};
|
|
68
|
-
const envEntries = spec.env
|
|
69
|
-
? Object.entries(spec.env).map(([name, value]) => ({ name, value }))
|
|
70
|
-
: undefined;
|
|
71
|
-
const envFromEntries = spec.envFrom
|
|
72
|
-
? spec.envFrom.map((s) => ({
|
|
73
|
-
configMapRef: s.configMapRef ? { name: s.configMapRef } : undefined,
|
|
74
|
-
secretRef: s.secretRef ? { name: s.secretRef } : undefined,
|
|
75
|
-
}))
|
|
76
|
-
: undefined;
|
|
77
|
-
const volumeDefs = spec.volumes
|
|
78
|
-
? spec.volumes.map((v) => ({
|
|
79
|
-
name: v.name,
|
|
80
|
-
configMap: v.configMap ? { name: v.configMap } : undefined,
|
|
81
|
-
secret: v.secret ? { secretName: v.secret } : undefined,
|
|
82
|
-
persistentVolumeClaim: v.persistentVolumeClaim
|
|
83
|
-
? { claimName: v.persistentVolumeClaim }
|
|
84
|
-
: undefined,
|
|
85
|
-
}))
|
|
86
|
-
: undefined;
|
|
87
|
-
const volumeMountDefs = spec.volumeMounts
|
|
88
|
-
? spec.volumeMounts.map((m) => ({
|
|
89
|
-
name: m.name,
|
|
90
|
-
mountPath: m.mountPath,
|
|
91
|
-
readOnly: m.readOnly,
|
|
92
|
-
subPath: m.subPath,
|
|
93
|
-
}))
|
|
94
|
-
: undefined;
|
|
95
|
-
const templateLabels = { ...match, ...(spec.podLabels ?? {}) };
|
|
96
|
-
const dep = new ApiObject(this.chart, spec.name, {
|
|
97
|
-
apiVersion: 'apps/v1',
|
|
98
|
-
kind: 'Deployment',
|
|
99
|
-
metadata: {
|
|
100
|
-
name: spec.name,
|
|
101
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
102
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
103
|
-
},
|
|
104
|
-
spec: {
|
|
105
|
-
replicas: spec.replicas ?? 1,
|
|
106
|
-
selector: { matchLabels: match },
|
|
107
|
-
template: {
|
|
108
|
-
metadata: {
|
|
109
|
-
labels: templateLabels,
|
|
110
|
-
...(spec.podAnnotations ? { annotations: spec.podAnnotations } : {}),
|
|
111
|
-
},
|
|
112
|
-
spec: {
|
|
113
|
-
serviceAccountName: spec.serviceAccountName,
|
|
114
|
-
volumes: volumeDefs,
|
|
115
|
-
containers: [
|
|
116
|
-
{
|
|
117
|
-
name: spec.name,
|
|
118
|
-
image: spec.image,
|
|
119
|
-
imagePullPolicy: spec.imagePullPolicy,
|
|
120
|
-
ports: spec.containerPort ? [{ containerPort: spec.containerPort }] : undefined,
|
|
121
|
-
env: envEntries,
|
|
122
|
-
envFrom: envFromEntries,
|
|
123
|
-
volumeMounts: volumeMountDefs,
|
|
124
|
-
resources: spec.resources,
|
|
125
|
-
livenessProbe: spec.livenessProbe,
|
|
126
|
-
readinessProbe: spec.readinessProbe,
|
|
127
|
-
},
|
|
128
|
-
],
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
this.capture(dep, `${spec.name}-deployment`);
|
|
134
|
-
return dep;
|
|
135
|
-
}
|
|
136
|
-
addService(spec) {
|
|
137
|
-
const svc = new ApiObject(this.chart, spec.name, {
|
|
138
|
-
apiVersion: 'v1',
|
|
139
|
-
kind: 'Service',
|
|
140
|
-
metadata: {
|
|
141
|
-
name: spec.name,
|
|
142
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
143
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
144
|
-
},
|
|
145
|
-
spec: {
|
|
146
|
-
type: spec.type ?? 'ClusterIP',
|
|
147
|
-
selector: spec.selector,
|
|
148
|
-
ports: spec.ports.map((p) => ({
|
|
149
|
-
port: p.port,
|
|
150
|
-
targetPort: p.targetPort ?? p.port,
|
|
151
|
-
protocol: p.protocol ?? 'TCP',
|
|
152
|
-
name: p.name,
|
|
153
|
-
nodePort: p.nodePort,
|
|
154
|
-
})),
|
|
155
|
-
clusterIP: spec.clusterIP,
|
|
156
|
-
externalName: spec.externalName,
|
|
157
|
-
sessionAffinity: spec.sessionAffinity,
|
|
158
|
-
loadBalancerIP: spec.loadBalancerIP,
|
|
159
|
-
loadBalancerSourceRanges: spec.loadBalancerSourceRanges,
|
|
160
|
-
loadBalancerClass: spec.loadBalancerClass,
|
|
161
|
-
externalTrafficPolicy: spec.externalTrafficPolicy,
|
|
162
|
-
internalTrafficPolicy: spec.internalTrafficPolicy,
|
|
163
|
-
ipFamilyPolicy: spec.ipFamilyPolicy,
|
|
164
|
-
ipFamilies: spec.ipFamilies,
|
|
165
|
-
},
|
|
166
|
-
});
|
|
167
|
-
this.capture(svc, `${spec.name}-service`);
|
|
168
|
-
return svc;
|
|
169
|
-
}
|
|
170
|
-
addReplicaSet(spec) {
|
|
171
|
-
const match = spec.matchLabels ?? {
|
|
172
|
-
[Rutter.LABEL_NAME]: include(Rutter.HELPER_NAME),
|
|
173
|
-
[Rutter.LABEL_INSTANCE]: helm.releaseName,
|
|
174
|
-
};
|
|
175
|
-
const envEntries = spec.env
|
|
176
|
-
? Object.entries(spec.env).map(([name, value]) => ({ name, value }))
|
|
177
|
-
: undefined;
|
|
178
|
-
const envFromEntries = spec.envFrom
|
|
179
|
-
? spec.envFrom.map((s) => ({
|
|
180
|
-
configMapRef: s.configMapRef ? { name: s.configMapRef } : undefined,
|
|
181
|
-
secretRef: s.secretRef ? { name: s.secretRef } : undefined,
|
|
182
|
-
}))
|
|
183
|
-
: undefined;
|
|
184
|
-
const volumeDefs = spec.volumes
|
|
185
|
-
? spec.volumes.map((v) => ({
|
|
186
|
-
name: v.name,
|
|
187
|
-
configMap: v.configMap ? { name: v.configMap } : undefined,
|
|
188
|
-
secret: v.secret ? { secretName: v.secret } : undefined,
|
|
189
|
-
persistentVolumeClaim: v.persistentVolumeClaim
|
|
190
|
-
? { claimName: v.persistentVolumeClaim }
|
|
191
|
-
: undefined,
|
|
192
|
-
}))
|
|
193
|
-
: undefined;
|
|
194
|
-
const volumeMountDefs = spec.volumeMounts
|
|
195
|
-
? spec.volumeMounts.map((m) => ({
|
|
196
|
-
name: m.name,
|
|
197
|
-
mountPath: m.mountPath,
|
|
198
|
-
readOnly: m.readOnly,
|
|
199
|
-
subPath: m.subPath,
|
|
200
|
-
}))
|
|
201
|
-
: undefined;
|
|
202
|
-
const templateLabels = { ...match, ...(spec.podLabels ?? {}) };
|
|
203
|
-
const rs = new ApiObject(this.chart, spec.name, {
|
|
204
|
-
apiVersion: 'apps/v1',
|
|
205
|
-
kind: 'ReplicaSet',
|
|
206
|
-
metadata: {
|
|
207
|
-
name: spec.name,
|
|
208
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
209
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
210
|
-
},
|
|
211
|
-
spec: {
|
|
212
|
-
replicas: spec.replicas ?? 1,
|
|
213
|
-
selector: { matchLabels: match },
|
|
214
|
-
template: {
|
|
215
|
-
metadata: {
|
|
216
|
-
labels: templateLabels,
|
|
217
|
-
...(spec.podAnnotations ? { annotations: spec.podAnnotations } : {}),
|
|
218
|
-
},
|
|
219
|
-
spec: {
|
|
220
|
-
serviceAccountName: spec.serviceAccountName,
|
|
221
|
-
volumes: volumeDefs,
|
|
222
|
-
containers: [
|
|
223
|
-
{
|
|
224
|
-
name: spec.name,
|
|
225
|
-
image: spec.image,
|
|
226
|
-
imagePullPolicy: spec.imagePullPolicy,
|
|
227
|
-
ports: spec.containerPort ? [{ containerPort: spec.containerPort }] : undefined,
|
|
228
|
-
env: envEntries,
|
|
229
|
-
envFrom: envFromEntries,
|
|
230
|
-
volumeMounts: volumeMountDefs,
|
|
231
|
-
resources: spec.resources,
|
|
232
|
-
livenessProbe: spec.livenessProbe,
|
|
233
|
-
readinessProbe: spec.readinessProbe,
|
|
234
|
-
},
|
|
235
|
-
],
|
|
236
|
-
},
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
});
|
|
240
|
-
this.capture(rs, `${spec.name}-replicaset`);
|
|
241
|
-
return rs;
|
|
242
|
-
}
|
|
243
|
-
addJob(spec) {
|
|
244
|
-
// Validate Job specification
|
|
245
|
-
this.validateJobSpec(spec);
|
|
246
|
-
const { envEntries, envFromEntries, volumeDefs, volumeMountDefs } = this.buildPodEnvironment(spec);
|
|
247
|
-
const jobSpec = this.buildJobSpec(spec);
|
|
248
|
-
const podTemplate = this.buildJobPodTemplate(spec, envEntries, envFromEntries, volumeDefs, volumeMountDefs);
|
|
249
|
-
const job = new ApiObject(this.chart, spec.name, {
|
|
250
|
-
apiVersion: 'batch/v1',
|
|
251
|
-
kind: 'Job',
|
|
252
|
-
metadata: {
|
|
253
|
-
name: spec.name,
|
|
254
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
255
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
256
|
-
},
|
|
257
|
-
spec: {
|
|
258
|
-
...jobSpec,
|
|
259
|
-
template: podTemplate,
|
|
260
|
-
},
|
|
261
|
-
});
|
|
262
|
-
this.capture(job, `${spec.name}-job`);
|
|
263
|
-
return job;
|
|
264
|
-
}
|
|
265
|
-
addCronJob(spec) {
|
|
266
|
-
// Validate CronJob specification
|
|
267
|
-
this.validateCronJobSpec(spec);
|
|
268
|
-
const { envEntries, envFromEntries, volumeDefs, volumeMountDefs } = this.buildPodEnvironment(spec);
|
|
269
|
-
const cronJobSpec = this.buildCronJobSpec(spec);
|
|
270
|
-
const jobTemplate = this.buildCronJobTemplate(spec, envEntries, envFromEntries, volumeDefs, volumeMountDefs);
|
|
271
|
-
const cronJob = new ApiObject(this.chart, spec.name, {
|
|
272
|
-
apiVersion: 'batch/v1',
|
|
273
|
-
kind: 'CronJob',
|
|
274
|
-
metadata: {
|
|
275
|
-
name: spec.name,
|
|
276
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
277
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
278
|
-
},
|
|
279
|
-
spec: {
|
|
280
|
-
...cronJobSpec,
|
|
281
|
-
jobTemplate,
|
|
282
|
-
},
|
|
283
|
-
});
|
|
284
|
-
this.capture(cronJob, `${spec.name}-cronjob`);
|
|
285
|
-
return cronJob;
|
|
286
|
-
}
|
|
287
|
-
addIngress(spec) {
|
|
288
|
-
const ing = new ApiObject(this.chart, spec.name, {
|
|
289
|
-
apiVersion: Rutter.NETWORKING_API_VERSION,
|
|
290
|
-
kind: 'Ingress',
|
|
291
|
-
metadata: {
|
|
292
|
-
name: spec.name,
|
|
293
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
294
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
295
|
-
},
|
|
296
|
-
spec: {
|
|
297
|
-
ingressClassName: spec.ingressClassName,
|
|
298
|
-
defaultBackend: spec.defaultBackend,
|
|
299
|
-
tls: spec.tls,
|
|
300
|
-
rules: spec.rules.map((rule) => ({
|
|
301
|
-
host: rule.host,
|
|
302
|
-
http: {
|
|
303
|
-
paths: rule.paths.map((path) => ({
|
|
304
|
-
path: path.path,
|
|
305
|
-
pathType: path.pathType,
|
|
306
|
-
backend: path.backend,
|
|
307
|
-
})),
|
|
308
|
-
},
|
|
309
|
-
})),
|
|
310
|
-
},
|
|
311
|
-
});
|
|
312
|
-
this.capture(ing, `${spec.name}-ingress`);
|
|
313
|
-
return ing;
|
|
314
|
-
}
|
|
315
|
-
addPersistentVolume(spec) {
|
|
316
|
-
// Apply cloud provider optimizations
|
|
317
|
-
const optimizedSpec = this.optimizePVForCloud(spec);
|
|
318
|
-
const pvSpec = {
|
|
319
|
-
capacity: { storage: optimizedSpec.capacity },
|
|
320
|
-
accessModes: optimizedSpec.accessModes,
|
|
321
|
-
storageClassName: optimizedSpec.storageClassName,
|
|
322
|
-
volumeMode: optimizedSpec.volumeMode,
|
|
323
|
-
persistentVolumeReclaimPolicy: optimizedSpec.reclaimPolicy,
|
|
324
|
-
mountOptions: optimizedSpec.mountOptions,
|
|
325
|
-
nodeAffinity: optimizedSpec.nodeAffinity,
|
|
326
|
-
claimRef: optimizedSpec.claimRef,
|
|
327
|
-
};
|
|
328
|
-
// Handle known volume sources with validation
|
|
329
|
-
const knownSources = [
|
|
330
|
-
'csi',
|
|
331
|
-
'nfs',
|
|
332
|
-
'awsElasticBlockStore',
|
|
333
|
-
'hostPath',
|
|
334
|
-
'azureDisk',
|
|
335
|
-
'azureFile',
|
|
336
|
-
'gcePersistentDisk',
|
|
337
|
-
];
|
|
338
|
-
let sourceCount = 0;
|
|
339
|
-
for (const sourceType of knownSources) {
|
|
340
|
-
// eslint-disable-next-line security/detect-object-injection -- Safe: sourceType is from known const array
|
|
341
|
-
const sourceValue = optimizedSpec.source[sourceType];
|
|
342
|
-
if (sourceValue) {
|
|
343
|
-
// eslint-disable-next-line security/detect-object-injection -- Safe: sourceType is from known const array
|
|
344
|
-
pvSpec[sourceType] = sourceValue;
|
|
345
|
-
sourceCount++;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
// Validate single volume source
|
|
349
|
-
if (sourceCount === 0) {
|
|
350
|
-
throw new Error(`PersistentVolume ${spec.name}: No volume source specified`);
|
|
351
|
-
}
|
|
352
|
-
if (sourceCount > 1) {
|
|
353
|
-
throw new Error(`PersistentVolume ${spec.name}: Multiple volume sources specified, only one allowed`);
|
|
354
|
-
}
|
|
355
|
-
// Handle custom volume sources
|
|
356
|
-
for (const [key, value] of Object.entries(optimizedSpec.source)) {
|
|
357
|
-
if (!knownSources.includes(key) && value !== undefined) {
|
|
358
|
-
// eslint-disable-next-line security/detect-object-injection -- Safe: controlled object construction
|
|
359
|
-
pvSpec[key] = value;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
const pv = new ApiObject(this.chart, spec.name, {
|
|
363
|
-
apiVersion: 'v1',
|
|
364
|
-
kind: 'PersistentVolume',
|
|
365
|
-
metadata: {
|
|
366
|
-
name: spec.name,
|
|
367
|
-
...(optimizedSpec.labels ? { labels: optimizedSpec.labels } : {}),
|
|
368
|
-
...(optimizedSpec.annotations ? { annotations: optimizedSpec.annotations } : {}),
|
|
369
|
-
},
|
|
370
|
-
spec: pvSpec,
|
|
371
|
-
});
|
|
372
|
-
this.capture(pv, `${spec.name}-pv`);
|
|
373
|
-
return pv;
|
|
374
|
-
}
|
|
375
|
-
optimizePVForCloud(spec) {
|
|
376
|
-
const optimized = { ...spec };
|
|
377
|
-
const EBS_CSI_DRIVER = 'ebs.csi.aws.com';
|
|
378
|
-
// Apply cloud-specific optimizations
|
|
379
|
-
if (spec.cloudProvider === 'aws') {
|
|
380
|
-
optimized.annotations = {
|
|
381
|
-
...optimized.annotations,
|
|
382
|
-
'volume.beta.kubernetes.io/storage-provisioner': EBS_CSI_DRIVER,
|
|
383
|
-
};
|
|
384
|
-
// Default to gp3 for better performance/cost
|
|
385
|
-
if (spec.source.csi?.driver === EBS_CSI_DRIVER && optimized.source.csi) {
|
|
386
|
-
optimized.source.csi.volumeAttributes = {
|
|
387
|
-
type: 'gp3',
|
|
388
|
-
...spec.source.csi.volumeAttributes,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
else if (spec.cloudProvider === 'azure') {
|
|
393
|
-
optimized.annotations = {
|
|
394
|
-
...optimized.annotations,
|
|
395
|
-
[Rutter.AZURE_DISK_PROVISIONER_ANNOTATION]: Rutter.AZURE_DISK_CSI_DRIVER,
|
|
396
|
-
};
|
|
397
|
-
// Default to Premium_ZRS for multi-zone clusters
|
|
398
|
-
if (spec.source.csi?.driver === Rutter.AZURE_DISK_CSI_DRIVER && optimized.source.csi) {
|
|
399
|
-
optimized.source.csi.volumeAttributes = {
|
|
400
|
-
skuName: 'Premium_ZRS',
|
|
401
|
-
...spec.source.csi.volumeAttributes,
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
else if (spec.cloudProvider === 'gcp') {
|
|
406
|
-
optimized.annotations = {
|
|
407
|
-
...optimized.annotations,
|
|
408
|
-
'volume.beta.kubernetes.io/storage-provisioner': 'pd.csi.storage.gke.io',
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
// Set sensible defaults
|
|
412
|
-
optimized.reclaimPolicy = optimized.reclaimPolicy ?? 'Delete';
|
|
413
|
-
optimized.volumeMode = optimized.volumeMode ?? 'Filesystem';
|
|
414
|
-
return optimized;
|
|
415
|
-
}
|
|
416
|
-
addPersistentVolumeClaim(spec) {
|
|
417
|
-
// Validate access modes compatibility
|
|
418
|
-
this.validatePVCAccessModes(spec);
|
|
419
|
-
const pvc = new ApiObject(this.chart, spec.name, {
|
|
420
|
-
apiVersion: 'v1',
|
|
421
|
-
kind: 'PersistentVolumeClaim',
|
|
422
|
-
metadata: {
|
|
423
|
-
name: spec.name,
|
|
424
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
425
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
426
|
-
},
|
|
427
|
-
spec: {
|
|
428
|
-
accessModes: spec.accessModes,
|
|
429
|
-
resources: spec.resources,
|
|
430
|
-
storageClassName: spec.storageClassName,
|
|
431
|
-
volumeMode: spec.volumeMode ?? 'Filesystem',
|
|
432
|
-
selector: spec.selector,
|
|
433
|
-
volumeName: spec.volumeName,
|
|
434
|
-
},
|
|
435
|
-
});
|
|
436
|
-
this.capture(pvc, `${spec.name}-pvc`);
|
|
437
|
-
return pvc;
|
|
438
|
-
}
|
|
439
|
-
validatePVCAccessModes(spec) {
|
|
440
|
-
const { accessModes, storageClassName } = spec;
|
|
441
|
-
// Validate ReadWriteOncePod (K8s 1.22+)
|
|
442
|
-
if (accessModes.includes('ReadWriteOncePod') && accessModes.length > 1) {
|
|
443
|
-
throw new Error(`PVC ${spec.name}: ReadWriteOncePod cannot be combined with other access modes`);
|
|
444
|
-
}
|
|
445
|
-
// Warn about common misconfigurations
|
|
446
|
-
if (storageClassName?.includes('azure-disk') && accessModes.includes('ReadWriteMany')) {
|
|
447
|
-
console.warn(`PVC ${spec.name}: Azure Disk does not support ReadWriteMany, consider Azure Files`);
|
|
448
|
-
}
|
|
449
|
-
if (storageClassName?.includes('ebs') && accessModes.includes('ReadWriteMany')) {
|
|
450
|
-
console.warn(`PVC ${spec.name}: EBS does not support ReadWriteMany, consider EFS`);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
addConfigMap(spec) {
|
|
454
|
-
const cm = new ApiObject(this.chart, spec.name, {
|
|
455
|
-
apiVersion: 'v1',
|
|
456
|
-
kind: 'ConfigMap',
|
|
457
|
-
metadata: {
|
|
458
|
-
name: spec.name,
|
|
459
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
460
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
461
|
-
},
|
|
462
|
-
data: spec.data,
|
|
463
|
-
binaryData: spec.binaryData,
|
|
464
|
-
immutable: spec.immutable,
|
|
465
|
-
});
|
|
466
|
-
this.capture(cm, `${spec.name}-configmap`);
|
|
467
|
-
return cm;
|
|
468
|
-
}
|
|
469
|
-
addSecret(spec) {
|
|
470
|
-
const sec = new ApiObject(this.chart, spec.name, {
|
|
471
|
-
apiVersion: 'v1',
|
|
472
|
-
kind: 'Secret',
|
|
473
|
-
metadata: {
|
|
474
|
-
name: spec.name,
|
|
475
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
476
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
477
|
-
},
|
|
478
|
-
type: spec.type ?? 'Opaque',
|
|
479
|
-
stringData: spec.stringData,
|
|
480
|
-
data: spec.data,
|
|
481
|
-
immutable: spec.immutable,
|
|
482
|
-
});
|
|
483
|
-
this.capture(sec, `${spec.name}-secret`);
|
|
484
|
-
return sec;
|
|
485
|
-
}
|
|
486
|
-
addServiceAccount(spec) {
|
|
487
|
-
const annotations = this.buildServiceAccountAnnotations(spec);
|
|
488
|
-
const sa = new ApiObject(this.chart, spec.name, {
|
|
489
|
-
apiVersion: 'v1',
|
|
490
|
-
kind: 'ServiceAccount',
|
|
491
|
-
metadata: {
|
|
492
|
-
name: spec.name,
|
|
493
|
-
...(Object.keys(annotations).length ? { annotations } : {}),
|
|
494
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
495
|
-
},
|
|
496
|
-
automountServiceAccountToken: spec.automountServiceAccountToken,
|
|
497
|
-
imagePullSecrets: spec.imagePullSecrets?.map((n) => ({ name: n })),
|
|
498
|
-
secrets: spec.secrets?.map((n) => ({ name: n })),
|
|
499
|
-
});
|
|
500
|
-
this.capture(sa, `${spec.name}-serviceaccount`);
|
|
501
|
-
return sa;
|
|
502
|
-
}
|
|
503
|
-
buildServiceAccountAnnotations(spec) {
|
|
504
|
-
const annotations = {
|
|
505
|
-
...(spec.annotations ?? {}),
|
|
506
|
-
};
|
|
507
|
-
this.addAWSIRSAAnnotations(annotations, spec);
|
|
508
|
-
this.addAzureWorkloadIdentityAnnotations(annotations, spec);
|
|
509
|
-
this.addGCPWorkloadIdentityAnnotations(annotations, spec);
|
|
510
|
-
return annotations;
|
|
511
|
-
}
|
|
512
|
-
addAWSIRSAAnnotations(annotations, spec) {
|
|
513
|
-
if (spec.awsRoleArn) {
|
|
514
|
-
annotations['eks.amazonaws.com/role-arn'] = spec.awsRoleArn;
|
|
515
|
-
}
|
|
516
|
-
if (spec.awsAudience) {
|
|
517
|
-
annotations['eks.amazonaws.com/audience'] = spec.awsAudience;
|
|
518
|
-
}
|
|
519
|
-
if (spec.awsStsEndpointType) {
|
|
520
|
-
annotations['eks.amazonaws.com/sts-regional-endpoints'] = spec.awsStsEndpointType;
|
|
521
|
-
}
|
|
522
|
-
if (spec.awsTokenExpiration !== undefined) {
|
|
523
|
-
annotations['eks.amazonaws.com/token-expiration'] = String(spec.awsTokenExpiration);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
addAzureWorkloadIdentityAnnotations(annotations, spec) {
|
|
527
|
-
if (spec.azureClientId) {
|
|
528
|
-
annotations['azure.workload.identity/client-id'] = spec.azureClientId;
|
|
529
|
-
}
|
|
530
|
-
if (spec.azureTenantId) {
|
|
531
|
-
annotations['azure.workload.identity/tenant-id'] = spec.azureTenantId;
|
|
532
|
-
}
|
|
533
|
-
if (spec.azureServiceAccountTokenExpiration !== undefined) {
|
|
534
|
-
annotations['azure.workload.identity/service-account-token-expiration'] = String(spec.azureServiceAccountTokenExpiration);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
addGCPWorkloadIdentityAnnotations(annotations, spec) {
|
|
538
|
-
if (spec.gcpServiceAccountEmail) {
|
|
539
|
-
annotations['iam.gke.io/gcp-service-account'] = spec.gcpServiceAccountEmail;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
addHorizontalPodAutoscaler(spec) {
|
|
543
|
-
// Default metrics if none provided (CPU utilization at 80%)
|
|
544
|
-
const defaultMetrics = [
|
|
545
|
-
{
|
|
546
|
-
type: 'Resource',
|
|
547
|
-
resource: {
|
|
548
|
-
name: 'cpu',
|
|
549
|
-
target: {
|
|
550
|
-
type: 'Utilization',
|
|
551
|
-
averageUtilization: 80,
|
|
552
|
-
},
|
|
553
|
-
},
|
|
554
|
-
},
|
|
555
|
-
];
|
|
556
|
-
const hpa = new ApiObject(this.chart, spec.name, {
|
|
557
|
-
apiVersion: 'autoscaling/v2',
|
|
558
|
-
kind: 'HorizontalPodAutoscaler',
|
|
559
|
-
metadata: {
|
|
560
|
-
name: spec.name,
|
|
561
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
562
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
563
|
-
},
|
|
564
|
-
spec: {
|
|
565
|
-
scaleTargetRef: spec.scaleTargetRef,
|
|
566
|
-
minReplicas: spec.minReplicas ?? 1,
|
|
567
|
-
maxReplicas: spec.maxReplicas,
|
|
568
|
-
metrics: spec.metrics ?? defaultMetrics,
|
|
569
|
-
...(spec.behavior ? { behavior: spec.behavior } : {}),
|
|
570
|
-
},
|
|
571
|
-
});
|
|
572
|
-
this.capture(hpa, `${spec.name}-hpa`);
|
|
573
|
-
return hpa;
|
|
574
|
-
}
|
|
575
|
-
addVerticalPodAutoscaler(spec) {
|
|
576
|
-
const vpa = new ApiObject(this.chart, spec.name, {
|
|
577
|
-
apiVersion: 'autoscaling.k8s.io/v1',
|
|
578
|
-
kind: 'VerticalPodAutoscaler',
|
|
579
|
-
metadata: {
|
|
580
|
-
name: spec.name,
|
|
581
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
582
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
583
|
-
},
|
|
584
|
-
spec: {
|
|
585
|
-
targetRef: spec.targetRef,
|
|
586
|
-
updatePolicy: spec.updatePolicy ?? { updateMode: 'Auto' },
|
|
587
|
-
...(spec.resourcePolicy ? { resourcePolicy: spec.resourcePolicy } : {}),
|
|
588
|
-
...(spec.recommenders ? { recommenders: spec.recommenders } : {}),
|
|
589
|
-
},
|
|
590
|
-
});
|
|
591
|
-
this.capture(vpa, `${spec.name}-vpa`);
|
|
592
|
-
return vpa;
|
|
593
|
-
}
|
|
594
|
-
addPodDisruptionBudget(spec) {
|
|
595
|
-
const pdb = new ApiObject(this.chart, spec.name, {
|
|
596
|
-
apiVersion: 'policy/v1',
|
|
597
|
-
kind: 'PodDisruptionBudget',
|
|
598
|
-
metadata: {
|
|
599
|
-
name: spec.name,
|
|
600
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
601
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
602
|
-
},
|
|
603
|
-
spec: {
|
|
604
|
-
...(spec.minAvailable !== undefined ? { minAvailable: spec.minAvailable } : {}),
|
|
605
|
-
...(spec.maxUnavailable !== undefined ? { maxUnavailable: spec.maxUnavailable } : {}),
|
|
606
|
-
selector: spec.selector,
|
|
607
|
-
},
|
|
608
|
-
});
|
|
609
|
-
this.capture(pdb, `${spec.name}-pdb`);
|
|
610
|
-
return pdb;
|
|
611
|
-
}
|
|
612
|
-
addAWSEBSStorageClass(spec) {
|
|
613
|
-
const DEFAULT_VOLUME_TYPE = 'gp3';
|
|
614
|
-
const parameters = {
|
|
615
|
-
type: spec.volumeType ?? DEFAULT_VOLUME_TYPE,
|
|
616
|
-
fsType: spec.fsType ?? 'ext4',
|
|
617
|
-
encrypted: String(spec.encrypted ?? true),
|
|
618
|
-
};
|
|
619
|
-
if (spec.kmsKeyId) {
|
|
620
|
-
parameters['kmsKeyId'] = spec.kmsKeyId;
|
|
621
|
-
}
|
|
622
|
-
const IOPS_SUPPORTED_TYPES = ['gp3', 'io1', 'io2'];
|
|
623
|
-
if (spec.iops && IOPS_SUPPORTED_TYPES.includes(spec.volumeType ?? DEFAULT_VOLUME_TYPE)) {
|
|
624
|
-
parameters['iops'] = String(spec.iops);
|
|
625
|
-
}
|
|
626
|
-
if (spec.throughput && spec.volumeType === DEFAULT_VOLUME_TYPE) {
|
|
627
|
-
parameters['throughput'] = String(spec.throughput);
|
|
628
|
-
}
|
|
629
|
-
const sc = new ApiObject(this.chart, spec.name, {
|
|
630
|
-
apiVersion: Rutter.STORAGE_API_VERSION,
|
|
631
|
-
kind: 'StorageClass',
|
|
632
|
-
metadata: {
|
|
633
|
-
name: spec.name,
|
|
634
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
635
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
636
|
-
},
|
|
637
|
-
provisioner: 'ebs.csi.aws.com',
|
|
638
|
-
parameters,
|
|
639
|
-
reclaimPolicy: spec.reclaimPolicy ?? 'Delete',
|
|
640
|
-
allowVolumeExpansion: spec.allowVolumeExpansion ?? true,
|
|
641
|
-
volumeBindingMode: spec.volumeBindingMode ?? 'WaitForFirstConsumer',
|
|
642
|
-
});
|
|
643
|
-
this.capture(sc, `${spec.name}-storageclass`);
|
|
644
|
-
return sc;
|
|
645
|
-
}
|
|
646
|
-
addAWSEBSPersistentVolumeClaim(spec) {
|
|
647
|
-
const pvc = new ApiObject(this.chart, spec.name, {
|
|
648
|
-
apiVersion: 'v1',
|
|
649
|
-
kind: 'PersistentVolumeClaim',
|
|
650
|
-
metadata: {
|
|
651
|
-
name: spec.name,
|
|
652
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
653
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
654
|
-
},
|
|
655
|
-
spec: {
|
|
656
|
-
accessModes: spec.accessModes ?? ['ReadWriteOnce'],
|
|
657
|
-
storageClassName: spec.storageClassName,
|
|
658
|
-
resources: {
|
|
659
|
-
requests: {
|
|
660
|
-
storage: spec.size,
|
|
661
|
-
},
|
|
662
|
-
},
|
|
663
|
-
},
|
|
664
|
-
});
|
|
665
|
-
this.capture(pvc, `${spec.name}-pvc`);
|
|
666
|
-
return pvc;
|
|
667
|
-
}
|
|
668
|
-
addAWSEFSStorageClass(spec) {
|
|
669
|
-
const sc = new ApiObject(this.chart, spec.name, {
|
|
670
|
-
apiVersion: Rutter.STORAGE_API_VERSION,
|
|
671
|
-
kind: 'StorageClass',
|
|
672
|
-
metadata: {
|
|
673
|
-
name: spec.name,
|
|
674
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
675
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
676
|
-
},
|
|
677
|
-
provisioner: 'efs.csi.aws.com',
|
|
678
|
-
reclaimPolicy: spec.reclaimPolicy ?? 'Delete',
|
|
679
|
-
volumeBindingMode: spec.volumeBindingMode ?? 'Immediate',
|
|
680
|
-
});
|
|
681
|
-
this.capture(sc, `${spec.name}-storageclass`);
|
|
682
|
-
return sc;
|
|
683
|
-
}
|
|
684
|
-
addAWSEFSPersistentVolume(spec) {
|
|
685
|
-
const csiSpec = {
|
|
686
|
-
driver: 'efs.csi.aws.com',
|
|
687
|
-
volumeHandle: spec.fileSystemId,
|
|
688
|
-
};
|
|
689
|
-
if (spec.accessPoint) {
|
|
690
|
-
csiSpec['volumeHandle'] = `${spec.fileSystemId}::${spec.accessPoint}`;
|
|
691
|
-
}
|
|
692
|
-
const volumeAttributes = {};
|
|
693
|
-
if (spec.directoryPerms) {
|
|
694
|
-
volumeAttributes['directoryPerms'] = spec.directoryPerms;
|
|
695
|
-
}
|
|
696
|
-
if (spec.gidRangeStart !== undefined) {
|
|
697
|
-
volumeAttributes['gidRangeStart'] = String(spec.gidRangeStart);
|
|
698
|
-
}
|
|
699
|
-
if (spec.gidRangeEnd !== undefined) {
|
|
700
|
-
volumeAttributes['gidRangeEnd'] = String(spec.gidRangeEnd);
|
|
701
|
-
}
|
|
702
|
-
if (spec.basePath) {
|
|
703
|
-
volumeAttributes['basePath'] = spec.basePath;
|
|
704
|
-
}
|
|
705
|
-
if (spec.subPath) {
|
|
706
|
-
volumeAttributes['subPath'] = spec.subPath;
|
|
707
|
-
}
|
|
708
|
-
if (Object.keys(volumeAttributes).length > 0) {
|
|
709
|
-
csiSpec['volumeAttributes'] = volumeAttributes;
|
|
710
|
-
}
|
|
711
|
-
const pv = new ApiObject(this.chart, spec.name, {
|
|
712
|
-
apiVersion: 'v1',
|
|
713
|
-
kind: 'PersistentVolume',
|
|
714
|
-
metadata: {
|
|
715
|
-
name: spec.name,
|
|
716
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
717
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
718
|
-
},
|
|
719
|
-
spec: {
|
|
720
|
-
capacity: {
|
|
721
|
-
storage: spec.capacity ?? '5Gi',
|
|
722
|
-
},
|
|
723
|
-
accessModes: spec.accessModes ?? ['ReadWriteMany'],
|
|
724
|
-
persistentVolumeReclaimPolicy: 'Retain',
|
|
725
|
-
storageClassName: spec.storageClassName,
|
|
726
|
-
csi: csiSpec,
|
|
727
|
-
},
|
|
728
|
-
});
|
|
729
|
-
this.capture(pv, `${spec.name}-pv`);
|
|
730
|
-
return pv;
|
|
731
|
-
}
|
|
732
|
-
addAWSEFSPersistentVolumeClaim(spec) {
|
|
733
|
-
const pvc = new ApiObject(this.chart, spec.name, {
|
|
734
|
-
apiVersion: 'v1',
|
|
735
|
-
kind: 'PersistentVolumeClaim',
|
|
736
|
-
metadata: {
|
|
737
|
-
name: spec.name,
|
|
738
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
739
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
740
|
-
},
|
|
741
|
-
spec: {
|
|
742
|
-
accessModes: spec.accessModes ?? ['ReadWriteMany'],
|
|
743
|
-
storageClassName: spec.storageClassName,
|
|
744
|
-
resources: {
|
|
745
|
-
requests: {
|
|
746
|
-
storage: spec.size ?? '5Gi',
|
|
747
|
-
},
|
|
748
|
-
},
|
|
749
|
-
},
|
|
750
|
-
});
|
|
751
|
-
this.capture(pvc, `${spec.name}-pvc`);
|
|
752
|
-
return pvc;
|
|
753
|
-
}
|
|
754
|
-
addAWSALBIngress(spec) {
|
|
755
|
-
const annotations = this.buildALBAnnotations(spec);
|
|
756
|
-
const ingress = new ApiObject(this.chart, spec.name, {
|
|
757
|
-
apiVersion: Rutter.NETWORKING_API_VERSION,
|
|
758
|
-
kind: 'Ingress',
|
|
759
|
-
metadata: {
|
|
760
|
-
name: spec.name,
|
|
761
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
762
|
-
annotations,
|
|
763
|
-
},
|
|
764
|
-
spec: {
|
|
765
|
-
tls: spec.tls,
|
|
766
|
-
rules: spec.rules.map((rule) => ({
|
|
767
|
-
host: rule.host,
|
|
768
|
-
http: {
|
|
769
|
-
paths: rule.paths.map((path) => ({
|
|
770
|
-
path: path.path,
|
|
771
|
-
pathType: path.pathType,
|
|
772
|
-
backend: path.backend,
|
|
773
|
-
})),
|
|
774
|
-
},
|
|
775
|
-
})),
|
|
776
|
-
},
|
|
777
|
-
});
|
|
778
|
-
this.capture(ingress, `${spec.name}-ingress`);
|
|
779
|
-
return ingress;
|
|
780
|
-
}
|
|
781
|
-
buildALBAnnotations(spec) {
|
|
782
|
-
const annotations = {
|
|
783
|
-
'kubernetes.io/ingress.class': 'alb',
|
|
784
|
-
'alb.ingress.kubernetes.io/scheme': spec.scheme ?? 'internet-facing',
|
|
785
|
-
'alb.ingress.kubernetes.io/target-type': spec.targetType ?? 'ip',
|
|
786
|
-
...(spec.annotations ?? {}),
|
|
787
|
-
};
|
|
788
|
-
this.addOptionalALBAnnotations(annotations, spec);
|
|
789
|
-
this.addHealthCheckAnnotations(annotations, spec);
|
|
790
|
-
this.addTagsAnnotation(annotations, spec);
|
|
791
|
-
return annotations;
|
|
792
|
-
}
|
|
793
|
-
addOptionalALBAnnotations(annotations, spec) {
|
|
794
|
-
if (spec.ipAddressType) {
|
|
795
|
-
annotations['alb.ingress.kubernetes.io/ip-address-type'] = spec.ipAddressType;
|
|
796
|
-
}
|
|
797
|
-
if (spec.groupName) {
|
|
798
|
-
annotations['alb.ingress.kubernetes.io/group.name'] = spec.groupName;
|
|
799
|
-
}
|
|
800
|
-
if (spec.groupOrder !== undefined) {
|
|
801
|
-
annotations['alb.ingress.kubernetes.io/group.order'] = String(spec.groupOrder);
|
|
802
|
-
}
|
|
803
|
-
if (spec.subnets && spec.subnets.length > 0) {
|
|
804
|
-
annotations['alb.ingress.kubernetes.io/subnets'] = spec.subnets.join(',');
|
|
805
|
-
}
|
|
806
|
-
if (spec.securityGroups && spec.securityGroups.length > 0) {
|
|
807
|
-
annotations['alb.ingress.kubernetes.io/security-groups'] = spec.securityGroups.join(',');
|
|
808
|
-
}
|
|
809
|
-
if (spec.certificateArn) {
|
|
810
|
-
annotations['alb.ingress.kubernetes.io/certificate-arn'] = spec.certificateArn;
|
|
811
|
-
}
|
|
812
|
-
if (spec.sslRedirect) {
|
|
813
|
-
annotations['alb.ingress.kubernetes.io/ssl-redirect'] = '443';
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
addHealthCheckAnnotations(annotations, spec) {
|
|
817
|
-
if (spec.healthCheckPath) {
|
|
818
|
-
annotations['alb.ingress.kubernetes.io/healthcheck-path'] = spec.healthCheckPath;
|
|
819
|
-
}
|
|
820
|
-
if (spec.healthCheckIntervalSeconds !== undefined) {
|
|
821
|
-
annotations['alb.ingress.kubernetes.io/healthcheck-interval-seconds'] = String(spec.healthCheckIntervalSeconds);
|
|
822
|
-
}
|
|
823
|
-
if (spec.healthCheckTimeoutSeconds !== undefined) {
|
|
824
|
-
annotations['alb.ingress.kubernetes.io/healthcheck-timeout-seconds'] = String(spec.healthCheckTimeoutSeconds);
|
|
825
|
-
}
|
|
826
|
-
if (spec.healthyThresholdCount !== undefined) {
|
|
827
|
-
annotations['alb.ingress.kubernetes.io/healthy-threshold-count'] = String(spec.healthyThresholdCount);
|
|
828
|
-
}
|
|
829
|
-
if (spec.unhealthyThresholdCount !== undefined) {
|
|
830
|
-
annotations['alb.ingress.kubernetes.io/unhealthy-threshold-count'] = String(spec.unhealthyThresholdCount);
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
addTagsAnnotation(annotations, spec) {
|
|
834
|
-
if (spec.tags && Object.keys(spec.tags).length > 0) {
|
|
835
|
-
const tagString = this.formatCloudResourceTags(spec.tags);
|
|
836
|
-
annotations['alb.ingress.kubernetes.io/tags'] = tagString;
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
/**
|
|
840
|
-
* Add Azure Application Gateway Ingress Controller (AGIC) Ingress.
|
|
841
|
-
* Creates an Ingress resource with AGIC-specific annotations for AKS deployments.
|
|
842
|
-
*
|
|
843
|
-
* @param spec - AGIC Ingress specification
|
|
844
|
-
* @returns The created Ingress ApiObject
|
|
845
|
-
*
|
|
846
|
-
* @example
|
|
847
|
-
* ```typescript
|
|
848
|
-
* rutter.addAzureAGICIngress({
|
|
849
|
-
* name: 'app-ingress',
|
|
850
|
-
* rules: [{
|
|
851
|
-
* host: 'app.example.com',
|
|
852
|
-
* paths: [{
|
|
853
|
-
* path: '/',
|
|
854
|
-
* pathType: 'Prefix',
|
|
855
|
-
* backend: { service: { name: 'app-service', port: { number: 80 } } }
|
|
856
|
-
* }]
|
|
857
|
-
* }],
|
|
858
|
-
* sslRedirect: true,
|
|
859
|
-
* backendProtocol: 'https',
|
|
860
|
-
* healthProbePath: '/health'
|
|
861
|
-
* });
|
|
862
|
-
* ```
|
|
863
|
-
*/
|
|
864
|
-
addAzureAGICIngress(spec) {
|
|
865
|
-
this.validateAGICHealthProbeParams(spec);
|
|
866
|
-
this.validateAGICSecurityParams(spec);
|
|
867
|
-
this.validateAGICAnnotationConsistency(spec);
|
|
868
|
-
const annotations = this.buildAGICAnnotations(spec);
|
|
869
|
-
const ingress = new ApiObject(this.chart, spec.name, {
|
|
870
|
-
apiVersion: Rutter.NETWORKING_API_VERSION,
|
|
871
|
-
kind: 'Ingress',
|
|
872
|
-
metadata: {
|
|
873
|
-
name: spec.name,
|
|
874
|
-
annotations,
|
|
875
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
876
|
-
},
|
|
877
|
-
spec: {
|
|
878
|
-
tls: spec.tls,
|
|
879
|
-
rules: spec.rules.map((rule) => ({
|
|
880
|
-
host: rule.host,
|
|
881
|
-
http: {
|
|
882
|
-
paths: rule.paths.map((path) => ({
|
|
883
|
-
path: path.path,
|
|
884
|
-
pathType: path.pathType,
|
|
885
|
-
backend: path.backend,
|
|
886
|
-
})),
|
|
887
|
-
},
|
|
888
|
-
})),
|
|
889
|
-
},
|
|
890
|
-
});
|
|
891
|
-
this.capture(ingress, `${spec.name}-ingress`);
|
|
892
|
-
return ingress;
|
|
893
|
-
}
|
|
894
|
-
buildAGICAnnotations(spec) {
|
|
895
|
-
const annotations = {
|
|
896
|
-
'kubernetes.io/ingress.class': 'azure/application-gateway',
|
|
897
|
-
...(spec.annotations ?? {}),
|
|
898
|
-
};
|
|
899
|
-
this.addAGICPathAnnotations(annotations, spec);
|
|
900
|
-
this.addAGICProtocolAnnotations(annotations, spec);
|
|
901
|
-
this.addAGICHealthProbeAnnotations(annotations, spec);
|
|
902
|
-
this.addAGICConnectionAnnotations(annotations, spec);
|
|
903
|
-
this.addAGICSecurityAnnotations(annotations, spec);
|
|
904
|
-
this.addAGICAdvancedAnnotations(annotations, spec);
|
|
905
|
-
return annotations;
|
|
906
|
-
}
|
|
907
|
-
addAGICPathAnnotations(annotations, spec) {
|
|
908
|
-
if (spec.backendPathPrefix) {
|
|
909
|
-
annotations['appgw.ingress.kubernetes.io/backend-path-prefix'] = spec.backendPathPrefix;
|
|
910
|
-
}
|
|
911
|
-
if (spec.backendHostname) {
|
|
912
|
-
annotations['appgw.ingress.kubernetes.io/backend-hostname'] = spec.backendHostname;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
addAGICProtocolAnnotations(annotations, spec) {
|
|
916
|
-
if (spec.backendProtocol) {
|
|
917
|
-
annotations['appgw.ingress.kubernetes.io/backend-protocol'] = spec.backendProtocol;
|
|
918
|
-
}
|
|
919
|
-
if (spec.sslRedirect) {
|
|
920
|
-
annotations['appgw.ingress.kubernetes.io/ssl-redirect'] = 'true';
|
|
921
|
-
}
|
|
922
|
-
if (spec.usePrivateIp) {
|
|
923
|
-
annotations['appgw.ingress.kubernetes.io/use-private-ip'] = 'true';
|
|
924
|
-
}
|
|
925
|
-
if (spec.overrideFrontendPort !== undefined) {
|
|
926
|
-
annotations['appgw.ingress.kubernetes.io/override-frontend-port'] = String(spec.overrideFrontendPort);
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
addAGICHealthProbeAnnotations(annotations, spec) {
|
|
930
|
-
if (spec.healthProbeHostname) {
|
|
931
|
-
annotations['appgw.ingress.kubernetes.io/health-probe-hostname'] = spec.healthProbeHostname;
|
|
932
|
-
}
|
|
933
|
-
if (spec.healthProbePort !== undefined) {
|
|
934
|
-
annotations['appgw.ingress.kubernetes.io/health-probe-port'] = String(spec.healthProbePort);
|
|
935
|
-
}
|
|
936
|
-
if (spec.healthProbePath) {
|
|
937
|
-
annotations['appgw.ingress.kubernetes.io/health-probe-path'] = spec.healthProbePath;
|
|
938
|
-
}
|
|
939
|
-
if (spec.healthProbeStatusCodes) {
|
|
940
|
-
annotations['appgw.ingress.kubernetes.io/health-probe-status-codes'] =
|
|
941
|
-
spec.healthProbeStatusCodes;
|
|
942
|
-
}
|
|
943
|
-
if (spec.healthProbeInterval !== undefined) {
|
|
944
|
-
annotations['appgw.ingress.kubernetes.io/health-probe-interval'] = String(spec.healthProbeInterval);
|
|
945
|
-
}
|
|
946
|
-
if (spec.healthProbeTimeout !== undefined) {
|
|
947
|
-
annotations['appgw.ingress.kubernetes.io/health-probe-timeout'] = String(spec.healthProbeTimeout);
|
|
948
|
-
}
|
|
949
|
-
if (spec.healthProbeUnhealthyThreshold !== undefined) {
|
|
950
|
-
annotations['appgw.ingress.kubernetes.io/health-probe-unhealthy-threshold'] = String(spec.healthProbeUnhealthyThreshold);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
addAGICConnectionAnnotations(annotations, spec) {
|
|
954
|
-
if (spec.cookieBasedAffinity) {
|
|
955
|
-
annotations['appgw.ingress.kubernetes.io/cookie-based-affinity'] = 'true';
|
|
956
|
-
}
|
|
957
|
-
if (spec.requestTimeout !== undefined) {
|
|
958
|
-
annotations['appgw.ingress.kubernetes.io/request-timeout'] = String(spec.requestTimeout);
|
|
959
|
-
}
|
|
960
|
-
if (spec.connectionDraining) {
|
|
961
|
-
annotations['appgw.ingress.kubernetes.io/connection-draining'] = 'true';
|
|
962
|
-
}
|
|
963
|
-
if (spec.connectionDrainingTimeout !== undefined) {
|
|
964
|
-
annotations['appgw.ingress.kubernetes.io/connection-draining-timeout'] = String(spec.connectionDrainingTimeout);
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
addAGICSecurityAnnotations(annotations, spec) {
|
|
968
|
-
if (spec.appgwSslCertificate) {
|
|
969
|
-
annotations['appgw.ingress.kubernetes.io/appgw-ssl-certificate'] = spec.appgwSslCertificate;
|
|
970
|
-
}
|
|
971
|
-
if (spec.appgwSslProfile) {
|
|
972
|
-
annotations['appgw.ingress.kubernetes.io/appgw-ssl-profile'] = spec.appgwSslProfile;
|
|
973
|
-
}
|
|
974
|
-
if (spec.appgwTrustedRootCertificate && spec.appgwTrustedRootCertificate.length > 0) {
|
|
975
|
-
annotations['appgw.ingress.kubernetes.io/appgw-trusted-root-certificate'] =
|
|
976
|
-
spec.appgwTrustedRootCertificate.join(',');
|
|
977
|
-
}
|
|
978
|
-
if (spec.wafPolicyForPath) {
|
|
979
|
-
annotations['appgw.ingress.kubernetes.io/waf-policy-for-path'] = spec.wafPolicyForPath;
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
addAGICAdvancedAnnotations(annotations, spec) {
|
|
983
|
-
if (spec.hostnameExtension && spec.hostnameExtension.length > 0) {
|
|
984
|
-
annotations['appgw.ingress.kubernetes.io/hostname-extension'] =
|
|
985
|
-
spec.hostnameExtension.join(', ');
|
|
986
|
-
}
|
|
987
|
-
if (spec.rewriteRuleSet) {
|
|
988
|
-
annotations['appgw.ingress.kubernetes.io/rewrite-rule-set'] = spec.rewriteRuleSet;
|
|
989
|
-
}
|
|
990
|
-
if (spec.rulePriority !== undefined) {
|
|
991
|
-
annotations['appgw.ingress.kubernetes.io/rule-priority'] = String(spec.rulePriority);
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
addAWSIRSAServiceAccount(spec) {
|
|
995
|
-
const annotations = {
|
|
996
|
-
'eks.amazonaws.com/role-arn': spec.roleArn,
|
|
997
|
-
...(spec.audience ? { 'eks.amazonaws.com/audience': spec.audience } : {}),
|
|
998
|
-
...(spec.stsEndpointType
|
|
999
|
-
? { 'eks.amazonaws.com/sts-regional-endpoints': spec.stsEndpointType }
|
|
1000
|
-
: { 'eks.amazonaws.com/sts-regional-endpoints': 'regional' }),
|
|
1001
|
-
...(spec.tokenExpiration !== undefined
|
|
1002
|
-
? { 'eks.amazonaws.com/token-expiration': String(spec.tokenExpiration) }
|
|
1003
|
-
: {}),
|
|
1004
|
-
...(spec.annotations ?? {}),
|
|
1005
|
-
};
|
|
1006
|
-
const sa = new ApiObject(this.chart, spec.name, {
|
|
1007
|
-
apiVersion: 'v1',
|
|
1008
|
-
kind: 'ServiceAccount',
|
|
1009
|
-
metadata: {
|
|
1010
|
-
name: spec.name,
|
|
1011
|
-
annotations,
|
|
1012
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
1013
|
-
},
|
|
1014
|
-
automountServiceAccountToken: spec.automountServiceAccountToken,
|
|
1015
|
-
imagePullSecrets: spec.imagePullSecrets?.map((n) => ({ name: n })),
|
|
1016
|
-
secrets: spec.secrets?.map((n) => ({ name: n })),
|
|
1017
|
-
});
|
|
1018
|
-
this.capture(sa, `${spec.name}-serviceaccount`);
|
|
1019
|
-
return sa;
|
|
1020
|
-
}
|
|
1021
|
-
addAWSSecretProviderClass(spec) {
|
|
1022
|
-
const objects = spec.objects.map((obj) => {
|
|
1023
|
-
const baseObj = {
|
|
1024
|
-
objectName: obj.objectName,
|
|
1025
|
-
objectType: obj.objectType,
|
|
1026
|
-
};
|
|
1027
|
-
if ('objectAlias' in obj && obj.objectAlias) {
|
|
1028
|
-
baseObj['objectAlias'] = obj.objectAlias;
|
|
1029
|
-
}
|
|
1030
|
-
if ('jmesPath' in obj && obj.jmesPath) {
|
|
1031
|
-
baseObj['jmesPath'] = obj.jmesPath;
|
|
1032
|
-
}
|
|
1033
|
-
return baseObj;
|
|
1034
|
-
});
|
|
1035
|
-
const objectsYaml = YAML.stringify(objects, { indent: 2 }).trim();
|
|
1036
|
-
const parameters = {
|
|
1037
|
-
objects: `|\n ${objectsYaml.split('\n').join('\n ')}`,
|
|
1038
|
-
};
|
|
1039
|
-
if (spec.region) {
|
|
1040
|
-
parameters['region'] = spec.region;
|
|
1041
|
-
}
|
|
1042
|
-
const spc = new ApiObject(this.chart, spec.name, {
|
|
1043
|
-
apiVersion: 'secrets-store.csi.x-k8s.io/v1',
|
|
1044
|
-
kind: 'SecretProviderClass',
|
|
1045
|
-
metadata: {
|
|
1046
|
-
name: spec.name,
|
|
1047
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
1048
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
1049
|
-
},
|
|
1050
|
-
spec: {
|
|
1051
|
-
provider: 'aws',
|
|
1052
|
-
parameters,
|
|
1053
|
-
},
|
|
1054
|
-
});
|
|
1055
|
-
this.capture(spc, `${spec.name}-secretproviderclass`);
|
|
1056
|
-
return spc;
|
|
1057
|
-
}
|
|
1058
|
-
/**
|
|
1059
|
-
* Add Azure Workload Identity ServiceAccount.
|
|
1060
|
-
* Creates a ServiceAccount with Azure Workload Identity annotations for secure authentication.
|
|
1061
|
-
*
|
|
1062
|
-
* @param spec - Azure Workload Identity ServiceAccount specification
|
|
1063
|
-
* @returns The created ServiceAccount ApiObject
|
|
1064
|
-
*
|
|
1065
|
-
* @example
|
|
1066
|
-
* ```typescript
|
|
1067
|
-
* rutter.addAzureWorkloadIdentityServiceAccount({
|
|
1068
|
-
* name: 'workload-identity-sa',
|
|
1069
|
-
* clientId: '12345678-1234-1234-1234-123456789012',
|
|
1070
|
-
* tenantId: '87654321-4321-4321-4321-210987654321',
|
|
1071
|
-
* tokenExpiration: 3600
|
|
1072
|
-
* });
|
|
1073
|
-
* ```
|
|
1074
|
-
*/
|
|
1075
|
-
addAzureWorkloadIdentityServiceAccount(spec) {
|
|
1076
|
-
// Validate all inputs first for security
|
|
1077
|
-
if (spec.tokenExpiration !== undefined &&
|
|
1078
|
-
(spec.tokenExpiration < 3600 || spec.tokenExpiration > 86400)) {
|
|
1079
|
-
throw new Error(`Azure Workload Identity ${spec.name}: tokenExpiration must be between 3600 and 86400 seconds`);
|
|
1080
|
-
}
|
|
1081
|
-
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)) {
|
|
1082
|
-
throw new Error(`Azure Workload Identity ${spec.name}: Invalid client ID format`);
|
|
1083
|
-
}
|
|
1084
|
-
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)) {
|
|
1085
|
-
throw new Error(`Azure Workload Identity ${spec.name}: Invalid tenant ID format`);
|
|
1086
|
-
}
|
|
1087
|
-
const annotations = {
|
|
1088
|
-
'azure.workload.identity/client-id': spec.clientId,
|
|
1089
|
-
'azure.workload.identity/tenant-id': spec.tenantId,
|
|
1090
|
-
...(spec.tokenExpiration !== undefined
|
|
1091
|
-
? {
|
|
1092
|
-
'azure.workload.identity/service-account-token-expiration': String(spec.tokenExpiration),
|
|
1093
|
-
}
|
|
1094
|
-
: {}),
|
|
1095
|
-
...(spec.annotations ?? {}),
|
|
1096
|
-
};
|
|
1097
|
-
const sa = new ApiObject(this.chart, spec.name, {
|
|
1098
|
-
apiVersion: 'v1',
|
|
1099
|
-
kind: 'ServiceAccount',
|
|
1100
|
-
metadata: {
|
|
1101
|
-
name: spec.name,
|
|
1102
|
-
annotations,
|
|
1103
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
1104
|
-
},
|
|
1105
|
-
automountServiceAccountToken: spec.automountServiceAccountToken,
|
|
1106
|
-
imagePullSecrets: spec.imagePullSecrets?.map((n) => ({ name: n })),
|
|
1107
|
-
secrets: spec.secrets?.map((n) => ({ name: n })),
|
|
1108
|
-
});
|
|
1109
|
-
this.capture(sa, `${spec.name}-serviceaccount`);
|
|
1110
|
-
return sa;
|
|
1111
|
-
}
|
|
1112
|
-
/**
|
|
1113
|
-
* Add Azure Key Vault SecretProviderClass.
|
|
1114
|
-
* Creates a SecretProviderClass for mounting secrets from Azure Key Vault using CSI driver.
|
|
1115
|
-
*
|
|
1116
|
-
* @param spec - Azure Key Vault SecretProviderClass specification
|
|
1117
|
-
* @returns The created SecretProviderClass ApiObject
|
|
1118
|
-
*
|
|
1119
|
-
* @example
|
|
1120
|
-
* ```typescript
|
|
1121
|
-
* rutter.addAzureKeyVaultSecretProviderClass({
|
|
1122
|
-
* name: 'app-secrets',
|
|
1123
|
-
* keyVaultName: 'my-keyvault',
|
|
1124
|
-
* tenantId: '87654321-4321-4321-4321-210987654321',
|
|
1125
|
-
* objects: [
|
|
1126
|
-
* { objectName: 'database-password', objectType: 'secret' },
|
|
1127
|
-
* { objectName: 'api-key', objectType: 'secret', objectAlias: 'API_KEY' }
|
|
1128
|
-
* ],
|
|
1129
|
-
* userAssignedIdentityID: '12345678-1234-1234-1234-123456789012'
|
|
1130
|
-
* });
|
|
1131
|
-
* ```
|
|
1132
|
-
*/
|
|
1133
|
-
addAzureKeyVaultSecretProviderClass(spec) {
|
|
1134
|
-
// Validate object names for security - prevent path traversal attacks
|
|
1135
|
-
spec.objects.forEach((obj) => {
|
|
1136
|
-
if (obj.objectName.includes('../') || obj.objectName.includes('..\\')) {
|
|
1137
|
-
throw new Error(`Azure Key Vault ${spec.name}: Object name '${obj.objectName}' contains invalid path traversal sequences`);
|
|
1138
|
-
}
|
|
1139
|
-
});
|
|
1140
|
-
const objects = spec.objects.map((obj) => {
|
|
1141
|
-
const baseObj = {
|
|
1142
|
-
objectName: obj.objectName,
|
|
1143
|
-
objectType: obj.objectType,
|
|
1144
|
-
};
|
|
1145
|
-
if (obj.objectAlias) {
|
|
1146
|
-
baseObj['objectAlias'] = obj.objectAlias;
|
|
1147
|
-
}
|
|
1148
|
-
if (obj.objectVersion) {
|
|
1149
|
-
baseObj['objectVersion'] = obj.objectVersion;
|
|
1150
|
-
}
|
|
1151
|
-
return baseObj;
|
|
1152
|
-
});
|
|
1153
|
-
const objectsYaml = YAML.stringify(objects, { indent: 2 }).trim();
|
|
1154
|
-
const parameters = {
|
|
1155
|
-
keyvaultName: spec.keyVaultName,
|
|
1156
|
-
tenantId: spec.tenantId,
|
|
1157
|
-
objects: `|\n ${objectsYaml.split('\n').join('\n ')}`,
|
|
1158
|
-
};
|
|
1159
|
-
if (spec.userAssignedIdentityID) {
|
|
1160
|
-
parameters['userAssignedIdentityID'] = spec.userAssignedIdentityID;
|
|
1161
|
-
}
|
|
1162
|
-
if (spec.cloudName) {
|
|
1163
|
-
parameters['cloudName'] = spec.cloudName;
|
|
1164
|
-
}
|
|
1165
|
-
const spc = new ApiObject(this.chart, spec.name, {
|
|
1166
|
-
apiVersion: 'secrets-store.csi.x-k8s.io/v1',
|
|
1167
|
-
kind: 'SecretProviderClass',
|
|
1168
|
-
metadata: {
|
|
1169
|
-
name: spec.name,
|
|
1170
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
1171
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
1172
|
-
},
|
|
1173
|
-
spec: {
|
|
1174
|
-
provider: 'azure',
|
|
1175
|
-
parameters,
|
|
1176
|
-
},
|
|
1177
|
-
});
|
|
1178
|
-
this.capture(spc, `${spec.name}-secretproviderclass`);
|
|
1179
|
-
return spc;
|
|
1180
|
-
}
|
|
1181
|
-
/**
|
|
1182
|
-
* Add Azure Files StorageClass.
|
|
1183
|
-
* Creates a StorageClass for dynamic provisioning of Azure Files volumes.
|
|
1184
|
-
*
|
|
1185
|
-
* @param spec - Azure Files StorageClass specification
|
|
1186
|
-
* @returns The created StorageClass ApiObject
|
|
1187
|
-
*
|
|
1188
|
-
* @example
|
|
1189
|
-
* ```typescript
|
|
1190
|
-
* rutter.addAzureFilesStorageClass({
|
|
1191
|
-
* name: 'azure-files-premium',
|
|
1192
|
-
* skuName: 'Premium_LRS',
|
|
1193
|
-
* protocol: 'smb',
|
|
1194
|
-
* allowSharedAccess: true
|
|
1195
|
-
* });
|
|
1196
|
-
* ```
|
|
1197
|
-
*/
|
|
1198
|
-
addAzureFilesStorageClass(spec) {
|
|
1199
|
-
// Validate Azure Files parameters for security and compliance
|
|
1200
|
-
this.validateAzureFilesParameters(spec);
|
|
1201
|
-
const parameters = this.buildAzureFilesParameters(spec);
|
|
1202
|
-
const sc = new ApiObject(this.chart, spec.name, {
|
|
1203
|
-
apiVersion: Rutter.STORAGE_API_VERSION,
|
|
1204
|
-
kind: 'StorageClass',
|
|
1205
|
-
metadata: {
|
|
1206
|
-
name: spec.name,
|
|
1207
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
1208
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
1209
|
-
},
|
|
1210
|
-
provisioner: Rutter.AZURE_FILES_CSI_DRIVER,
|
|
1211
|
-
parameters,
|
|
1212
|
-
reclaimPolicy: spec.reclaimPolicy ?? 'Delete',
|
|
1213
|
-
allowVolumeExpansion: spec.allowVolumeExpansion ?? true,
|
|
1214
|
-
volumeBindingMode: spec.volumeBindingMode ?? 'Immediate',
|
|
1215
|
-
...(spec.mountOptions ? { mountOptions: spec.mountOptions } : {}),
|
|
1216
|
-
});
|
|
1217
|
-
this.capture(sc, `${spec.name}-storageclass`);
|
|
1218
|
-
return sc;
|
|
1219
|
-
}
|
|
1220
|
-
/**
|
|
1221
|
-
* Add Azure Files PersistentVolume.
|
|
1222
|
-
* Creates a PersistentVolume for static Azure Files volume provisioning.
|
|
1223
|
-
*
|
|
1224
|
-
* @param spec - Azure Files PersistentVolume specification
|
|
1225
|
-
* @returns The created PersistentVolume ApiObject
|
|
1226
|
-
*/
|
|
1227
|
-
addAzureFilesPersistentVolume(spec) {
|
|
1228
|
-
const csiSpec = this.buildAzureFilesPVCSISpec(spec);
|
|
1229
|
-
const pv = new ApiObject(this.chart, spec.name, {
|
|
1230
|
-
apiVersion: 'v1',
|
|
1231
|
-
kind: 'PersistentVolume',
|
|
1232
|
-
metadata: {
|
|
1233
|
-
name: spec.name,
|
|
1234
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
1235
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
1236
|
-
},
|
|
1237
|
-
spec: {
|
|
1238
|
-
capacity: { storage: spec.capacity },
|
|
1239
|
-
accessModes: spec.accessModes ?? ['ReadWriteMany'],
|
|
1240
|
-
persistentVolumeReclaimPolicy: spec.reclaimPolicy ?? 'Retain',
|
|
1241
|
-
storageClassName: spec.storageClassName,
|
|
1242
|
-
...(spec.mountOptions ? { mountOptions: spec.mountOptions } : {}),
|
|
1243
|
-
csi: csiSpec,
|
|
1244
|
-
},
|
|
1245
|
-
});
|
|
1246
|
-
this.capture(pv, `${spec.name}-pv`);
|
|
1247
|
-
return pv;
|
|
1248
|
-
}
|
|
1249
|
-
/**
|
|
1250
|
-
* Add Azure Files PersistentVolumeClaim.
|
|
1251
|
-
* Creates a PersistentVolumeClaim for Azure Files storage.
|
|
1252
|
-
*
|
|
1253
|
-
* @param spec - Azure Files PersistentVolumeClaim specification
|
|
1254
|
-
* @returns The created PersistentVolumeClaim ApiObject
|
|
1255
|
-
*/
|
|
1256
|
-
addAzureFilesPersistentVolumeClaim(spec) {
|
|
1257
|
-
const pvc = new ApiObject(this.chart, spec.name, {
|
|
1258
|
-
apiVersion: 'v1',
|
|
1259
|
-
kind: 'PersistentVolumeClaim',
|
|
1260
|
-
metadata: {
|
|
1261
|
-
name: spec.name,
|
|
1262
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
1263
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
1264
|
-
},
|
|
1265
|
-
spec: {
|
|
1266
|
-
accessModes: spec.accessModes ?? ['ReadWriteMany'],
|
|
1267
|
-
storageClassName: spec.storageClassName,
|
|
1268
|
-
resources: {
|
|
1269
|
-
requests: { storage: spec.size },
|
|
1270
|
-
},
|
|
1271
|
-
},
|
|
1272
|
-
});
|
|
1273
|
-
this.capture(pvc, `${spec.name}-pvc`);
|
|
1274
|
-
return pvc;
|
|
1275
|
-
}
|
|
1276
|
-
/**
|
|
1277
|
-
* Add Azure Container Registry ServiceAccount.
|
|
1278
|
-
* Creates a ServiceAccount configured for ACR access.
|
|
1279
|
-
*
|
|
1280
|
-
* @param spec - Azure ACR ServiceAccount specification
|
|
1281
|
-
* @returns The created ServiceAccount ApiObject
|
|
1282
|
-
*/
|
|
1283
|
-
addAzureACRServiceAccount(spec) {
|
|
1284
|
-
const annotations = {
|
|
1285
|
-
...(spec.annotations ?? {}),
|
|
1286
|
-
};
|
|
1287
|
-
// Add Workload Identity annotations if provided
|
|
1288
|
-
if (spec.clientId) {
|
|
1289
|
-
annotations['azure.workload.identity/client-id'] = spec.clientId;
|
|
1290
|
-
}
|
|
1291
|
-
if (spec.tenantId) {
|
|
1292
|
-
annotations['azure.workload.identity/tenant-id'] = spec.tenantId;
|
|
1293
|
-
}
|
|
1294
|
-
const sa = new ApiObject(this.chart, spec.name, {
|
|
1295
|
-
apiVersion: 'v1',
|
|
1296
|
-
kind: 'ServiceAccount',
|
|
1297
|
-
metadata: {
|
|
1298
|
-
name: spec.name,
|
|
1299
|
-
...(Object.keys(annotations).length ? { annotations } : {}),
|
|
1300
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
1301
|
-
},
|
|
1302
|
-
automountServiceAccountToken: spec.automountServiceAccountToken,
|
|
1303
|
-
imagePullSecrets: spec.imagePullSecrets?.map((n) => ({ name: n })),
|
|
1304
|
-
secrets: spec.secrets?.map((n) => ({ name: n })),
|
|
1305
|
-
});
|
|
1306
|
-
this.capture(sa, `${spec.name}-serviceaccount`);
|
|
1307
|
-
return sa;
|
|
1308
|
-
}
|
|
1309
|
-
addAzureDiskStorageClass(spec) {
|
|
1310
|
-
// Validate numeric parameters to prevent runtime errors
|
|
1311
|
-
this.validateAzureDiskNumericParameters(spec);
|
|
1312
|
-
const parameters = this.buildAzureDiskParameters(spec);
|
|
1313
|
-
const sc = new ApiObject(this.chart, spec.name, {
|
|
1314
|
-
apiVersion: Rutter.STORAGE_API_VERSION,
|
|
1315
|
-
kind: 'StorageClass',
|
|
1316
|
-
metadata: {
|
|
1317
|
-
name: spec.name,
|
|
1318
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
1319
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
1320
|
-
},
|
|
1321
|
-
provisioner: Rutter.AZURE_DISK_CSI_DRIVER,
|
|
1322
|
-
parameters,
|
|
1323
|
-
reclaimPolicy: spec.reclaimPolicy ?? 'Delete',
|
|
1324
|
-
allowVolumeExpansion: spec.allowVolumeExpansion ?? true,
|
|
1325
|
-
volumeBindingMode: spec.volumeBindingMode ?? 'WaitForFirstConsumer',
|
|
1326
|
-
});
|
|
1327
|
-
this.capture(sc, `${spec.name}-storageclass`);
|
|
1328
|
-
return sc;
|
|
1329
|
-
}
|
|
1330
|
-
buildAzureDiskParameters(spec) {
|
|
1331
|
-
const parameters = {
|
|
1332
|
-
skuName: spec.skuName ?? 'StandardSSD_LRS',
|
|
1333
|
-
fsType: spec.fsType ?? 'ext4',
|
|
1334
|
-
// Security best practices: default to private network access and encryption
|
|
1335
|
-
networkAccessPolicy: spec.networkAccessPolicy ?? 'AllowPrivate',
|
|
1336
|
-
};
|
|
1337
|
-
this.addOptionalAzureDiskParameters(parameters, spec);
|
|
1338
|
-
this.addAzureDiskEncryptionParameters(parameters, spec);
|
|
1339
|
-
this.addAzureDiskPerformanceParameters(parameters, spec);
|
|
1340
|
-
return parameters;
|
|
1341
|
-
}
|
|
1342
|
-
addOptionalAzureDiskParameters(parameters, spec) {
|
|
1343
|
-
if (spec.cachingMode) {
|
|
1344
|
-
parameters['cachingMode'] = spec.cachingMode;
|
|
1345
|
-
}
|
|
1346
|
-
if (spec.resourceGroup) {
|
|
1347
|
-
parameters['resourceGroup'] = spec.resourceGroup;
|
|
1348
|
-
}
|
|
1349
|
-
if (spec.logicalSectorSize !== undefined) {
|
|
1350
|
-
parameters['LogicalSectorSize'] = String(spec.logicalSectorSize);
|
|
1351
|
-
}
|
|
1352
|
-
if (spec.tags && Object.keys(spec.tags).length > 0) {
|
|
1353
|
-
const tagString = this.formatCloudResourceTags(spec.tags);
|
|
1354
|
-
parameters['tags'] = tagString;
|
|
1355
|
-
}
|
|
1356
|
-
// networkAccessPolicy is now set as default in buildAzureDiskParameters
|
|
1357
|
-
if (spec.networkAccessPolicy && spec.networkAccessPolicy !== 'AllowPrivate') {
|
|
1358
|
-
parameters['networkAccessPolicy'] = spec.networkAccessPolicy;
|
|
1359
|
-
}
|
|
1360
|
-
if (spec.diskAccessID) {
|
|
1361
|
-
parameters['diskAccessID'] = spec.diskAccessID;
|
|
1362
|
-
}
|
|
1363
|
-
if (spec.maxShares !== undefined) {
|
|
1364
|
-
parameters['maxShares'] = String(spec.maxShares);
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
addAzureDiskEncryptionParameters(parameters, spec) {
|
|
1368
|
-
if (spec.diskEncryptionSetID) {
|
|
1369
|
-
parameters['diskEncryptionSetID'] = spec.diskEncryptionSetID;
|
|
1370
|
-
}
|
|
1371
|
-
if (spec.diskEncryptionType) {
|
|
1372
|
-
parameters['diskEncryptionType'] = spec.diskEncryptionType;
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
addAzureDiskPerformanceParameters(parameters, spec) {
|
|
1376
|
-
if (spec.diskIOPSReadWrite !== undefined) {
|
|
1377
|
-
parameters['DiskIOPSReadWrite'] = String(spec.diskIOPSReadWrite);
|
|
1378
|
-
}
|
|
1379
|
-
if (spec.diskMBpsReadWrite !== undefined) {
|
|
1380
|
-
parameters['DiskMBpsReadWrite'] = String(spec.diskMBpsReadWrite);
|
|
1381
|
-
}
|
|
1382
|
-
if (spec.writeAcceleratorEnabled !== undefined) {
|
|
1383
|
-
parameters['writeAcceleratorEnabled'] = String(spec.writeAcceleratorEnabled);
|
|
1384
|
-
}
|
|
1385
|
-
if (spec.enableBursting !== undefined) {
|
|
1386
|
-
parameters['enableBursting'] = String(spec.enableBursting);
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
validateAzureDiskNumericParameters(spec) {
|
|
1390
|
-
this.validateAzureDiskIOPS(spec);
|
|
1391
|
-
this.validateAzureDiskThroughput(spec);
|
|
1392
|
-
this.validateAzureDiskShares(spec);
|
|
1393
|
-
this.validateAzureDiskSectorSize(spec);
|
|
1394
|
-
}
|
|
1395
|
-
validateAzureDiskIOPS(spec) {
|
|
1396
|
-
if (spec.diskIOPSReadWrite === undefined)
|
|
1397
|
-
return;
|
|
1398
|
-
if (spec.diskIOPSReadWrite < 100) {
|
|
1399
|
-
throw new Error(`Azure Disk ${spec.name}: diskIOPSReadWrite must be at least 100 IOPS`);
|
|
1400
|
-
}
|
|
1401
|
-
if (spec.diskIOPSReadWrite > 400000) {
|
|
1402
|
-
throw new Error(`Azure Disk ${spec.name}: diskIOPSReadWrite cannot exceed 400,000 IOPS`);
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
validateAzureDiskThroughput(spec) {
|
|
1406
|
-
if (spec.diskMBpsReadWrite === undefined)
|
|
1407
|
-
return;
|
|
1408
|
-
if (spec.diskMBpsReadWrite < 1) {
|
|
1409
|
-
throw new Error(`Azure Disk ${spec.name}: diskMBpsReadWrite must be at least 1 MB/s`);
|
|
1410
|
-
}
|
|
1411
|
-
if (spec.diskMBpsReadWrite > 10000) {
|
|
1412
|
-
throw new Error(`Azure Disk ${spec.name}: diskMBpsReadWrite cannot exceed 10,000 MB/s`);
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
validateAzureDiskShares(spec) {
|
|
1416
|
-
if (spec.maxShares === undefined)
|
|
1417
|
-
return;
|
|
1418
|
-
if (spec.maxShares < 1) {
|
|
1419
|
-
throw new Error(`Azure Disk ${spec.name}: maxShares must be at least 1`);
|
|
1420
|
-
}
|
|
1421
|
-
if (spec.maxShares > 5) {
|
|
1422
|
-
throw new Error(`Azure Disk ${spec.name}: maxShares cannot exceed 5`);
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
validateAzureDiskSectorSize(spec) {
|
|
1426
|
-
if (spec.logicalSectorSize === undefined)
|
|
1427
|
-
return;
|
|
1428
|
-
if (spec.logicalSectorSize !== 512 && spec.logicalSectorSize !== 4096) {
|
|
1429
|
-
throw new Error(`Azure Disk ${spec.name}: logicalSectorSize must be either 512 or 4096 bytes`);
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
/**
|
|
1433
|
-
* Format cloud resource tags for provider-specific string format.
|
|
1434
|
-
* Provides basic validation and consistent formatting across cloud providers.
|
|
1435
|
-
*/
|
|
1436
|
-
formatCloudResourceTags(tags) {
|
|
1437
|
-
this.validateCloudResourceTags(tags);
|
|
1438
|
-
return Object.entries(tags)
|
|
1439
|
-
.map(([key, value]) => `${key}=${value}`)
|
|
1440
|
-
.join(',');
|
|
1441
|
-
}
|
|
1442
|
-
/**
|
|
1443
|
-
* Validate cloud resource tags with basic rules.
|
|
1444
|
-
* Each cloud provider has specific validation, but these are common basics.
|
|
1445
|
-
*/
|
|
1446
|
-
validateCloudResourceTags(tags) {
|
|
1447
|
-
for (const [key, value] of Object.entries(tags)) {
|
|
1448
|
-
if (!key || key.trim() === '') {
|
|
1449
|
-
throw new Error('Tag key cannot be empty');
|
|
1450
|
-
}
|
|
1451
|
-
if (value === undefined || value === null) {
|
|
1452
|
-
throw new Error(`Tag value for key '${key}' cannot be null or undefined`);
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
addNetworkPolicy(spec) {
|
|
1457
|
-
// Validate policy types
|
|
1458
|
-
this.validateNetworkPolicyTypes(spec);
|
|
1459
|
-
// Validate CIDR blocks if present
|
|
1460
|
-
this.validateNetworkPolicyCIDRs(spec);
|
|
1461
|
-
const np = new ApiObject(this.chart, spec.name, {
|
|
1462
|
-
apiVersion: Rutter.NETWORKING_API_VERSION,
|
|
1463
|
-
kind: 'NetworkPolicy',
|
|
1464
|
-
metadata: {
|
|
1465
|
-
name: spec.name,
|
|
1466
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
1467
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
1468
|
-
},
|
|
1469
|
-
spec: {
|
|
1470
|
-
podSelector: spec.podSelector ?? {},
|
|
1471
|
-
policyTypes: spec.policyTypes,
|
|
1472
|
-
...(spec.ingress ? { ingress: spec.ingress } : {}),
|
|
1473
|
-
...(spec.egress ? { egress: spec.egress } : {}),
|
|
1474
|
-
},
|
|
1475
|
-
});
|
|
1476
|
-
this.capture(np, `${spec.name}-networkpolicy`);
|
|
1477
|
-
return np;
|
|
1478
|
-
}
|
|
1479
|
-
/**
|
|
1480
|
-
* Create a deny-all ingress NetworkPolicy for the current namespace
|
|
1481
|
-
*/
|
|
1482
|
-
addDenyAllIngressNetworkPolicy(name = 'deny-all-ingress') {
|
|
1483
|
-
return this.addNetworkPolicy({
|
|
1484
|
-
name,
|
|
1485
|
-
podSelector: Rutter.EMPTY_POD_SELECTOR,
|
|
1486
|
-
policyTypes: ['Ingress'],
|
|
1487
|
-
});
|
|
1488
|
-
}
|
|
1489
|
-
/**
|
|
1490
|
-
* Create a deny-all egress NetworkPolicy for the current namespace
|
|
1491
|
-
*/
|
|
1492
|
-
addDenyAllEgressNetworkPolicy(name = 'deny-all-egress') {
|
|
1493
|
-
return this.addNetworkPolicy({
|
|
1494
|
-
name,
|
|
1495
|
-
podSelector: Rutter.EMPTY_POD_SELECTOR,
|
|
1496
|
-
policyTypes: ['Egress'],
|
|
1497
|
-
});
|
|
1498
|
-
}
|
|
1499
|
-
/**
|
|
1500
|
-
* Create a deny-all NetworkPolicy for both ingress and egress
|
|
1501
|
-
*/
|
|
1502
|
-
addDenyAllNetworkPolicy(name = 'deny-all') {
|
|
1503
|
-
return this.addNetworkPolicy({
|
|
1504
|
-
name,
|
|
1505
|
-
podSelector: Rutter.EMPTY_POD_SELECTOR,
|
|
1506
|
-
policyTypes: ['Ingress', 'Egress'],
|
|
1507
|
-
});
|
|
1508
|
-
}
|
|
1509
|
-
/**
|
|
1510
|
-
* Allow traffic from specific pods to target pods on specific ports
|
|
1511
|
-
*/
|
|
1512
|
-
addAllowFromPodsNetworkPolicy(spec) {
|
|
1513
|
-
return this.addNetworkPolicy({
|
|
1514
|
-
name: spec.name,
|
|
1515
|
-
podSelector: { matchLabels: spec.targetPodSelector },
|
|
1516
|
-
policyTypes: ['Ingress'],
|
|
1517
|
-
ingress: [
|
|
1518
|
-
{
|
|
1519
|
-
from: [{ podSelector: { matchLabels: spec.sourcePodSelector } }],
|
|
1520
|
-
...(spec.ports ? { ports: spec.ports } : {}),
|
|
1521
|
-
},
|
|
1522
|
-
],
|
|
1523
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
1524
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
1525
|
-
});
|
|
1526
|
-
}
|
|
1527
|
-
/**
|
|
1528
|
-
* Allow traffic from specific namespace to target pods
|
|
1529
|
-
*/
|
|
1530
|
-
addAllowFromNamespaceNetworkPolicy(spec) {
|
|
1531
|
-
return this.addNetworkPolicy({
|
|
1532
|
-
name: spec.name,
|
|
1533
|
-
podSelector: { matchLabels: spec.targetPodSelector },
|
|
1534
|
-
policyTypes: ['Ingress'],
|
|
1535
|
-
ingress: [
|
|
1536
|
-
{
|
|
1537
|
-
from: [{ namespaceSelector: { matchLabels: spec.sourceNamespaceSelector } }],
|
|
1538
|
-
...(spec.ports ? { ports: spec.ports } : {}),
|
|
1539
|
-
},
|
|
1540
|
-
],
|
|
1541
|
-
...(spec.labels ? { labels: spec.labels } : {}),
|
|
1542
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
1543
|
-
});
|
|
1544
|
-
}
|
|
1545
|
-
validateNetworkPolicyTypes(spec) {
|
|
1546
|
-
if (!spec.policyTypes || spec.policyTypes.length === 0) {
|
|
1547
|
-
throw new Error(`NetworkPolicy ${spec.name}: policyTypes must be specified`);
|
|
1548
|
-
}
|
|
1549
|
-
this.validatePolicyTypeValues(spec);
|
|
1550
|
-
this.validatePolicyTypeRules(spec);
|
|
1551
|
-
}
|
|
1552
|
-
validatePolicyTypeValues(spec) {
|
|
1553
|
-
const validTypes = ['Ingress', 'Egress'];
|
|
1554
|
-
for (const type of spec.policyTypes) {
|
|
1555
|
-
if (!validTypes.includes(type)) {
|
|
1556
|
-
throw new Error(`NetworkPolicy ${spec.name}: invalid policyType '${type}', must be 'Ingress' or 'Egress'`);
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
validatePolicyTypeRules(spec) {
|
|
1561
|
-
if (spec.policyTypes.includes('Ingress') && spec.ingress === undefined) {
|
|
1562
|
-
console.warn(`NetworkPolicy ${spec.name}: policyType 'Ingress' specified but no ingress rules defined (will deny all ingress)`);
|
|
1563
|
-
}
|
|
1564
|
-
if (spec.policyTypes.includes('Egress') && spec.egress === undefined) {
|
|
1565
|
-
console.warn(`NetworkPolicy ${spec.name}: policyType 'Egress' specified but no egress rules defined (will deny all egress)`);
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
validateNetworkPolicyCIDRs(spec) {
|
|
1569
|
-
const validateCIDR = (cidr, context) => {
|
|
1570
|
-
if (!this.isValidCIDR(cidr)) {
|
|
1571
|
-
throw new Error(`NetworkPolicy ${spec.name}: invalid CIDR '${cidr}' in ${context}`);
|
|
1572
|
-
}
|
|
1573
|
-
};
|
|
1574
|
-
this.validateIngressCIDRs(spec, validateCIDR);
|
|
1575
|
-
this.validateEgressCIDRs(spec, validateCIDR);
|
|
1576
|
-
}
|
|
1577
|
-
isValidCIDR(cidr) {
|
|
1578
|
-
// IPv4 CIDR validation
|
|
1579
|
-
const ipv4Parts = cidr.split('/');
|
|
1580
|
-
if (ipv4Parts.length === 2) {
|
|
1581
|
-
const [ip, prefix] = ipv4Parts;
|
|
1582
|
-
if (ip && prefix && this.isValidIPv4(ip) && this.isValidPrefix(prefix, 32)) {
|
|
1583
|
-
return true;
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
// IPv6 CIDR validation (basic)
|
|
1587
|
-
if (cidr.includes(':') && ipv4Parts.length === 2) {
|
|
1588
|
-
const [, prefix] = ipv4Parts;
|
|
1589
|
-
if (prefix) {
|
|
1590
|
-
return this.isValidPrefix(prefix, 128);
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
return false;
|
|
1594
|
-
}
|
|
1595
|
-
isValidIPv4(ip) {
|
|
1596
|
-
const parts = ip.split('.');
|
|
1597
|
-
if (parts.length !== 4)
|
|
1598
|
-
return false;
|
|
1599
|
-
return parts.every((part) => {
|
|
1600
|
-
const num = parseInt(part, 10);
|
|
1601
|
-
return !isNaN(num) && num >= 0 && num <= 255 && String(num) === part;
|
|
1602
|
-
});
|
|
1603
|
-
}
|
|
1604
|
-
isValidPrefix(prefix, maxPrefix) {
|
|
1605
|
-
const num = parseInt(prefix, 10);
|
|
1606
|
-
return !isNaN(num) && num >= 0 && num <= maxPrefix && String(num) === prefix;
|
|
1607
|
-
}
|
|
1608
|
-
validateIngressCIDRs(spec, validateCIDR) {
|
|
1609
|
-
if (!spec.ingress)
|
|
1610
|
-
return;
|
|
1611
|
-
for (const rule of spec.ingress) {
|
|
1612
|
-
if (rule.from) {
|
|
1613
|
-
for (const peer of rule.from) {
|
|
1614
|
-
this.validatePeerCIDRs(peer, validateCIDR, 'ingress rule');
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
validateEgressCIDRs(spec, validateCIDR) {
|
|
1620
|
-
if (!spec.egress)
|
|
1621
|
-
return;
|
|
1622
|
-
for (const rule of spec.egress) {
|
|
1623
|
-
if (rule.to) {
|
|
1624
|
-
for (const peer of rule.to) {
|
|
1625
|
-
this.validatePeerCIDRs(peer, validateCIDR, 'egress rule');
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
|
-
validatePeerCIDRs(peer, validateCIDR, context) {
|
|
1631
|
-
if (peer.ipBlock?.cidr) {
|
|
1632
|
-
validateCIDR(peer.ipBlock.cidr, context);
|
|
1633
|
-
if (peer.ipBlock.except) {
|
|
1634
|
-
for (const except of peer.ipBlock.except) {
|
|
1635
|
-
validateCIDR(except, `${context} except block`);
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
validateAGICHealthProbeParams(spec) {
|
|
1641
|
-
if (spec.healthProbeInterval !== undefined &&
|
|
1642
|
-
(spec.healthProbeInterval < 1 || spec.healthProbeInterval > 86400)) {
|
|
1643
|
-
throw new Error('Health probe interval must be between 1 and 86400 seconds');
|
|
1644
|
-
}
|
|
1645
|
-
if (spec.healthProbeTimeout !== undefined &&
|
|
1646
|
-
(spec.healthProbeTimeout < 1 || spec.healthProbeTimeout > 86400)) {
|
|
1647
|
-
throw new Error('Health probe timeout must be between 1 and 86400 seconds');
|
|
1648
|
-
}
|
|
1649
|
-
if (spec.healthProbeUnhealthyThreshold !== undefined &&
|
|
1650
|
-
(spec.healthProbeUnhealthyThreshold < 1 || spec.healthProbeUnhealthyThreshold > 20)) {
|
|
1651
|
-
throw new Error('Health probe unhealthy threshold must be between 1 and 20');
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
validateAGICSecurityParams(spec) {
|
|
1655
|
-
if (spec.wafPolicyForPath &&
|
|
1656
|
-
!/^\/subscriptions\/[^/]+\/resourceGroups\/[^/]+\/providers\/Microsoft\.Network\/applicationGatewayWebApplicationFirewallPolicies\/[^/]+$/.test(spec.wafPolicyForPath)) {
|
|
1657
|
-
throw new Error('Invalid WAF policy resource ID format');
|
|
1658
|
-
}
|
|
1659
|
-
if (spec.hostnameExtension?.some((hostname) => !this.isValidHostname(hostname))) {
|
|
1660
|
-
throw new Error('Invalid hostname format in hostnameExtension');
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
isValidHostname(hostname) {
|
|
1664
|
-
// Basic hostname validation without complex regex to prevent ReDoS
|
|
1665
|
-
if (!hostname || hostname.length > 253)
|
|
1666
|
-
return false;
|
|
1667
|
-
if (hostname.startsWith('.') || hostname.endsWith('.'))
|
|
1668
|
-
return false;
|
|
1669
|
-
const labels = hostname.split('.');
|
|
1670
|
-
return labels.every((label) => {
|
|
1671
|
-
if (!label || label.length > 63)
|
|
1672
|
-
return false;
|
|
1673
|
-
if (label.startsWith('-') || label.endsWith('-'))
|
|
1674
|
-
return false;
|
|
1675
|
-
return /^[a-zA-Z0-9-]+$/.test(label);
|
|
1676
|
-
});
|
|
1677
|
-
}
|
|
1678
|
-
validateAGICAnnotationConsistency(spec) {
|
|
1679
|
-
if (spec.sslRedirect && !spec.appgwSslCertificate) {
|
|
1680
|
-
throw new Error('SSL redirect requires an SSL certificate to be specified');
|
|
1681
|
-
}
|
|
1682
|
-
if (spec.backendProtocol === 'https' && !spec.appgwTrustedRootCertificate?.length) {
|
|
1683
|
-
throw new Error('HTTPS backend protocol requires trusted root certificates');
|
|
1684
|
-
}
|
|
1685
|
-
if (spec.overrideFrontendPort === 443 && !spec.appgwSslCertificate) {
|
|
1686
|
-
throw new Error('Port 443 requires an SSL certificate to be specified');
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
buildAzureFilesParameters(spec) {
|
|
1690
|
-
const parameters = {
|
|
1691
|
-
skuName: spec.skuName ?? 'Standard_LRS',
|
|
1692
|
-
};
|
|
1693
|
-
if (spec.protocol) {
|
|
1694
|
-
parameters['protocol'] = spec.protocol;
|
|
1695
|
-
}
|
|
1696
|
-
if (spec.allowSharedAccess !== undefined) {
|
|
1697
|
-
parameters['allowSharedAccess'] = String(spec.allowSharedAccess);
|
|
1698
|
-
}
|
|
1699
|
-
if (spec.resourceGroup) {
|
|
1700
|
-
parameters['resourceGroup'] = spec.resourceGroup;
|
|
1701
|
-
}
|
|
1702
|
-
if (spec.storageAccount) {
|
|
1703
|
-
parameters['storageAccount'] = spec.storageAccount;
|
|
1704
|
-
}
|
|
1705
|
-
if (spec.location) {
|
|
1706
|
-
parameters['location'] = spec.location;
|
|
1707
|
-
}
|
|
1708
|
-
if (spec.networkEndpointType) {
|
|
1709
|
-
parameters['networkEndpointType'] = spec.networkEndpointType;
|
|
1710
|
-
}
|
|
1711
|
-
// NFS-specific parameters
|
|
1712
|
-
if (spec.protocol === 'nfs' && spec.mountPermissions) {
|
|
1713
|
-
parameters['mountPermissions'] = spec.mountPermissions;
|
|
1714
|
-
}
|
|
1715
|
-
if (spec.protocol === 'nfs' && spec.rootSquashType) {
|
|
1716
|
-
parameters['rootSquashType'] = spec.rootSquashType;
|
|
1717
|
-
}
|
|
1718
|
-
return parameters;
|
|
1719
|
-
}
|
|
1720
|
-
buildAzureFilesPVCSISpec(spec) {
|
|
1721
|
-
// Validate protocol and secret configuration
|
|
1722
|
-
if (spec.protocol === 'nfs' && spec.secretName) {
|
|
1723
|
-
throw new Error(`Azure Files PV ${spec.name}: NFS protocol does not use secrets for authentication`);
|
|
1724
|
-
}
|
|
1725
|
-
const volumeHandle = this.sanitizeVolumeHandle(spec.storageAccount, spec.shareName);
|
|
1726
|
-
const volumeAttributes = {
|
|
1727
|
-
storageAccount: spec.storageAccount,
|
|
1728
|
-
shareName: spec.shareName,
|
|
1729
|
-
};
|
|
1730
|
-
if (spec.resourceGroup) {
|
|
1731
|
-
volumeAttributes['resourceGroup'] = spec.resourceGroup;
|
|
1732
|
-
}
|
|
1733
|
-
if (spec.protocol) {
|
|
1734
|
-
volumeAttributes['protocol'] = spec.protocol;
|
|
1735
|
-
}
|
|
1736
|
-
if (spec.server) {
|
|
1737
|
-
volumeAttributes['server'] = spec.server;
|
|
1738
|
-
}
|
|
1739
|
-
if (spec.folderName) {
|
|
1740
|
-
volumeAttributes['folderName'] = spec.folderName;
|
|
1741
|
-
}
|
|
1742
|
-
if (spec.mountPermissions) {
|
|
1743
|
-
volumeAttributes['mountPermissions'] = spec.mountPermissions;
|
|
1744
|
-
}
|
|
1745
|
-
const csiSpec = {
|
|
1746
|
-
driver: Rutter.AZURE_FILES_CSI_DRIVER,
|
|
1747
|
-
volumeHandle,
|
|
1748
|
-
volumeAttributes,
|
|
1749
|
-
};
|
|
1750
|
-
// Add secret reference for SMB protocol
|
|
1751
|
-
if (spec.protocol !== 'nfs' && spec.secretName) {
|
|
1752
|
-
csiSpec['nodeStageSecretRef'] = {
|
|
1753
|
-
name: spec.secretName,
|
|
1754
|
-
namespace: spec.secretNamespace ?? 'default',
|
|
1755
|
-
};
|
|
1756
|
-
}
|
|
1757
|
-
return csiSpec;
|
|
1758
|
-
}
|
|
1759
|
-
buildPodEnvironment(spec) {
|
|
1760
|
-
const envEntries = spec.env
|
|
1761
|
-
? Object.entries(spec.env).map(([name, value]) => this.sanitizeEnvVar(name, value))
|
|
1762
|
-
: undefined;
|
|
1763
|
-
const envFromEntries = spec.envFrom
|
|
1764
|
-
? spec.envFrom
|
|
1765
|
-
.map((s) => {
|
|
1766
|
-
const entry = {};
|
|
1767
|
-
if (s.configMapRef) {
|
|
1768
|
-
entry.configMapRef = { name: s.configMapRef };
|
|
1769
|
-
}
|
|
1770
|
-
if (s.secretRef) {
|
|
1771
|
-
entry.secretRef = { name: s.secretRef };
|
|
1772
|
-
}
|
|
1773
|
-
return entry;
|
|
1774
|
-
})
|
|
1775
|
-
.filter((entry) => entry.configMapRef || entry.secretRef)
|
|
1776
|
-
: undefined;
|
|
1777
|
-
const volumeDefs = spec.volumes
|
|
1778
|
-
? spec.volumes.map((v) => ({
|
|
1779
|
-
name: v.name,
|
|
1780
|
-
configMap: v.configMap ? { name: v.configMap } : undefined,
|
|
1781
|
-
secret: v.secret ? { secretName: v.secret } : undefined,
|
|
1782
|
-
persistentVolumeClaim: v.persistentVolumeClaim
|
|
1783
|
-
? { claimName: v.persistentVolumeClaim }
|
|
1784
|
-
: undefined,
|
|
1785
|
-
}))
|
|
1786
|
-
: undefined;
|
|
1787
|
-
const volumeMountDefs = spec.volumeMounts
|
|
1788
|
-
? spec.volumeMounts.map((m) => ({
|
|
1789
|
-
name: m.name,
|
|
1790
|
-
mountPath: m.mountPath,
|
|
1791
|
-
readOnly: m.readOnly,
|
|
1792
|
-
subPath: m.subPath,
|
|
1793
|
-
}))
|
|
1794
|
-
: undefined;
|
|
1795
|
-
return { envEntries, envFromEntries, volumeDefs, volumeMountDefs };
|
|
1796
|
-
}
|
|
1797
|
-
buildJobSpec(spec) {
|
|
1798
|
-
return {
|
|
1799
|
-
...(spec.backoffLimit !== undefined ? { backoffLimit: spec.backoffLimit } : {}),
|
|
1800
|
-
...(spec.activeDeadlineSeconds !== undefined
|
|
1801
|
-
? { activeDeadlineSeconds: spec.activeDeadlineSeconds }
|
|
1802
|
-
: {}),
|
|
1803
|
-
...(spec.ttlSecondsAfterFinished !== undefined
|
|
1804
|
-
? { ttlSecondsAfterFinished: spec.ttlSecondsAfterFinished }
|
|
1805
|
-
: {}),
|
|
1806
|
-
...(spec.completions !== undefined ? { completions: spec.completions } : {}),
|
|
1807
|
-
...(spec.parallelism !== undefined ? { parallelism: spec.parallelism } : {}),
|
|
1808
|
-
...(spec.completionMode !== undefined ? { completionMode: spec.completionMode } : {}),
|
|
1809
|
-
...(spec.suspend !== undefined ? { suspend: spec.suspend } : {}),
|
|
1810
|
-
};
|
|
1811
|
-
}
|
|
1812
|
-
buildJobPodTemplate(spec, envEntries, envFromEntries, volumeDefs, volumeMountDefs) {
|
|
1813
|
-
return {
|
|
1814
|
-
metadata: {
|
|
1815
|
-
...(spec.podLabels ? { labels: spec.podLabels } : {}),
|
|
1816
|
-
...(spec.podAnnotations ? { annotations: spec.podAnnotations } : {}),
|
|
1817
|
-
},
|
|
1818
|
-
spec: {
|
|
1819
|
-
restartPolicy: spec.restartPolicy ?? 'Never',
|
|
1820
|
-
serviceAccountName: spec.serviceAccountName,
|
|
1821
|
-
volumes: volumeDefs,
|
|
1822
|
-
containers: [
|
|
1823
|
-
{
|
|
1824
|
-
name: spec.name,
|
|
1825
|
-
image: spec.image,
|
|
1826
|
-
imagePullPolicy: spec.imagePullPolicy,
|
|
1827
|
-
...(spec.command ? { command: spec.command } : {}),
|
|
1828
|
-
...(spec.args ? { args: spec.args } : {}),
|
|
1829
|
-
env: envEntries,
|
|
1830
|
-
envFrom: envFromEntries,
|
|
1831
|
-
volumeMounts: volumeMountDefs,
|
|
1832
|
-
resources: spec.resources ?? this.getDefaultResources(),
|
|
1833
|
-
},
|
|
1834
|
-
],
|
|
1835
|
-
},
|
|
1836
|
-
};
|
|
1837
|
-
}
|
|
1838
|
-
buildCronJobSpec(spec) {
|
|
1839
|
-
return {
|
|
1840
|
-
schedule: spec.schedule,
|
|
1841
|
-
...(spec.startingDeadlineSeconds !== undefined
|
|
1842
|
-
? { startingDeadlineSeconds: spec.startingDeadlineSeconds }
|
|
1843
|
-
: {}),
|
|
1844
|
-
...(spec.concurrencyPolicy !== undefined
|
|
1845
|
-
? { concurrencyPolicy: spec.concurrencyPolicy }
|
|
1846
|
-
: {}),
|
|
1847
|
-
...(spec.suspend !== undefined ? { suspend: spec.suspend } : {}),
|
|
1848
|
-
...(spec.successfulJobsHistoryLimit !== undefined
|
|
1849
|
-
? { successfulJobsHistoryLimit: spec.successfulJobsHistoryLimit }
|
|
1850
|
-
: {}),
|
|
1851
|
-
...(spec.failedJobsHistoryLimit !== undefined
|
|
1852
|
-
? { failedJobsHistoryLimit: spec.failedJobsHistoryLimit }
|
|
1853
|
-
: {}),
|
|
1854
|
-
...(spec.timeZone !== undefined ? { timeZone: spec.timeZone } : {}),
|
|
1855
|
-
};
|
|
1856
|
-
}
|
|
1857
|
-
buildCronJobTemplate(spec, envEntries, envFromEntries, volumeDefs, volumeMountDefs) {
|
|
1858
|
-
return {
|
|
1859
|
-
spec: {
|
|
1860
|
-
...(spec.backoffLimit !== undefined ? { backoffLimit: spec.backoffLimit } : {}),
|
|
1861
|
-
...(spec.activeDeadlineSeconds !== undefined
|
|
1862
|
-
? { activeDeadlineSeconds: spec.activeDeadlineSeconds }
|
|
1863
|
-
: {}),
|
|
1864
|
-
...(spec.ttlSecondsAfterFinished !== undefined
|
|
1865
|
-
? { ttlSecondsAfterFinished: spec.ttlSecondsAfterFinished }
|
|
1866
|
-
: {}),
|
|
1867
|
-
...(spec.completions !== undefined ? { completions: spec.completions } : {}),
|
|
1868
|
-
...(spec.parallelism !== undefined ? { parallelism: spec.parallelism } : {}),
|
|
1869
|
-
...(spec.completionMode !== undefined ? { completionMode: spec.completionMode } : {}),
|
|
1870
|
-
template: {
|
|
1871
|
-
metadata: {
|
|
1872
|
-
...(spec.podLabels ? { labels: spec.podLabels } : {}),
|
|
1873
|
-
...(spec.podAnnotations ? { annotations: spec.podAnnotations } : {}),
|
|
1874
|
-
},
|
|
1875
|
-
spec: {
|
|
1876
|
-
restartPolicy: spec.restartPolicy ?? 'Never',
|
|
1877
|
-
serviceAccountName: spec.serviceAccountName,
|
|
1878
|
-
volumes: volumeDefs,
|
|
1879
|
-
containers: [
|
|
1880
|
-
{
|
|
1881
|
-
name: spec.name,
|
|
1882
|
-
image: spec.image,
|
|
1883
|
-
imagePullPolicy: spec.imagePullPolicy,
|
|
1884
|
-
...(spec.command ? { command: spec.command } : {}),
|
|
1885
|
-
...(spec.args ? { args: spec.args } : {}),
|
|
1886
|
-
env: envEntries,
|
|
1887
|
-
envFrom: envFromEntries,
|
|
1888
|
-
volumeMounts: volumeMountDefs,
|
|
1889
|
-
resources: spec.resources ?? this.getDefaultResources(),
|
|
1890
|
-
},
|
|
1891
|
-
],
|
|
1892
|
-
},
|
|
1893
|
-
},
|
|
1894
|
-
},
|
|
1895
|
-
};
|
|
1896
|
-
}
|
|
1897
|
-
/**
|
|
1898
|
-
* Validate Job specification for security and best practices
|
|
1899
|
-
*/
|
|
1900
|
-
validateJobSpec(spec) {
|
|
1901
|
-
if (spec.activeDeadlineSeconds !== undefined && spec.activeDeadlineSeconds <= 0) {
|
|
1902
|
-
throw new Error('activeDeadlineSeconds must be a positive integer');
|
|
1903
|
-
}
|
|
1904
|
-
if (spec.ttlSecondsAfterFinished !== undefined && spec.ttlSecondsAfterFinished < 0) {
|
|
1905
|
-
throw new Error('ttlSecondsAfterFinished must be a non-negative integer');
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
/**
|
|
1909
|
-
* Validate CronJob specification for security and best practices
|
|
1910
|
-
*/
|
|
1911
|
-
validateCronJobSpec(spec) {
|
|
1912
|
-
this.validateCronSchedule(spec.schedule);
|
|
1913
|
-
if (spec.suspend && spec.startingDeadlineSeconds !== undefined) {
|
|
1914
|
-
throw new Error('startingDeadlineSeconds should not be set when job is suspended');
|
|
1915
|
-
}
|
|
1916
|
-
if (spec.successfulJobsHistoryLimit !== undefined && spec.successfulJobsHistoryLimit < 0) {
|
|
1917
|
-
throw new Error('successfulJobsHistoryLimit must be non-negative');
|
|
1918
|
-
}
|
|
1919
|
-
if (spec.failedJobsHistoryLimit !== undefined && spec.failedJobsHistoryLimit < 0) {
|
|
1920
|
-
throw new Error('failedJobsHistoryLimit must be non-negative');
|
|
1921
|
-
}
|
|
1922
|
-
if (spec.activeDeadlineSeconds !== undefined && spec.activeDeadlineSeconds <= 0) {
|
|
1923
|
-
throw new Error('activeDeadlineSeconds must be a positive integer');
|
|
1924
|
-
}
|
|
1925
|
-
if (spec.ttlSecondsAfterFinished !== undefined && spec.ttlSecondsAfterFinished < 0) {
|
|
1926
|
-
throw new Error('ttlSecondsAfterFinished must be a non-negative integer');
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
/**
|
|
1930
|
-
* Validate cron schedule expression
|
|
1931
|
-
*/
|
|
1932
|
-
validateCronSchedule(schedule) {
|
|
1933
|
-
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]))$/;
|
|
1934
|
-
if (!cronRegex.test(schedule)) {
|
|
1935
|
-
throw new Error('Invalid cron schedule expression');
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
/**
|
|
1939
|
-
* Sanitize environment variable names and values
|
|
1940
|
-
*/
|
|
1941
|
-
sanitizeEnvVar(name, value) {
|
|
1942
|
-
const nameRegex = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1943
|
-
if (!nameRegex.test(name)) {
|
|
1944
|
-
throw new Error(`Invalid environment variable name: ${name}`);
|
|
1945
|
-
}
|
|
1946
|
-
// Remove control characters from value (ASCII 0-31 and 127)
|
|
1947
|
-
const sanitizedValue = value.replace(/[\cA-\c_\x7F]/g, '');
|
|
1948
|
-
return { name, value: sanitizedValue };
|
|
1949
|
-
}
|
|
1950
|
-
/**
|
|
1951
|
-
* Get default resource limits for Jobs and CronJobs
|
|
1952
|
-
*/
|
|
1953
|
-
getDefaultResources() {
|
|
1954
|
-
return {
|
|
1955
|
-
limits: {
|
|
1956
|
-
cpu: '1',
|
|
1957
|
-
memory: '512Mi',
|
|
1958
|
-
},
|
|
1959
|
-
requests: {
|
|
1960
|
-
cpu: '100m',
|
|
1961
|
-
memory: '128Mi',
|
|
1962
|
-
},
|
|
1963
|
-
};
|
|
1964
|
-
}
|
|
1965
|
-
/**
|
|
1966
|
-
* Validate Azure Files parameters for security and compliance
|
|
1967
|
-
*/
|
|
1968
|
-
validateAzureFilesParameters(spec) {
|
|
1969
|
-
const validSkuNames = [
|
|
1970
|
-
'Standard_LRS',
|
|
1971
|
-
'Standard_GRS',
|
|
1972
|
-
'Standard_RAGRS',
|
|
1973
|
-
'Standard_ZRS',
|
|
1974
|
-
'Premium_LRS',
|
|
1975
|
-
'Premium_ZRS',
|
|
1976
|
-
];
|
|
1977
|
-
if (spec.skuName && !validSkuNames.includes(spec.skuName)) {
|
|
1978
|
-
throw new Error(`Invalid skuName: ${spec.skuName}. Must be one of: ${validSkuNames.join(', ')}`);
|
|
1979
|
-
}
|
|
1980
|
-
if (spec.protocol === 'nfs' && spec.mountPermissions) {
|
|
1981
|
-
const permRegex = /^[0-7]{3,4}$/;
|
|
1982
|
-
if (!permRegex.test(spec.mountPermissions)) {
|
|
1983
|
-
throw new Error('mountPermissions must be a valid octal permission string (e.g., "0777")');
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
/**
|
|
1988
|
-
* Sanitize volume handle to ensure valid format for CSI driver
|
|
1989
|
-
*/
|
|
1990
|
-
sanitizeVolumeHandle(account, share) {
|
|
1991
|
-
// Remove special characters and ensure valid format
|
|
1992
|
-
const sanitizedAccount = account.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
|
1993
|
-
const sanitizedShare = share.replace(/[^a-z0-9-]/gi, '').toLowerCase();
|
|
1994
|
-
return `${sanitizedAccount}#${sanitizedShare}`;
|
|
1995
|
-
}
|
|
1996
|
-
/**
|
|
1997
|
-
* Capture the YAML of an ApiObject or Construct into assets.
|
|
1998
|
-
*/
|
|
1999
|
-
capture(_obj, id) {
|
|
2000
|
-
// Ensure the app can synth
|
|
2001
|
-
// We defer synth until write(), but we can capture logical ids now
|
|
2002
|
-
this.assets.push({ id, yaml: `# captured:${id}` });
|
|
2003
|
-
}
|
|
2004
|
-
/**
|
|
2005
|
-
* Extract a meaningful name from a Kubernetes resource object
|
|
2006
|
-
*/
|
|
2007
|
-
getResourceName(obj) {
|
|
2008
|
-
if (obj && typeof obj === 'object') {
|
|
2009
|
-
const resource = obj;
|
|
2010
|
-
if (resource.kind && resource.metadata?.name) {
|
|
2011
|
-
return `${resource.kind.toLowerCase()}-${resource.metadata.name}`;
|
|
2012
|
-
}
|
|
2013
|
-
if (resource.kind) {
|
|
2014
|
-
return resource.kind.toLowerCase();
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
return 'resource';
|
|
2018
|
-
}
|
|
2019
|
-
/**
|
|
2020
|
-
* Add a raw CRD manifest to the chart (written under crds/).
|
|
2021
|
-
*/
|
|
2022
|
-
addCrd(yaml, id = 'crd') {
|
|
2023
|
-
this.assets.push({ id, yaml, target: 'crds' });
|
|
2024
|
-
}
|
|
2025
|
-
/**
|
|
2026
|
-
* Synthesize the cdk8s app and write a Helm chart to outDir.
|
|
2027
|
-
*/
|
|
2028
|
-
write(outDir) {
|
|
2029
|
-
// Use cdk8s Testing.synth to obtain manifest objects
|
|
2030
|
-
const manifestObjs = Testing.synth(this.chart);
|
|
2031
|
-
// Enforce common labels best-practice on all rendered objects
|
|
2032
|
-
const enriched = manifestObjs.map((obj) => {
|
|
2033
|
-
if (obj && typeof obj === 'object') {
|
|
2034
|
-
const o = obj;
|
|
2035
|
-
o.metadata = o.metadata ?? {};
|
|
2036
|
-
o.metadata.labels = o.metadata.labels ?? {};
|
|
2037
|
-
const labels = o.metadata.labels;
|
|
2038
|
-
const defaults = {
|
|
2039
|
-
'helm.sh/chart': '{{ .Chart.Name }}-{{ .Chart.Version }}',
|
|
2040
|
-
'app.kubernetes.io/name': include(Rutter.HELPER_NAME),
|
|
2041
|
-
'app.kubernetes.io/instance': helm.releaseName,
|
|
2042
|
-
'app.kubernetes.io/version': helm.chartVersion,
|
|
2043
|
-
'app.kubernetes.io/managed-by': '{{ .Release.Service }}',
|
|
2044
|
-
'app.kubernetes.io/part-of': helm.chartName,
|
|
2045
|
-
};
|
|
2046
|
-
for (const [k, v] of Object.entries(defaults)) {
|
|
2047
|
-
if (!(k in labels)) {
|
|
2048
|
-
// eslint-disable-next-line security/detect-object-injection -- Safe: controlled label assignment
|
|
2049
|
-
labels[k] = v;
|
|
2050
|
-
}
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
return obj;
|
|
2054
|
-
});
|
|
2055
|
-
if (this.props.singleManifestFile) {
|
|
2056
|
-
// Combine all resources into a single manifest file
|
|
2057
|
-
const combinedYaml = enriched
|
|
2058
|
-
.map((obj) => YAML.stringify(obj).trim())
|
|
2059
|
-
.filter(Boolean)
|
|
2060
|
-
.join('\n---\n');
|
|
2061
|
-
this.assets.length = 0;
|
|
2062
|
-
const manifestId = this.props.manifestName ?? 'manifests';
|
|
2063
|
-
this.assets.push({ id: manifestId, yaml: combinedYaml, singleFile: true });
|
|
2064
|
-
}
|
|
2065
|
-
else {
|
|
2066
|
-
// Create separate files for each resource (default behavior)
|
|
2067
|
-
this.assets.length = 0;
|
|
2068
|
-
enriched.forEach((obj, _index) => {
|
|
2069
|
-
const yaml = YAML.stringify(obj).trim();
|
|
2070
|
-
if (yaml) {
|
|
2071
|
-
const resourceName = this.getResourceName(obj);
|
|
2072
|
-
const manifestId = this.props.manifestName
|
|
2073
|
-
? `${this.props.manifestName}-${resourceName}`
|
|
2074
|
-
: resourceName;
|
|
2075
|
-
this.assets.push({ id: manifestId, yaml });
|
|
2076
|
-
}
|
|
2077
|
-
});
|
|
2078
|
-
}
|
|
2079
|
-
const options = {
|
|
2080
|
-
outDir,
|
|
2081
|
-
meta: this.props.meta,
|
|
2082
|
-
defaultValues: this.props.defaultValues
|
|
2083
|
-
? this.applyOverrides(this.props.defaultValues)
|
|
2084
|
-
: undefined,
|
|
2085
|
-
envValues: this.props.envValues,
|
|
2086
|
-
assets: this.assets,
|
|
2087
|
-
...(this.props.helpersTpl !== undefined
|
|
2088
|
-
? { helpersTpl: this.props.helpersTpl }
|
|
2089
|
-
: {
|
|
2090
|
-
helpersTpl: [
|
|
2091
|
-
{
|
|
2092
|
-
name: 'timonel.name',
|
|
2093
|
-
body: '{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}',
|
|
2094
|
-
},
|
|
2095
|
-
{
|
|
2096
|
-
name: 'timonel.fullname',
|
|
2097
|
-
body: '{{- if .Values.fullnameOverride -}}\n' +
|
|
2098
|
-
'{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}\n' +
|
|
2099
|
-
'{{- else -}}\n' +
|
|
2100
|
-
'{{- printf "%s-%s" .Chart.Name .Release.Name | trunc 63 | trimSuffix "-" -}}\n' +
|
|
2101
|
-
'{{- end -}}',
|
|
2102
|
-
},
|
|
2103
|
-
{
|
|
2104
|
-
name: 'timonel.chart',
|
|
2105
|
-
body: '{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}',
|
|
2106
|
-
},
|
|
2107
|
-
{
|
|
2108
|
-
name: 'timonel.labels',
|
|
2109
|
-
body: 'helm.sh/chart: {{ include "timonel.chart" . }}\n' +
|
|
2110
|
-
'app.kubernetes.io/name: {{ include "timonel.name" . }}\n' +
|
|
2111
|
-
'app.kubernetes.io/instance: {{ .Release.Name }}\n' +
|
|
2112
|
-
'app.kubernetes.io/version: {{ .Chart.Version }}\n' +
|
|
2113
|
-
'app.kubernetes.io/managed-by: {{ .Release.Service }}\n' +
|
|
2114
|
-
'app.kubernetes.io/part-of: {{ .Chart.Name }}',
|
|
2115
|
-
},
|
|
2116
|
-
{
|
|
2117
|
-
name: 'timonel.selectorLabels',
|
|
2118
|
-
body: 'app.kubernetes.io/name: {{ include "timonel.name" . }}\n' +
|
|
2119
|
-
'app.kubernetes.io/instance: {{ .Release.Name }}',
|
|
2120
|
-
},
|
|
2121
|
-
],
|
|
2122
|
-
}),
|
|
2123
|
-
...(this.props.notesTpl !== undefined ? { notesTpl: this.props.notesTpl } : {}),
|
|
2124
|
-
...(this.props.valuesSchema !== undefined ? { valuesSchema: this.props.valuesSchema } : {}),
|
|
2125
|
-
};
|
|
2126
|
-
HelmChartWriter.write(options);
|
|
2127
|
-
}
|
|
2128
|
-
}
|
|
2129
|
-
Rutter.LABEL_NAME = 'app.kubernetes.io/name';
|
|
2130
|
-
Rutter.LABEL_INSTANCE = 'app.kubernetes.io/instance';
|
|
2131
|
-
Rutter.HELPER_NAME = 'timonel.name';
|
|
2132
|
-
Rutter.NETWORKING_API_VERSION = 'networking.k8s.io/v1';
|
|
2133
|
-
Rutter.STORAGE_API_VERSION = 'storage.k8s.io/v1';
|
|
2134
|
-
Rutter.AZURE_DISK_CSI_DRIVER = 'disk.csi.azure.com';
|
|
2135
|
-
Rutter.AZURE_DISK_PROVISIONER_ANNOTATION = 'volume.beta.kubernetes.io/storage-provisioner';
|
|
2136
|
-
Rutter.AZURE_FILES_CSI_DRIVER = 'file.csi.azure.com';
|
|
2137
|
-
Rutter.EMPTY_POD_SELECTOR = {};
|
|
2138
|
-
//# sourceMappingURL=Rutter.js.map
|