timonel 2.9.0 → 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,16 @@
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
+
7
+ ## [2.9.1](https://github.com/KenkoGeek/timonel/compare/v2.9.0...v2.9.1) (2025-09-19)
8
+
9
+ ### Bug Fixes
10
+
11
+ - **deps:** merge conflict solved [skip ci] ([3422464](https://github.com/KenkoGeek/timonel/commit/3422464ddee4a1408019ba4617b6f8e6f6c8fd12))
12
+ - **deps:** upgrade timonel version [skip ci] ([63901ef](https://github.com/KenkoGeek/timonel/commit/63901ef4277a1ebf702fc55337ed1eb7f6b226ea))
13
+
1
14
  # [2.9.0](https://github.com/KenkoGeek/timonel/compare/v2.8.3...v2.9.0) (2025-09-17)
2
15
 
3
16
  ### Features
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # Timonel
2
+
3
+ [![License: MIT][license-badge]][license-url]
4
+ [![npm version][npm-badge]][npm-url]
5
+ [![Security][security-badge]][security-url]
6
+ [![CodeQL][codeql-badge]][codeql-url]
7
+ [![CI][ci-badge]][ci-url]
8
+ [![pnpm][pnpm-badge]][pnpm-url]
9
+ [![Node.js][node-badge]][node-url]
10
+ [![TypeScript][ts-badge]][ts-url]
11
+ [![Maintained by KenkoGeek][maintained-badge]][maintained-url]
12
+
13
+ Timonel (Spanish for "helmsman") is a TypeScript library to programmatically generate Helm charts
14
+ using cdk8s. Define Kubernetes resources with classes and synthesize a full Helm chart with
15
+ `Chart.yaml`, `values.yaml`, per‑environment values files, and `templates/`.
16
+
17
+ ## ✨ Key Features
18
+
19
+ - **Type-safe API** with strict TypeScript and cdk8s constructs.
20
+ - **Flexible resource creation** with built-in methods and `addManifest()` for custom resources.
21
+ - **Multi-environment support** with automatic values files generation.
22
+ - **Umbrella Charts** for managing multiple subcharts as a single unit.
23
+ - **AWS integrations**:
24
+ - **AWS**: EBS/EFS StorageClass, ALB Ingress, IRSA ServiceAccount.
25
+ - **Security-first approach** with NetworkPolicies and best practices.
26
+ - **Minimal CLI** (`tl`) for scaffolding and chart generation.
27
+
28
+ ## 🚀 Quick Start
29
+
30
+ ```bash
31
+ # Install Timonel globally
32
+ npm install -g timonel
33
+
34
+ # Create your first chart
35
+ tl init my-app
36
+
37
+ # Generate Helm chart
38
+ tl synth charts/my-app charts/my-app-dist
39
+
40
+ # Use with Helm
41
+ helm install my-app charts/my-app-dist
42
+ ```
43
+
44
+ ## 📚 Documentation
45
+
46
+ ## 🔧 Troubleshooting
47
+
48
+ ### CDK8s Module Not Found Error
49
+
50
+ If you get `Error: Cannot find module 'cdk8s'` when running `tl umbrella synth`:
51
+
52
+ **Problem**: Timonel is installed globally, but your project needs CDK8s dependencies locally.
53
+
54
+ **Solution**: Create a `package.json` in your project directory:
55
+
56
+ ```json
57
+ {
58
+ "name": "my-timonel-project",
59
+ "version": "1.0.0",
60
+ "type": "module",
61
+ "dependencies": {
62
+ "cdk8s": "^2.70.11",
63
+ "cdk8s-plus-33": "^2.3.5",
64
+ "constructs": "^10.4.2",
65
+ "timonel": "^2.9.0"
66
+ },
67
+ "devDependencies": {
68
+ "@types/node": "^24.3.0",
69
+ "typescript": "^5.9.2"
70
+ }
71
+ }
72
+ ```
73
+
74
+ Then run:
75
+
76
+ ```bash
77
+ npm install
78
+ tl umbrella synth # Now it works!
79
+ ```
80
+
81
+ ## 🤝 Contributing
82
+
83
+ See our [Contributing Guide](https://github.com/KenkoGeek/timonel/wiki/Contributing) for development
84
+ setup and guidelines.
85
+
86
+ ## 📄 License
87
+
88
+ MIT
89
+
90
+ <!-- Badges -->
91
+
92
+ [license-badge]: https://img.shields.io/badge/License-MIT-yellow.svg
93
+ [license-url]: https://opensource.org/licenses/MIT
94
+ [npm-badge]: https://img.shields.io/npm/v/timonel.svg
95
+ [npm-url]: https://www.npmjs.com/package/timonel
96
+ [security-badge]: https://img.shields.io/badge/Security-Policy-2ea44f?logo=security&logoColor=fff
97
+ [security-url]: SECURITY.md
98
+ [pnpm-badge]: https://img.shields.io/badge/pm-pnpm-ffd95a?logo=pnpm&logoColor=fff&labelColor=24292e
99
+ [pnpm-url]: https://pnpm.io/
100
+ [node-badge]: https://img.shields.io/badge/node-%3E%3D20-339933?logo=node.js&logoColor=fff
101
+ [node-url]: https://nodejs.org/
102
+ [ts-badge]: https://img.shields.io/badge/TypeScript-5.x-3178C6?logo=typescript&logoColor=fff
103
+ [ts-url]: https://www.typescriptlang.org/
104
+ [maintained-badge]: https://img.shields.io/badge/maintained%20by-KenkoGeek-6C78AF?style=flat
105
+ [maintained-url]: https://github.com/kenkogeek/
106
+ [ci-badge]: https://github.com/KenkoGeek/timonel/actions/workflows/test.yaml/badge.svg?branch=main
107
+ [ci-url]: https://github.com/KenkoGeek/timonel/actions/workflows/teast.yaml
108
+ [codeql-badge]: https://github.com/KenkoGeek/timonel/actions/workflows/codeql.yaml/badge.svg
109
+ [codeql-url]: https://github.com/KenkoGeek/timonel/actions/workflows/codeql.yaml
package/dist/cli.js CHANGED
@@ -13,42 +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
- ' --version, -v Show version information',
41
- ' --help, -h Show this help message',
42
- '',
43
- 'Examples:',
44
- ' tl init my-app',
45
- ' tl synth my-app my-app/dist',
46
- ' tl validate my-app',
47
- ' tl deploy my-app my-release --env prod',
48
- ' tl synth my-app --dry-run --silent',
49
- ' tl synth my-app --set replicas=5 --set image.tag=v2.0.0',
50
- ' tl deploy my-app my-release --set service.port=8080',
51
- ].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
+ }
52
60
  process.exit(msg ? 1 : 0);
53
61
  }
54
62
  async function cmdInit(name, silent = false) {
@@ -166,14 +174,13 @@ async function cmdTemplates(flags) {
166
174
  },
167
175
  ];
168
176
  if (flags?.silent) {
169
- console.log(JSON.stringify(templates));
177
+ console.log(JSON.stringify(templates, null, 2));
170
178
  }
171
179
  else {
172
180
  console.log('Available templates:');
173
181
  templates.forEach((t) => {
174
- console.log(`\n${t.name}`);
175
- console.log(` Description: ${t.description}`);
176
- console.log(` Usage: ${t.usage}`);
182
+ console.log(` ${t.name.padEnd(15)} - ${t.description}`);
183
+ console.log(` ${''.padEnd(15)} Usage: ${t.usage}`);
177
184
  });
178
185
  }
179
186
  }
@@ -389,15 +396,17 @@ function parseFlags(args) {
389
396
  }
390
397
  case '--version':
391
398
  case '-v':
392
- console.log('Timonel v0.1.0');
399
+ if (!flags.silent) {
400
+ console.log('Timonel v0.1.0');
401
+ }
393
402
  process.exit(0);
394
403
  break;
395
404
  case '--help':
396
405
  case '-h':
397
- usageAndExit();
406
+ usageAndExit(undefined, flags.silent);
398
407
  break;
399
408
  default:
400
- usageAndExit(`Unknown flag: ${flag}`);
409
+ usageAndExit(`Unknown flag: ${flag}`, flags.silent);
401
410
  }
