timonel 2.13.0 → 3.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,12 @@
1
1
  import { ApiObject, App, Chart, Testing } from 'cdk8s';
2
- import * as jsYaml from 'js-yaml';
2
+ import { parse } from 'yaml';
3
3
  import { include } from './helm.js';
4
4
  import { HelmChartWriter } from './helmChartWriter.js';
5
5
  import { AWSResources } from './resources/cloud/aws/awsResources.js';
6
6
  import { createLogger } from './utils/logger.js';
7
7
  import { KarpenterResources } from './resources/cloud/aws/karpenterResources.js';
8
- import { dumpHelmAwareYaml, isHelmExpression } from './utils/helmYamlSerializer.js';
8
+ import { isHelmExpression, isHelmConstruct } from './utils/helmControlStructures.js';
9
+ import { dumpHelmAwareYaml, preprocessHelmConstructs } from './utils/helmYamlSerializer.js';
9
10
  import { generateHelpersTemplate } from './utils/helmHelpers.js';
10
11
  export class Rutter {
11
12
  constructor(props) {
@@ -63,7 +64,7 @@ export class Rutter {
63
64
  let manifestObject;
64
65
  if (typeof yamlOrObject === 'string') {
65
66
  try {
66
- manifestObject = jsYaml.load(yamlOrObject);
67
+ manifestObject = parse(yamlOrObject);
67
68
  }
68
69
  catch (error) {
69
70
  throw new Error(`Invalid YAML provided to addManifest(): ${error instanceof Error ? error.message : 'Unknown error'}`);
@@ -76,12 +77,14 @@ export class Rutter {
76
77
  throw new Error('addManifest() requires either a YAML string or an object');
77
78
  }
78
79
  this.validateManifestStructure(manifestObject);
79
- return new ApiObject(this.chart, id, {
80
- apiVersion: manifestObject['apiVersion'],
81
- kind: manifestObject['kind'],
82
- metadata: manifestObject['metadata'],
83
- spec: manifestObject['spec'],
84
- });
80
+ const preprocessedManifest = preprocessHelmConstructs(manifestObject);
81
+ const apiObjectProps = {
82
+ apiVersion: preprocessedManifest.apiVersion,
83
+ kind: preprocessedManifest.kind,
84
+ metadata: preprocessedManifest.metadata,
85
+ ...preprocessedManifest,
86
+ };
87
+ return new ApiObject(this.chart, id, apiObjectProps);
85
88
  }
86
89
  addTemplateManifest(yamlTemplate, id) {
87
90
  const templateAsset = {
@@ -169,13 +172,10 @@ ${yamlContent.trim()}
169
172
  });
170
173
  throw new Error('Manifest metadata must have a name');
171
174
  }
172
- if (typeof metadata.name !== 'string' && !isHelmExpression(metadata.name)) {
173
- this.logger.error('Validation failed: metadata.name must be a string or HelmExpression', {
174
- operation: 'manifest_validation',
175
- issue: 'invalid_metadata_name_type',
176
- actual_type: typeof metadata.name,
177
- });
178
- throw new Error('Manifest metadata.name must be a string or HelmExpression');
175
+ const name = manifest['metadata']?.['name'];
176
+ if (typeof name !== 'string' && !isHelmExpression(name) && !isHelmConstruct(name)) {
177
+ const actualType = Array.isArray(name) ? 'array' : name === null ? 'null' : typeof name;
178
+ throw new Error(`Manifest metadata.name must be a string, HelmExpression, or HelmConstruct. Got ${actualType}`);
179
179
  }
180
180
  }
181
181
  getMeta() {
@@ -218,8 +218,9 @@ ${yamlContent.trim()}
218
218
  operation: 'manifest_processing',
219
219
  });
220
220
  const enriched = manifestObjs.map((obj) => {
221
- if (obj && typeof obj === 'object') {
222
- const o = obj;
221
+ const preprocessed = preprocessHelmConstructs(obj);
222
+ if (preprocessed && typeof preprocessed === 'object') {
223
+ const o = preprocessed;
223
224
  o.metadata = o.metadata ?? {};
224
225
  o.metadata.labels = o.metadata.labels ?? {};
225
226
  const labels = o.metadata.labels;
@@ -237,12 +238,12 @@ ${yamlContent.trim()}
237
238
  }
238
239
  }
239
240
  }
240
- return obj;
241
+ return preprocessed;
241
242
  });
242
243
  const synthAssets = [];
243
244
  if (this.props.singleManifestFile) {
244
245
  const combinedYaml = enriched
245
- .map((obj) => this.processHelmTemplates(dumpHelmAwareYaml(obj).trim()))
246
+ .map((obj) => dumpHelmAwareYaml(obj).trim())
246
247
  .filter(Boolean)
247
248
  .join('\n---\n');
248
249
  const manifestId = this.props.manifestPrefix ?? 'manifests';
@@ -252,7 +253,7 @@ ${yamlContent.trim()}
252
253
  enriched.forEach((obj, index) => {
253
254
  const apiObjectId = apiObjectIds[index];
254
255
  const manifestId = apiObjectId || `manifest-${index + 1}`;
255
- const yaml = this.processHelmTemplates(dumpHelmAwareYaml(obj).trim());
256
+ const yaml = dumpHelmAwareYaml(obj).trim();
256
257
  if (yaml) {
257
258
  synthAssets.push({ id: manifestId, yaml });
258
259
  }
@@ -270,173 +271,6 @@ ${yamlContent.trim()}
270
271
  timer();
271
272
  return synthAssets;
272
273
  }
273
- processHelmTemplates(yaml) {
274
- let processed = this.fixCharacterMappingIssues(yaml);
275
- processed = this.applyHelmTemplateReplacements(processed);
276
- return processed;
277
- }
278
- fixCharacterMappingIssues(yaml) {
279
- const lines = yaml.split('\n');
280
- const fixedLines = [];
281
- let i = 0;
282
- while (i < lines.length) {
283
- const line = lines[i];
284
- if (!line) {
285
- i++;
286
- continue;
287
- }
288
- const charKeyMatch = line.match(/^(\s*)"(\d+)":\s*(.+)$/);
289
- if (charKeyMatch && charKeyMatch[2] && parseInt(charKeyMatch[2]) === 0) {
290
- const indent = charKeyMatch[1] || '';
291
- const result = this.processCharacterMapping(lines, i, indent);
292
- if (result.reconstructed) {
293
- fixedLines.push(result.reconstructed);
294
- i = result.nextIndex;
295
- continue;
296
- }
297
- }
298
- if (line) {
299
- fixedLines.push(line);
300
- }
301
- i++;
302
- }
303
- return fixedLines.join('\n');
304
- }
305
- processCharacterMapping(lines, startIndex, indent) {
306
- const charMappings = [];
307
- let j = startIndex;
308
- while (j < lines.length) {
309
- const currentLine = lines[j];
310
- if (!currentLine) {
311
- break;
312
- }
313
- const currentMatch = currentLine.match(/^(\s*)"(\d+)":\s*(.+)$/);
314
- if (currentMatch && currentMatch[1] === indent && currentMatch[2] && currentMatch[3]) {
315
- const index = parseInt(currentMatch[2]);
316
- let char = currentMatch[3];
317
- if (char && char.startsWith('"') && char.endsWith('"')) {
318
- char = char.slice(1, -1);
319
- }
320
- charMappings.push({ index, char });
321
- j++;
322
- }
323
- else {
324
- break;
325
- }
326
- }
327
- if (charMappings.length > 3) {
328
- charMappings.sort((a, b) => a.index - b.index);
329
- const reconstructed = charMappings.map((m) => m.char).join('');
330
- if (reconstructed.includes('{{') && reconstructed.includes('}}')) {
331
- return { reconstructed: `${indent}${reconstructed}`, nextIndex: j };
332
- }
333
- }
334
- return { nextIndex: j };
335
- }
336
- applyHelmTemplateReplacements(processed) {
337
- processed = this.fixConditionalBlocks(processed);
338
- processed = this.fixPipeExpressions(processed);
339
- processed = this.fixNestedQuotes(processed);
340
- processed = this.cleanupArtifacts(processed);
341
- processed = this.removeHelmExpressionQuotes(processed);
342
- return processed;
343
- }
344
- fixIncludeStatements(processed) {
345
- processed = processed.replace(/\{\{ include \\"([^"]+)\\" \. \| nindent (\d+) \}\}/g, '{{ include "$1" . | nindent $2 }}');
346
- processed = processed.replace(/\{\{ include \\"([^"]+)\\" \. \}\}/g, '{{ include "$1" . }}');
347
- return processed;
348
- }
349
- fixFunctionCalls(processed) {
350
- return processed.replace(/\{\{ (\w+) \\"([^"]*)\\" ([^}]*) \}\}/g, '{{ $1 "$2" $3 }}');
351
- }
352
- fixChartReferences(processed) {
353
- return processed.replace(/\{\{ \.Chart\.(\w+) \| replace \\"([^"]*)\\" \\"([^"]*)\\" ([^}]*) \}\}/g, '{{ .Chart.$1 | replace "$2" "$3" $4 }}');
354
- }
355
- removeHelmExpressionQuotes(processed) {
356
- const HELM_EXPRESSION_REPLACEMENT = '$1$2: {{$3}}';
357
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\|\s*(?:int|float|bool|number)[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
358
- processed = processed.replace(/^(\s*)(port):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
359
- processed = processed.replace(/^(\s*)(port\.number):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
360
- processed = processed.replace(/^(\s*)(service\.port):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
361
- processed = processed.replace(/^(\s*)(backend\.service\.port\.number):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
362
- processed = processed.replace(/^(\s*)(spec\.port):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
363
- processed = processed.replace(/^(\s*)([^:\s]*\.number):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
364
- processed = processed.replace(/^(\s*)(number):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
365
- processed = processed.replace(/^(\s*)(replicas):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
366
- processed = processed.replace(/^(\s*)(targetPort):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
367
- processed = processed.replace(/^(\s*)(nodePort):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
368
- processed = processed.replace(/^(\s*)(containerPort):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
369
- processed = processed.replace(/^(\s*)(hostPort):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
370
- processed = processed.replace(/^(\s*)(weight):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
371
- processed = processed.replace(/^(\s*)(priority):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
372
- processed = processed.replace(/^(\s*)(timeoutSeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
373
- processed = processed.replace(/^(\s*)(periodSeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
374
- processed = processed.replace(/^(\s*)(successThreshold):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
375
- processed = processed.replace(/^(\s*)(failureThreshold):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
376
- processed = processed.replace(/^(\s*)(initialDelaySeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
377
- processed = processed.replace(/^(\s*)(terminationGracePeriodSeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
378
- processed = processed.replace(/^(\s*)(activeDeadlineSeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
379
- processed = processed.replace(/^(\s*)(backoffLimit):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
380
- processed = processed.replace(/^(\s*)(parallelism):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
381
- processed = processed.replace(/^(\s*)(completions):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
382
- processed = processed.replace(/^(\s*)(revisionHistoryLimit):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
383
- processed = processed.replace(/^(\s*)(progressDeadlineSeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
384
- processed = processed.replace(/^(\s*)(minReadySeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
385
- processed = processed.replace(/^(\s*)(maxUnavailable):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
386
- processed = processed.replace(/^(\s*)(maxSurge):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
387
- processed = processed.replace(/^(\s*)(cpu):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
388
- processed = processed.replace(/^(\s*)(memory):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
389
- processed = processed.replace(/^(\s*)(enabled):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
390
- processed = processed.replace(/^(\s*)(create):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
391
- processed = processed.replace(/^(\s*)(allow):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
392
- processed = processed.replace(/^(\s*)(disable):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
393
- processed = processed.replace(/^(\s*)(force):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
394
- processed = processed.replace(/^(\s*)(required):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
395
- processed = processed.replace(/^(\s*)(optional):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
396
- processed = processed.replace(/^(\s*)(readOnly):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
397
- processed = processed.replace(/^(\s*)(runAsNonRoot):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
398
- processed = processed.replace(/^(\s*)(privileged):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
399
- processed = processed.replace(/^(\s*)(allowPrivilegeEscalation):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
400
- processed = processed.replace(/^(\s*)(readOnlyRootFilesystem):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
401
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*true[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
402
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*false[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
403
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\d+[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
404
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\beq\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
405
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\bne\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
406
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\blt\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
407
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\ble\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
408
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\bgt\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
409
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\bge\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
410
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\band\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
411
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\bor\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
412
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\bnot\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
413
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*[+][^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
414
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*[-][^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
415
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*[*][^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
416
- processed = processed.replace(/:\s*"([^"]*\{\{[^}]+\}\}[^"]*)"/g, ': $1');
417
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"(true|false)"$/gm, '$1$2: $3');
418
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"(-?\d+)"$/gm, '$1$2: $3');
419
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"(-?\d+\.\d+)"$/gm, '$1$2: $3');
420
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"(-?\d+\.?\d*[eE][+-]?\d+)"$/gm, '$1$2: $3');
421
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
422
- processed = processed.replace(/"\{\{([^}]*)\}\}"/g, '{{$1}}');
423
- processed = processed.replace(/^(\s*)-\s*"\{\{([^}]*)\}\}"/gm, '$1- {{$2}}');
424
- return processed;
425
- }
426
- fixConditionalBlocks(processed) {
427
- return processed.replace(/"\{\{-\s*(if|with|range)([^}]*)\}\}([^"]*)\{\{-\s*end\s*\}\}"/g, '{{- $1$2}}$3{{- end }}');
428
- }
429
- fixPipeExpressions(processed) {
430
- return processed.replace(/"\{\{([^}]*\|[^}]*)\}\}"/g, '{{$1}}');
431
- }
432
- fixNestedQuotes(processed) {
433
- return processed.replace(/\{\{([^}]*)\\"([^"]*)\\"([^}]*)\}\}/g, '{{$1"$2"$3}}');
434
- }
435
- cleanupArtifacts(processed) {
436
- processed = processed.replace(/": "\.nan"/g, ': .nan');
437
- processed = processed.replace(/: \{\{([^}]+)\}\}$/gm, ': {{$1}}');
438
- return processed;
439
- }
440
274
  write(outDir) {
441
275
  const timer = this.logger.time('chart_write');
442
276
  this.logger.info('Starting chart write operation', {
@@ -107,10 +107,10 @@ export function createFlexibleSubchart(scope, id, config) {
107
107
  }
108
108
  export function generateFlexibleSubchartTemplate(name) {
109
109
  return `import { App } from 'cdk8s';
110
- import { Rutter } from 'timonel';
110
+ import { Rutter, helmInclude, helmIf, helmWith, createHelmExpression as helm } from 'timonel';
111
111
 
112
112
  /**
113
- * Creates a new chart with simple Rutter implementation
113
+ * Creates a new chart with type-safe Helm helpers
114
114
  * @returns Rutter instance for Helm chart generation
115
115
  * @since 2.11.0
116
116
  */
@@ -126,45 +126,43 @@ export default function createChart() {
126
126
  scope: app,
127
127
  defaultValues: {
128
128
  appName: '${name}',
129
- image: 'nginx:latest',
129
+ image: {
130
+ repository: 'nginx',
131
+ tag: 'latest'
132
+ },
130
133
  port: 80,
131
134
  replicas: 1,
132
135
  },
133
136
  });
134
137
 
135
- // Add simple deployment
138
+ // Add Deployment with type-safe helpers
136
139
  rutter.addManifest({
137
140
  apiVersion: 'apps/v1',
138
141
  kind: 'Deployment',
139
142
  metadata: {
140
- name: '${name}',
141
- labels: {
142
- 'app.kubernetes.io/name': '${name}'
143
- }
143
+ name: helmInclude('chart.fullname', '.'),
144
+ labels: helmInclude('chart.labels', '.', { pipe: 'nindent 4' })
144
145
  },
145
146
  spec: {
146
- replicas: 1,
147
+ replicas: helmIf('.Values.autoscaling.enabled', '{{ .Values.replicas }}', '1'),
147
148
  selector: {
148
- matchLabels: {
149
- 'app.kubernetes.io/name': '${name}'
150
- }
149
+ matchLabels: helmInclude('chart.selectorLabels', '.', { pipe: 'nindent 6' })
151
150
  },
152
151
  template: {
153
152
  metadata: {
154
- labels: {
155
- 'app.kubernetes.io/name': '${name}'
156
- }
153
+ labels: helmInclude('chart.selectorLabels', '.', { pipe: 'nindent 8' }),
154
+ annotations: helmWith('.Values.podAnnotations', helm('{{- toYaml . | nindent 8 }}'))
157
155
  },
158
156
  spec: {
159
157
  containers: [{
160
158
  name: '${name}',
161
- image: 'nginx:latest',
159
+ image: helm('{{ .Values.image.repository }}:{{ .Values.image.tag }}'),
162
160
  ports: [{
163
- containerPort: 80
161
+ containerPort: helm('{{ .Values.port }}')
164
162
  }],
165
163
  env: [
166
- { name: 'APP_NAME', value: '${name}' },
167
- { name: 'PORT', value: '80' }
164
+ { name: 'APP_NAME', value: helm('{{ .Values.appName }}') },
165
+ { name: 'PORT', value: helm('{{ .Values.port | toString }}') }
168
166
  ]
169
167
  }]
170
168
  }
@@ -172,27 +170,23 @@ export default function createChart() {
172
170
  }
173
171
  }, 'deployment');
174
172
 
175
- // Add Service
173
+ // Add Service with type-safe helpers
176
174
  rutter.addManifest({
177
175
  apiVersion: 'v1',
178
176
  kind: 'Service',
179
177
  metadata: {
180
- name: '${name}',
181
- labels: {
182
- 'app.kubernetes.io/name': '${name}'
183
- }
178
+ name: helmInclude('chart.fullname', '.'),
179
+ labels: helmInclude('chart.labels', '.', { pipe: 'nindent 4' })
184
180
  },
185
181
  spec: {
186
182
  type: 'ClusterIP',
187
183
  ports: [{
188
- port: 80,
189
- targetPort: 80,
184
+ port: helm('{{ .Values.port }}'),
185
+ targetPort: helm('{{ .Values.port }}'),
190
186
  protocol: 'TCP',
191
187
  name: 'http'
192
188
  }],
193
- selector: {
194
- 'app.kubernetes.io/name': '${name}'
195
- }
189
+ selector: helmInclude('chart.selectorLabels', '.', { pipe: 'nindent 4' })
196
190
  }
197
191
  }, 'service');
198
192
 
@@ -1,16 +1,16 @@
1
1
  import { writeFileSync, mkdirSync, existsSync, copyFileSync, readdirSync, rmSync } from 'fs';
2
2
  import { join } from 'path';
3
- import * as jsYaml from 'js-yaml';
3
+ import { parse } from 'yaml';
4
4
  import { App, Chart, ApiObject } from 'cdk8s';
5
5
  import { dumpHelmAwareYaml } from '../utils/helmYamlSerializer.js';
6
6
  import { generateHelpersTemplate } from '../utils/helmHelpers.js';
7
7
  import { createFlexibleSubchart } from './flexible-subchart.js';
8
8
  export function generateUmbrellaChart(name) {
9
9
  return `import { App } from 'cdk8s';
10
- import { Rutter } from 'timonel';
10
+ import { Rutter, helmInclude, helmIf, helmWith, createHelmExpression as helm } from 'timonel';
11
11
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
12
12
  import { join } from 'path';
13
- import * as jsYaml from 'js-yaml';
13
+ import { parse, stringify } from 'yaml';
14
14
  // Import subcharts - add your subchart imports here
15
15
 
16
16
  type SynthMode = 'dependencies' | 'inline';
@@ -42,7 +42,7 @@ function readYamlFile(filePath: string): Record<string, unknown> {
42
42
  if (!existsSync(filePath)) {
43
43
  return {};
44
44
  }
45
- const content = jsYaml.load(readFileSync(filePath, 'utf8'));
45
+ const content = parse(readFileSync(filePath, 'utf8'));
46
46
  return content && typeof content === 'object' ? (content as Record<string, unknown>) : {};
47
47
  }
48
48
 
@@ -147,8 +147,8 @@ export function synth(outDir: string, options?: SynthOptions) {
147
147
  });
148
148
  }
149
149
 
150
- writeFileSync(chartPath, jsYaml.dump(chartDoc));
151
- writeFileSync(valuesPath, jsYaml.dump(valuesDoc));
150
+ writeFileSync(chartPath, stringify(chartDoc));
151
+ writeFileSync(valuesPath, stringify(valuesDoc));
152
152
 
153
153
  app.synth();
154
154
 
@@ -327,7 +327,7 @@ export class UmbrellaChartTemplate extends Chart {
327
327
  const assets = getAssets?.() || [];
328
328
  assets.forEach((asset, assetIndex) => {
329
329
  try {
330
- const manifest = jsYaml.load(asset.yaml);
330
+ const manifest = parse(asset.yaml);
331
331
  if (manifest?.apiVersion && manifest?.kind) {
332
332
  new ApiObject(flexibleSubchart, `${String(manifest.kind).toLowerCase()}-${assetIndex}`, {
333
333
  apiVersion: String(manifest.apiVersion),
@@ -0,0 +1,19 @@
1
+ export interface EnvVarConfig {
2
+ name: string;
3
+ type: 'value' | 'secret';
4
+ scope?: string;
5
+ defaultValue?: string;
6
+ secretName?: string;
7
+ }
8
+ export interface LoadEnvVarsOptions {
9
+ configPath?: string;
10
+ defaultScope?: string;
11
+ fallbackConfig?: EnvVarConfig[];
12
+ }
13
+ export declare function loadEnvVarsConfig(options?: LoadEnvVarsOptions): EnvVarConfig[];
14
+ export declare function generateEnvVars(config: EnvVarConfig[], options?: {
15
+ defaultScope?: string;
16
+ }): Array<Record<string, unknown>>;
17
+ export declare function loadAndGenerateEnvVars(options?: LoadEnvVarsOptions & {
18
+ defaultScope?: string;
19
+ }): Array<Record<string, unknown>>;
@@ -0,0 +1,57 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { parse as parseYaml } from 'yaml';
4
+ import { createHelmExpression } from './helmControlStructures.js';
5
+ export function loadEnvVarsConfig(options = {}) {
6
+ const { configPath, fallbackConfig = [] } = options;
7
+ try {
8
+ if (configPath && existsSync(configPath)) {
9
+ const data = readFileSync(configPath, 'utf-8');
10
+ return configPath.endsWith('.yaml') || configPath.endsWith('.yml')
11
+ ? parseYaml(data)
12
+ : JSON.parse(data);
13
+ }
14
+ const yamlPath = join(process.cwd(), 'env-config.yaml');
15
+ if (existsSync(yamlPath)) {
16
+ const data = readFileSync(yamlPath, 'utf-8');
17
+ return parseYaml(data);
18
+ }
19
+ const jsonPath = join(process.cwd(), 'env-config.json');
20
+ if (existsSync(jsonPath)) {
21
+ const data = readFileSync(jsonPath, 'utf-8');
22
+ return JSON.parse(data);
23
+ }
24
+ }
25
+ catch {
26
+ }
27
+ return fallbackConfig;
28
+ }
29
+ export function generateEnvVars(config, options = {}) {
30
+ const { defaultScope = 'global.env' } = options;
31
+ return config.map((item) => {
32
+ const scope = item.scope || defaultScope;
33
+ if (item.type === 'value') {
34
+ return {
35
+ name: item.name,
36
+ value: createHelmExpression(`{{ .Values.${scope}.${item.name} | default "${item.defaultValue || ''}" }}`),
37
+ };
38
+ }
39
+ else {
40
+ return {
41
+ name: item.name,
42
+ valueFrom: {
43
+ secretKeyRef: {
44
+ name: item.secretName || createHelmExpression('{{ .Values.secretName }}'),
45
+ key: item.name,
46
+ optional: true,
47
+ },
48
+ },
49
+ };
50
+ }
51
+ });
52
+ }
53
+ export function loadAndGenerateEnvVars(options = {}) {
54
+ const config = loadEnvVarsConfig(options);
55
+ const genOptions = options.defaultScope ? { defaultScope: options.defaultScope } : {};
56
+ return generateEnvVars(config, genOptions);
57
+ }
@@ -0,0 +1,3 @@
1
+ import type { HelmConstruct, HelmContent } from './helmControlStructures.js';
2
+ export declare function serializeHelmConstruct(construct: HelmConstruct, indent?: number): string;
3
+ export declare function serializeHelmContent(content: HelmContent, indent?: number): string;
@@ -0,0 +1,61 @@
1
+ import { isHelmConstruct } from './helmControlStructures.js';
2
+ export function serializeHelmConstruct(construct, indent = 0) {
3
+ const trimLeft = construct.options?.trimLeft ?? true;
4
+ const trimRight = construct.options?.trimRight ?? true;
5
+ const openTag = `{{${trimLeft ? '-' : ''} `;
6
+ const closeTag = ` ${trimRight ? '-' : ''}}}`;
7
+ switch (construct.type) {
8
+ case 'if': {
9
+ const data = construct.data;
10
+ let result = `${openTag}if ${data.condition}${closeTag}\n`;
11
+ result += serializeHelmContent(data.then, indent);
12
+ if (data.else !== undefined) {
13
+ result += `\n${openTag}else${closeTag}\n`;
14
+ result += serializeHelmContent(data.else, indent);
15
+ }
16
+ result += `\n${openTag}end${closeTag}`;
17
+ return result;
18
+ }
19
+ default:
20
+ throw new Error(`Unknown Helm construct type: ${construct.type}`);
21
+ }
22
+ }
23
+ export function serializeHelmContent(content, indent = 0) {
24
+ const indentStr = ' '.repeat(indent);
25
+ if (isHelmConstruct(content)) {
26
+ return serializeHelmConstruct(content, indent);
27
+ }
28
+ if (content === null || content === undefined) {
29
+ return '';
30
+ }
31
+ if (typeof content === 'string' || typeof content === 'number' || typeof content === 'boolean') {
32
+ return String(content);
33
+ }
34
+ if (Array.isArray(content)) {
35
+ return content
36
+ .map((item) => {
37
+ if (isHelmConstruct(item)) {
38
+ return serializeHelmConstruct(item, indent);
39
+ }
40
+ return serializeHelmContent(item, indent);
41
+ })
42
+ .join('\n');
43
+ }
44
+ if (typeof content === 'object') {
45
+ const lines = [];
46
+ for (const [key, value] of Object.entries(content)) {
47
+ if (isHelmConstruct(value)) {
48
+ lines.push(`${indentStr}${key}: ${serializeHelmConstruct(value, indent + 1)}`);
49
+ }
50
+ else if (typeof value === 'object' && value !== null) {
51
+ lines.push(`${indentStr}${key}:`);
52
+ lines.push(serializeHelmContent(value, indent + 1));
53
+ }
54
+ else {
55
+ lines.push(`${indentStr}${key}: ${value}`);
56
+ }
57
+ }
58
+ return lines.join('\n');
59
+ }
60
+ return '';
61
+ }
@@ -0,0 +1,33 @@
1
+ export interface HelmConstruct {
2
+ __helmConstruct: true;
3
+ type: 'if' | 'range' | 'with' | 'include' | 'define' | 'var' | 'block' | 'comment' | 'fragment' | 'fieldConditional';
4
+ data: unknown;
5
+ options?: HelmWhitespaceOptions;
6
+ }
7
+ export declare function helmFragment(...contents: HelmContent[]): HelmConstruct;
8
+ export interface HelmExpression {
9
+ __helmExpression: true;
10
+ value: string;
11
+ }
12
+ export declare function createHelmExpression(value: string): HelmExpression;
13
+ export declare function isHelmExpression(value: unknown): value is HelmExpression;
14
+ export declare function helmRange(vars: string, collection: string, content: HelmContent, options?: HelmWhitespaceOptions): HelmConstruct;
15
+ export declare function helmWith(scope: string, content: HelmContent, options?: HelmWhitespaceOptions): HelmConstruct;
16
+ export declare function helmInclude(templateName: string, scope?: string, options?: {
17
+ pipe?: string;
18
+ } & HelmWhitespaceOptions): HelmConstruct;
19
+ export declare function helmDefine(name: string, content: HelmContent, options?: HelmWhitespaceOptions): HelmConstruct;
20
+ export declare function helmVar(name: string, value: string, options?: HelmWhitespaceOptions): HelmConstruct;
21
+ export declare function helmBlock(name: string, content: HelmContent, options?: HelmWhitespaceOptions): HelmConstruct;
22
+ export declare function helmComment(text: string): HelmConstruct;
23
+ export type HelmContent = string | number | boolean | null | undefined | HelmConstruct | HelmExpression | {
24
+ [key: string]: HelmContent;
25
+ } | HelmContent[];
26
+ export interface HelmWhitespaceOptions {
27
+ trimLeft?: boolean;
28
+ trimRight?: boolean;
29
+ inline?: boolean;
30
+ }
31
+ export declare function helmIf(condition: string, thenContent: HelmContent, elseContent?: HelmContent, options?: HelmWhitespaceOptions): HelmConstruct;
32
+ export declare function helmIfSimple(condition: string, thenContent: HelmContent, options?: HelmWhitespaceOptions): HelmConstruct;
33
+ export declare function isHelmConstruct(value: unknown): value is HelmConstruct;