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.
- package/CHANGELOG.md +12 -0
- package/README.md +432 -1
- package/SECURITY.md +2 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- 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/rutter.d.ts +7 -2
- package/dist/lib/rutter.js +177 -8
- package/dist/lib/templates/flexible-subchart.js +4 -2
- package/dist/lib/templates/umbrella-chart.js +8 -8
- package/dist/lib/umbrellaRutter.d.ts +1 -1
- package/dist/lib/umbrellaRutter.js +2 -2
- 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 +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
|
+
}
|
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,
|
|
@@ -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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|