402
411
  }
403
412
  return flags;
@@ -422,20 +431,35 @@ async function executeCommand(command, args, flags) {
422
431
  case 'umbrella':
423
432
  await cmdUmbrella(args[0], args.slice(1), flags);
424
433
  break;
434
+ case 'help':
425
435
  case undefined:
426
- usageAndExit('Missing command');
436
+ usageAndExit(undefined, flags.silent);
427
437
  break;
428
438
  default:
429
- usageAndExit(`Unknown command: ${command}`);
439
+ usageAndExit(`Unknown command: ${command}`, flags.silent);
430
440
  }
431
441
  }
432
442
  async function main() {
433
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
+ }
434
455
  const command = args.shift();
435
456
  const flags = parseFlags(args);
436
457
  await executeCommand(command, args, flags);
437
458
  }
438
459
  main().catch((error) => {
439
- console.error('Error:', error.message);
460
+ const flags = parseFlags(process.argv.slice(2));
461
+ if (!flags.silent) {
462
+ console.error('Error:', error.message);
463
+ }
440
464
  process.exit(1);
441
465
  });
@@ -1,31 +1,27 @@
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;
8
+ console.log(`📝 Writing Helm chart: ${meta.name} v${meta.version} to ${outDir}`);
8
9
  const validatedOutDir = SecurityUtils.validatePath(outDir, process.cwd());
