timonel 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +432 -1
  3. package/SECURITY.md +2 -2
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.js +2 -0
  6. package/dist/lib/policy/configurationLoader.d.ts +46 -0
  7. package/dist/lib/policy/configurationLoader.js +251 -0
  8. package/dist/lib/policy/errorContextGenerator.d.ts +63 -0
  9. package/dist/lib/policy/errorContextGenerator.js +302 -0
  10. package/dist/lib/policy/errors.d.ts +40 -0
  11. package/dist/lib/policy/errors.js +109 -0
  12. package/dist/lib/policy/index.d.ts +11 -0
  13. package/dist/lib/policy/index.js +10 -0
  14. package/dist/lib/policy/parallelExecutor.d.ts +58 -0
  15. package/dist/lib/policy/parallelExecutor.js +215 -0
  16. package/dist/lib/policy/pluginLoader.d.ts +41 -0
  17. package/dist/lib/policy/pluginLoader.js +220 -0
  18. package/dist/lib/policy/pluginRegistry.d.ts +14 -0
  19. package/dist/lib/policy/pluginRegistry.js +69 -0
  20. package/dist/lib/policy/policyEngine.d.ts +39 -0
  21. package/dist/lib/policy/policyEngine.js +495 -0
  22. package/dist/lib/policy/resultAggregator.d.ts +8 -0
  23. package/dist/lib/policy/resultAggregator.js +138 -0
  24. package/dist/lib/policy/resultFormatter.d.ts +25 -0
  25. package/dist/lib/policy/resultFormatter.js +217 -0
  26. package/dist/lib/policy/types.d.ts +111 -0
  27. package/dist/lib/policy/types.js +1 -0
  28. package/dist/lib/policy/validationCache.d.ts +58 -0
  29. package/dist/lib/policy/validationCache.js +289 -0
  30. package/dist/lib/rutter.d.ts +7 -2
  31. package/dist/lib/rutter.js +177 -8
  32. package/dist/lib/templates/flexible-subchart.js +4 -2
  33. package/dist/lib/templates/umbrella-chart.js +8 -8
  34. package/dist/lib/umbrellaRutter.d.ts +1 -1
  35. package/dist/lib/umbrellaRutter.js +2 -2
  36. package/dist/lib/validation/inputValidator.d.ts +26 -0
  37. package/dist/lib/validation/inputValidator.js +176 -0
  38. package/dist/types/index.d.ts +27 -0
  39. package/dist/types/index.js +1 -0
  40. package/package.json +22 -20
