timonel 2.9.1 → 2.9.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 CHANGED
@@ -1,3 +1,9 @@
1
+ ## [2.9.2](https://github.com/KenkoGeek/timonel/compare/v2.9.1...v2.9.2) (2025-09-19)
2
+
3
+ ### Bug Fixes
4
+
5
+ - **helm:** implement Helm-aware YAML serializer ([#112](https://github.com/KenkoGeek/timonel/issues/112)) ([daf4276](https://github.com/KenkoGeek/timonel/commit/daf427618eea73db832682f6b37287a9176bc31e))
6
+
1
7
  ## [2.9.1](https://github.com/KenkoGeek/timonel/compare/v2.9.0...v2.9.1) (2025-09-19)
2
8
 
3
9
  ### Bug Fixes
package/dist/cli.js CHANGED
@@ -13,41 +13,50 @@ function log(msg, silent = false) {
13
13
  console.log(msg);
14
14
  }
15
15
  }
16
- function usageAndExit(msg) {
16
+ function logError(msg, silent = false) {
17
+ if (!silent) {
18
+ console.error(msg);
19
+ }
20
+ }
21
+ function usageAndExit(msg, silent = false) {
17
22
  if (msg) {
18
- console.error(`Error: ${msg}`);
19
- }
20
- console.log([
21
- 'Usage: tl <command> [options]',
22
- '',
23
- 'Commands:',
24
- ' tl init <chart-name> Create new chart',
25
- ' tl synth [outDir] Generate Helm chart',
26
- ' tl validate Validate chart',
27
- ' tl deploy <release> [namespace] Deploy chart',
28
- ' tl templates List available templates',
29
- '',
30
- 'Umbrella Charts:',
31
- ' tl umbrella init <n> Create umbrella chart structure',
32
- ' tl umbrella add <subchart> Add subchart to umbrella',
33
- ' tl umbrella synth [outDir] Generate umbrella chart',
34
- '',
35
- 'Flags:',
36
- ' --dry-run Show what would be done without executing',
37
- ' --silent Suppress output (useful for CI)',
38
- ' --env <environment> Use environment-specific values',
39
- ' --set <key=value> Override values (can be used multiple times)',
40
- ' --help, -h Show this help message',
41
- '',
42
- 'Examples:',
43
- ' tl init my-app',
44
- ' tl synth my-app my-app/dist',
45
- ' tl validate my-app',
46
- ' tl deploy my-app my-release --env prod',
47
- ' tl synth my-app --dry-run --silent',
48
- ' tl synth my-app --set replicas=5 --set image.tag=v2.0.0',
49
- ' tl deploy my-app my-release --set service.port=8080',
50
- ].join('\n'));
23
+ logError(`Error: ${msg}`, silent);
24
+ }
25
+ if (!silent) {
26
+ console.log([
27
+ 'Usage: tl <command> [options]',
28
+ '',
29
+ 'Commands:',
30
+ ' tl init <chart-name> Create new chart',
31
+ ' tl synth [outDir] Generate Helm chart',
32
+ ' tl validate Validate chart',
33
+ ' tl deploy <release> [namespace] Deploy chart',
34
+ ' tl templates List available templates',
35
+ ' tl help Show this help message',
36
+ '',
37
+ 'Umbrella Charts:',
38
+ ' tl umbrella init <n> Create umbrella chart structure',
39
+ ' tl umbrella add <subchart> Add subchart to umbrella',
40
+ ' tl umbrella synth [outDir] Generate umbrella chart',
41
+ '',
42
+ 'Flags:',
43
+ ' --dry-run Show what would be done without executing',
44
+ ' --silent Suppress output (useful for CI)',
45
+ ' --env <environment> Use environment-specific values',
46
+ ' --set <key=value> Override values (can be used multiple times)',
47
+ ' --help, -h Show this help message',
48
+ ' --version, -v Show version information',
49
+ '',
50
+ 'Examples:',
51
+ ' tl init my-app',
52
+ ' tl synth my-app my-app/dist',
53
+ ' tl validate my-app',
54
+ ' tl deploy my-app my-release --env prod',
55
+ ' tl synth --dry-run --silent',
56
+ ' tl synth --set replicas=5 --set image.tag=v2.0.0',
57
+ ' tl deploy my-app my-release --set service.port=8080',
58
+ ].join('\n'));
59
+ }
51
60
  process.exit(msg ? 1 : 0);
52
61
  }
53
62
  async function cmdInit(name, silent = false) {
@@ -165,14 +174,13 @@ async function cmdTemplates(flags) {
165
174
  },
166
175
  ];
167
176
  if (flags?.silent) {
168
- console.log(JSON.stringify(templates));
177
+ console.log(JSON.stringify(templates, null, 2));
169
178
  }
170
179
  else {
171
180
  console.log('Available templates:');
172
181
  templates.forEach((t) => {
173
- console.log(`\n${t.name}`);
174
- console.log(` Description: ${t.description}`);
175
- console.log(` Usage: ${t.usage}`);
182
+ console.log(` ${t.name.padEnd(15)} - ${t.description}`);
183
+ console.log(` ${''.padEnd(15)} Usage: ${t.usage}`);
176
184
  });
177
185
  }
178
186
  }