9
- console.log('📝 Validated outDir:', validatedOutDir);
10
10
  this.createDirectories(validatedOutDir);
11
- console.log('📝 Directories created');
12
11
  this.writeChartYaml(validatedOutDir, meta);
13
- console.log('📝 Chart.yaml written');
14
12
  this.writeValuesFiles(validatedOutDir, defaultValues, envValues);
15
- console.log('📝 Values files written');
16
13
  this.writeAssets(validatedOutDir, assets);
17
- console.log('📝 Assets written');
18
14
  this.writeHelpers(validatedOutDir, helpersTpl);
19
15
  this.writeNotes(validatedOutDir, notesTpl);
20
16
  this.writeSchema(validatedOutDir, valuesSchema);
21
17
  this.writeHelmIgnore(validatedOutDir);
22
- console.log('📝 HelmChartWriter.write completed');
18
+ console.log(`✅ Helm chart written successfully to ${validatedOutDir}`);
23
19
  }
24
20
  static createDirectories(outDir) {
25
21
  fs.mkdirSync(path.join(outDir, 'templates'), { recursive: true });
26
22
  }
27
23
  static writeChartYaml(outDir, meta) {
28
- const chartYaml = YAML.stringify({
24
+ const chartYaml = dumpHelmAwareYaml({
29
25
  apiVersion: 'v2',
30
26
  name: meta.name,
31
27
  version: meta.version,
@@ -43,10 +39,10 @@ export class HelmChartWriter {
43
39
  fs.writeFileSync(path.join(outDir, 'Chart.yaml'), chartYaml);
44
40
  }
45
41
  static writeValuesFiles(outDir, defaultValues, envValues) {
46
- fs.writeFileSync(path.join(outDir, 'values.yaml'), YAML.stringify(defaultValues));
42
+ fs.writeFileSync(path.join(outDir, 'values.yaml'), dumpHelmAwareYaml(defaultValues));
47
43
  for (const [env, values] of Object.entries(envValues)) {
48
44
  const sanitizedEnv = SecurityUtils.sanitizeEnvironmentName(env);
49
- fs.writeFileSync(path.join(outDir, `values-${sanitizedEnv}.yaml`), YAML.stringify(values));
45
+ fs.writeFileSync(path.join(outDir, `values-${sanitizedEnv}.yaml`), dumpHelmAwareYaml(values));
50
46
  }
51
47
  }
52
48
  static writeAssets(outDir, assets) {
@@ -112,25 +108,24 @@ function splitDocs(yamlStr) {
112
108
  .filter((p) => p.length);
113
109
  }
114
110
  function writeAssets(outDir, assets) {
115
- console.log('🔍 writeAssets called with', assets.length, 'assets');
116
111
  for (const asset of assets) {
117
112
  const sanitizedId = asset.id.replace(/[^a-zA-Z0-9-_]/g, '');
118
113
  if (sanitizedId !== asset.id) {
114
+ console.error(`❌ Invalid asset ID detected: ${SecurityUtils.sanitizeLogMessage(asset.id)}`);
119
115
  throw new Error(`Invalid asset ID: ${SecurityUtils.sanitizeLogMessage(asset.id)}`);
120
116
  }
121
117
  const targetDir = getTargetDirectory(asset.target);
122
- console.log(`🔍 Processing asset: ${asset.id} -> ${sanitizedId}, singleFile: ${asset.singleFile}, target: ${asset.target}`);
123
- if (asset.id === 'ingress') {
124
- console.log('🔍 Ingress asset YAML preview:', asset.yaml.substring(0, 200));
125
- if (asset.yaml.includes('number:')) {
126
- console.log('🔍 Ingress contains number field:', asset.yaml.split('\n').filter((line) => line.includes('number:')));
118
+ try {
119
+ if (asset.singleFile) {
120
+ writeSingleAssetFile(outDir, targetDir, sanitizedId, asset.yaml);
121
+ }
122
+ else {
123
+ writeMultipleAssetFiles(outDir, targetDir, sanitizedId, asset.yaml);
127
124
  }
128
125
  }
129
- if (asset.singleFile) {
130
- writeSingleAssetFile(outDir, targetDir, sanitizedId, asset.yaml);
131
- }
132
- else {
133
- writeMultipleAssetFiles(outDir, targetDir, sanitizedId, asset.yaml);
126
+ catch (error) {
127
+ console.error(`❌ Failed to write asset ${asset.id}:`, error);
128
+ throw error;
134
129
  }
135
130
  }
136
131
  }
@@ -1,14 +1,15 @@
1
1
  import { ApiObject, App, Chart, Testing } from 'cdk8s';
2
- import Handlebars from 'handlebars';
3
- import YAML from 'yaml';
4
- import { helm, include } from './helm.js';
2
+ import * as jsYaml from 'js-yaml';
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';
9
9
  export class Rutter {
10
10
  constructor(props) {
11
11
  this.assets = [];
12
+ console.log(`🚀 Initializing Rutter chart: ${props.meta.name} v${props.meta.version}`);
12
13
  this.defaultValues = props.defaultValues ?? {};
13
14
  this.envValues = props.envValues ?? {};
14
15
  this.meta = props.meta;
@@ -55,7 +56,7 @@ export class Rutter {
55
56
  let manifestObject;
56
57
  if (typeof yamlOrObject === 'string') {
57
58
  try {
58
- manifestObject = YAML.parse(yamlOrObject);
59
+ manifestObject = jsYaml.load(yamlOrObject);
59
60
  }
60
61
  catch (error) {
61
62
  throw new Error(`Invalid YAML provided to addManifest(): ${error instanceof Error ? error.message : 'Unknown error'}`);
@@ -101,20 +102,12 @@ export class Rutter {
101
102
  helmCondition = `.Values.${condition}`;
102
103
  }
103
104
  try {
104
- const yamlContent = YAML.stringify(manifestObject, {
105
+ const yamlContent = dumpHelmAwareYaml(manifestObject, {
105
106
  lineWidth: 0,
106
- doubleQuotedAsJSON: false,
107
- simpleKeys: true,
108
- });
109
- const conditionalTemplate = `\\{{- if ${helmCondition} }}
110
- {{{manifestYaml}}}
111
- \\{{- end }}`;
112
- const template = Handlebars.compile(conditionalTemplate, {
113
- noEscape: true,
114
- });
115
- const conditionalYaml = template({
116
- manifestYaml: yamlContent.trim(),
117
107
  });
108
+ const conditionalYaml = `{{- if ${helmCondition} }}
109
+ ${yamlContent.trim()}
110
+ {{- end }}`;
118
111
  const conditionalAsset = {
119
112
  id,
120
113
  yaml: conditionalYaml,
@@ -123,7 +116,7 @@ export class Rutter {
123
116
  this.assets.push(conditionalAsset);
124
117
  }
125
118
  catch (error) {
126
- 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'}`);
127
120
  }
128
121
  return new ApiObject(this.chart, `${id}-placeholder`, {
129
122
  apiVersion: manifestObject['apiVersion'],
@@ -140,33 +133,26 @@ export class Rutter {
140
133
  });
141
134
  }
142
135
  validateManifestStructure(manifest) {
143
- if (!manifest['apiVersion'] || typeof manifest['apiVersion'] !== 'string') {
144
- throw new Error('Kubernetes manifest must have a valid apiVersion');
136
+ if (!manifest.apiVersion) {
137
+ console.error(' Manifest validation failed: Missing apiVersion');
138
+ throw new Error('Manifest must have an apiVersion');
145
139
  }
146
- if (!manifest['kind'] || typeof manifest['kind'] !== 'string') {
147
- throw new Error('Kubernetes manifest must have a valid kind');
140
+ if (!manifest.kind) {
141
+ console.error(' Manifest validation failed: Missing kind');
142
+ throw new Error('Manifest must have a kind');
148
143
  }
149
- if (!manifest['metadata'] ||
150
- typeof manifest['metadata'] !== 'object' ||
151
- manifest['metadata'] === null) {
152
- throw new Error('Kubernetes manifest must have valid metadata');
144
+ if (!manifest.metadata) {
145
+ console.error(' Manifest validation failed: Missing metadata');
146
+ throw new Error('Manifest must have metadata');
153
147
  }
154
- const metadata = manifest['metadata'];
155
- if (!metadata['name'] || typeof metadata['name'] !== 'string') {
156
- throw new Error('Kubernetes manifest metadata must have a valid name');
148
+ const metadata = manifest.metadata;
149
+ if (!metadata.name) {
150
+ console.error(' Manifest validation failed: Missing metadata.name');
151
+ throw new Error('Manifest metadata must have a name');
157
152
  }
158
- const kind = manifest['kind'];
159
- if (kind === 'CustomResourceDefinition') {
160
- if (!manifest['spec'] || typeof manifest['spec'] !== 'object' || manifest['spec'] === null) {
161
- throw new Error('CustomResourceDefinition must have a valid spec');
162
- }
163
- const spec = manifest['spec'];
164
- if (!spec['group'] || typeof spec['group'] !== 'string') {
165
- throw new Error('CustomResourceDefinition spec must have a valid group');
166
- }
167
- if (!Array.isArray(spec['versions']) || spec['versions'].length === 0) {
168
- throw new Error('CustomResourceDefinition must have at least one version in spec.versions');
169
- }
153
+ if (typeof metadata.name !== 'string') {
154
+ console.error('❌ Manifest validation failed: metadata.name must be a string');
155
+ throw new Error('Manifest metadata.name must be a string');
170
156
  }
171
157
  }
172
158
  getMeta() {
@@ -182,7 +168,7 @@ export class Rutter {
182
168
  return [...this.assets];
183
169
  }
184
170
  toSynthArray() {
185
- console.log('🔍 toSynthArray called');
171
+ console.log(`📦 Synthesizing chart assets for: ${this.meta.name}`);
186
172
  const apiObjectIds = [];
187
173
  for (const child of this.chart.node.children) {
188
174
  if (child instanceof ApiObject && !child.node.id.endsWith('-placeholder')) {
@@ -198,6 +184,7 @@ export class Rutter {
198
184
  }
199
185
  return true;
200
186
  });
187
+ console.log(`📋 Found ${manifestObjs.length} manifest objects to process`);
201
188
  const enriched = manifestObjs.map((obj) => {
202
189
  if (obj && typeof obj === 'object') {
203
190
  const o = obj;
@@ -205,12 +192,12 @@ export class Rutter {
205
192
  o.metadata.labels = o.metadata.labels ?? {};
206
193
  const labels = o.metadata.labels;
207
194
  const defaults = {
208
- 'helm.sh/chart': '{{ .Chart.Name }}-{{ .Chart.Version }}',
209
- 'app.kubernetes.io/name': include(Rutter.HELPER_NAME),
210
- 'app.kubernetes.io/instance': helm.releaseName,
211
- 'app.kubernetes.io/version': helm.chartVersion,
195
+ 'helm.sh/chart': `{{ .Chart.Name }}-{{ .Chart.Version }}`,
196
+ 'app.kubernetes.io/name': `{{ ${include} "${Rutter.HELPER_NAME}" . }}`,
197
+ 'app.kubernetes.io/instance': '{{ .Release.Name }}',
198
+ 'app.kubernetes.io/version': '{{ .Chart.Version }}',
212
199
  'app.kubernetes.io/managed-by': '{{ .Release.Service }}',
213
- 'app.kubernetes.io/part-of': helm.chartName,
200
+ 'app.kubernetes.io/part-of': '{{ .Chart.Name }}',
214
201
  };
215
202
  for (const [key, value] of Object.entries(defaults)) {
216
203
  if (!(key in labels)) {
@@ -221,10 +208,9 @@ export class Rutter {
221
208
  return obj;
222
209
  });
223
210
  const synthAssets = [];
224
- console.log('🔍 singleManifestFile:', this.props.singleManifestFile);
225
211
  if (this.props.singleManifestFile) {
226
212
  const combinedYaml = enriched
227
- .map((obj) => this.processHelmTemplates(YAML.stringify(obj).trim()))
213
+ .map((obj) => this.processHelmTemplates(dumpHelmAwareYaml(obj).trim()))
228
214
  .filter(Boolean)
229
215
  .join('\n---\n');
230
216
  const manifestId = this.props.manifestPrefix ?? 'manifests';
@@ -234,8 +220,7 @@ export class Rutter {
234
220
  enriched.forEach((obj, index) => {
235
221
  const apiObjectId = apiObjectIds[index];
236
222
  const manifestId = apiObjectId || `manifest-${index + 1}`;
237
- console.log(`🔍 Processing asset ${manifestId} (index ${index})`);
238
- const yaml = this.processHelmTemplates(YAML.stringify(obj).trim());
223
+ const yaml = this.processHelmTemplates(dumpHelmAwareYaml(obj).trim());
239
224
  if (yaml) {
240
225
  synthAssets.push({ id: manifestId, yaml });
241
226
  }
@@ -247,15 +232,8 @@ export class Rutter {
247
232
  return synthAssets;
248
233
  }
249
234
  processHelmTemplates(yaml) {
250
- console.log('🔍 processHelmTemplates called with YAML length:', yaml.length);
251
- if (yaml.includes('{{ .Values.port }}')) {
252
- console.log('🎯 Found YAML with port template:');
253
- const portIndex = yaml.indexOf('{{ .Values.port }}');
254
- console.log(yaml.substring(Math.max(0, portIndex - 50), portIndex + 100));
255
- }
256
235
  let processed = this.fixCharacterMappingIssues(yaml);
257
236
  processed = this.applyHelmTemplateReplacements(processed);
258
- console.log('✅ processHelmTemplates completed');
259
237
  return processed;
260
238
  }
261
239
  fixCharacterMappingIssues(yaml) {
@@ -317,38 +295,11 @@ export class Rutter {
317
295
  return { nextIndex: j };
318
296
  }
319
297
  applyHelmTemplateReplacements(processed) {
320
- processed = this.fixIncludeStatements(processed);
321
- processed = this.fixFunctionCalls(processed);
322
- processed = this.fixChartReferences(processed);
323
- processed = this.removeHelmExpressionQuotes(processed);
324
- if (processed.includes('number:')) {
325
- console.log('DEBUG: Before fixConditionalBlocks:', processed.split('\n').filter((line) => line.includes('number:')));
326
- }
327
298
  processed = this.fixConditionalBlocks(processed);
328
- if (processed.includes('number:')) {
329
- console.log('DEBUG: After fixConditionalBlocks:', processed.split('\n').filter((line) => line.includes('number:')));
330
- }
331
- if (processed.includes('number:')) {
332
- console.log('DEBUG: Before fixPipeExpressions:', processed.split('\n').filter((line) => line.includes('number:')));
333
- }
334
299
  processed = this.fixPipeExpressions(processed);
335
- if (processed.includes('number:')) {
336
- console.log('DEBUG: After fixPipeExpressions:', processed.split('\n').filter((line) => line.includes('number:')));
337
- }
338
- if (processed.includes('number:')) {
339
- console.log('DEBUG: Before fixNestedQuotes:', processed.split('\n').filter((line) => line.includes('number:')));
340
- }
341
300
  processed = this.fixNestedQuotes(processed);
342
- if (processed.includes('number:')) {
343
- console.log('DEBUG: After fixNestedQuotes:', processed.split('\n').filter((line) => line.includes('number:')));
344
- }
345
- if (processed.includes('number:')) {
346
- console.log('DEBUG: Before cleanupArtifacts:', processed.split('\n').filter((line) => line.includes('number:')));
347
- }
348
301
  processed = this.cleanupArtifacts(processed);
349
- if (processed.includes('number:')) {
350
- console.log('DEBUG: After cleanupArtifacts:', processed.split('\n').filter((line) => line.includes('number:')));
351
- }
302
+ processed = this.removeHelmExpressionQuotes(processed);
352
303
  return processed;
353
304
  }
354
305
  fixIncludeStatements(processed) {
@@ -364,10 +315,6 @@ export class Rutter {
364
315
  }
365
316
  removeHelmExpressionQuotes(processed) {
366
317
  const HELM_EXPRESSION_REPLACEMENT = '$1$2: {{$3}}';
367
- console.log('🔧 removeHelmExpressionQuotes called');
368
- if (processed.includes('number:')) {
369
- console.log('DEBUG: removeHelmExpressionQuotes input contains number:', processed.split('\n').filter((line) => line.includes('number:')));
370
- }
371
318
  processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\|\s*(?:int|float|bool|number)[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
372
319
  processed = processed.replace(/^(\s*)(port):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
373
320
  processed = processed.replace(/^(\s*)(port\.number):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
@@ -375,23 +322,7 @@ export class Rutter {
375
322
  processed = processed.replace(/^(\s*)(backend\.service\.port\.number):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
376
323
  processed = processed.replace(/^(\s*)(spec\.port):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
377
324
  processed = processed.replace(/^(\s*)([^:\s]*\.number):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
378
- const numberMatches = processed.match(/^(\s*)(number):\s*"\{\{([^}]*)\}\}"/gm);
379
- if (numberMatches) {
380
- console.log('DEBUG: Found number pattern matches:', numberMatches);
381
- }
382
- else {
383
- const anyNumberFields = processed.match(/number:\s*"[^"]*"/gm);
384
- if (anyNumberFields) {
385
- console.log('DEBUG: Found number fields but regex did not match:', anyNumberFields);
386
- const lines = processed.split('\n');
387
- const numberLines = lines.filter((line) => line.includes('number:'));
388
- console.log('DEBUG: Number field lines:', numberLines);
389
- }
390
- }
391
325
  processed = processed.replace(/^(\s*)(number):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
392
- if (processed.includes('number:')) {
393
- console.log('DEBUG: removeHelmExpressionQuotes after number replacement:', processed.split('\n').filter((line) => line.includes('number:')));
394
- }
395
326
  processed = processed.replace(/^(\s*)(replicas):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
396
327
  processed = processed.replace(/^(\s*)(targetPort):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
397
328
  processed = processed.replace(/^(\s*)(nodePort):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
@@ -468,8 +399,7 @@ export class Rutter {
468
399
  return processed;
469
400
  }
470
401
  write(outDir) {
471
- console.log('🚀 Rutter.write called with outDir:', outDir);
472
- console.log('📊 Assets count:', this.assets.length);
402
+ console.log(`✍️ Writing Helm chart '${this.meta.name}' to: ${outDir}`);
473
403
  let helpersContent;
474
404
  if (this.props.helpersTpl) {
475
405
  if (typeof this.props.helpersTpl === 'string') {
@@ -490,7 +420,7 @@ ${helper.template}
490
420
  helpersContent = generateHelpersTemplate(this.props.cloudProvider);
491
421
  }
492
422
  const synthAssets = this.toSynthArray();
493
- console.log('📦 Generated synth assets:', synthAssets.length);
423
+ console.log(`📄 Generated ${synthAssets.length} assets for chart`);
494
424
  HelmChartWriter.write({
495
425
  outDir,
496
426
  meta: this.meta,
@@ -499,7 +429,7 @@ ${helper.template}
499
429
  assets: synthAssets,
500
430
  helpersTpl: helpersContent,
501
431
  });
502
- console.log('✅ Rutter.write completed');
432
+ console.log(`✅ Helm chart '${this.meta.name}' written successfully`);
503
433
  }
504
434
  }
505
435
  Rutter.HELPER_NAME = 'chart.name';
@@ -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,63 +136,53 @@ 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;
185
183
  }
186
184
  writeHelmChart(outDir) {
187
- console.log('🔧 Subchart.writeHelmChart called with outDir:', outDir);
188
- console.log('🔧 Rutter instance exists:', !!this.rutter);
189
185
  this.rutter.write(outDir);
190
- console.log('🔧 Subchart.writeHelmChart completed');
191
186
  }
192
187
  }
193
188
  export function generateSubchart(props = {}, outDir = 'dist') {
@@ -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.0",
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",
@@ -37,13 +37,9 @@
37
37
  "deps:update": "pnpm update --latest",
38
38
  "md:lint": "markdownlint \"**/*.md\"",
39
39
  "md:fix": "markdownlint --fix \"**/*.md\"",
40
- "_comment_tests": "ALL TEST SCRIPTS DISABLED TEMPORARILY",
41
- "_test": "pnpm test:unit && pnpm test:integration",
42
- "_test:unit": "vitest run --config vitest.config.ts",
43
- "_test:integration": "vitest run --config vitest.integration.config.ts",
44
- "_test:watch": "vitest",
45
- "_test:coverage": "vitest run --coverage",
46
- "_test:basic-integration": "vitest run tests/integration/basic-chart-generation.int.spec.ts --config vitest.integration.config.ts",
40
+ "test:integration": "vitest run --config vitest.config.ts",
41
+ "test:watch": "vitest",
42
+ "test:coverage": "vitest run --coverage",
47
43
  "ci:check": "pnpm typecheck && pnpm lint && pnpm format:check && pnpm build",
48
44
  "release": "semantic-release",
49
45
  "release:dry": "semantic-release --dry-run",
@@ -89,6 +85,8 @@
89
85
  "cdk8s-plus-33": "^2.3.5",
90
86
  "constructs": "^10.4.2",
91
87
  "handlebars": "^4.7.8",
88
+ "js-yaml": "^4.1.0",
89
+ "timonel": "^2.9.0",
92
90
  "ts-node": "^10.9.2",
93
91
  "yaml": "^2.8.1"
94
92
  },
@@ -99,6 +97,7 @@
99
97
  "@semantic-release/changelog": "^6.0.3",
100
98
  "@semantic-release/exec": "^7.1.0",
101
99
  "@semantic-release/git": "^10.0.1",
100
+ "@types/js-yaml": "^4.0.9",
102
101
  "@types/node": "^24.3.0",
103
102
  "@typescript-eslint/eslint-plugin": "^8.42.0",
104
103
  "@typescript-eslint/parser": "^8.42.0",