@@ -0,0 +1,289 @@
1
+ import { createLogger } from '../utils/logger.js';
2
+ const DEFAULT_CACHE_OPTIONS = {
3
+ maxEntries: 1000,
4
+ maxAge: 30 * 60 * 1000,
5
+ enableCompression: false,
6
+ evictionStrategy: 'lru',
7
+ cacheFailures: true,
8
+ minExecutionTimeToCache: 10,
9
+ };
10
+ export class ValidationCache {
11
+ constructor(options = {}) {
12
+ this.cache = new Map();
13
+ this.stats = {
14
+ hits: 0,
15
+ misses: 0,
16
+ entries: 0,
17
+ memoryUsage: 0,
18
+ hitRatio: 0,
19
+ averageAccessCount: 0,
20
+ };
21
+ this.options = { ...DEFAULT_CACHE_OPTIONS, ...options };
22
+ this.logger = createLogger('validation-cache');
23
+ this.logger.debug('ValidationCache initialized', {
24
+ options: this.options,
25
+ operation: 'cache_init',
26
+ });
27
+ this.startPeriodicCleanup();
28
+ }
29
+ get(manifestHash, pluginHash) {
30
+ const cacheKey = this.generateCacheKey(manifestHash, pluginHash);
31
+ const entry = this.cache.get(cacheKey);
32
+ if (!entry) {
33
+ this.stats.misses++;
34
+ this.updateStats();
35
+ this.logger.debug('Cache miss', {
36
+ manifestHash: manifestHash.substring(0, 8),
37
+ pluginHash: pluginHash.substring(0, 8),
38
+ operation: 'cache_miss',
39
+ });
40
+ return undefined;
41
+ }
42
+ if (this.isExpired(entry)) {
43
+ this.cache.delete(cacheKey);
44
+ this.stats.misses++;
45
+ this.updateStats();
46
+ this.logger.debug('Cache entry expired', {
47
+ manifestHash: manifestHash.substring(0, 8),
48
+ pluginHash: pluginHash.substring(0, 8),
49
+ age: Date.now() - entry.timestamp,
50
+ operation: 'cache_expired',
51
+ });
52
+ return undefined;
53
+ }
54
+ entry.accessCount++;
55
+ entry.lastAccessed = Date.now();
56
+ this.stats.hits++;
57
+ this.updateStats();
58
+ this.logger.debug('Cache hit', {
59
+ manifestHash: manifestHash.substring(0, 8),
60
+ pluginHash: pluginHash.substring(0, 8),
61
+ accessCount: entry.accessCount,
62
+ age: Date.now() - entry.timestamp,
63
+ operation: 'cache_hit',
64
+ });
65
+ return entry.result;
66
+ }
67
+ set(manifestHash, pluginHash, result) {
68
+ if (!this.shouldCache(result)) {
69
+ this.logger.debug('Skipping cache storage', {
70
+ manifestHash: manifestHash.substring(0, 8),
71
+ pluginHash: pluginHash.substring(0, 8),
72
+ executionTime: result.metadata.executionTime,
73
+ hasViolations: result.violations.length > 0,
74
+ operation: 'cache_skip',
75
+ });
76
+ return;
77
+ }
78
+ const cacheKey = this.generateCacheKey(manifestHash, pluginHash);
79
+ if (this.cache.size >= this.options.maxEntries) {
80
+ this.evictEntries();
81
+ }
82
+ const entry = {
83
+ result,
84
+ timestamp: Date.now(),
85
+ manifestHash,
86
+ pluginHash,
87
+ accessCount: 0,
88
+ lastAccessed: Date.now(),
89
+ };
90
+ this.cache.set(cacheKey, entry);
91
+ this.updateStats();
92
+ this.logger.debug('Cache entry stored', {
93
+ manifestHash: manifestHash.substring(0, 8),
94
+ pluginHash: pluginHash.substring(0, 8),
95
+ executionTime: result.metadata.executionTime,
96
+ violationCount: result.violations.length,
97
+ warningCount: result.warnings.length,
98
+ operation: 'cache_store',
99
+ });
100
+ }
101
+ invalidate(criteria) {
102
+ if (criteria.all) {
103
+ return this.invalidateAll();
104
+ }
105
+ return this.invalidateSelective(criteria);
106
+ }
107
+ invalidateAll() {
108
+ const invalidatedCount = this.cache.size;
109
+ this.cache.clear();
110
+ this.logger.info('Cache cleared completely', {
111
+ invalidatedCount,
112
+ operation: 'cache_clear_all',
113
+ });
114
+ this.updateStats();
115
+ return invalidatedCount;
116
+ }
117
+ invalidateSelective(criteria) {
118
+ const keysToDelete = [];
119
+ for (const [key, entry] of this.cache.entries()) {
120
+ if (this.shouldInvalidateEntry(entry, criteria)) {
121
+ keysToDelete.push(key);
122
+ }
123
+ }
124
+ for (const key of keysToDelete) {
125
+ this.cache.delete(key);
126
+ }
127
+ this.logger.debug('Cache entries invalidated', {
128
+ invalidatedCount: keysToDelete.length,
129
+ criteria,
130
+ operation: 'cache_invalidate',
131
+ });
132
+ this.updateStats();
133
+ return keysToDelete.length;
134
+ }
135
+ shouldInvalidateEntry(entry, criteria) {
136
+ if (criteria.manifestHash && entry.manifestHash === criteria.manifestHash) {
137
+ return true;
138
+ }
139
+ if (criteria.pluginHash && entry.pluginHash === criteria.pluginHash) {
140
+ return true;
141
+ }
142
+ if (criteria.olderThan && entry.timestamp < criteria.olderThan) {
143
+ return true;
144
+ }
145
+ return false;
146
+ }
147
+ getStats() {
148
+ return { ...this.stats };
149
+ }
150
+ clearStats() {
151
+ this.stats = {
152
+ hits: 0,
153
+ misses: 0,
154
+ entries: this.cache.size,
155
+ memoryUsage: this.estimateMemoryUsage(),
156
+ hitRatio: 0,
157
+ averageAccessCount: 0,
158
+ };
159
+ this.logger.debug('Cache statistics cleared', {
160
+ operation: 'cache_stats_clear',
161
+ });
162
+ }
163
+ generateCacheKey(manifestHash, pluginHash) {
164
+ return `${manifestHash}:${pluginHash}`;
165
+ }
166
+ isExpired(entry) {
167
+ return Date.now() - entry.timestamp > this.options.maxAge;
168
+ }
169
+ shouldCache(result) {
170
+ if (result.metadata.executionTime < this.options.minExecutionTimeToCache) {
171
+ return false;
172
+ }
173
+ if (!this.options.cacheFailures && !result.valid) {
174
+ return false;
175
+ }
176
+ return true;
177
+ }
178
+ evictEntries() {
179
+ const entriesToEvict = Math.max(1, Math.floor(this.options.maxEntries * 0.1));
180
+ switch (this.options.evictionStrategy) {
181
+ case 'lru':
182
+ this.evictLRU(entriesToEvict);
183
+ break;
184
+ case 'lfu':
185
+ this.evictLFU(entriesToEvict);
186
+ break;
187
+ case 'ttl':
188
+ this.evictTTL(entriesToEvict);
189
+ break;
190
+ }
191
+ this.logger.debug('Cache entries evicted', {
192
+ strategy: this.options.evictionStrategy,
193
+ evictedCount: entriesToEvict,
194
+ remainingEntries: this.cache.size,
195
+ operation: 'cache_evict',
196
+ });
197
+ }
198
+ evictLRU(count) {
199
+ const entries = Array.from(this.cache.entries())
200
+ .sort(([, a], [, b]) => a.lastAccessed - b.lastAccessed)
201
+ .slice(0, count);
202
+ for (const [key] of entries) {
203
+ this.cache.delete(key);
204
+ }
205
+ }
206
+ evictLFU(count) {
207
+ const entries = Array.from(this.cache.entries())
208
+ .sort(([, a], [, b]) => a.accessCount - b.accessCount)
209
+ .slice(0, count);
210
+ for (const [key] of entries) {
211
+ this.cache.delete(key);
212
+ }
213
+ }
214
+ evictTTL(count) {
215
+ const entries = Array.from(this.cache.entries())
216
+ .sort(([, a], [, b]) => a.timestamp - b.timestamp)
217
+ .slice(0, count);
218
+ for (const [key] of entries) {
219
+ this.cache.delete(key);
220
+ }
221
+ }
222
+ updateStats() {
223
+ const totalRequests = this.stats.hits + this.stats.misses;
224
+ this.stats.entries = this.cache.size;
225
+ this.stats.memoryUsage = this.estimateMemoryUsage();
226
+ this.stats.hitRatio = totalRequests > 0 ? this.stats.hits / totalRequests : 0;
227
+ if (this.cache.size > 0) {
228
+ const totalAccessCount = Array.from(this.cache.values()).reduce((sum, entry) => sum + entry.accessCount, 0);
229
+ this.stats.averageAccessCount = totalAccessCount / this.cache.size;
230
+ }
231
+ else {
232
+ this.stats.averageAccessCount = 0;
233
+ }
234
+ }
235
+ estimateMemoryUsage() {
236
+ let totalSize = 0;
237
+ for (const entry of this.cache.values()) {
238
+ totalSize += JSON.stringify(entry.result).length * 2;
239
+ totalSize += entry.manifestHash.length * 2;
240
+ totalSize += entry.pluginHash.length * 2;
241
+ totalSize += 64;
242
+ }
243
+ return totalSize;
244
+ }
245
+ startPeriodicCleanup() {
246
+ const cleanupInterval = Math.min(this.options.maxAge / 4, 5 * 60 * 1000);
247
+ globalThis.setInterval(() => {
248
+ this.cleanupExpiredEntries();
249
+ }, cleanupInterval);
250
+ }
251
+ cleanupExpiredEntries() {
252
+ const keysToDelete = [];
253
+ const now = Date.now();
254
+ for (const [key, entry] of this.cache.entries()) {
255
+ if (now - entry.timestamp > this.options.maxAge) {
256
+ keysToDelete.push(key);
257
+ }
258
+ }
259
+ if (keysToDelete.length > 0) {
260
+ for (const key of keysToDelete) {
261
+ this.cache.delete(key);
262
+ }
263
+ this.updateStats();
264
+ this.logger.debug('Expired cache entries cleaned up', {
265
+ cleanedCount: keysToDelete.length,
266
+ remainingEntries: this.cache.size,
267
+ operation: 'cache_cleanup',
268
+ });
269
+ }
270
+ }
271
+ }
272
+ export function generateManifestHash(manifests) {
273
+ const manifestString = JSON.stringify(manifests, Object.keys(manifests).sort());
274
+ return hashString(manifestString);
275
+ }
276
+ export function generatePluginHash(plugins, pluginConfigs) {
277
+ const configString = JSON.stringify({
278
+ plugins: plugins.sort(),
279
+ configs: pluginConfigs,
280
+ }, Object.keys({ plugins: plugins.sort(), configs: pluginConfigs }).sort());
281
+ return hashString(configString);
282
+ }
283
+ function hashString(str) {
284
+ let hash = 5381;
285
+ for (let i = 0; i < str.length; i++) {
286
+ hash = (hash << 5) + hash + str.charCodeAt(i);
287
+ }
288
+ return Math.abs(hash).toString(36);
289
+ }
@@ -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
- private toSynthArray;
96
- write(outDir: string): void;
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;
@@ -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 : 'Unknown error'}`);
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 : 'Unknown error'}`);
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({ id: asset.id, yaml: asset.yaml });
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
- write(outDir) {
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,
@@ -204,8 +204,10 @@ export default function createChart() {
204
204
 
205
205
  // Auto-execute when run directly
206
206
  if (import.meta.url === new URL(import.meta.url).href) {
207
- const chart = createChart();
208
- chart.write('dist');
207
+ (async () => {
208
+ const chart = createChart();
209
+ await chart.write('dist');
210
+ })();
209
211
  }
210
212
  `;
211
213
  }
@@ -47,7 +47,7 @@ function readYamlFile(filePath: string): Record<string, unknown> {
47
47
  return content && typeof content === 'object' ? (content as Record<string, unknown>) : {};
48
48
  }
49
49
 
50
- export function synth(outDir: string, options?: SynthOptions) {
50
+ export async function synth(outDir: string, options?: SynthOptions) {
51
51
  const mode = resolveMode(options);
52
52
  const app = new App({
53
53
  outdir: outDir,
@@ -82,7 +82,7 @@ export function synth(outDir: string, options?: SynthOptions) {
82
82
  'namespace',
83
83
  );
84
84
 
85
- umbrella.write(outDir);
85
+ await umbrella.write(outDir);
86
86
 
87
87
  const chartPath = join(outDir, 'Chart.yaml');
88
88
  const valuesPath = join(outDir, 'values.yaml');
@@ -98,11 +98,11 @@ export function synth(outDir: string, options?: SynthOptions) {
98
98
 
99
99
  const dependencies: Array<{ name: string; version: string; repository: string }> = [];
100
100
 
101
- SUBCHARTS.forEach((subchart) => {
101
+ for (const subchart of SUBCHARTS) {
102
102
  const instance = subchart.factory();
103
103
  const targetDir = join(chartsDir, subchart.name);
104
104
  rmSync(targetDir, { recursive: true, force: true });
105
- instance.write(targetDir);
105
+ await instance.write(targetDir);
106
106
  const meta = instance.getMeta();
107
107
  const version = meta.version ?? '0.1.0';
108
108
  dependencies.push({
@@ -114,7 +114,7 @@ export function synth(outDir: string, options?: SynthOptions) {
114
114
  if (Object.keys(subchartValues).length > 0) {
115
115
  valuesDoc[subchart.name] = subchartValues;
116
116
  }
117
- });
117
+ }
118
118
 
119
119
  chartDoc.dependencies = dependencies;
120
120
  } else {
@@ -124,11 +124,11 @@ export function synth(outDir: string, options?: SynthOptions) {
124
124
  }
125
125
  delete chartDoc.dependencies;
126
126
 
127
- SUBCHARTS.forEach((subchart) => {
127
+ for (const subchart of SUBCHARTS) {
128
128
  const instance = subchart.factory();
129
129
  const tempDir = join(outDir, '.timonel-inline-' + subchart.name);
130
130
  rmSync(tempDir, { recursive: true, force: true });
131
- instance.write(tempDir);
131
+ await instance.write(tempDir);
132
132
 
133
133
  const subTemplatesDir = join(tempDir, 'templates');
134
134
  if (existsSync(subTemplatesDir)) {
@@ -145,7 +145,7 @@ export function synth(outDir: string, options?: SynthOptions) {
145
145
  }
146
146
 
147
147
  rmSync(tempDir, { recursive: true, force: true });
148
- });
148
+ }
149
149
  }
150
150
 
151
151
  writeFileSync(chartPath, stringify(chartDoc));
@@ -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,7 +22,7 @@ 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
  });
@@ -37,7 +37,7 @@ export class UmbrellaRutter {
37
37
  }
38
38
  const sanitizedName = subchart.name;
39
39
  const subchartDir = SecurityUtils.validatePath(join(validatedOutDir, 'charts', sanitizedName), process.cwd(), { allowAbsolute: true });
40
- subchart.rutter.write(subchartDir);
40
+ await subchart.rutter.write(subchartDir);
41
41
  }
42
42
  this.writeParentChart(validatedOutDir);
43
43
  this.writeParentValues(validatedOutDir);