@@ -388,15 +396,17 @@ function parseFlags(args) {
388
396
  }
389
397
  case '--version':
390
398
  case '-v':
391
- console.log('Timonel v0.1.0');
399
+ if (!flags.silent) {
400
+ console.log('Timonel v0.1.0');
401
+ }
392
402
  process.exit(0);
393
403
  break;
394
404
  case '--help':
395
405
  case '-h':
396
- usageAndExit();
406
+ usageAndExit(undefined, flags.silent);
397
407
  break;
398
408
  default:
399
- usageAndExit(`Unknown flag: ${flag}`);
409
+ usageAndExit(`Unknown flag: ${flag}`, flags.silent);
400
410
  }
401
411
  }
402
412
  return flags;
@@ -421,20 +431,35 @@ async function executeCommand(command, args, flags) {
421
431
  case 'umbrella':
422
432
  await cmdUmbrella(args[0], args.slice(1), flags);
423
433
  break;
434
+ case 'help':
424
435
  case undefined:
425
- usageAndExit('Missing command');
436
+ usageAndExit(undefined, flags.silent);
426
437
  break;
427
438
  default:
428
- usageAndExit(`Unknown command: ${command}`);
439
+ usageAndExit(`Unknown command: ${command}`, flags.silent);
429
440
  }
430
441
  }
431
442
  async function main() {
432
443
  const args = process.argv.slice(2);
444
+ const isSilent = args.includes('--silent');
445
+ if (args.includes('--help') || args.includes('-h')) {
446
+ usageAndExit(undefined, isSilent);
447
+ return;
448
+ }
449
+ if (args.includes('--version') || args.includes('-v')) {
450
+ if (!isSilent) {
451
+ console.log('Timonel v0.1.0');
452
+ }
453
+ process.exit(0);
454
+ }
433
455
  const command = args.shift();
434
456
  const flags = parseFlags(args);
435
457
  await executeCommand(command, args, flags);
436
458
  }
437
459
  main().catch((error) => {
438
- console.error('Error:', error.message);
460
+ const flags = parseFlags(process.argv.slice(2));
461
+ if (!flags.silent) {
462
+ console.error('Error:', error.message);
463
+ }
439
464
  process.exit(1);
440
465
  });
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import YAML from 'yaml';
4
3
  import { SecurityUtils } from './security.js';
