timonel 3.1.1 → 3.1.2
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 +7 -0
- package/README.md +323 -695
- package/dist/cli.js +275 -15
- package/dist/index.d.ts +5 -1
- package/dist/index.js +12 -0
- package/dist/lib/helm.d.ts +471 -0
- package/dist/lib/helm.js +483 -0
- package/dist/lib/helmChartWriter.d.ts +157 -0
- package/dist/lib/helmChartWriter.js +171 -1
- package/dist/lib/policy/configurationLoader.d.ts +132 -0
- package/dist/lib/policy/configurationLoader.js +132 -0
- package/dist/lib/policy/errorContextGenerator.d.ts +89 -0
- package/dist/lib/policy/errorContextGenerator.js +99 -2
- package/dist/lib/policy/errors.d.ts +35 -0
- package/dist/lib/policy/errors.js +36 -0
- package/dist/lib/policy/index.d.ts +9 -0
- package/dist/lib/policy/index.js +16 -0
- package/dist/lib/policy/parallelExecutor.d.ts +90 -0
- package/dist/lib/policy/parallelExecutor.js +86 -3
- package/dist/lib/policy/pluginLoader.d.ts +92 -0
- package/dist/lib/policy/pluginLoader.js +92 -1
- package/dist/lib/policy/pluginRegistry.d.ts +54 -0
- package/dist/lib/policy/pluginRegistry.js +56 -0
- package/dist/lib/policy/policyEngine.d.ts +137 -0
- package/dist/lib/policy/policyEngine.js +191 -5
- package/dist/lib/policy/resultAggregator.d.ts +46 -0
- package/dist/lib/policy/resultAggregator.js +69 -1
- package/dist/lib/policy/resultFormatter.d.ts +88 -0
- package/dist/lib/policy/resultFormatter.js +101 -0
- package/dist/lib/policy/types.d.ts +136 -0
- package/dist/lib/policy/types.js +8 -0
- package/dist/lib/policy/validationCache.d.ts +146 -0
- package/dist/lib/policy/validationCache.js +142 -6
- package/dist/lib/resources/baseResourceProvider.d.ts +45 -0
- package/dist/lib/resources/baseResourceProvider.js +48 -1
- package/dist/lib/resources/cloud/aws/awsResources.d.ts +192 -0
- package/dist/lib/resources/cloud/aws/awsResources.js +163 -1
- package/dist/lib/resources/cloud/aws/karpenterResources.d.ts +131 -0
- package/dist/lib/resources/cloud/aws/karpenterResources.js +77 -0
- package/dist/lib/rutter.d.ts +381 -3
- package/dist/lib/rutter.js +439 -28
- package/dist/lib/security.d.ts +123 -0
- package/dist/lib/security.js +162 -4
- package/dist/lib/templates/flexible-subchart.d.ts +52 -0
- package/dist/lib/templates/flexible-subchart.js +70 -0
- package/dist/lib/templates/umbrella-chart.d.ts +27 -0
- package/dist/lib/templates/umbrella-chart.js +89 -0
- package/dist/lib/types.d.ts +26 -0
- package/dist/lib/umbrella.d.ts +23 -0
- package/dist/lib/umbrella.js +23 -0
- package/dist/lib/umbrellaRutter.d.ts +75 -0
- package/dist/lib/umbrellaRutter.js +82 -2
- package/dist/lib/utils/envVarsLoader.d.ts +49 -0
- package/dist/lib/utils/envVarsLoader.js +53 -0
- package/dist/lib/utils/helmConstructSerializer.d.ts +17 -0
- package/dist/lib/utils/helmConstructSerializer.js +22 -0
- package/dist/lib/utils/helmControlStructures.d.ts +194 -0
- package/dist/lib/utils/helmControlStructures.js +180 -0
- package/dist/lib/utils/helmHelpers/envHelpers.d.ts +13 -0
- package/dist/lib/utils/helmHelpers/envHelpers.js +13 -0
- package/dist/lib/utils/helmHelpers/gitopsHelpers.d.ts +13 -0
- package/dist/lib/utils/helmHelpers/gitopsHelpers.js +13 -0
- package/dist/lib/utils/helmHelpers/index.d.ts +74 -0
- package/dist/lib/utils/helmHelpers/index.js +85 -1
- package/dist/lib/utils/helmHelpers/observabilityHelpers.d.ts +13 -0
- package/dist/lib/utils/helmHelpers/observabilityHelpers.js +13 -0
- package/dist/lib/utils/helmHelpers/types.d.ts +23 -0
- package/dist/lib/utils/helmHelpers/types.js +4 -0
- package/dist/lib/utils/helmHelpers/validationHelpers.d.ts +13 -0
- package/dist/lib/utils/helmHelpers/validationHelpers.js +13 -0
- package/dist/lib/utils/helmHelpers.d.ts +62 -0
- package/dist/lib/utils/helmHelpers.js +77 -0
- package/dist/lib/utils/helmYamlSerializer.d.ts +77 -0
- package/dist/lib/utils/helmYamlSerializer.js +398 -21
- package/dist/lib/utils/logger.d.ts +153 -0
- package/dist/lib/utils/logger.js +170 -2
- package/dist/lib/utils/valuesRef.d.ts +181 -50
- package/dist/lib/utils/valuesRef.js +168 -170
- package/dist/lib/validation/inputValidator.d.ts +45 -0
- package/dist/lib/validation/inputValidator.js +67 -2
- package/dist/types/index.d.ts +34 -0
- package/dist/types/index.js +3 -0
- package/package.json +31 -38
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { Document, Scalar, isMap, isScalar, visit, Pair } from 'yaml';
|
|
2
2
|
import { SecurityUtils } from '../security.js';
|
|
3
3
|
import { isHelmConstruct, isHelmExpression, createHelmExpression, } from './helmControlStructures.js';
|
|
4
|
-
import { isHelmValue, isHelmFieldConditional, isHelmRange, isHelmWith, } from './valuesRef.js';
|
|
4
|
+
import { isHelmValue, isHelmFieldConditional, isHelmRange, isHelmWith, createHelmValueProxy, } from './valuesRef.js';
|
|
5
|
+
// Constants for field markers used throughout serialization
|
|
6
|
+
// eslint-disable-next-line sonarjs/no-duplicate-string -- Used as markers in multiple places
|
|
5
7
|
const FIELD_CONDITIONAL = '__FIELD_CONDITIONAL__:';
|
|
8
|
+
// eslint-disable-next-line sonarjs/no-duplicate-string -- Used as markers in multiple places
|
|
6
9
|
const FIELD_WITH = '__FIELD_WITH__:';
|
|
10
|
+
// eslint-disable-next-line sonarjs/no-duplicate-string -- Used as markers in multiple places
|
|
7
11
|
const FIELD_WITH_MARKER = '__FIELD_WITH_MARKER__:';
|
|
12
|
+
/**
|
|
13
|
+
* Simple YAML serialization that handles HelmExpressions without recursion
|
|
14
|
+
* Used internally to serialize content within HelmFieldConditional
|
|
15
|
+
*/
|
|
8
16
|
function simpleHelmYaml(obj) {
|
|
17
|
+
// First, recursively convert HelmValue and HelmExpression to plain strings
|
|
18
|
+
/** Convert nested Helm marker objects into YAML-serializable scalar strings. */
|
|
9
19
|
function convertToPlain(value) {
|
|
10
20
|
if (isHelmValue(value)) {
|
|
11
21
|
return `{{ ${value.__path} }}`;
|
|
@@ -19,6 +29,7 @@ function simpleHelmYaml(obj) {
|
|
|
19
29
|
if (value !== null && typeof value === 'object') {
|
|
20
30
|
const result = {};
|
|
21
31
|
for (const [k, v] of Object.entries(value)) {
|
|
32
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
22
33
|
result[k] = convertToPlain(v);
|
|
23
34
|
}
|
|
24
35
|
return result;
|
|
@@ -29,9 +40,17 @@ function simpleHelmYaml(obj) {
|
|
|
29
40
|
const doc = new Document(plain);
|
|
30
41
|
return doc.toString({ lineWidth: 0 });
|
|
31
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Helper function to serialize else-if chains
|
|
45
|
+
* @param elseContent The else content to process
|
|
46
|
+
* @param openTag Opening Helm tag
|
|
47
|
+
* @param closeTag Closing Helm tag
|
|
48
|
+
* @returns Object with serialized result and remaining else content
|
|
49
|
+
*/
|
|
32
50
|
function serializeElseIfChain(elseContent, openTag, closeTag) {
|
|
33
51
|
let result = '';
|
|
34
52
|
let currentElse = elseContent;
|
|
53
|
+
// Check for nested if (else if pattern)
|
|
35
54
|
if (isHelmConstruct(currentElse) && currentElse.type === 'if') {
|
|
36
55
|
while (isHelmConstruct(currentElse) && currentElse.type === 'if') {
|
|
37
56
|
const elseIfData = currentElse.data;
|
|
@@ -42,7 +61,15 @@ function serializeElseIfChain(elseContent, openTag, closeTag) {
|
|
|
42
61
|
}
|
|
43
62
|
return { result, remainingElse: currentElse };
|
|
44
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Serialize a HelmConstruct into a Helm template string
|
|
66
|
+
* @param construct The HelmConstruct to serialize
|
|
67
|
+
* @returns Helm template string
|
|
68
|
+
*/
|
|
69
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Complex serialization logic
|
|
45
70
|
function serializeHelmConstruct(construct) {
|
|
71
|
+
// Handle fieldConditional specially - it should not be serialized here
|
|
72
|
+
// as it needs special handling in the YAML structure
|
|
46
73
|
if (construct.type === 'fieldConditional') {
|
|
47
74
|
throw new Error('fieldConditional should not be serialized directly. It must be handled in preprocessHelmConstructs.');
|
|
48
75
|
}
|
|
@@ -77,6 +104,7 @@ function serializeHelmConstruct(construct) {
|
|
|
77
104
|
}
|
|
78
105
|
case 'fragment': {
|
|
79
106
|
const data = construct.data;
|
|
107
|
+
// Separate strings/HelmExpressions from objects
|
|
80
108
|
const strings = [];
|
|
81
109
|
const objects = [];
|
|
82
110
|
for (const item of data) {
|
|
@@ -94,15 +122,19 @@ function serializeHelmConstruct(construct) {
|
|
|
94
122
|
}
|
|
95
123
|
}
|
|
96
124
|
}
|
|
125
|
+
// If we have both strings and objects, we need to combine them intelligently
|
|
97
126
|
if (strings.length > 0 && objects.length > 0) {
|
|
127
|
+
// Combine all objects into one
|
|
98
128
|
const combinedObject = objects.reduce((acc, obj) => {
|
|
99
129
|
if (typeof obj === 'object' && obj !== null) {
|
|
100
130
|
return { ...acc, ...obj };
|
|
101
131
|
}
|
|
102
132
|
return acc;
|
|
103
133
|
}, {});
|
|
134
|
+
// Pre-process the combined object to convert HelmConstruct instances to HelmExpression
|
|
104
135
|
const preprocessed = preprocessHelmConstructs(combinedObject);
|
|
105
136
|
const doc = new Document(preprocessed);
|
|
137
|
+
// Visit the document to transform HelmExpression objects
|
|
106
138
|
visit(doc, (_key, node) => {
|
|
107
139
|
if (isMap(node)) {
|
|
108
140
|
const isHelmExpr = node.items.some((pair) => isScalar(pair.key) &&
|
|
@@ -127,8 +159,11 @@ function serializeHelmConstruct(construct) {
|
|
|
127
159
|
return undefined;
|
|
128
160
|
});
|
|
129
161
|
let objectYaml = doc.toString({ lineWidth: 0 }).trim();
|
|
162
|
+
// Combine strings and object YAML
|
|
163
|
+
// The strings come first (like conditional replicas), then the object
|
|
130
164
|
return [...strings, objectYaml].filter((s) => s.trim().length > 0).join('\n');
|
|
131
165
|
}
|
|
166
|
+
// If only objects, combine them
|
|
132
167
|
if (objects.length > 0) {
|
|
133
168
|
const combinedObject = objects.reduce((acc, obj) => {
|
|
134
169
|
if (typeof obj === 'object' && obj !== null) {
|
|
@@ -138,6 +173,7 @@ function serializeHelmConstruct(construct) {
|
|
|
138
173
|
}, {});
|
|
139
174
|
const preprocessed = preprocessHelmConstructs(combinedObject);
|
|
140
175
|
const doc = new Document(preprocessed);
|
|
176
|
+
// Visit the document to transform HelmExpression objects
|
|
141
177
|
visit(doc, (_key, node) => {
|
|
142
178
|
if (isMap(node)) {
|
|
143
179
|
const isHelmExpr = node.items.some((pair) => isScalar(pair.key) &&
|
|
@@ -163,6 +199,7 @@ function serializeHelmConstruct(construct) {
|
|
|
163
199
|
});
|
|
164
200
|
return doc.toString({ lineWidth: 0 }).trim();
|
|
165
201
|
}
|
|
202
|
+
// If only strings, just join them
|
|
166
203
|
return strings.filter((s) => s.trim().length > 0).join('\n');
|
|
167
204
|
}
|
|
168
205
|
case 'range': {
|
|
@@ -214,6 +251,9 @@ function serializeHelmConstruct(construct) {
|
|
|
214
251
|
throw new Error(`Unknown Helm construct type: ${construct.type}`);
|
|
215
252
|
}
|
|
216
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Serialize HelmContent (primitives, objects, arrays, or HelmConstructs) to string
|
|
256
|
+
*/
|
|
217
257
|
function serializeHelmContent(content) {
|
|
218
258
|
if (isHelmConstruct(content)) {
|
|
219
259
|
return serializeHelmConstruct(content);
|
|
@@ -230,60 +270,88 @@ function serializeHelmContent(content) {
|
|
|
230
270
|
if (typeof content === 'number' || typeof content === 'boolean') {
|
|
231
271
|
return String(content);
|
|
232
272
|
}
|
|
273
|
+
// For objects and arrays, use the yaml library to serialize them properly
|
|
233
274
|
if (typeof content === 'object') {
|
|
275
|
+
// Pre-process the object to convert HelmConstruct instances to HelmExpression
|
|
276
|
+
// This ensures nested HelmConstructs are properly serialized
|
|
234
277
|
const preprocessed = preprocessHelmConstructs(content);
|
|
235
278
|
const doc = new Document(preprocessed);
|
|
236
279
|
let yaml = doc.toString({ lineWidth: 0 });
|
|
280
|
+
// Remove the trailing newline and any leading/trailing whitespace
|
|
237
281
|
yaml = yaml.trim();
|
|
238
282
|
return yaml;
|
|
239
283
|
}
|
|
240
284
|
return '';
|
|
241
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Pre-process an object to convert HelmConstruct instances to HelmExpression
|
|
288
|
+
* This allows the yaml library to serialize them correctly
|
|
289
|
+
*/
|
|
290
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Complex preprocessing logic
|
|
242
291
|
export function preprocessHelmConstructs(obj) {
|
|
292
|
+
// Handle HelmValue (from valuesRef)
|
|
243
293
|
if (isHelmValue(obj)) {
|
|
244
294
|
const value = obj;
|
|
245
295
|
return createHelmExpression(`{{ ${value.__path} }}`);
|
|
246
296
|
}
|
|
297
|
+
// Handle HelmFieldConditional (from valuesRef v.if())
|
|
247
298
|
if (isHelmFieldConditional(obj)) {
|
|
299
|
+
// This is handled in the object iteration below
|
|
248
300
|
return obj;
|
|
249
301
|
}
|
|
302
|
+
// Handle HelmRange (from valuesRef v.range())
|
|
250
303
|
if (isHelmRange(obj)) {
|
|
251
304
|
const range = obj;
|
|
252
305
|
const sourcePath = range.source.__path;
|
|
253
|
-
const itemProxy =
|
|
254
|
-
const indexProxy =
|
|
306
|
+
const itemProxy = createHelmValueProxy('$item');
|
|
307
|
+
const indexProxy = createHelmValueProxy('$index');
|
|
255
308
|
const content = range.callback(itemProxy, indexProxy);
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
const contentStr =
|
|
259
|
-
return createHelmExpression(`{{
|
|
309
|
+
const rangeItems = Array.isArray(content) ? content : [content];
|
|
310
|
+
const processedContent = preprocessHelmConstructs(rangeItems);
|
|
311
|
+
const contentStr = dumpHelmAwareYaml(processedContent).trim();
|
|
312
|
+
return createHelmExpression(`{{ range $index, $item := ${sourcePath} }}\n${contentStr}\n{{ end }}`);
|
|
260
313
|
}
|
|
314
|
+
// Handle HelmWith (from valuesRef v.with())
|
|
315
|
+
// DON'T process it here - let it pass through so the visit phase can detect it as field-level
|
|
261
316
|
if (isHelmWith(obj)) {
|
|
262
317
|
return obj;
|
|
263
318
|
}
|
|
319
|
+
// Handle HelmConstruct
|
|
320
|
+
// BUT: preserve fieldConditional constructs as-is so they can be handled in visit phase
|
|
321
|
+
// NOTE: We don't process helmIf without else here - that's handled in the object processing below
|
|
264
322
|
if (isHelmConstruct(obj)) {
|
|
265
323
|
if (obj.type === 'fieldConditional') {
|
|
324
|
+
// Keep fieldConditional as-is, don't serialize it yet
|
|
266
325
|
return obj;
|
|
267
326
|
}
|
|
327
|
+
// For helmIf without else, we need to handle it in the parent object context
|
|
328
|
+
// So we return it as-is to be processed when iterating over object entries
|
|
329
|
+
// UNLESS it has inline: true, in which case serialize it immediately
|
|
268
330
|
if (obj.type === 'if') {
|
|
269
331
|
const ifData = obj.data;
|
|
270
332
|
const isInline = obj.options?.inline ?? false;
|
|
333
|
+
// Check if else is undefined or not present in the data object
|
|
271
334
|
if (!isInline && (ifData.else === undefined || !('else' in ifData))) {
|
|
335
|
+
// Return as-is so it can be processed in the object iteration
|
|
272
336
|
return obj;
|
|
273
337
|
}
|
|
274
338
|
}
|
|
275
339
|
const helmTemplate = serializeHelmConstruct(obj);
|
|
276
340
|
return createHelmExpression(helmTemplate);
|
|
277
341
|
}
|
|
342
|
+
// Handle arrays
|
|
278
343
|
if (Array.isArray(obj)) {
|
|
279
344
|
return obj.map((item) => preprocessHelmConstructs(item));
|
|
280
345
|
}
|
|
346
|
+
// Handle objects
|
|
281
347
|
if (obj !== null && typeof obj === 'object') {
|
|
348
|
+
// Don't process HelmExpression objects
|
|
282
349
|
if (isHelmExpression(obj)) {
|
|
283
350
|
return obj;
|
|
284
351
|
}
|
|
285
352
|
const result = {};
|
|
286
353
|
for (const [key, value] of Object.entries(obj)) {
|
|
354
|
+
// Check if value is HelmWith - treat as field-level construct
|
|
287
355
|
if (isHelmWith(value)) {
|
|
288
356
|
const withBlock = value;
|
|
289
357
|
const source = withBlock.source;
|
|
@@ -291,26 +359,40 @@ export function preprocessHelmConstructs(obj) {
|
|
|
291
359
|
if (!sourcePath) {
|
|
292
360
|
throw new Error('HelmWith source must have a valid __path');
|
|
293
361
|
}
|
|
294
|
-
|
|
362
|
+
// Create placeholder for callback context
|
|
363
|
+
const ctxProxy = createHelmValueProxy('.');
|
|
295
364
|
const content = withBlock.callback(ctxProxy);
|
|
365
|
+
// Extract content string
|
|
296
366
|
let contentStr;
|
|
297
367
|
if (isHelmExpression(content)) {
|
|
298
368
|
contentStr = content.value;
|
|
299
369
|
}
|
|
300
370
|
else {
|
|
301
371
|
const processedContent = preprocessHelmConstructs(content);
|
|
302
|
-
|
|
303
|
-
contentStr = contentDoc.toString({ lineWidth: 0 }).trim();
|
|
372
|
+
contentStr = dumpHelmAwareYaml(processedContent).trim();
|
|
304
373
|
}
|
|
305
|
-
|
|
374
|
+
// Generate field-level with template
|
|
375
|
+
const indentedContent = contentStr
|
|
376
|
+
.split('\n')
|
|
377
|
+
.map((line) => ` ${line}`)
|
|
378
|
+
.join('\n');
|
|
379
|
+
const template = `{{ with ${sourcePath} }}\n${key}:\n${indentedContent}\n{{ end }}`;
|
|
380
|
+
// Use special marker for field-level with
|
|
306
381
|
const marker = `__FIELD_WITH__:${key}:${template}`;
|
|
307
382
|
result[`__fieldWithTemplate_${key}`] = createHelmExpression(marker);
|
|
308
383
|
continue;
|
|
309
384
|
}
|
|
385
|
+
// Special handling: if value is a helmIf with elseContent as undefined,
|
|
386
|
+
// this means we want to conditionally include the entire field (key + value)
|
|
387
|
+
// We generate the template complete as a string and insert it as a special field
|
|
388
|
+
// IMPORTANT: Check this BEFORE processing recursively to avoid serialization
|
|
310
389
|
if (isHelmConstruct(value) && value.type === 'if') {
|
|
311
390
|
const ifData = value.data;
|
|
312
391
|
const isInline = value.options?.inline ?? false;
|
|
392
|
+
// If elseContent is undefined (or not present) AND not inline, this is a field-level conditional
|
|
393
|
+
// This is the case for helmIfSimple() or helmIf() with undefined else
|
|
313
394
|
if (!isInline && (ifData.else === undefined || !('else' in ifData))) {
|
|
395
|
+
// Serialize the then value to get its string representation
|
|
314
396
|
const preprocessedThen = preprocessHelmConstructs(ifData.then);
|
|
315
397
|
const serializedThen = (() => {
|
|
316
398
|
if (isHelmExpression(preprocessedThen)) {
|
|
@@ -328,12 +410,16 @@ export function preprocessHelmConstructs(obj) {
|
|
|
328
410
|
isMultilineObject: Array.isArray(preprocessedThen) || valueStr.includes('\n'),
|
|
329
411
|
};
|
|
330
412
|
})();
|
|
413
|
+
// Create the conditional template
|
|
414
|
+
// Format: "{{- if condition }}\nfieldKey: value\n{{- end }}"
|
|
331
415
|
const constructOptions = isHelmConstruct(value) ? value.options : undefined;
|
|
332
416
|
const trimLeft = constructOptions?.trimLeft ?? true;
|
|
333
417
|
const openTag = `{{${trimLeft ? '-' : ''} `;
|
|
334
418
|
const closeTag = ` ${(constructOptions?.trimRight ?? true) ? '-' : ''}}}`;
|
|
419
|
+
// Format the value based on whether it's multiline or not
|
|
335
420
|
let formattedContent;
|
|
336
421
|
if (serializedThen.isMultilineObject) {
|
|
422
|
+
// For multiline values (arrays, objects), put on new line with proper indentation
|
|
337
423
|
const indentedValue = serializedThen.thenValueStr
|
|
338
424
|
.split('\n')
|
|
339
425
|
.map((line) => ' ' + line)
|
|
@@ -341,21 +427,35 @@ export function preprocessHelmConstructs(obj) {
|
|
|
341
427
|
formattedContent = `${key}:\n${indentedValue}`;
|
|
342
428
|
}
|
|
343
429
|
else {
|
|
430
|
+
// For simple values, put inline
|
|
344
431
|
formattedContent = `${key}: ${serializedThen.thenValueStr}`;
|
|
345
432
|
}
|
|
346
433
|
const conditionalTemplate = `${openTag}if ${ifData.condition}${closeTag}\n ${formattedContent}\n${openTag}end${closeTag}`;
|
|
434
|
+
// Instead of creating a special field that might be lost during cdk8s serialization,
|
|
435
|
+
// we create a HelmExpression (type-safe) with the complete template that will be inserted
|
|
436
|
+
// directly in the YAML. We use a special marker prefix in the value so postProcessFieldConditionals
|
|
437
|
+
// can detect and transform it correctly. This preserves type-safety because the field
|
|
438
|
+
// exists with a HelmExpression type, not just a string.
|
|
439
|
+
// The marker "__FIELD_CONDITIONAL__:" is used to identify field-level conditionals
|
|
440
|
+
// eslint-disable-next-line security/detect-object-injection -- Safe: iterating over own entries
|
|
347
441
|
result[key] = createHelmExpression(`__FIELD_CONDITIONAL__:${key}:${conditionalTemplate}`);
|
|
348
442
|
continue;
|
|
349
443
|
}
|
|
350
444
|
}
|
|
445
|
+
// Handle HelmFieldConditional from valuesRef v.if()
|
|
351
446
|
if (isHelmFieldConditional(value)) {
|
|
352
447
|
const fieldCond = value;
|
|
353
448
|
const condition = fieldCond.condition.__condition;
|
|
449
|
+
// Serialize the then value using simpleHelmYaml to avoid recursion
|
|
354
450
|
const thenValueStr = simpleHelmYaml(fieldCond.thenValue).trim();
|
|
355
451
|
const isMultilineObject = thenValueStr.includes('\n');
|
|
356
452
|
const startsWithDash = thenValueStr.trimStart().startsWith('-');
|
|
453
|
+
// Create the conditional template without base indentation
|
|
454
|
+
// postProcessFieldConditionals will add the correct indentation later
|
|
357
455
|
let formattedContent;
|
|
358
456
|
if (isMultilineObject || startsWithDash) {
|
|
457
|
+
// For multiline or arrays, put key on own line
|
|
458
|
+
// Indent the value by 2 spaces relative to the key
|
|
359
459
|
const lines = thenValueStr.split('\n');
|
|
360
460
|
const indentedValue = lines.map((line) => ' ' + line).join('\n');
|
|
361
461
|
formattedContent = `${key}:\n${indentedValue}`;
|
|
@@ -363,41 +463,69 @@ export function preprocessHelmConstructs(obj) {
|
|
|
363
463
|
else {
|
|
364
464
|
formattedContent = `${key}: ${thenValueStr}`;
|
|
365
465
|
}
|
|
466
|
+
// Template format: no base indent - postProcessFieldConditionals will add it
|
|
366
467
|
const conditionalTemplate = `{{- if ${condition} }}\n${formattedContent}\n{{- end }}`;
|
|
468
|
+
// eslint-disable-next-line security/detect-object-injection -- Safe: iterating over own entries
|
|
367
469
|
result[key] = createHelmExpression(`__FIELD_CONDITIONAL__:${key}:${conditionalTemplate}`);
|
|
368
470
|
continue;
|
|
369
471
|
}
|
|
472
|
+
// Also handle fieldConditional constructs that were already created
|
|
370
473
|
if (isHelmConstruct(value) && value.type === 'fieldConditional') {
|
|
371
474
|
const fieldData = value.data;
|
|
475
|
+
// Serialize the then value
|
|
372
476
|
const preprocessedThen = preprocessHelmConstructs(fieldData.then);
|
|
373
477
|
const thenValueStr = isHelmExpression(preprocessedThen)
|
|
374
478
|
? preprocessedThen.value
|
|
375
479
|
: typeof preprocessedThen === 'string'
|
|
376
480
|
? preprocessedThen
|
|
377
481
|
: new Document(preprocessedThen).toString({ lineWidth: 0 }).trim();
|
|
482
|
+
// Create the conditional template
|
|
378
483
|
const conditionalTemplate = `{{- if ${fieldData.condition} }}\n ${fieldData.fieldKey}: ${thenValueStr}\n{{- end }}`;
|
|
379
484
|
result[`__fieldConditionalTemplate_${fieldData.fieldKey}`] = conditionalTemplate;
|
|
380
485
|
continue;
|
|
381
486
|
}
|
|
487
|
+
// eslint-disable-next-line security/detect-object-injection -- Safe: iterating over own entries
|
|
382
488
|
result[key] = preprocessHelmConstructs(value);
|
|
383
489
|
}
|
|
384
490
|
return result;
|
|
385
491
|
}
|
|
492
|
+
// Return primitives as-is
|
|
386
493
|
return obj;
|
|
387
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* Main method to serialize an object to YAML with Helm template awareness.
|
|
497
|
+
* Uses 'yaml' library to natively handle Helm expressions by forcing QUOTE_DOUBLE style.
|
|
498
|
+
*
|
|
499
|
+
* @param obj Input object to serialize.
|
|
500
|
+
* @param options Options (lineWidth, etc.) - partially supported mapping from legacy options.
|
|
501
|
+
* @returns Helm-aware YAML string.
|
|
502
|
+
* @since 2.11.0
|
|
503
|
+
* @since 2.13.1 Refactored to use 'yaml' library
|
|
504
|
+
* @since 2.14.0 Added HelmConstruct pre-processing
|
|
505
|
+
*/
|
|
506
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Complex YAML serialization logic
|
|
388
507
|
export function dumpHelmAwareYaml(obj, options = {}) {
|
|
508
|
+
// Pre-process to convert HelmConstruct objects to HelmExpression
|
|
509
|
+
// This also processes field-level conditionals and creates __fieldConditionalTemplate_* fields
|
|
389
510
|
const preprocessed = preprocessHelmConstructs(obj);
|
|
390
511
|
const doc = new Document(preprocessed);
|
|
512
|
+
// Visit the document to transform HelmExpression objects
|
|
513
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Complex YAML node transformation
|
|
391
514
|
visit(doc, (_key, node) => {
|
|
515
|
+
// Handle explicit HelmExpression objects
|
|
392
516
|
if (isMap(node)) {
|
|
393
517
|
const isHelmExpr = node.items.some((pair) => isScalar(pair.key) &&
|
|
394
518
|
pair.key.value === '__helmExpression' &&
|
|
395
519
|
isScalar(pair.value) &&
|
|
396
520
|
pair.value.value === true);
|
|
397
521
|
if (isHelmExpr) {
|
|
522
|
+
// Find the 'value' property
|
|
398
523
|
const valuePair = node.items.find((pair) => isScalar(pair.key) && pair.key.value === 'value');
|
|
399
524
|
if (valuePair && isScalar(valuePair.value)) {
|
|
400
525
|
const value = String(valuePair.value.value);
|
|
526
|
+
// Check if this is a field-level conditional marker
|
|
527
|
+
// Format: "__FIELD_CONDITIONAL__:fieldKey:{{- if ... }}\n fieldKey: value\n{{- end }}"
|
|
528
|
+
// We need to preserve it as a block literal so postProcessFieldConditionals can process it
|
|
401
529
|
if (value.startsWith(FIELD_CONDITIONAL) ||
|
|
402
530
|
value.startsWith(FIELD_WITH) ||
|
|
403
531
|
value.startsWith(FIELD_WITH_MARKER)) {
|
|
@@ -406,31 +534,44 @@ export function dumpHelmAwareYaml(obj, options = {}) {
|
|
|
406
534
|
return scalar;
|
|
407
535
|
}
|
|
408
536
|
const scalar = new Scalar(value);
|
|
537
|
+
// Check if this is a multiline Helm template (contains newlines and starts with {{)
|
|
409
538
|
const isMultilineTemplate = value.includes('\n') && value.trim().startsWith('{{');
|
|
410
|
-
|
|
539
|
+
// Special handling for with/range blocks - they should NOT be block literals
|
|
540
|
+
// because they need to be at the same level as the field key
|
|
541
|
+
const trimmedValue = value.trim();
|
|
542
|
+
const isWithOrRange = /^\{\{-?\s*with\s/.test(trimmedValue) || /^\{\{-?\s*range\s/.test(trimmedValue);
|
|
411
543
|
if (isMultilineTemplate && !isWithOrRange) {
|
|
412
|
-
scalar.type = 'BLOCK_LITERAL';
|
|
544
|
+
scalar.type = 'BLOCK_LITERAL'; // Force block style (|)
|
|
413
545
|
}
|
|
414
546
|
else if (isWithOrRange) {
|
|
547
|
+
// For with/range, use PLAIN style so it renders without quotes or block markers
|
|
415
548
|
scalar.type = 'PLAIN';
|
|
416
549
|
}
|
|
417
550
|
else {
|
|
418
|
-
scalar.type = 'QUOTE_DOUBLE';
|
|
551
|
+
scalar.type = 'QUOTE_DOUBLE'; // Force double quotes for simple expressions
|
|
419
552
|
}
|
|
420
|
-
return scalar;
|
|
553
|
+
return scalar; // Replace the Map node with this Scalar
|
|
421
554
|
}
|
|
422
555
|
}
|
|
423
556
|
}
|
|
424
557
|
return undefined;
|
|
425
558
|
});
|
|
559
|
+
// Configure output options
|
|
426
560
|
const toStringOptions = { lineWidth: 0 };
|
|
427
561
|
if (options.lineWidth !== undefined) {
|
|
428
562
|
toStringOptions.lineWidth = options.lineWidth;
|
|
429
563
|
}
|
|
564
|
+
// Ensure we don't use flow style (JSON-like) for the root or children unless necessary
|
|
565
|
+
// doc.options.collectionStyle = 'block'; // Default is usually fine
|
|
566
|
+
// Third visit: transform the special template pairs into the final format
|
|
567
|
+
// We need to replace pairs with key "__fieldConditionalTemplate_*" with just the template content
|
|
568
|
+
// Since we can't have "content without a key" in YAML, we'll use a different approach:
|
|
569
|
+
// We'll create a custom serialization that handles these special pairs
|
|
430
570
|
const templatePairs = [];
|
|
431
571
|
visit(doc, (_key, node) => {
|
|
432
572
|
if (isMap(node)) {
|
|
433
573
|
for (let i = 0; i < node.items.length; i++) {
|
|
574
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
434
575
|
const pair = node.items[i];
|
|
435
576
|
if (pair && isScalar(pair.key)) {
|
|
436
577
|
const keyStr = String(pair.key.value);
|
|
@@ -444,33 +585,81 @@ export function dumpHelmAwareYaml(obj, options = {}) {
|
|
|
444
585
|
}
|
|
445
586
|
return undefined;
|
|
446
587
|
});
|
|
588
|
+
// Process template pairs: replace them with the template content directly
|
|
589
|
+
// We'll create a new Document structure that represents the conditional correctly
|
|
590
|
+
// The challenge is that YAML requires keys, so we need to serialize the template
|
|
591
|
+
// in a way that when converted to string, produces the desired output
|
|
447
592
|
for (let i = templatePairs.length - 1; i >= 0; i--) {
|
|
593
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
448
594
|
const templatePair = templatePairs[i];
|
|
449
595
|
if (!templatePair)
|
|
450
596
|
continue;
|
|
451
597
|
const { parent, index, template } = templatePair;
|
|
598
|
+
// Remove the special pair
|
|
452
599
|
parent.items.splice(index, 1);
|
|
600
|
+
// The template already contains the full conditional with proper indentation
|
|
601
|
+
// We need to insert it in a way that serializes correctly
|
|
602
|
+
// Since we can't insert "raw content", we'll create a Pair with an empty key
|
|
603
|
+
// and the template as a BLOCK_LITERAL value, then process it in the final step
|
|
604
|
+
// Actually, the best approach is to parse the template and insert its content
|
|
605
|
+
// But the template is already a string with the correct format
|
|
606
|
+
// Solution: Create a temporary Document with just the template to get its YAML representation
|
|
607
|
+
// Then parse that and extract the content
|
|
453
608
|
const tempDoc = new Document({ __temp: template });
|
|
454
609
|
const _tempYaml = tempDoc.toString({ lineWidth: 0 });
|
|
610
|
+
// Extract just the value part (the template)
|
|
611
|
+
// The tempYaml will be "__temp: |\n template content"
|
|
612
|
+
// We need just "template content" with proper indentation
|
|
613
|
+
// Better: Since the template is already correctly formatted, we can create
|
|
614
|
+
// a Scalar with it and use it directly, but we need a Pair
|
|
615
|
+
// Final solution: Create a Pair with a key that when serialized will be removed
|
|
616
|
+
// We'll use a key that's a comment or special marker that gets filtered out
|
|
617
|
+
// Actually, the simplest type-safe solution: Create a Pair with the template
|
|
618
|
+
// as a BLOCK_LITERAL, and then in the final string processing, we'll replace
|
|
619
|
+
// the pattern "specialKey: |\n template" with just "template" (but this uses string processing)
|
|
620
|
+
// True type-safe solution: Parse the template string as YAML and extract its structure
|
|
621
|
+
// But the template is not valid YAML by itself
|
|
622
|
+
// Best approach: Store the template in a way that we can extract it later
|
|
623
|
+
// We'll create a Pair with a known special key pattern that we can detect
|
|
624
|
+
// and transform using only yaml API operations
|
|
625
|
+
// Create a Scalar with the template
|
|
455
626
|
const templateScalar = new Scalar(template);
|
|
456
627
|
templateScalar.type = 'BLOCK_LITERAL';
|
|
628
|
+
// Create a Pair with a special key that we'll process
|
|
629
|
+
// The key will be a Scalar with a value that we can detect
|
|
457
630
|
const specialKeyScalar = new Scalar('__fieldConditionalContent');
|
|
458
631
|
const templatePairObj = new Pair(specialKeyScalar, templateScalar);
|
|
632
|
+
// Insert the template pair
|
|
459
633
|
parent.items.splice(index, 0, templatePairObj);
|
|
460
634
|
}
|
|
635
|
+
// Serialize to string first (with __fieldConditionalContent pairs still present)
|
|
461
636
|
let result = doc.toString(toStringOptions);
|
|
637
|
+
// Transform __fieldConditionalContent pairs in the string representation
|
|
638
|
+
// Pattern: "__fieldConditionalContent: |\n {{- if ... }}\n fieldKey: value\n{{- end }}"
|
|
639
|
+
// Should become: "{{- if ... }}\n fieldKey: value\n{{- end }}"
|
|
640
|
+
// We do this type-safely by:
|
|
641
|
+
// 1. Using yaml API to identify the patterns (already done above)
|
|
642
|
+
// 2. Using simple string operations (not regex) to transform them
|
|
643
|
+
// 3. This is the minimal string manipulation needed given YAML's structural constraints
|
|
462
644
|
while (result.includes('__fieldConditionalContent:')) {
|
|
463
645
|
const markerIndex = result.indexOf('__fieldConditionalContent:');
|
|
464
646
|
if (markerIndex === -1)
|
|
465
647
|
break;
|
|
648
|
+
// Find the start of the line containing the marker to get base indentation
|
|
466
649
|
const lineStart = result.lastIndexOf('\n', markerIndex);
|
|
467
650
|
const lineBeforeMarker = result.substring(lineStart + 1, markerIndex);
|
|
468
651
|
const indentMatch = lineBeforeMarker.match(/^(\s*)/);
|
|
469
652
|
const baseIndent = indentMatch && indentMatch[1] ? indentMatch[1] : '';
|
|
653
|
+
// Debug: log the transformation attempt
|
|
654
|
+
// console.log('Transforming __fieldConditionalContent at index', markerIndex, 'baseIndent:', JSON.stringify(baseIndent));
|
|
655
|
+
// Find the content after the marker
|
|
470
656
|
const afterMarker = result.substring(markerIndex);
|
|
657
|
+
// Find the pipe character (| or |-) that indicates block literal
|
|
658
|
+
// It can be on the same line or on the next line
|
|
471
659
|
let pipeLineMatch = afterMarker.match(/^__fieldConditionalContent:\s*(\|-?)\s*\n(\s*)/);
|
|
472
660
|
let pipeOnSameLine = true;
|
|
473
661
|
if (!pipeLineMatch) {
|
|
662
|
+
// Try pattern with pipe on next line
|
|
474
663
|
pipeLineMatch = afterMarker.match(/^__fieldConditionalContent:\s*\n(\s*)(\|-?)\s*\n/);
|
|
475
664
|
pipeOnSameLine = false;
|
|
476
665
|
}
|
|
@@ -480,10 +669,15 @@ export function dumpHelmAwareYaml(obj, options = {}) {
|
|
|
480
669
|
const pipeIndent = pipeOnSameLine ? pipeLineMatch[2] || '' : pipeLineMatch[1] || '';
|
|
481
670
|
if (!pipeChar)
|
|
482
671
|
break;
|
|
672
|
+
// Calculate the index after the pipe line (including the newline)
|
|
483
673
|
const pipeIndex = markerIndex + pipeLineMatch[0].length;
|
|
674
|
+
// Find the template start ({{- if)
|
|
675
|
+
// After the pipe line, there's a newline and then spaces before the template
|
|
484
676
|
const afterPipe = result.substring(pipeIndex);
|
|
677
|
+
// Match: newline (required), then spaces, then {{-
|
|
485
678
|
const templateStartMatch = afterPipe.match(/^\n(\s*)(\{\{-)/);
|
|
486
679
|
if (!templateStartMatch) {
|
|
680
|
+
// Try without newline (pipe on same line case)
|
|
487
681
|
const templateStartMatchNoNewline = afterPipe.match(/^(\s*)(\{\{-)/);
|
|
488
682
|
if (!templateStartMatchNoNewline)
|
|
489
683
|
break;
|
|
@@ -492,6 +686,7 @@ export function dumpHelmAwareYaml(obj, options = {}) {
|
|
|
492
686
|
if (!templateStart)
|
|
493
687
|
break;
|
|
494
688
|
const templateStartIndex = pipeIndex + templateStartMatchNoNewline[0].length;
|
|
689
|
+
// Find the template end
|
|
495
690
|
const templateSection = result.substring(templateStartIndex);
|
|
496
691
|
const endMatch = templateSection.match(/(\{\{-?\s*end\s*-?\}\})/);
|
|
497
692
|
if (!endMatch)
|
|
@@ -504,7 +699,9 @@ export function dumpHelmAwareYaml(obj, options = {}) {
|
|
|
504
699
|
const beforeMarker = result.substring(0, lineStart + 1);
|
|
505
700
|
const afterTemplate = result.substring(templateEndIndex);
|
|
506
701
|
const pipeIndentEscaped = pipeIndent.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
507
|
-
const adjustedTemplate = templateContent.replace(
|
|
702
|
+
const adjustedTemplate = templateContent.replace(
|
|
703
|
+
// eslint-disable-next-line security/detect-non-literal-regexp -- Safe: pipeIndent is escaped
|
|
704
|
+
new RegExp(`^${pipeIndentEscaped}`, 'gm'), baseIndent);
|
|
508
705
|
result = beforeMarker + adjustedTemplate + afterTemplate;
|
|
509
706
|
continue;
|
|
510
707
|
}
|
|
@@ -512,7 +709,9 @@ export function dumpHelmAwareYaml(obj, options = {}) {
|
|
|
512
709
|
const templateStart = templateStartMatch[2];
|
|
513
710
|
if (!templateStart)
|
|
514
711
|
break;
|
|
712
|
+
// templateStartIndex is after the newline and spaces, at the start of {{-
|
|
515
713
|
const templateStartIndex = pipeIndex + templateStartMatch[0].length;
|
|
714
|
+
// Find the template end ({{- end }})
|
|
516
715
|
const templateSection = result.substring(templateStartIndex);
|
|
517
716
|
const endMatch = templateSection.match(/(\{\{-?\s*end\s*-?\}\})/);
|
|
518
717
|
if (!endMatch)
|
|
@@ -521,22 +720,45 @@ export function dumpHelmAwareYaml(obj, options = {}) {
|
|
|
521
720
|
if (endIndex === undefined)
|
|
522
721
|
break;
|
|
523
722
|
const templateEndIndex = templateStartIndex + endIndex + endMatch[0].length;
|
|
723
|
+
// Extract the template content
|
|
524
724
|
const templateContent = result.substring(templateStartIndex, templateEndIndex);
|
|
725
|
+
// Remove the marker line, pipe line, and adjust the template
|
|
726
|
+
// The template should start at the baseIndent level
|
|
525
727
|
const beforeMarker = result.substring(0, lineStart + 1);
|
|
526
728
|
const afterTemplate = result.substring(templateEndIndex);
|
|
729
|
+
// The template content has indentation from the pipe (pipeIndent)
|
|
730
|
+
// We need to replace that with baseIndent to get the correct final indentation
|
|
731
|
+
// The template format is: "{{- if ... }}\n fieldKey: value\n{{- end }}"
|
|
732
|
+
// After removing pipe indent, we want: "{{- if ... }}\n fieldKey: value\n{{- end }}"
|
|
527
733
|
const pipeIndentEscaped = pipeIndent.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
528
|
-
|
|
734
|
+
// Replace pipe indent with base indent on each line
|
|
735
|
+
const adjustedTemplate = templateContent.replace(
|
|
736
|
+
// eslint-disable-next-line security/detect-non-literal-regexp -- Safe: pipeIndent is escaped
|
|
737
|
+
new RegExp(`^${pipeIndentEscaped}`, 'gm'), baseIndent);
|
|
529
738
|
result = beforeMarker + adjustedTemplate + afterTemplate;
|
|
530
739
|
}
|
|
740
|
+
// Post-process to fix double-escaped quotes within Helm templates
|
|
741
|
+
// The yaml library escapes quotes in strings, but Helm templates need unescaped quotes
|
|
742
|
+
// Simply replace all \" with " globally - this is safe because we're only processing
|
|
743
|
+
// Helm template strings that should never have escaped quotes
|
|
531
744
|
result = result.replace(/\\"/g, '"');
|
|
745
|
+
// Fix templates that start with {{- if but are on the same line as the field key
|
|
746
|
+
// Pattern: "fieldKey: {{- if ... }}\n content\n{{- end }}"
|
|
747
|
+
// Should become: "{{- if ... }}\n fieldKey: content\n{{- end }}"
|
|
748
|
+
// But only if the fieldKey is not already wrapped in a conditional
|
|
532
749
|
result = result.replace(/^(\s+)(\w+):\s+(\{\{-?\s*if\s+[^}]+\}\})\s*\n(\s+)(.+?)\n(\s+)(\{\{-?\s*end\s*-?\}\})/gm, (match, baseIndent, fieldKey, ifTag, contentIndent, content, endIndent, endTag) => {
|
|
750
|
+
// Check if content starts with the fieldKey (duplicate)
|
|
533
751
|
const contentLines = content.split('\n');
|
|
534
752
|
const firstContentLine = contentLines[0]?.trim() || '';
|
|
535
753
|
if (firstContentLine.startsWith(`${fieldKey}:`)) {
|
|
754
|
+
// Remove duplicate fieldKey from content
|
|
755
|
+
// Escape fieldKey to prevent regex injection
|
|
536
756
|
const escapedFieldKey = fieldKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
757
|
+
// eslint-disable-next-line security/detect-non-literal-regexp -- Safe: fieldKey is escaped
|
|
537
758
|
contentLines[0] = contentLines[0].replace(new RegExp(`^\\s*${escapedFieldKey}:\\s*`), '');
|
|
538
759
|
content = contentLines.join('\n');
|
|
539
760
|
}
|
|
761
|
+
// Reconstruct as field-level conditional
|
|
540
762
|
const contentFormatted = content
|
|
541
763
|
? '\n' +
|
|
542
764
|
content
|
|
@@ -546,15 +768,30 @@ export function dumpHelmAwareYaml(obj, options = {}) {
|
|
|
546
768
|
: '';
|
|
547
769
|
return `${baseIndent}${ifTag}\n${baseIndent} ${fieldKey}:${contentFormatted}\n${baseIndent}${endTag}`;
|
|
548
770
|
});
|
|
771
|
+
// Apply post-processing for field-level conditionals
|
|
549
772
|
result = postProcessFieldConditionals(result);
|
|
550
773
|
return result;
|
|
551
774
|
}
|
|
775
|
+
/**
|
|
776
|
+
* Post-processes YAML string to transform field-level conditionals
|
|
777
|
+
* This function should be called after dumpHelmAwareYaml to transform
|
|
778
|
+
* __fieldConditionalContent markers into the correct Helm template format.
|
|
779
|
+
*
|
|
780
|
+
* @param yaml YAML string that may contain __fieldConditionalContent markers
|
|
781
|
+
* @returns YAML string with field-level conditionals properly formatted
|
|
782
|
+
* @since 2.14.0
|
|
783
|
+
*/
|
|
784
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Complex post-processing logic
|
|
552
785
|
export function postProcessFieldConditionals(yaml) {
|
|
553
786
|
let result = yaml;
|
|
787
|
+
// Transform field-level with blocks (__FIELD_WITH_MARKER__ marker)
|
|
788
|
+
// Pattern: "fieldKey: |-\n __FIELD_WITH_MARKER__:path:content"
|
|
789
|
+
// Should become: "{{- with path }}\nfieldKey:\n content\n{{- end }}"
|
|
554
790
|
while (result.includes('__FIELD_WITH_MARKER__:')) {
|
|
555
791
|
const markerIndex = result.indexOf('__FIELD_WITH_MARKER__:');
|
|
556
792
|
if (markerIndex === -1)
|
|
557
793
|
break;
|
|
794
|
+
// Find the field key line (e.g., " nodeSelector: |-")
|
|
558
795
|
const beforeMarker = result.substring(0, markerIndex);
|
|
559
796
|
const lastNewline = beforeMarker.lastIndexOf('\n');
|
|
560
797
|
const secondLastNewline = beforeMarker.lastIndexOf('\n', lastNewline - 1);
|
|
@@ -564,6 +801,7 @@ export function postProcessFieldConditionals(yaml) {
|
|
|
564
801
|
break;
|
|
565
802
|
const baseIndent = fieldKeyMatch[1] || '';
|
|
566
803
|
const fieldKey = fieldKeyMatch[2];
|
|
804
|
+
// Extract marker: __FIELD_WITH_MARKER__:path:content
|
|
567
805
|
const afterMarker = result.substring(markerIndex);
|
|
568
806
|
const markerMatch = afterMarker.match(/^__FIELD_WITH_MARKER__:([^:]+):(.+?)$/m);
|
|
569
807
|
if (!markerMatch || !markerMatch[1] || !markerMatch[2])
|
|
@@ -571,13 +809,17 @@ export function postProcessFieldConditionals(yaml) {
|
|
|
571
809
|
const path = markerMatch[1];
|
|
572
810
|
const content = markerMatch[2].trim();
|
|
573
811
|
const markerEnd = markerIndex + markerMatch[0].length;
|
|
812
|
+
// Generate the with block
|
|
574
813
|
const template = `${baseIndent}{{- with ${path} }}\n${baseIndent}${fieldKey}:\n${baseIndent} ${content}\n${baseIndent}{{- end }}`;
|
|
814
|
+
// Replace: remove fieldKey line and marker, insert template
|
|
575
815
|
result = result.substring(0, secondLastNewline + 1) + template + result.substring(markerEnd);
|
|
576
816
|
}
|
|
817
|
+
// Transform field-level with blocks (__FIELD_WITH__ marker)
|
|
577
818
|
while (result.includes('__FIELD_WITH__:')) {
|
|
578
819
|
const markerIndex = result.indexOf('__FIELD_WITH__:');
|
|
579
820
|
if (markerIndex === -1)
|
|
580
821
|
break;
|
|
822
|
+
// Find the field key line (e.g., " __fieldWithTemplate_nodeSelector: |-")
|
|
581
823
|
const beforeMarker = result.substring(0, markerIndex);
|
|
582
824
|
const lastNewline = beforeMarker.lastIndexOf('\n');
|
|
583
825
|
const secondLastNewline = beforeMarker.lastIndexOf('\n', lastNewline - 1);
|
|
@@ -586,27 +828,40 @@ export function postProcessFieldConditionals(yaml) {
|
|
|
586
828
|
if (!fieldKeyMatch)
|
|
587
829
|
break;
|
|
588
830
|
const baseIndent = fieldKeyMatch[1];
|
|
831
|
+
// Extract template: __FIELD_WITH__:fieldKey:TEMPLATE
|
|
589
832
|
const afterMarker = result.substring(markerIndex);
|
|
590
833
|
const markerMatch = afterMarker.match(/^__FIELD_WITH__:\w+:(.+?\{\{-?\s*end\s*-?\}\})/s);
|
|
591
834
|
if (!markerMatch || !markerMatch[1])
|
|
592
835
|
break;
|
|
593
836
|
const template = markerMatch[1];
|
|
594
837
|
const markerEnd = markerIndex + markerMatch[0].length;
|
|
838
|
+
// Remove only the YAML block scalar's common indentation while preserving
|
|
839
|
+
// indentation intentionally produced inside the with block.
|
|
595
840
|
const lines = template.split('\n');
|
|
841
|
+
const nestedIndents = lines
|
|
842
|
+
.slice(1)
|
|
843
|
+
.filter((line) => line.trim().length > 0)
|
|
844
|
+
.map((line) => line.length - line.trimStart().length);
|
|
845
|
+
const commonIndent = nestedIndents.length > 0 ? Math.min(...nestedIndents) : 0;
|
|
596
846
|
const processed = lines
|
|
597
|
-
.map((line) => {
|
|
847
|
+
.map((line, index) => {
|
|
598
848
|
if (!line.trim())
|
|
599
849
|
return '';
|
|
600
|
-
const
|
|
601
|
-
return baseIndent +
|
|
850
|
+
const normalized = index === 0 ? line.trimStart() : line.slice(commonIndent);
|
|
851
|
+
return baseIndent + normalized;
|
|
602
852
|
})
|
|
603
853
|
.join('\n');
|
|
854
|
+
// Replace: remove fieldKey line and marker, insert processed template
|
|
604
855
|
result = result.substring(0, secondLastNewline + 1) + processed + result.substring(markerEnd);
|
|
605
856
|
}
|
|
857
|
+
// Transform field-level conditionals with __FIELD_CONDITIONAL__ marker
|
|
858
|
+
// Pattern: "fieldKey: |-\n __FIELD_CONDITIONAL__:fieldKey:{{- if ... }}\n fieldKey: value\n{{- end }}"
|
|
859
|
+
// Should become: "{{- if ... }}\n fieldKey: value\n{{- end }}"
|
|
606
860
|
while (result.includes('__FIELD_CONDITIONAL__:')) {
|
|
607
861
|
const markerIndex = result.indexOf('__FIELD_CONDITIONAL__:');
|
|
608
862
|
if (markerIndex === -1)
|
|
609
863
|
break;
|
|
864
|
+
// Find the field key line (e.g., " fieldKey: |-")
|
|
610
865
|
const beforeMarker = result.substring(0, markerIndex);
|
|
611
866
|
const lastNewline = beforeMarker.lastIndexOf('\n');
|
|
612
867
|
const secondLastNewline = beforeMarker.lastIndexOf('\n', lastNewline - 1);
|
|
@@ -615,30 +870,49 @@ export function postProcessFieldConditionals(yaml) {
|
|
|
615
870
|
if (!fieldKeyMatch)
|
|
616
871
|
break;
|
|
617
872
|
const baseIndent = fieldKeyMatch[1];
|
|
873
|
+
// Extract template: __FIELD_CONDITIONAL__:fieldKey:TEMPLATE
|
|
618
874
|
const afterMarker = result.substring(markerIndex);
|
|
619
875
|
const markerMatch = afterMarker.match(/^__FIELD_CONDITIONAL__:\w+:(.+?\{\{-?\s*end\s*-?\}\})/s);
|
|
620
876
|
if (!markerMatch || !markerMatch[1])
|
|
621
877
|
break;
|
|
622
878
|
const template = markerMatch[1];
|
|
623
879
|
const markerEnd = markerIndex + markerMatch[0].length;
|
|
880
|
+
// Process template: remove block indent, add base indent
|
|
624
881
|
const lines = template.split('\n');
|
|
625
882
|
const processed = lines
|
|
626
883
|
.map((line) => {
|
|
627
884
|
if (!line.trim())
|
|
628
885
|
return '';
|
|
886
|
+
// Remove leading spaces (block indent from YAML serialization)
|
|
629
887
|
const trimmed = line.trimStart();
|
|
888
|
+
// Add base indent
|
|
630
889
|
return baseIndent + trimmed;
|
|
631
890
|
})
|
|
632
891
|
.join('\n');
|
|
892
|
+
// Replace: remove fieldKey line and marker, insert processed template
|
|
633
893
|
result = result.substring(0, secondLastNewline + 1) + processed + result.substring(markerEnd);
|
|
634
894
|
}
|
|
895
|
+
// Clean up pipe literals
|
|
896
|
+
// Use possessive quantifier pattern to prevent ReDoS (CWE-1333)
|
|
635
897
|
result = result.replace(/:\s*\|[-+]?\s*\n([ \t]*\{\{)/g, ':\n$1');
|
|
898
|
+
// Remove quotes around Helm expressions
|
|
636
899
|
result = result.replace(/"(\{\{[\s\S]*?\}\})"/g, '$1');
|
|
637
900
|
return result;
|
|
638
901
|
}
|
|
902
|
+
/**
|
|
903
|
+
* Legacy compatible stringify method.
|
|
904
|
+
* @param obj Object to serialize.
|
|
905
|
+
* @param options Options.
|
|
906
|
+
* @returns YAML string.
|
|
907
|
+
* @since 2.11.0
|
|
908
|
+
*/
|
|
639
909
|
export function stringify(obj, options = {}) {
|
|
640
910
|
return dumpHelmAwareYaml(obj, options.lineWidth !== undefined ? { lineWidth: options.lineWidth } : {});
|
|
641
911
|
}
|
|
912
|
+
/**
|
|
913
|
+
* Compile and cache Helm expression patterns with types for optimized single pass processing
|
|
914
|
+
* @since 2.11.0
|
|
915
|
+
*/
|
|
642
916
|
const COMPILED_PATTERNS = [
|
|
643
917
|
{ regex: /\{\{-?\s*define\s+[^}]+\s*-?\}\}[\s\S]*?\{\{-?\s*end\s*-?\}\}/g, type: 'block' },
|
|
644
918
|
{ regex: /\{\{[^}]*\{\{[^}]*\}\}[^}]*\}\}/g, type: 'nested' },
|
|
@@ -647,10 +921,24 @@ const COMPILED_PATTERNS = [
|
|
|
647
921
|
{ regex: /\{\{`[\s\S]*?`\}\}/g, type: 'raw' },
|
|
648
922
|
{ regex: /\{\{\s*include\s+"[^"]+"\s+[^}]+\s*\}\}/g, type: 'include-context' },
|
|
649
923
|
];
|
|
924
|
+
/**
|
|
925
|
+
* Validate YAML string with Helm template expressions.
|
|
926
|
+
* - Parses Helm expressions from the string,
|
|
927
|
+
* - Performs balanced parentheses and quote checks,
|
|
928
|
+
* - Collects syntax errors, semantic warnings,
|
|
929
|
+
* - Provides statistics including complexity.
|
|
930
|
+
*
|
|
931
|
+
* @param yaml YAML content string to validate.
|
|
932
|
+
* @returns Validation result with errors, warnings, and stats.
|
|
933
|
+
* @since 2.11.0
|
|
934
|
+
* @since 2.14.0 Restored full validation functionality
|
|
935
|
+
*/
|
|
650
936
|
export function validateHelmYaml(yaml) {
|
|
651
937
|
const errors = [];
|
|
652
938
|
const warnings = [];
|
|
939
|
+
// Detect all Helm expressions with types and positions
|
|
653
940
|
const expressions = parseHelmExpressions(yaml);
|
|
941
|
+
// Global check for unbalanced braces
|
|
654
942
|
const openBraces = (yaml.match(/\{\{/g) || []).length;
|
|
655
943
|
const closeBraces = (yaml.match(/\}\}/g) || []).length;
|
|
656
944
|
if (openBraces !== closeBraces) {
|
|
@@ -662,6 +950,7 @@ export function validateHelmYaml(yaml) {
|
|
|
662
950
|
expression: '',
|
|
663
951
|
});
|
|
664
952
|
}
|
|
953
|
+
// Syntax validation with error capturing
|
|
665
954
|
for (const expr of expressions) {
|
|
666
955
|
try {
|
|
667
956
|
validateHelmSyntax(expr.expression);
|
|
@@ -699,17 +988,27 @@ export function validateHelmYaml(yaml) {
|
|
|
699
988
|
},
|
|
700
989
|
};
|
|
701
990
|
}
|
|
991
|
+
/**
|
|
992
|
+
* Internal helper to parse Helm expressions with line and column positions.
|
|
993
|
+
* Supports advanced expression types and range matching.
|
|
994
|
+
* @param content YAML string content.
|
|
995
|
+
* @returns List of detected Helm expressions with positional info.
|
|
996
|
+
* @since 2.11.0
|
|
997
|
+
*/
|
|
702
998
|
export function parseHelmExpressions(content) {
|
|
703
999
|
const expressions = [];
|
|
704
1000
|
const lines = content.split('\n');
|
|
705
1001
|
for (let i = 0; i < lines.length; i++) {
|
|
1002
|
+
// eslint-disable-next-line security/detect-object-injection -- Safe: iterating over array indices
|
|
706
1003
|
const line = lines[i];
|
|
707
1004
|
if (!line)
|
|
708
1005
|
continue;
|
|
709
1006
|
for (const { regex, type } of COMPILED_PATTERNS) {
|
|
710
1007
|
let match;
|
|
711
1008
|
regex.lastIndex = 0;
|
|
1009
|
+
// Note: regex.exec() is RegExp matching, NOT OS command execution
|
|
712
1010
|
while ((match = regex.exec(line)) !== null) {
|
|
1011
|
+
// Sanitize matched expression to prevent any injection in downstream processing
|
|
713
1012
|
const sanitizedExpression = SecurityUtils.sanitizeLogMessage(match[0] || '');
|
|
714
1013
|
expressions.push({
|
|
715
1014
|
type,
|
|
@@ -726,7 +1025,16 @@ export function parseHelmExpressions(content) {
|
|
|
726
1025
|
}
|
|
727
1026
|
return expressions;
|
|
728
1027
|
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Validate Helm syntax for a single Helm expression string.
|
|
1030
|
+
* Checks for balanced parentheses, balanced quotes, basic function call validity.
|
|
1031
|
+
* Throws errors with descriptive messages on failure.
|
|
1032
|
+
* @param expression Helm template expression string.
|
|
1033
|
+
* @throws Error on syntax validation failure.
|
|
1034
|
+
* @since 2.11.0
|
|
1035
|
+
*/
|
|
729
1036
|
function validateHelmSyntax(expression) {
|
|
1037
|
+
// Strip outer braces for parsing content
|
|
730
1038
|
const content = expression.replace(/^\{\{-?\s*/, '').replace(/\s*-?\}\}$/, '');
|
|
731
1039
|
if (!isBalanced(content, '(', ')')) {
|
|
732
1040
|
throw new Error('Unbalanced parentheses in Helm expression');
|
|
@@ -736,6 +1044,14 @@ function validateHelmSyntax(expression) {
|
|
|
736
1044
|
}
|
|
737
1045
|
validateFunctionCalls(content);
|
|
738
1046
|
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Check that string has balanced pairs of the specified characters.
|
|
1049
|
+
* @param str String to check.
|
|
1050
|
+
* @param open Open character.
|
|
1051
|
+
* @param close Close character.
|
|
1052
|
+
* @returns True if balanced.
|
|
1053
|
+
* @since 2.11.0
|
|
1054
|
+
*/
|
|
739
1055
|
function isBalanced(str, open, close) {
|
|
740
1056
|
let count = 0;
|
|
741
1057
|
for (const c of str) {
|
|
@@ -748,6 +1064,13 @@ function isBalanced(str, open, close) {
|
|
|
748
1064
|
}
|
|
749
1065
|
return count === 0;
|
|
750
1066
|
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Checks if quotes in string are balanced.
|
|
1069
|
+
* Supports single and double quotes ignoring escaped quotes.
|
|
1070
|
+
* @param str String to check.
|
|
1071
|
+
* @returns True if balanced.
|
|
1072
|
+
* @since 2.11.0
|
|
1073
|
+
*/
|
|
751
1074
|
function isQuotesBalanced(str) {
|
|
752
1075
|
let singleQuoteOpen = false;
|
|
753
1076
|
let doubleQuoteOpen = false;
|
|
@@ -768,18 +1091,40 @@ function isQuotesBalanced(str) {
|
|
|
768
1091
|
}
|
|
769
1092
|
return !singleQuoteOpen && !doubleQuoteOpen;
|
|
770
1093
|
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Validate simple function call well-formedness for Helm expressions.
|
|
1096
|
+
* Checks for basic invalid nested parentheses or arguments.
|
|
1097
|
+
* Throws error if checks fail.
|
|
1098
|
+
* @param content Expression string content (without outer braces).
|
|
1099
|
+
* @throws Error on invalid functions.
|
|
1100
|
+
* @since 2.11.0
|
|
1101
|
+
*/
|
|
771
1102
|
function validateFunctionCalls(_content) {
|
|
1103
|
+
// Basic sanity: function names followed by open paren, balanced args are checked by previous functions
|
|
1104
|
+
// Additional validations can be implemented here as needed.
|
|
1105
|
+
// For now, no error thrown by default.
|
|
772
1106
|
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Checks for common semantic issues in the YAML string to emit warnings.
|
|
1109
|
+
* Checks for deprecated functions and problematic usage.
|
|
1110
|
+
* @param yaml YAML string content.
|
|
1111
|
+
* @param warnings Array to populate warnings.
|
|
1112
|
+
* @since 2.11.0
|
|
1113
|
+
*/
|
|
773
1114
|
function checkCommonIssues(yaml, warnings) {
|
|
774
1115
|
const deprecatedFunctions = ['template'];
|
|
775
1116
|
for (const func of deprecatedFunctions) {
|
|
1117
|
+
// Validate func is alphanumeric only to prevent injection
|
|
776
1118
|
if (!/^[a-zA-Z0-9_]+$/.test(func)) {
|
|
777
|
-
continue;
|
|
1119
|
+
continue; // Skip invalid function names
|
|
778
1120
|
}
|
|
1121
|
+
// Escape special regex characters to prevent injection
|
|
779
1122
|
const escapedFunc = func.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1123
|
+
// eslint-disable-next-line security/detect-non-literal-regexp -- Safe: func is validated and escaped
|
|
780
1124
|
const pattern = new RegExp(`\\{\\{[^}]*\\b${escapedFunc}\\b[^}]*\\}\\}`, 'g');
|
|
781
1125
|
let match;
|
|
782
1126
|
while ((match = pattern.exec(yaml)) !== null) {
|
|
1127
|
+
// Sanitize function name for output to prevent injection in error messages
|
|
783
1128
|
const sanitizedFunc = SecurityUtils.sanitizeLogMessage(func);
|
|
784
1129
|
warnings.push({
|
|
785
1130
|
type: 'semantic',
|
|
@@ -790,12 +1135,21 @@ function checkCommonIssues(yaml, warnings) {
|
|
|
790
1135
|
}
|
|
791
1136
|
}
|
|
792
1137
|
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Check for quoted Helm expressions, which is an anti-pattern.
|
|
1140
|
+
* Emits warnings advising to remove quotes.
|
|
1141
|
+
* @param yaml YAML string content.
|
|
1142
|
+
* @param warnings Array to populate warnings.
|
|
1143
|
+
* @since 2.11.0
|
|
1144
|
+
* @since 2.11.1 Corrected pattern matching to capture quoted Helm templates
|
|
1145
|
+
*/
|
|
793
1146
|
function checkQuotedExpressions(yaml, warnings) {
|
|
794
1147
|
const quotedPatterns = [/'(\{\{[^}]+\}\})'/g, /"(\{\{[^}]+\}\})"/g];
|
|
795
1148
|
for (const pattern of quotedPatterns) {
|
|
796
1149
|
let match;
|
|
797
1150
|
pattern.lastIndex = 0;
|
|
798
1151
|
while ((match = pattern.exec(yaml)) !== null) {
|
|
1152
|
+
// Sanitize matched content to prevent injection in suggestion text
|
|
799
1153
|
const sanitizedMatch = SecurityUtils.sanitizeLogMessage(match[1] || '');
|
|
800
1154
|
const sanitizedExpression = SecurityUtils.sanitizeLogMessage(match[0] || '');
|
|
801
1155
|
warnings.push({
|
|
@@ -807,6 +1161,13 @@ function checkQuotedExpressions(yaml, warnings) {
|
|
|
807
1161
|
}
|
|
808
1162
|
}
|
|
809
1163
|
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Naive calculation of complexity score based on number and type of Helm expressions.
|
|
1166
|
+
* Used for validation statistics.
|
|
1167
|
+
* @param expressions Array of Helm expression matches.
|
|
1168
|
+
* @returns Complexity score number.
|
|
1169
|
+
* @since 2.11.0
|
|
1170
|
+
*/
|
|
810
1171
|
function calculateComplexity(expressions) {
|
|
811
1172
|
let score = 0;
|
|
812
1173
|
for (const expr of expressions) {
|
|
@@ -826,12 +1187,28 @@ function calculateComplexity(expressions) {
|
|
|
826
1187
|
}
|
|
827
1188
|
return score;
|
|
828
1189
|
}
|
|
1190
|
+
// Export other helpers for compatibility if needed
|
|
1191
|
+
/**
|
|
1192
|
+
* Detect Helm template expressions using Timonel's parser.
|
|
1193
|
+
* @param str - YAML/template content to inspect
|
|
1194
|
+
* @returns Detected expression descriptors
|
|
1195
|
+
*/
|
|
829
1196
|
export function detectHelmExpressions(str) {
|
|
830
1197
|
return parseHelmExpressions(str);
|
|
831
1198
|
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Preprocess Helm marker objects before YAML serialization.
|
|
1201
|
+
* @param obj - Value tree containing Helm constructs or ValuesRef proxies
|
|
1202
|
+
* @returns Serializer-ready value tree
|
|
1203
|
+
*/
|
|
832
1204
|
export function preprocessHelmExpressions(obj) {
|
|
833
1205
|
return preprocessHelmConstructs(obj);
|
|
834
1206
|
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Preserve the historical post-processing compatibility hook.
|
|
1209
|
+
* @param yaml - YAML content to return unchanged
|
|
1210
|
+
* @returns The original YAML content
|
|
1211
|
+
*/
|
|
835
1212
|
export function postProcessHelmExpressions(yaml) {
|
|
836
1213
|
return yaml;
|
|
837
1214
|
}
|