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.
Files changed (83) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +323 -695
  3. package/dist/cli.js +275 -15
  4. package/dist/index.d.ts +5 -1
  5. package/dist/index.js +12 -0
  6. package/dist/lib/helm.d.ts +471 -0
  7. package/dist/lib/helm.js +483 -0
  8. package/dist/lib/helmChartWriter.d.ts +157 -0
  9. package/dist/lib/helmChartWriter.js +171 -1
  10. package/dist/lib/policy/configurationLoader.d.ts +132 -0
  11. package/dist/lib/policy/configurationLoader.js +132 -0
  12. package/dist/lib/policy/errorContextGenerator.d.ts +89 -0
  13. package/dist/lib/policy/errorContextGenerator.js +99 -2
  14. package/dist/lib/policy/errors.d.ts +35 -0
  15. package/dist/lib/policy/errors.js +36 -0
  16. package/dist/lib/policy/index.d.ts +9 -0
  17. package/dist/lib/policy/index.js +16 -0
  18. package/dist/lib/policy/parallelExecutor.d.ts +90 -0
  19. package/dist/lib/policy/parallelExecutor.js +86 -3
  20. package/dist/lib/policy/pluginLoader.d.ts +92 -0
  21. package/dist/lib/policy/pluginLoader.js +92 -1
  22. package/dist/lib/policy/pluginRegistry.d.ts +54 -0
  23. package/dist/lib/policy/pluginRegistry.js +56 -0
  24. package/dist/lib/policy/policyEngine.d.ts +137 -0
  25. package/dist/lib/policy/policyEngine.js +191 -5
  26. package/dist/lib/policy/resultAggregator.d.ts +46 -0
  27. package/dist/lib/policy/resultAggregator.js +69 -1
  28. package/dist/lib/policy/resultFormatter.d.ts +88 -0
  29. package/dist/lib/policy/resultFormatter.js +101 -0
  30. package/dist/lib/policy/types.d.ts +136 -0
  31. package/dist/lib/policy/types.js +8 -0
  32. package/dist/lib/policy/validationCache.d.ts +146 -0
  33. package/dist/lib/policy/validationCache.js +142 -6
  34. package/dist/lib/resources/baseResourceProvider.d.ts +45 -0
  35. package/dist/lib/resources/baseResourceProvider.js +48 -1
  36. package/dist/lib/resources/cloud/aws/awsResources.d.ts +192 -0
  37. package/dist/lib/resources/cloud/aws/awsResources.js +163 -1
  38. package/dist/lib/resources/cloud/aws/karpenterResources.d.ts +131 -0
  39. package/dist/lib/resources/cloud/aws/karpenterResources.js +77 -0
  40. package/dist/lib/rutter.d.ts +381 -3
  41. package/dist/lib/rutter.js +439 -28
  42. package/dist/lib/security.d.ts +123 -0
  43. package/dist/lib/security.js +162 -4
  44. package/dist/lib/templates/flexible-subchart.d.ts +52 -0
  45. package/dist/lib/templates/flexible-subchart.js +70 -0
  46. package/dist/lib/templates/umbrella-chart.d.ts +27 -0
  47. package/dist/lib/templates/umbrella-chart.js +89 -0
  48. package/dist/lib/types.d.ts +26 -0
  49. package/dist/lib/umbrella.d.ts +23 -0
  50. package/dist/lib/umbrella.js +23 -0
  51. package/dist/lib/umbrellaRutter.d.ts +75 -0
  52. package/dist/lib/umbrellaRutter.js +82 -2
  53. package/dist/lib/utils/envVarsLoader.d.ts +49 -0
  54. package/dist/lib/utils/envVarsLoader.js +53 -0
  55. package/dist/lib/utils/helmConstructSerializer.d.ts +17 -0
  56. package/dist/lib/utils/helmConstructSerializer.js +22 -0
  57. package/dist/lib/utils/helmControlStructures.d.ts +194 -0
  58. package/dist/lib/utils/helmControlStructures.js +180 -0
  59. package/dist/lib/utils/helmHelpers/envHelpers.d.ts +13 -0
  60. package/dist/lib/utils/helmHelpers/envHelpers.js +13 -0
  61. package/dist/lib/utils/helmHelpers/gitopsHelpers.d.ts +13 -0
  62. package/dist/lib/utils/helmHelpers/gitopsHelpers.js +13 -0
  63. package/dist/lib/utils/helmHelpers/index.d.ts +74 -0
  64. package/dist/lib/utils/helmHelpers/index.js +85 -1
  65. package/dist/lib/utils/helmHelpers/observabilityHelpers.d.ts +13 -0
  66. package/dist/lib/utils/helmHelpers/observabilityHelpers.js +13 -0
  67. package/dist/lib/utils/helmHelpers/types.d.ts +23 -0
  68. package/dist/lib/utils/helmHelpers/types.js +4 -0
  69. package/dist/lib/utils/helmHelpers/validationHelpers.d.ts +13 -0
  70. package/dist/lib/utils/helmHelpers/validationHelpers.js +13 -0
  71. package/dist/lib/utils/helmHelpers.d.ts +62 -0
  72. package/dist/lib/utils/helmHelpers.js +77 -0
  73. package/dist/lib/utils/helmYamlSerializer.d.ts +77 -0
  74. package/dist/lib/utils/helmYamlSerializer.js +398 -21
  75. package/dist/lib/utils/logger.d.ts +153 -0
  76. package/dist/lib/utils/logger.js +170 -2
  77. package/dist/lib/utils/valuesRef.d.ts +181 -50
  78. package/dist/lib/utils/valuesRef.js +168 -170
  79. package/dist/lib/validation/inputValidator.d.ts +45 -0
  80. package/dist/lib/validation/inputValidator.js +67 -2
  81. package/dist/types/index.d.ts +34 -0
  82. package/dist/types/index.js +3 -0
  83. 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 = { __path: '$item' };
254
- const indexProxy = { __path: '$index' };
306
+ const itemProxy = createHelmValueProxy('$item');
307
+ const indexProxy = createHelmValueProxy('$index');
255
308
  const content = range.callback(itemProxy, indexProxy);
256
- const processedContent = preprocessHelmConstructs(content);
257
- const contentDoc = new Document(processedContent);
258
- const contentStr = contentDoc.toString({ lineWidth: 0 }).trim();
259
- return createHelmExpression(`{{- range ${sourcePath} }}\n${contentStr}\n{{- end }}`);
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
- const ctxProxy = { __path: '.' };
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
- const contentDoc = new Document(processedContent);
303
- contentStr = contentDoc.toString({ lineWidth: 0 }).trim();
372
+ contentStr = dumpHelmAwareYaml(processedContent).trim();
304
373
  }
305
- const template = `{{- with ${sourcePath} }}\n${key}:\n ${contentStr}\n{{- end }}`;
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
- const isWithOrRange = value.trim().startsWith('{{- with ') || value.trim().startsWith('{{- range ');
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(new RegExp(`^${pipeIndentEscaped}`, 'gm'), baseIndent);
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
- const adjustedTemplate = templateContent.replace(new RegExp(`^${pipeIndentEscaped}`, 'gm'), baseIndent);
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 trimmed = line.trimStart();
601
- return baseIndent + trimmed;
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
  }