timonel 3.0.0-beta.1 → 3.1.0-beta.1
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 +192 -0
- package/README.md +606 -119
- package/SECURITY.md +25 -11
- package/dist/cli.js +54 -15
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/lib/helm.js +28 -1
- package/dist/lib/helmChartWriter.js +27 -8
- package/dist/lib/policy/configurationLoader.d.ts +46 -0
- package/dist/lib/policy/configurationLoader.js +251 -0
- package/dist/lib/policy/errorContextGenerator.d.ts +63 -0
- package/dist/lib/policy/errorContextGenerator.js +302 -0
- package/dist/lib/policy/errors.d.ts +40 -0
- package/dist/lib/policy/errors.js +109 -0
- package/dist/lib/policy/index.d.ts +11 -0
- package/dist/lib/policy/index.js +10 -0
- package/dist/lib/policy/parallelExecutor.d.ts +58 -0
- package/dist/lib/policy/parallelExecutor.js +215 -0
- package/dist/lib/policy/pluginLoader.d.ts +41 -0
- package/dist/lib/policy/pluginLoader.js +220 -0
- package/dist/lib/policy/pluginRegistry.d.ts +14 -0
- package/dist/lib/policy/pluginRegistry.js +69 -0
- package/dist/lib/policy/policyEngine.d.ts +39 -0
- package/dist/lib/policy/policyEngine.js +495 -0
- package/dist/lib/policy/resultAggregator.d.ts +8 -0
- package/dist/lib/policy/resultAggregator.js +138 -0
- package/dist/lib/policy/resultFormatter.d.ts +25 -0
- package/dist/lib/policy/resultFormatter.js +217 -0
- package/dist/lib/policy/types.d.ts +111 -0
- package/dist/lib/policy/types.js +1 -0
- package/dist/lib/policy/validationCache.d.ts +58 -0
- package/dist/lib/policy/validationCache.js +289 -0
- package/dist/lib/resources/baseResourceProvider.js +5 -0
- package/dist/lib/resources/cloud/aws/awsResources.js +2 -1
- package/dist/lib/resources/cloud/aws/karpenterResources.js +16 -2
- package/dist/lib/rutter.d.ts +7 -2
- package/dist/lib/rutter.js +177 -8
- package/dist/lib/security.js +4 -4
- package/dist/lib/templates/flexible-subchart.js +18 -7
- package/dist/lib/templates/umbrella-chart.js +29 -18
- package/dist/lib/umbrellaRutter.d.ts +1 -1
- package/dist/lib/umbrellaRutter.js +21 -9
- package/dist/lib/utils/envVarsLoader.js +13 -7
- package/dist/lib/utils/helmHelpers.js +10 -2
- package/dist/lib/utils/helmYamlSerializer.js +19 -9
- package/dist/lib/utils/logger.js +54 -39
- package/dist/lib/utils/valuesRef.js +9 -0
- package/dist/lib/validation/inputValidator.d.ts +26 -0
- package/dist/lib/validation/inputValidator.js +176 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/types/index.js +1 -0
- package/package.json +21 -19
|
@@ -15,8 +15,16 @@ export function isValidDisruptionBudget(budget) {
|
|
|
15
15
|
return nodePattern.test(budget.nodes);
|
|
16
16
|
}
|
|
17
17
|
export function isValidKubernetesDuration(duration) {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
try {
|
|
19
|
+
if (typeof duration !== 'string') {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const durationPattern = /^(\d+h)?(\d+m)?(\d+s)?$/;
|
|
23
|
+
return durationPattern.test(duration) && duration.length > 0;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
20
28
|
}
|
|
21
29
|
export const DEFAULT_TERMINATION_GRACE_PERIOD = '30s';
|
|
22
30
|
export class KarpenterResources extends BaseResourceProvider {
|
|
@@ -121,9 +129,15 @@ export class KarpenterResources extends BaseResourceProvider {
|
|
|
121
129
|
nodePoolConfig['weight'] = spec.weight;
|
|
122
130
|
}
|
|
123
131
|
if (spec.labels) {
|
|
132
|
+
if (typeof spec.labels !== 'object' || Array.isArray(spec.labels)) {
|
|
133
|
+
throw new Error('Labels must be a valid object');
|
|
134
|
+
}
|
|
124
135
|
nodePoolConfig['labels'] = spec.labels;
|
|
125
136
|
}
|
|
126
137
|
if (spec.annotations) {
|
|
138
|
+
if (typeof spec.annotations !== 'object' || Array.isArray(spec.annotations)) {
|
|
139
|
+
throw new Error('Annotations must be a valid object');
|
|
140
|
+
}
|
|
127
141
|
nodePoolConfig['annotations'] = spec.annotations;
|
|
128
142
|
}
|
|
129
143
|
return this.addKarpenterNodePool(nodePoolConfig);
|
package/dist/lib/rutter.d.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { ApiObject } from 'cdk8s';
|
|
|
2
2
|
import type { ChartProps } from 'cdk8s';
|
|
3
3
|
import type { Ingress, ServiceAccount } from 'cdk8s-plus-33';
|
|
4
4
|
import type { Construct } from 'constructs';
|
|
5
|
+
import { type SynthAsset } from './helmChartWriter.js';
|
|
5
6
|
import { type TimonelLogger } from './utils/logger.js';
|
|
6
7
|
import type { AWSALBIngressSpec, AWSEBSStorageClassSpec, AWSECRServiceAccountSpec, AWSEFSStorageClassSpec, AWSIRSAServiceAccountSpec } from './resources/cloud/aws/awsResources.js';
|
|
7
8
|
import type { KarpenterEC2NodeClassSpec, KarpenterNodeClaimSpec, KarpenterNodePoolSpec } from './resources/cloud/aws/karpenterResources.js';
|
|
8
9
|
import type { HelperDefinition } from './utils/helmHelpers.js';
|
|
10
|
+
import type { PolicyEngine } from './policy/index.js';
|
|
9
11
|
export declare class Rutter {
|
|
10
12
|
private static readonly HELPER_NAME;
|
|
11
13
|
private readonly app;
|
|
@@ -92,8 +94,10 @@ export declare class Rutter {
|
|
|
92
94
|
yaml: string;
|
|
93
95
|
target: string;
|
|
94
96
|
}>;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
toSynthArray(): Promise<SynthAsset[]>;
|
|
98
|
+
toSynthArraySync(): SynthAsset[];
|
|
99
|
+
private formatPolicyErrors;
|
|
100
|
+
write(outDir: string): Promise<void>;
|
|
97
101
|
}
|
|
98
102
|
export interface ChartMetadata {
|
|
99
103
|
description?: string;
|
|
@@ -117,6 +121,7 @@ export interface RutterProps {
|
|
|
117
121
|
manifestPrefix?: string;
|
|
118
122
|
meta: ChartMetadata;
|
|
119
123
|
namespace?: string;
|
|
124
|
+
policyEngine?: PolicyEngine;
|
|
120
125
|
scope?: Construct;
|
|
121
126
|
singleManifestFile?: boolean;
|
|
122
127
|
logger?: TimonelLogger;
|
package/dist/lib/rutter.js
CHANGED
|
@@ -8,6 +8,7 @@ import { KarpenterResources } from './resources/cloud/aws/karpenterResources.js'
|
|
|
8
8
|
import { isHelmExpression, isHelmConstruct } from './utils/helmControlStructures.js';
|
|
9
9
|
import { dumpHelmAwareYaml, preprocessHelmConstructs } from './utils/helmYamlSerializer.js';
|
|
10
10
|
import { generateHelpersTemplate } from './utils/helmHelpers.js';
|
|
11
|
+
const UNKNOWN_ERROR_MESSAGE = 'Unknown error';
|
|
11
12
|
export class Rutter {
|
|
12
13
|
constructor(props) {
|
|
13
14
|
this.assets = [];
|
|
@@ -29,6 +30,13 @@ export class Rutter {
|
|
|
29
30
|
});
|
|
30
31
|
this.awsResources = new AWSResources(this.chart);
|
|
31
32
|
this.karpenterResources = new KarpenterResources(this.chart);
|
|
33
|
+
const originalToSynthArray = this.toSynthArray.bind(this);
|
|
34
|
+
this['toSynthArray'] = (..._args) => {
|
|
35
|
+
if (!this.props.policyEngine) {
|
|
36
|
+
return this.toSynthArraySync();
|
|
37
|
+
}
|
|
38
|
+
return originalToSynthArray();
|
|
39
|
+
};
|
|
32
40
|
}
|
|
33
41
|
addAWSEBSStorageClass(spec) {
|
|
34
42
|
return this.awsResources.addEBSStorageClass(spec);
|
|
@@ -67,7 +75,7 @@ export class Rutter {
|
|
|
67
75
|
manifestObject = parse(yamlOrObject);
|
|
68
76
|
}
|
|
69
77
|
catch (error) {
|
|
70
|
-
throw new Error(`Invalid YAML provided to addManifest(): ${error instanceof Error ? error.message :
|
|
78
|
+
throw new Error(`Invalid YAML provided to addManifest(): ${error instanceof Error ? error.message : UNKNOWN_ERROR_MESSAGE}`);
|
|
71
79
|
}
|
|
72
80
|
}
|
|
73
81
|
else if (typeof yamlOrObject === 'object' && yamlOrObject !== null) {
|
|
@@ -126,7 +134,7 @@ ${yamlContent.trim()}
|
|
|
126
134
|
this.assets.push(conditionalAsset);
|
|
127
135
|
}
|
|
128
136
|
catch (error) {
|
|
129
|
-
throw new Error(`Failed to generate conditional template for manifest '${id}': ${error instanceof Error ? error.message :
|
|
137
|
+
throw new Error(`Failed to generate conditional template for manifest '${id}': ${error instanceof Error ? error.message : UNKNOWN_ERROR_MESSAGE}`);
|
|
130
138
|
}
|
|
131
139
|
return new ApiObject(this.chart, `${id}-placeholder`, {
|
|
132
140
|
apiVersion: manifestObject['apiVersion'],
|
|
@@ -190,7 +198,7 @@ ${yamlContent.trim()}
|
|
|
190
198
|
getAssets() {
|
|
191
199
|
return [...this.assets];
|
|
192
200
|
}
|
|
193
|
-
toSynthArray() {
|
|
201
|
+
async toSynthArray() {
|
|
194
202
|
const timer = this.logger.time('chart_synthesis');
|
|
195
203
|
this.logger.debug('Starting chart synthesis', {
|
|
196
204
|
chartName: this.meta.name,
|
|
@@ -217,6 +225,51 @@ ${yamlContent.trim()}
|
|
|
217
225
|
apiObjectCount: apiObjectIds.length,
|
|
218
226
|
operation: 'manifest_processing',
|
|
219
227
|
});
|
|
228
|
+
if (this.props.policyEngine) {
|
|
229
|
+
this.logger.debug('Starting policy validation before enrichment', {
|
|
230
|
+
chartName: this.meta.name,
|
|
231
|
+
manifestCount: manifestObjs.length,
|
|
232
|
+
operation: 'policy_validation_start',
|
|
233
|
+
});
|
|
234
|
+
try {
|
|
235
|
+
const validationResult = await this.props.policyEngine.validate(manifestObjs, this.meta);
|
|
236
|
+
if (!validationResult.valid) {
|
|
237
|
+
const errorMessage = this.formatPolicyErrors(validationResult);
|
|
238
|
+
this.logger.error('Policy validation failed', {
|
|
239
|
+
chartName: this.meta.name,
|
|
240
|
+
violationCount: validationResult.violations.length,
|
|
241
|
+
operation: 'policy_validation_failed',
|
|
242
|
+
});
|
|
243
|
+
throw new Error(`Policy validation failed: ${errorMessage}`);
|
|
244
|
+
}
|
|
245
|
+
if (validationResult.warnings.length > 0) {
|
|
246
|
+
this.logger.warn('Policy validation warnings', {
|
|
247
|
+
chartName: this.meta.name,
|
|
248
|
+
warningCount: validationResult.warnings.length,
|
|
249
|
+
warnings: validationResult.warnings.map((w) => ({
|
|
250
|
+
plugin: w.plugin,
|
|
251
|
+
message: w.message,
|
|
252
|
+
severity: w.severity,
|
|
253
|
+
})),
|
|
254
|
+
operation: 'policy_validation_warnings',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
this.logger.info('Policy validation completed successfully before enrichment', {
|
|
258
|
+
chartName: this.meta.name,
|
|
259
|
+
pluginCount: validationResult.metadata.pluginCount,
|
|
260
|
+
executionTime: validationResult.metadata.executionTime,
|
|
261
|
+
operation: 'policy_validation_success',
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
this.logger.error('Policy validation error', {
|
|
266
|
+
chartName: this.meta.name,
|
|
267
|
+
error: error instanceof Error ? error.message : UNKNOWN_ERROR_MESSAGE,
|
|
268
|
+
operation: 'policy_validation_error',
|
|
269
|
+
});
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
220
273
|
const enriched = manifestObjs.map((obj) => {
|
|
221
274
|
const preprocessed = preprocessHelmConstructs(obj);
|
|
222
275
|
if (preprocessed && typeof preprocessed === 'object') {
|
|
@@ -247,7 +300,7 @@ ${yamlContent.trim()}
|
|
|
247
300
|
.filter(Boolean)
|
|
248
301
|
.join('\n---\n');
|
|
249
302
|
const manifestId = this.props.manifestPrefix ?? 'manifests';
|
|
250
|
-
synthAssets.push({ id: manifestId, yaml: combinedYaml });
|
|
303
|
+
synthAssets.push({ id: manifestId, yaml: combinedYaml, target: 'templates' });
|
|
251
304
|
}
|
|
252
305
|
else {
|
|
253
306
|
enriched.forEach((obj, index) => {
|
|
@@ -255,12 +308,16 @@ ${yamlContent.trim()}
|
|
|
255
308
|
const manifestId = apiObjectId || `manifest-${index + 1}`;
|
|
256
309
|
const yaml = dumpHelmAwareYaml(obj).trim();
|
|
257
310
|
if (yaml) {
|
|
258
|
-
synthAssets.push({ id: manifestId, yaml });
|
|
311
|
+
synthAssets.push({ id: manifestId, yaml, target: 'templates' });
|
|
259
312
|
}
|
|
260
313
|
});
|
|
261
314
|
}
|
|
262
315
|
this.assets.forEach((asset) => {
|
|
263
|
-
synthAssets.push({
|
|
316
|
+
synthAssets.push({
|
|
317
|
+
id: asset.id,
|
|
318
|
+
yaml: asset.yaml,
|
|
319
|
+
target: asset.target || 'templates',
|
|
320
|
+
});
|
|
264
321
|
});
|
|
265
322
|
this.logger.info('Chart synthesis completed', {
|
|
266
323
|
chartName: this.meta.name,
|
|
@@ -271,7 +328,119 @@ ${yamlContent.trim()}
|
|
|
271
328
|
timer();
|
|
272
329
|
return synthAssets;
|
|
273
330
|
}
|
|
274
|
-
|
|
331
|
+
toSynthArraySync() {
|
|
332
|
+
if (this.props.policyEngine) {
|
|
333
|
+
throw new Error('toSynthArraySync() cannot be used with policy engine. Use toSynthArray() instead.');
|
|
334
|
+
}
|
|
335
|
+
const timer = this.logger.time('chart_synthesis_sync');
|
|
336
|
+
this.logger.debug('Starting synchronous chart synthesis', {
|
|
337
|
+
chartName: this.meta.name,
|
|
338
|
+
operation: 'synthesis_start_sync',
|
|
339
|
+
});
|
|
340
|
+
const apiObjectIds = [];
|
|
341
|
+
for (const child of this.chart.node.children) {
|
|
342
|
+
if (child instanceof ApiObject && !child.node.id.endsWith('-placeholder')) {
|
|
343
|
+
apiObjectIds.push(child.node.id);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const allManifestObjs = Testing.synth(this.chart);
|
|
347
|
+
const manifestObjs = allManifestObjs.filter((obj) => {
|
|
348
|
+
if (obj && typeof obj === 'object') {
|
|
349
|
+
const o = obj;
|
|
350
|
+
const annotations = o.metadata?.annotations || {};
|
|
351
|
+
return annotations['timonel.sh/placeholder'] !== 'true';
|
|
352
|
+
}
|
|
353
|
+
return true;
|
|
354
|
+
});
|
|
355
|
+
this.logger.info('Processing manifest objects synchronously', {
|
|
356
|
+
chartName: this.meta.name,
|
|
357
|
+
manifestCount: manifestObjs.length,
|
|
358
|
+
apiObjectCount: apiObjectIds.length,
|
|
359
|
+
operation: 'manifest_processing_sync',
|
|
360
|
+
});
|
|
361
|
+
const enriched = manifestObjs.map((obj) => {
|
|
362
|
+
const preprocessed = preprocessHelmConstructs(obj);
|
|
363
|
+
if (preprocessed && typeof preprocessed === 'object') {
|
|
364
|
+
const o = preprocessed;
|
|
365
|
+
o.metadata = o.metadata ?? {};
|
|
366
|
+
o.metadata.labels = o.metadata.labels ?? {};
|
|
367
|
+
const labels = o.metadata.labels;
|
|
368
|
+
const defaults = {
|
|
369
|
+
'helm.sh/chart': `{{ .Chart.Name }}-{{ .Chart.Version }}`,
|
|
370
|
+
'app.kubernetes.io/name': include(Rutter.HELPER_NAME),
|
|
371
|
+
'app.kubernetes.io/instance': '{{ .Release.Name }}',
|
|
372
|
+
'app.kubernetes.io/version': '{{ .Chart.Version }}',
|
|
373
|
+
'app.kubernetes.io/managed-by': '{{ .Release.Service }}',
|
|
374
|
+
'app.kubernetes.io/part-of': '{{ .Chart.Name }}',
|
|
375
|
+
};
|
|
376
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
377
|
+
if (!(key in labels)) {
|
|
378
|
+
labels[key] = value;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return preprocessed;
|
|
383
|
+
});
|
|
384
|
+
const synthAssets = [];
|
|
385
|
+
if (this.props.singleManifestFile) {
|
|
386
|
+
const combinedYaml = enriched
|
|
387
|
+
.map((obj) => dumpHelmAwareYaml(obj).trim())
|
|
388
|
+
.filter(Boolean)
|
|
389
|
+
.join('\n---\n');
|
|
390
|
+
const manifestId = this.props.manifestPrefix ?? 'manifests';
|
|
391
|
+
synthAssets.push({ id: manifestId, yaml: combinedYaml, target: 'templates' });
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
enriched.forEach((obj, index) => {
|
|
395
|
+
const apiObjectId = apiObjectIds[index];
|
|
396
|
+
const manifestId = apiObjectId || `manifest-${index + 1}`;
|
|
397
|
+
const yaml = dumpHelmAwareYaml(obj).trim();
|
|
398
|
+
if (yaml) {
|
|
399
|
+
synthAssets.push({ id: manifestId, yaml, target: 'templates' });
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
this.assets.forEach((asset) => {
|
|
404
|
+
synthAssets.push({
|
|
405
|
+
id: asset.id,
|
|
406
|
+
yaml: asset.yaml,
|
|
407
|
+
target: asset.target || 'templates',
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
this.logger.info('Synchronous chart synthesis completed', {
|
|
411
|
+
chartName: this.meta.name,
|
|
412
|
+
totalAssets: synthAssets.length,
|
|
413
|
+
additionalAssets: this.assets.length,
|
|
414
|
+
operation: 'synthesis_complete_sync',
|
|
415
|
+
});
|
|
416
|
+
timer();
|
|
417
|
+
return synthAssets;
|
|
418
|
+
}
|
|
419
|
+
formatPolicyErrors(result) {
|
|
420
|
+
const errorMessages = [];
|
|
421
|
+
if (result.violations && result.violations.length > 0) {
|
|
422
|
+
errorMessages.push(`Found ${result.violations.length} policy violation(s):`);
|
|
423
|
+
result.violations.forEach((violation, index) => {
|
|
424
|
+
const parts = [`${index + 1}. [${violation.plugin}] ${violation.message}`];
|
|
425
|
+
if (violation.resourcePath) {
|
|
426
|
+
parts.push(`Resource: ${violation.resourcePath}`);
|
|
427
|
+
}
|
|
428
|
+
if (violation.field) {
|
|
429
|
+
parts.push(`Field: ${violation.field}`);
|
|
430
|
+
}
|
|
431
|
+
if (violation.suggestion) {
|
|
432
|
+
parts.push(`Suggestion: ${violation.suggestion}`);
|
|
433
|
+
}
|
|
434
|
+
errorMessages.push(` ${parts.join(' | ')}`);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
if (result.summary) {
|
|
438
|
+
const summary = result.summary;
|
|
439
|
+
errorMessages.push(`Summary: ${summary.violationsBySeverity.error} error(s), ${summary.violationsBySeverity.warning} warning(s), ${summary.violationsBySeverity.info} info(s)`);
|
|
440
|
+
}
|
|
441
|
+
return errorMessages.join('\n');
|
|
442
|
+
}
|
|
443
|
+
async write(outDir) {
|
|
275
444
|
const timer = this.logger.time('chart_write');
|
|
276
445
|
this.logger.info('Starting chart write operation', {
|
|
277
446
|
chartName: this.meta.name,
|
|
@@ -297,7 +466,7 @@ ${helper.template}
|
|
|
297
466
|
else {
|
|
298
467
|
helpersContent = generateHelpersTemplate(this.props.cloudProvider);
|
|
299
468
|
}
|
|
300
|
-
const synthAssets = this.toSynthArray();
|
|
469
|
+
const synthAssets = await this.toSynthArray();
|
|
301
470
|
this.logger.info('Generated assets for chart', {
|
|
302
471
|
chartName: this.meta.name,
|
|
303
472
|
assetCount: synthAssets.length,
|
package/dist/lib/security.js
CHANGED
|
@@ -84,7 +84,7 @@ export class SecurityUtils {
|
|
|
84
84
|
}
|
|
85
85
|
const sanitized = env.replace(/[^a-zA-Z0-9-_]/g, '');
|
|
86
86
|
if (sanitized !== env) {
|
|
87
|
-
throw new Error(`Invalid environment name: ${
|
|
87
|
+
throw new Error(`Invalid environment name: ${SecurityUtils.sanitizeLogMessage(env)}`);
|
|
88
88
|
}
|
|
89
89
|
if (sanitized.length === 0 || sanitized.length > 63) {
|
|
90
90
|
throw new Error('Environment name must be 1-63 characters long');
|
|
@@ -150,7 +150,7 @@ export class SecurityUtils {
|
|
|
150
150
|
}
|
|
151
151
|
const nameRegex = /^[A-Z_][A-Z0-9_]*$/;
|
|
152
152
|
if (!nameRegex.test(sanitizedName)) {
|
|
153
|
-
throw new Error(`Invalid environment variable name: ${
|
|
153
|
+
throw new Error(`Invalid environment variable name: ${SecurityUtils.sanitizeLogMessage(name)}`);
|
|
154
154
|
}
|
|
155
155
|
const hasDangerousPattern = (val) => {
|
|
156
156
|
if (val.includes('$(') && val.includes(')'))
|
|
@@ -173,7 +173,7 @@ export class SecurityUtils {
|
|
|
173
173
|
};
|
|
174
174
|
const dangerousPattern = hasDangerousPattern(value);
|
|
175
175
|
if (dangerousPattern) {
|
|
176
|
-
throw new Error(`Environment variable value contains dangerous pattern (${dangerousPattern}): ${
|
|
176
|
+
throw new Error(`Environment variable value contains dangerous pattern (${dangerousPattern}): ${SecurityUtils.sanitizeLogMessage(value)}`);
|
|
177
177
|
}
|
|
178
178
|
if (value.length > 32768) {
|
|
179
179
|
throw new Error('Environment variable value exceeds maximum length (32KB)');
|
|
@@ -194,7 +194,7 @@ export class SecurityUtils {
|
|
|
194
194
|
}
|
|
195
195
|
const tagRegex = /^[a-zA-Z0-9._-]+$/;
|
|
196
196
|
if (!tagRegex.test(trimmedTag)) {
|
|
197
|
-
throw new Error(`Invalid image tag format: ${
|
|
197
|
+
throw new Error(`Invalid image tag format: ${SecurityUtils.sanitizeLogMessage(trimmedTag)}`);
|
|
198
198
|
}
|
|
199
199
|
if (trimmedTag.length > 128) {
|
|
200
200
|
throw new Error('Image tag exceeds maximum length (128 characters)');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { Chart } from 'cdk8s';
|
|
4
|
+
import { SecurityUtils } from '../security.js';
|
|
4
5
|
import { dumpHelmAwareYaml } from '../utils/helmYamlSerializer.js';
|
|
5
6
|
import { generateHelpersTemplate } from '../utils/helmHelpers.js';
|
|
6
7
|
export class FlexibleSubchart extends Chart {
|
|
@@ -51,13 +52,19 @@ export class FlexibleSubchart extends Chart {
|
|
|
51
52
|
version: this.config.version || '1.0.0',
|
|
52
53
|
appVersion: this.config.version || '1.0.0',
|
|
53
54
|
};
|
|
54
|
-
|
|
55
|
+
const chartYamlPath = SecurityUtils.validatePath(join(outputDir, 'Chart.yaml'), process.cwd(), {
|
|
56
|
+
allowAbsolute: true,
|
|
57
|
+
});
|
|
58
|
+
writeFileSync(chartYamlPath, dumpHelmAwareYaml(subchartYaml));
|
|
55
59
|
const subchartValues = {
|
|
56
60
|
enabled: true,
|
|
57
61
|
...Object.fromEntries(Object.entries(this.config).filter(([key, value]) => key !== 'chart' && typeof value !== 'function' && value !== null && value !== undefined)),
|
|
58
62
|
};
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
const valuesYamlPath = SecurityUtils.validatePath(join(outputDir, 'values.yaml'), process.cwd(), { allowAbsolute: true });
|
|
64
|
+
writeFileSync(valuesYamlPath, dumpHelmAwareYaml(subchartValues));
|
|
65
|
+
const templatesDir = SecurityUtils.validatePath(join(outputDir, 'templates'), process.cwd(), {
|
|
66
|
+
allowAbsolute: true,
|
|
67
|
+
});
|
|
61
68
|
if (!existsSync(templatesDir)) {
|
|
62
69
|
mkdirSync(templatesDir, { recursive: true });
|
|
63
70
|
}
|
|
@@ -65,13 +72,15 @@ export class FlexibleSubchart extends Chart {
|
|
|
65
72
|
includeKubernetes: true,
|
|
66
73
|
includeSprig: true,
|
|
67
74
|
});
|
|
68
|
-
|
|
75
|
+
const helpersTplPath = SecurityUtils.validatePath(join(templatesDir, '_helpers.tpl'), process.cwd(), { allowAbsolute: true });
|
|
76
|
+
writeFileSync(helpersTplPath, helpersTpl);
|
|
69
77
|
this._manifests.forEach((item) => {
|
|
70
78
|
if (item && typeof item === 'object' && 'manifest' in item && 'id' in item) {
|
|
71
79
|
const { manifest, id } = item;
|
|
72
80
|
if (manifest && typeof manifest === 'object') {
|
|
73
81
|
const templateContent = this._generateManifestTemplate(manifest, id);
|
|
74
|
-
|
|
82
|
+
const manifestPath = SecurityUtils.validatePath(join(templatesDir, `${id}.yaml`), process.cwd(), { allowAbsolute: true });
|
|
83
|
+
writeFileSync(manifestPath, templateContent);
|
|
75
84
|
}
|
|
76
85
|
}
|
|
77
86
|
});
|
|
@@ -195,8 +204,10 @@ export default function createChart() {
|
|
|
195
204
|
|
|
196
205
|
// Auto-execute when run directly
|
|
197
206
|
if (import.meta.url === new URL(import.meta.url).href) {
|
|
198
|
-
|
|
199
|
-
|
|
207
|
+
(async () => {
|
|
208
|
+
const chart = createChart();
|
|
209
|
+
await chart.write('dist');
|
|
210
|
+
})();
|
|
200
211
|
}
|
|
201
212
|
`;
|
|
202
213
|
}
|
|
@@ -2,6 +2,7 @@ import { writeFileSync, mkdirSync, existsSync, copyFileSync, readdirSync, rmSync
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { parse } from 'yaml';
|
|
4
4
|
import { App, Chart, ApiObject } from 'cdk8s';
|
|
5
|
+
import { SecurityUtils } from '../security.js';
|
|
5
6
|
import { dumpHelmAwareYaml } from '../utils/helmYamlSerializer.js';
|
|
6
7
|
import { generateHelpersTemplate } from '../utils/helmHelpers.js';
|
|
7
8
|
import { createFlexibleSubchart } from './flexible-subchart.js';
|
|
@@ -46,7 +47,7 @@ function readYamlFile(filePath: string): Record<string, unknown> {
|
|
|
46
47
|
return content && typeof content === 'object' ? (content as Record<string, unknown>) : {};
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
export function synth(outDir: string, options?: SynthOptions) {
|
|
50
|
+
export async function synth(outDir: string, options?: SynthOptions) {
|
|
50
51
|
const mode = resolveMode(options);
|
|
51
52
|
const app = new App({
|
|
52
53
|
outdir: outDir,
|
|
@@ -81,7 +82,7 @@ export function synth(outDir: string, options?: SynthOptions) {
|
|
|
81
82
|
'namespace',
|
|
82
83
|
);
|
|
83
84
|
|
|
84
|
-
umbrella.write(outDir);
|
|
85
|
+
await umbrella.write(outDir);
|
|
85
86
|
|
|
86
87
|
const chartPath = join(outDir, 'Chart.yaml');
|
|
87
88
|
const valuesPath = join(outDir, 'values.yaml');
|
|
@@ -97,11 +98,11 @@ export function synth(outDir: string, options?: SynthOptions) {
|
|
|
97
98
|
|
|
98
99
|
const dependencies: Array<{ name: string; version: string; repository: string }> = [];
|
|
99
100
|
|
|
100
|
-
|
|
101
|
+
for (const subchart of SUBCHARTS) {
|
|
101
102
|
const instance = subchart.factory();
|
|
102
103
|
const targetDir = join(chartsDir, subchart.name);
|
|
103
104
|
rmSync(targetDir, { recursive: true, force: true });
|
|
104
|
-
instance.write(targetDir);
|
|
105
|
+
await instance.write(targetDir);
|
|
105
106
|
const meta = instance.getMeta();
|
|
106
107
|
const version = meta.version ?? '0.1.0';
|
|
107
108
|
dependencies.push({
|
|
@@ -113,7 +114,7 @@ export function synth(outDir: string, options?: SynthOptions) {
|
|
|
113
114
|
if (Object.keys(subchartValues).length > 0) {
|
|
114
115
|
valuesDoc[subchart.name] = subchartValues;
|
|
115
116
|
}
|
|
116
|
-
}
|
|
117
|
+
}
|
|
117
118
|
|
|
118
119
|
chartDoc.dependencies = dependencies;
|
|
119
120
|
} else {
|
|
@@ -123,11 +124,11 @@ export function synth(outDir: string, options?: SynthOptions) {
|
|
|
123
124
|
}
|
|
124
125
|
delete chartDoc.dependencies;
|
|
125
126
|
|
|
126
|
-
|
|
127
|
+
for (const subchart of SUBCHARTS) {
|
|
127
128
|
const instance = subchart.factory();
|
|
128
129
|
const tempDir = join(outDir, '.timonel-inline-' + subchart.name);
|
|
129
130
|
rmSync(tempDir, { recursive: true, force: true });
|
|
130
|
-
instance.write(tempDir);
|
|
131
|
+
await instance.write(tempDir);
|
|
131
132
|
|
|
132
133
|
const subTemplatesDir = join(tempDir, 'templates');
|
|
133
134
|
if (existsSync(subTemplatesDir)) {
|
|
@@ -144,7 +145,7 @@ export function synth(outDir: string, options?: SynthOptions) {
|
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
rmSync(tempDir, { recursive: true, force: true });
|
|
147
|
-
}
|
|
148
|
+
}
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
writeFileSync(chartPath, stringify(chartDoc));
|
|
@@ -227,10 +228,12 @@ export class UmbrellaChartTemplate extends Chart {
|
|
|
227
228
|
else if (subchart.chart && typeof subchart.chart === 'object') {
|
|
228
229
|
this._handleObjectChart(subchart, flexibleSubchart);
|
|
229
230
|
}
|
|
230
|
-
|
|
231
|
+
const sanitizedName = subchart.name.replace(/[\r\n]/g, '');
|
|
232
|
+
console.log(`Added flexible subchart: ${sanitizedName}`);
|
|
231
233
|
}
|
|
232
234
|
catch (_error) {
|
|
233
|
-
|
|
235
|
+
const sanitizedName = subchart.name.replace(/[\r\n]/g, '');
|
|
236
|
+
console.warn(`Failed to add subchart ${sanitizedName}:`, _error);
|
|
234
237
|
this._createFallbackSubchart(subchart, index);
|
|
235
238
|
}
|
|
236
239
|
}
|
|
@@ -249,7 +252,8 @@ export class UmbrellaChartTemplate extends Chart {
|
|
|
249
252
|
version: subchart.version || '1.0.0',
|
|
250
253
|
description: `${subchart.name} subchart (fallback)`,
|
|
251
254
|
});
|
|
252
|
-
|
|
255
|
+
const sanitizedName = subchart.name.replace(/[\r\n]/g, '');
|
|
256
|
+
console.log(`Created fallback subchart: ${sanitizedName}`);
|
|
253
257
|
}
|
|
254
258
|
_handleFunctionChart(subchart, index, flexibleSubchart) {
|
|
255
259
|
try {
|
|
@@ -263,7 +267,8 @@ export class UmbrellaChartTemplate extends Chart {
|
|
|
263
267
|
}
|
|
264
268
|
}
|
|
265
269
|
catch (error) {
|
|
266
|
-
|
|
270
|
+
const sanitizedName = subchart.name.replace(/[\r\n]/g, '');
|
|
271
|
+
console.log(`Failed to process subchart ${sanitizedName}:`, error);
|
|
267
272
|
}
|
|
268
273
|
}
|
|
269
274
|
_handleObjectChart(subchart, flexibleSubchart) {
|
|
@@ -338,7 +343,8 @@ export class UmbrellaChartTemplate extends Chart {
|
|
|
338
343
|
}
|
|
339
344
|
}
|
|
340
345
|
catch (error) {
|
|
341
|
-
|
|
346
|
+
const sanitizedId = asset.id.replace(/[\r\n]/g, '');
|
|
347
|
+
console.log(`Failed to parse asset ${sanitizedId}:`, error);
|
|
342
348
|
}
|
|
343
349
|
});
|
|
344
350
|
}
|
|
@@ -375,7 +381,8 @@ export class UmbrellaChartTemplate extends Chart {
|
|
|
375
381
|
includeSprig: true,
|
|
376
382
|
});
|
|
377
383
|
writeFileSync(join(templatesDir, '_helpers.tpl'), helpersTpl);
|
|
378
|
-
|
|
384
|
+
const sanitizedName = subchartName.replace(/[\r\n]/g, '');
|
|
385
|
+
console.log(`Created basic Helm chart structure for subchart: ${sanitizedName}`);
|
|
379
386
|
}
|
|
380
387
|
writeHelmChart(outputDir) {
|
|
381
388
|
if (!existsSync(outputDir)) {
|
|
@@ -412,7 +419,7 @@ export class UmbrellaChartTemplate extends Chart {
|
|
|
412
419
|
mkdirSync(chartsDir, { recursive: true });
|
|
413
420
|
}
|
|
414
421
|
this.config.subcharts?.forEach((subchart) => {
|
|
415
|
-
const subchartDir = join(chartsDir, subchart.name);
|
|
422
|
+
const subchartDir = SecurityUtils.validatePath(join(chartsDir, subchart.name), process.cwd(), { allowAbsolute: true });
|
|
416
423
|
if (!existsSync(subchartDir)) {
|
|
417
424
|
mkdirSync(subchartDir, { recursive: true });
|
|
418
425
|
}
|
|
@@ -444,7 +451,8 @@ export class UmbrellaChartTemplate extends Chart {
|
|
|
444
451
|
}
|
|
445
452
|
}
|
|
446
453
|
catch (fallbackError) {
|
|
447
|
-
|
|
454
|
+
const sanitizedName = subchart.name.replace(/[\r\n]/g, '');
|
|
455
|
+
console.warn(`Failed to create subchart ${sanitizedName}:`, fallbackError);
|
|
448
456
|
flexibleSubchart.writeHelmChart(subchartDir);
|
|
449
457
|
}
|
|
450
458
|
}
|
|
@@ -462,7 +470,9 @@ export class UmbrellaChartTemplate extends Chart {
|
|
|
462
470
|
flexibleSubchart.writeHelmChart(subchartDir);
|
|
463
471
|
}
|
|
464
472
|
});
|
|
465
|
-
const templatesDir = join(outputDir, 'templates')
|
|
473
|
+
const templatesDir = SecurityUtils.validatePath(join(outputDir, 'templates'), process.cwd(), {
|
|
474
|
+
allowAbsolute: true,
|
|
475
|
+
});
|
|
466
476
|
if (!existsSync(templatesDir)) {
|
|
467
477
|
mkdirSync(templatesDir, { recursive: true });
|
|
468
478
|
}
|
|
@@ -479,7 +489,8 @@ export class UmbrellaChartTemplate extends Chart {
|
|
|
479
489
|
},
|
|
480
490
|
},
|
|
481
491
|
};
|
|
482
|
-
|
|
492
|
+
const namespaceYamlPath = SecurityUtils.validatePath(join(templatesDir, 'namespace.yaml'), process.cwd(), { allowAbsolute: true });
|
|
493
|
+
writeFileSync(namespaceYamlPath, dumpHelmAwareYaml(namespaceYaml));
|
|
483
494
|
const helpersTpl = generateHelpersTemplate('aws', undefined, {
|
|
484
495
|
includeKubernetes: true,
|
|
485
496
|
includeSprig: true,
|
|
@@ -21,7 +21,7 @@ export declare class UmbrellaRutter {
|
|
|
21
21
|
private readonly logger;
|
|
22
22
|
constructor(props: UmbrellaRutterProps);
|
|
23
23
|
private validateMetadata;
|
|
24
|
-
write(outDir: string): void
|
|
24
|
+
write(outDir: string): Promise<void>;
|
|
25
25
|
private writeParentChart;
|
|
26
26
|
private writeParentValues;
|
|
27
27
|
private deepMerge;
|
|
@@ -22,24 +22,29 @@ export class UmbrellaRutter {
|
|
|
22
22
|
throw new Error('Chart version must follow semantic versioning (e.g., 1.0.0)');
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
write(outDir) {
|
|
25
|
+
async write(outDir) {
|
|
26
26
|
const validatedOutDir = SecurityUtils.validatePath(outDir, process.cwd(), {
|
|
27
27
|
allowAbsolute: true,
|
|
28
28
|
});
|
|
29
29
|
mkdirSync(validatedOutDir, { recursive: true });
|
|
30
|
-
|
|
30
|
+
const chartsDir = SecurityUtils.validatePath(join(validatedOutDir, 'charts'), process.cwd(), {
|
|
31
|
+
allowAbsolute: true,
|
|
32
|
+
});
|
|
33
|
+
mkdirSync(chartsDir, { recursive: true });
|
|
31
34
|
for (const subchart of this.props.subcharts) {
|
|
32
35
|
if (!SecurityUtils.isValidSubchartName(subchart.name)) {
|
|
33
36
|
throw new Error(`Invalid subchart name: ${SecurityUtils.sanitizeLogMessage(subchart.name)}`);
|
|
34
37
|
}
|
|
35
38
|
const sanitizedName = subchart.name;
|
|
36
|
-
const subchartDir = join(validatedOutDir, 'charts', sanitizedName);
|
|
37
|
-
subchart.rutter.write(subchartDir);
|
|
39
|
+
const subchartDir = SecurityUtils.validatePath(join(validatedOutDir, 'charts', sanitizedName), process.cwd(), { allowAbsolute: true });
|
|
40
|
+
await subchart.rutter.write(subchartDir);
|
|
38
41
|
}
|
|
39
42
|
this.writeParentChart(validatedOutDir);
|
|
40
43
|
this.writeParentValues(validatedOutDir);
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
const templatesDir = SecurityUtils.validatePath(join(validatedOutDir, 'templates'), process.cwd(), { allowAbsolute: true });
|
|
45
|
+
mkdirSync(templatesDir, { recursive: true });
|
|
46
|
+
const notesPath = SecurityUtils.validatePath(join(validatedOutDir, 'templates', 'NOTES.txt'), process.cwd(), { allowAbsolute: true });
|
|
47
|
+
writeFileSync(notesPath, this.generateNotesTemplate());
|
|
43
48
|
}
|
|
44
49
|
writeParentChart(outDir) {
|
|
45
50
|
const chartYaml = {
|
|
@@ -63,7 +68,10 @@ export class UmbrellaRutter {
|
|
|
63
68
|
};
|
|
64
69
|
}),
|
|
65
70
|
};
|
|
66
|
-
|
|
71
|
+
const chartYamlPath = SecurityUtils.validatePath(join(outDir, 'Chart.yaml'), process.cwd(), {
|
|
72
|
+
allowAbsolute: true,
|
|
73
|
+
});
|
|
74
|
+
writeFileSync(chartYamlPath, dumpHelmAwareYaml(chartYaml));
|
|
67
75
|
}
|
|
68
76
|
writeParentValues(outDir) {
|
|
69
77
|
const values = {
|
|
@@ -77,12 +85,16 @@ export class UmbrellaRutter {
|
|
|
77
85
|
};
|
|
78
86
|
}
|
|
79
87
|
}
|
|
80
|
-
|
|
88
|
+
const valuesYamlPath = SecurityUtils.validatePath(join(outDir, 'values.yaml'), process.cwd(), {
|
|
89
|
+
allowAbsolute: true,
|
|
90
|
+
});
|
|
91
|
+
writeFileSync(valuesYamlPath, dumpHelmAwareYaml(values));
|
|
81
92
|
if (this.props.envValues) {
|
|
82
93
|
for (const [env, envVals] of Object.entries(this.props.envValues)) {
|
|
83
94
|
const sanitizedEnv = SecurityUtils.sanitizeEnvironmentName(env);
|
|
84
95
|
const envValues = this.deepMerge(values, envVals);
|
|
85
|
-
|
|
96
|
+
const envValuesPath = SecurityUtils.validatePath(join(outDir, `values-${sanitizedEnv}.yaml`), process.cwd(), { allowAbsolute: true });
|
|
97
|
+
writeFileSync(envValuesPath, dumpHelmAwareYaml(envValues));
|
|
86
98
|
}
|
|
87
99
|
}
|
|
88
100
|
}
|