4
+ import { dumpHelmAwareYaml } from './utils/helmYamlSerializer.js';
5
5
  export class HelmChartWriter {
6
6
  static write(opts) {
7
7
  const { outDir, meta, defaultValues = {}, envValues = {}, assets, helpersTpl, notesTpl, valuesSchema, } = opts;
@@ -21,7 +21,7 @@ export class HelmChartWriter {
21
21
  fs.mkdirSync(path.join(outDir, 'templates'), { recursive: true });
22
22
  }
23
23
  static writeChartYaml(outDir, meta) {
24
- const chartYaml = YAML.stringify({
24
+ const chartYaml = dumpHelmAwareYaml({
25
25
  apiVersion: 'v2',
26
26
  name: meta.name,
27
27
  version: meta.version,
@@ -39,10 +39,10 @@ export class HelmChartWriter {
39
39
  fs.writeFileSync(path.join(outDir, 'Chart.yaml'), chartYaml);
40
40
  }
41
41
  static writeValuesFiles(outDir, defaultValues, envValues) {
42
- fs.writeFileSync(path.join(outDir, 'values.yaml'), YAML.stringify(defaultValues));
42
+ fs.writeFileSync(path.join(outDir, 'values.yaml'), dumpHelmAwareYaml(defaultValues));
43
43
  for (const [env, values] of Object.entries(envValues)) {
44
44
  const sanitizedEnv = SecurityUtils.sanitizeEnvironmentName(env);
45
- fs.writeFileSync(path.join(outDir, `values-${sanitizedEnv}.yaml`), YAML.stringify(values));
45
+ fs.writeFileSync(path.join(outDir, `values-${sanitizedEnv}.yaml`), dumpHelmAwareYaml(values));
46
46
  }
47
47
  }
48
48
  static writeAssets(outDir, assets) {
@@ -1,8 +1,8 @@
1
1
  import { ApiObject, App, Chart, Testing } from 'cdk8s';
2
- import Handlebars from 'handlebars';
3
- import YAML from 'yaml';
2
+ import * as jsYaml from 'js-yaml';
4
3
  import { include } from './helm.js';
5
4
  import { HelmChartWriter } from './helmChartWriter.js';
5
+ import { dumpHelmAwareYaml } from './utils/helmYamlSerializer.js';
6
6
  import { AWSResources } from './resources/cloud/aws/awsResources.js';
7
7
  import { KarpenterResources } from './resources/cloud/aws/karpenterResources.js';
8
8
  import { generateHelpersTemplate } from './utils/helmHelpers.js';
@@ -56,7 +56,7 @@ export class Rutter {
56
56
  let manifestObject;
57
57
  if (typeof yamlOrObject === 'string') {
58
58
  try {
59
- manifestObject = YAML.parse(yamlOrObject);
59
+ manifestObject = jsYaml.load(yamlOrObject);
60
60
  }
61
61
  catch (error) {
62
62
  throw new Error(`Invalid YAML provided to addManifest(): ${error instanceof Error ? error.message : 'Unknown error'}`);
@@ -102,20 +102,12 @@ export class Rutter {
102
102
  helmCondition = `.Values.${condition}`;
103
103
  }
104
104
  try {
105
- const yamlContent = YAML.stringify(manifestObject, {
105
+ const yamlContent = dumpHelmAwareYaml(manifestObject, {
106
106
  lineWidth: 0,
107
- doubleQuotedAsJSON: false,
108
- simpleKeys: true,
109
- });
110
- const conditionalTemplate = `\\{{- if ${helmCondition} }}
111
- {{{manifestYaml}}}
112
- \\{{- end }}`;
113
- const template = Handlebars.compile(conditionalTemplate, {
114
- noEscape: true,
115
- });
116
- const conditionalYaml = template({
117
- manifestYaml: yamlContent.trim(),
118
107
  });
108
+ const conditionalYaml = `{{- if ${helmCondition} }}
109
+ ${yamlContent.trim()}
110
+ {{- end }}`;
119
111
  const conditionalAsset = {
120
112
  id,
121
113
  yaml: conditionalYaml,
@@ -124,7 +116,7 @@ export class Rutter {
124
116
  this.assets.push(conditionalAsset);
125
117
  }
126
118
  catch (error) {
127
- throw new Error(`Failed to compile Handlebars template for manifest '${id}': ${error instanceof Error ? error.message : 'Unknown error'}`);
119
+ throw new Error(`Failed to generate conditional template for manifest '${id}': ${error instanceof Error ? error.message : 'Unknown error'}`);
128
120
  }
129
121
  return new ApiObject(this.chart, `${id}-placeholder`, {
130
122
  apiVersion: manifestObject['apiVersion'],
@@ -218,7 +210,7 @@ export class Rutter {
218
210
  const synthAssets = [];
219
211
  if (this.props.singleManifestFile) {
220
212
  const combinedYaml = enriched
221
- .map((obj) => this.processHelmTemplates(YAML.stringify(obj).trim()))
213
+ .map((obj) => this.processHelmTemplates(dumpHelmAwareYaml(obj).trim()))
222
214
  .filter(Boolean)
223
215
  .join('\n---\n');
224
216
  const manifestId = this.props.manifestPrefix ?? 'manifests';
@@ -228,7 +220,7 @@ export class Rutter {
228
220
  enriched.forEach((obj, index) => {
229
221
  const apiObjectId = apiObjectIds[index];
230
222
  const manifestId = apiObjectId || `manifest-${index + 1}`;
231
- const yaml = this.processHelmTemplates(YAML.stringify(obj).trim());
223
+ const yaml = this.processHelmTemplates(dumpHelmAwareYaml(obj).trim());
232
224
  if (yaml) {
233
225
  synthAssets.push({ id: manifestId, yaml });
234
226
  }
@@ -1,5 +1,6 @@
1
1
  import { Chart, ApiObject } from 'cdk8s';
2
2
  import { Rutter } from '../rutter.js';
3
+ import { generateHelpersTemplate } from '../utils/helmHelpers.js';
3
4
  export class BasicChart extends Chart {
4
5
  constructor(scope, id, props = {}) {
5
6
  super(scope, id, props);
@@ -24,6 +25,10 @@ export class BasicChart extends Chart {
24
25
  replicas,
25
26
  createNamespace,
26
27
  },
28
+ helpersTpl: generateHelpersTemplate('aws', undefined, {
29
+ includeKubernetes: true,
30
+ includeSprig: true,
31
+ }),
27
32
  });
28
33
  if (createNamespace) {
29
34
  this.rutter.addConditionalManifest({
@@ -41,6 +46,11 @@ export class BasicChart extends Chart {
41
46
  name: `{{ .Values.appName }}`,
42
47
  labels: {
43
48
  app: `{{ .Values.appName }}`,
49
+ 'helm.sh/chart': `{{ .Chart.Name }}-{{ .Chart.Version }}`,
50
+ 'app.kubernetes.io/name': `{{ .Chart.Name }}`,
51
+ 'app.kubernetes.io/instance': `{{ .Release.Name }}`,
52
+ 'app.kubernetes.io/version': `{{ .Chart.AppVersion }}`,
53
+ 'app.kubernetes.io/managed-by': `{{ .Release.Service }}`,
44
54
  },
45
55
  },
46
56
  spec: {
@@ -48,12 +58,16 @@ export class BasicChart extends Chart {
48
58
  selector: {
49
59
  matchLabels: {
50
60
  app: `{{ .Values.appName }}`,
61
+ 'app.kubernetes.io/name': `{{ .Chart.Name }}`,
62
+ 'app.kubernetes.io/instance': `{{ .Release.Name }}`,
51
63
  },
52
64
  },
53
65
  template: {
54
66
  metadata: {
55
67
  labels: {
56
68
  app: `{{ .Values.appName }}`,
69
+ 'app.kubernetes.io/name': `{{ .Chart.Name }}`,
70
+ 'app.kubernetes.io/instance': `{{ .Release.Name }}`,
57
71
  },
58
72
  },
59
73
  spec: {
@@ -79,6 +93,11 @@ export class BasicChart extends Chart {
79
93
  name: `{{ .Values.appName }}`,
80
94
  labels: {
81
95
  app: `{{ .Values.appName }}`,
96
+ 'helm.sh/chart': `{{ .Chart.Name }}-{{ .Chart.Version }}`,
97
+ 'app.kubernetes.io/name': `{{ .Chart.Name }}`,
98
+ 'app.kubernetes.io/instance': `{{ .Release.Name }}`,
99
+ 'app.kubernetes.io/version': `{{ .Chart.AppVersion }}`,
100
+ 'app.kubernetes.io/managed-by': `{{ .Release.Service }}`,
82
101
  },
83
102
  },
84
103
  spec: {
@@ -90,6 +109,8 @@ export class BasicChart extends Chart {
90
109
  ],
91
110
  selector: {
92
111
  app: `{{ .Values.appName }}`,
112
+ 'app.kubernetes.io/name': `{{ .Chart.Name }}`,
113
+ 'app.kubernetes.io/instance': `{{ .Release.Name }}`,
93
114
  },
94
115
  },
95
116
  }, 'service');
@@ -1,5 +1,6 @@
1
1
  import { App, Chart } from 'cdk8s';
2
2
  import { Rutter } from '../rutter.js';
3
+ import { generateHelpersTemplate } from '../utils/helmHelpers.js';
3
4
  export class Subchart extends Chart {
4
5
  constructor(scope, id, props = {}) {
5
6
  super(scope, id, props);
@@ -39,6 +40,10 @@ export class Subchart extends Chart {
39
40
  tls: [],
40
41
  },
41
42
  },
43
+ helpersTpl: generateHelpersTemplate('aws', undefined, {
44
+ includeKubernetes: true,
45
+ includeSprig: true,
46
+ }),
42
47
  });
43
48
  this.rutter.addManifest({
44
49
  apiVersion: 'v1',
@@ -131,54 +136,47 @@ export class Subchart extends Chart {
131
136
  },
132
137
  },
133
138
  }, 'service');
134
- this.rutter.addTemplateManifest(`{{- if .Values.enableIngress }}
135
- apiVersion: networking.k8s.io/v1
136
- kind: Ingress
137
- metadata:
138
- name: {{ include "chart.fullname" . }}
139
- labels:
140
- {{- include "chart.labels" . | nindent 4 }}
141
- {{- with .Values.ingress.annotations }}
142
- annotations:
143
- {{- toYaml . | nindent 4 }}
144
- {{- end }}
145
- spec:
146
- {{- if and .Values.ingress.className (not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class")) }}
147
- ingressClassName: {{ .Values.ingress.className }}
148
- {{- end }}
149
- {{- if .Values.ingress.tls }}
150
- tls:
151
- {{- range .Values.ingress.tls }}
152
- - hosts:
153
- {{- range .hosts }}
154
- - {{ . | quote }}
155
- {{- end }}
156
- secretName: {{ .secretName }}
157
- {{- end }}
158
- {{- end }}
159
- rules:
160
- {{- range .Values.ingress.hosts }}
161
- - host: {{ .host | quote }}
162
- http:
163
- paths:
164
- {{- range .paths }}
165
- - path: {{ .path }}
166
- {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
167
- pathType: {{ .pathType }}
168
- {{- end }}
169
- backend:
170
- {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
171
- service:
172
- name: {{ include "chart.fullname" $ }}
173
- port:
174
- number: {{ $.Values.port | int }}
175
- {{- else }}
176
- serviceName: {{ include "chart.fullname" $ }}
177
- servicePort: {{ $.Values.port | int }}
178
- {{- end }}
179
- {{- end }}
180
- {{- end }}
181
- {{- end }}`, 'ingress');
139
+ this.rutter.addConditionalManifest({
140
+ apiVersion: 'networking.k8s.io/v1',
141
+ kind: 'Ingress',
142
+ metadata: {
143
+ name: `{{ .Release.Name }}-{{ .Values.appName }}`,
144
+ labels: {
145
+ app: `{{ .Values.appName }}`,
146
+ 'helm.sh/chart': `{{ .Chart.Name }}-{{ .Chart.Version }}`,
147
+ 'app.kubernetes.io/name': `{{ .Chart.Name }}`,
148
+ 'app.kubernetes.io/instance': `{{ .Release.Name }}`,
149
+ 'app.kubernetes.io/version': `{{ .Chart.AppVersion }}`,
150
+ 'app.kubernetes.io/managed-by': `{{ .Release.Service }}`,
151
+ },
152
+ annotations: `{{ toYaml .Values.ingress.annotations | nindent 4 }}`,
153
+ },
154
+ spec: {
155
+ ingressClassName: `{{ .Values.ingress.className }}`,
156
+ tls: `{{ toYaml .Values.ingress.tls | nindent 4 }}`,
157
+ rules: [
158
+ {
159
+ host: `{{ .Values.ingressHost }}`,
160
+ http: {
161
+ paths: [
162
+ {
163
+ path: '/',
164
+ pathType: 'Prefix',
165
+ backend: {
166
+ service: {
167
+ name: `{{ .Release.Name }}-{{ .Values.appName }}`,
168
+ port: {
169
+ number: `{{ .Values.port }}`,
170
+ },
171
+ },
172
+ },
173
+ },
174
+ ],
175
+ },
176
+ },
177
+ ],
178
+ },
179
+ }, 'enableIngress', 'ingress');
182
180
  }
183
181
  get rutterInstance() {
184
182
  return this.rutter;
@@ -1,7 +1,8 @@
1
1
  import { writeFileSync, mkdirSync, existsSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { Chart, ApiObject } from 'cdk8s';
4
- import YAML from 'yaml';
4
+ import { dumpHelmAwareYaml } from '../utils/helmYamlSerializer.js';
5
+ import { generateHelpersTemplate } from '../utils/helmHelpers.js';
5
6
  export function generateUmbrellaChart(name) {
6
7
  return `import { App } from 'cdk8s';
7
8
  import { UmbrellaChart } from 'timonel';
@@ -110,7 +111,7 @@ export class UmbrellaChartTemplate extends Chart {
110
111
  repository: 'file://./charts/' + subchart.name,
111
112
  })) || [],
112
113
  };
113
- writeFileSync(join(outputDir, 'Chart.yaml'), YAML.stringify(chartYaml));
114
+ writeFileSync(join(outputDir, 'Chart.yaml'), dumpHelmAwareYaml(chartYaml));
114
115
  const valuesYaml = {
115
116
  global: {
116
117
  namespace: this.config.name,
@@ -122,7 +123,7 @@ export class UmbrellaChartTemplate extends Chart {
122
123
  },
123
124
  ]) || []),
124
125
  };
125
- writeFileSync(join(outputDir, 'values.yaml'), YAML.stringify(valuesYaml));
126
+ writeFileSync(join(outputDir, 'values.yaml'), dumpHelmAwareYaml(valuesYaml));
126
127
  const chartsDir = join(outputDir, 'charts');
127
128
  if (!existsSync(chartsDir)) {
128
129
  mkdirSync(chartsDir, { recursive: true });
@@ -159,64 +160,18 @@ export class UmbrellaChartTemplate extends Chart {
159
160
  metadata: {
160
161
  name: '{{ .Values.global.namespace | default .Release.Name }}',
161
162
  labels: {
162
- 'app.kubernetes.io/name': '{{ include "chart.name" . }}',
163
+ 'app.kubernetes.io/name': '{{ .Chart.Name }}',
163
164
  'app.kubernetes.io/instance': '{{ .Release.Name }}',
164
165
  'app.kubernetes.io/version': '{{ .Chart.AppVersion }}',
165
166
  'app.kubernetes.io/managed-by': '{{ .Release.Service }}',
166
167
  },
167
168
  },
168
169
  };
169
- writeFileSync(join(templatesDir, 'namespace.yaml'), YAML.stringify(namespaceYaml));
170
- const helpersTpl = `{{/*
171
- Expand the name of the chart.
172
- */}}
173
- {{- define "chart.name" -}}
174
- {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
175
- {{- end }}
176
-
177
- {{/*
178
- Create a default fully qualified app name.
179
- */}}
180
- {{- define "chart.fullname" -}}
181
- {{- if .Values.fullnameOverride }}
182
- {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
183
- {{- else }}
184
- {{- $name := default .Chart.Name .Values.nameOverride }}
185
- {{- if contains $name .Release.Name }}
186
- {{- .Release.Name | trunc 63 | trimSuffix "-" }}
187
- {{- else }}
188
- {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
189
- {{- end }}
190
- {{- end }}
191
- {{- end }}
192
-
193
- {{/*
194
- Create chart name and version as used by the chart label.
195
- */}}
196
- {{- define "chart.chart" -}}
197
- {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
198
- {{- end }}
199
-
200
- {{/*
201
- Common labels
202
- */}}
203
- {{- define "chart.labels" -}}
204
- helm.sh/chart: {{ include "chart.chart" . }}
205
- {{ include "chart.selectorLabels" . }}
206
- {{- if .Chart.AppVersion }}
207
- app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
208
- {{- end }}
209
- app.kubernetes.io/managed-by: {{ .Release.Service }}
210
- {{- end }}
211
-
212
- {{/*
213
- Selector labels
214
- */}}
215
- {{- define "chart.selectorLabels" -}}
216
- app.kubernetes.io/name: {{ include "chart.name" . }}
217
- app.kubernetes.io/instance: {{ .Release.Name }}
218
- {{- end }}
219
- `;
170
+ writeFileSync(join(templatesDir, 'namespace.yaml'), dumpHelmAwareYaml(namespaceYaml));
171
+ const helpersTpl = generateHelpersTemplate('aws', undefined, {
172
+ includeKubernetes: true,
173
+ includeSprig: true,
174
+ });
220
175
  writeFileSync(join(templatesDir, '_helpers.tpl'), helpersTpl);
221
176
  }
222
177
  }
@@ -1,7 +1,7 @@
1
1
  import { writeFileSync, mkdirSync } from 'fs';
2
2
  import { join } from 'path';
3
- import YAML from 'yaml';
4
3
  import { SecurityUtils } from './security.js';
4
+ import { dumpHelmAwareYaml } from './utils/helmYamlSerializer.js';
5
5
  export class UmbrellaRutter {
6
6
  constructor(props) {
7
7
  this.validateMetadata(props.meta);
@@ -54,7 +54,7 @@ export class UmbrellaRutter {
54
54
  };
55
55
  }),
56
56
  };
57
- writeFileSync(join(outDir, 'Chart.yaml'), YAML.stringify(chartYaml));
57
+ writeFileSync(join(outDir, 'Chart.yaml'), dumpHelmAwareYaml(chartYaml));
58
58
  }
59
59
  writeParentValues(outDir) {
60
60
  const values = {
@@ -68,12 +68,12 @@ export class UmbrellaRutter {
68
68
  };
69
69
  }
70
70
  }
71
- writeFileSync(join(outDir, 'values.yaml'), YAML.stringify(values));
71
+ writeFileSync(join(outDir, 'values.yaml'), dumpHelmAwareYaml(values));
72
72
  if (this.props.envValues) {
73
73
  for (const [env, envVals] of Object.entries(this.props.envValues)) {
74
74
  const sanitizedEnv = SecurityUtils.sanitizeEnvironmentName(env);
75
75
  const envValues = this.deepMerge(values, envVals);
76
- writeFileSync(join(outDir, `values-${sanitizedEnv}.yaml`), YAML.stringify(envValues));
76
+ writeFileSync(join(outDir, `values-${sanitizedEnv}.yaml`), dumpHelmAwareYaml(envValues));
77
77
  }
78
78
  }
79
79
  }
@@ -0,0 +1,8 @@
1
+ import * as jsYaml from 'js-yaml';
2
+ export declare function dumpHelmAwareYaml(obj: unknown, options?: jsYaml.DumpOptions): string;
3
+ export declare function stringify(obj: unknown, options?: {
4
+ lineWidth?: number;
5
+ doubleQuotedAsJSON?: boolean;
6
+ simpleKeys?: boolean;
7
+ }): string;
8
+ export declare function validateHelmExpressions(yaml: string): boolean;
@@ -0,0 +1,88 @@
1
+ import * as jsYaml from 'js-yaml';
2
+ const HELM_EXPRESSION_PATTERNS = [
3
+ /\{\{\s*[^}]+\s*\}\}/g,
4
+ /\{\{\s*include\s+[^}]+\s*\}\}/g,
5
+ /\{\{\s*if\s+[^}]+\s*\}\}/g,
6
+ /\{\{\s*else\s*\}\}/g,
7
+ /\{\{\s*end\s*\}\}/g,
8
+ /\{\{\s*range\s+[^}]+\s*\}\}/g,
9
+ /\{\{\s*with\s+[^}]+\s*\}\}/g,
10
+ /\{\{\s*\$[^}]+\s*\}\}/g,
11
+ /\{\{\s*[^}]*\|\s*[^}]+\s*\}\}/g,
12
+ ];
13
+ function createHelmExpression(value) {
14
+ return {
15
+ __helmExpression: true,
16
+ value,
17
+ };
18
+ }
19
+ function isHelmExpression(value) {
20
+ return (typeof value === 'object' &&
21
+ value !== null &&
22
+ '__helmExpression' in value &&
23
+ value.__helmExpression === true);
24
+ }
25
+ function containsHelmExpression(str) {
26
+ return HELM_EXPRESSION_PATTERNS.some((pattern) => pattern.test(str));
27
+ }
28
+ function preprocessHelmExpressions(obj) {
29
+ if (typeof obj === 'string') {
30
+ if (containsHelmExpression(obj)) {
31
+ return createHelmExpression(obj);
32
+ }
33
+ return obj;
34
+ }
35
+ if (Array.isArray(obj)) {
36
+ return obj.map(preprocessHelmExpressions);
37
+ }
38
+ if (obj && typeof obj === 'object') {
39
+ const result = {};
40
+ for (const [key, value] of Object.entries(obj)) {
41
+ Object.defineProperty(result, key, {
42
+ value: preprocessHelmExpressions(value),
43
+ writable: true,
44
+ enumerable: true,
45
+ configurable: true,
46
+ });
47
+ }
48
+ return result;
49
+ }
50
+ return obj;
51
+ }
52
+ function helmAwareReplacer(key, value) {
53
+ if (isHelmExpression(value)) {
54
+ return value.value;
55
+ }
56
+ return value;
57
+ }
58
+ export function dumpHelmAwareYaml(obj, options = {}) {
59
+ const preprocessed = preprocessHelmExpressions(obj);
60
+ const helmOptions = {
61
+ forceQuotes: false,
62
+ lineWidth: options.lineWidth ?? 0,
63
+ flowLevel: options.flowLevel ?? -1,
64
+ replacer: helmAwareReplacer,
65
+ ...options,
66
+ };
67
+ let yamlOutput = jsYaml.dump(preprocessed, helmOptions);
68
+ yamlOutput = postProcessHelmExpressions(yamlOutput);
69
+ return yamlOutput;
70
+ }
71
+ function postProcessHelmExpressions(yaml) {
72
+ let processed = yaml;
73
+ processed = processed.replace(/'(\{\{[^}]*\}\})'/g, '$1');
74
+ processed = processed.replace(/"(\{\{[^}]*\}\})"/g, '$1');
75
+ processed = processed.replace(/\\(\{\{[^}]*\}\})/g, '$1');
76
+ return processed;
77
+ }
78
+ export function stringify(obj, options = {}) {
79
+ const jsYamlOptions = {
80
+ lineWidth: options.lineWidth ?? 0,
81
+ quotingType: options.doubleQuotedAsJSON ? '"' : "'",
82
+ };
83
+ return dumpHelmAwareYaml(obj, jsYamlOptions);
84
+ }
85
+ export function validateHelmExpressions(yaml) {
86
+ const quotedHelmExpressions = [/'(\{\{[^}]*\}\})'/g, /"(\{\{[^}]*\}\})"/g];
87
+ return !quotedHelmExpressions.some((pattern) => pattern.test(yaml));
88
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "timonel",
3
3
  "type": "module",
4
- "version": "2.9.1",
4
+ "version": "2.9.2",
5
5
  "description": "Timonel: programmatic Helm chart generator using cdk8s (TypeScript)",
6
6
  "bin": {
7
7
  "timonel": "dist/cli.js",
@@ -85,6 +85,7 @@
85
85
  "cdk8s-plus-33": "^2.3.5",
86
86
  "constructs": "^10.4.2",
87
87
  "handlebars": "^4.7.8",
88
+ "js-yaml": "^4.1.0",
88
89
  "timonel": "^2.9.0",
89
90
  "ts-node": "^10.9.2",
90
91
  "yaml": "^2.8.1"
@@ -96,6 +97,7 @@
96
97
  "@semantic-release/changelog": "^6.0.3",
97
98
  "@semantic-release/exec": "^7.1.0",
98
99
  "@semantic-release/git": "^10.0.1",
100
+ "@types/js-yaml": "^4.0.9",
99
101
  "@types/node": "^24.3.0",
100
102
  "@typescript-eslint/eslint-plugin": "^8.42.0",
101
103
  "@typescript-eslint/parser": "^8.42.0",