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 +13 -0
- package/README.md +109 -0
- package/dist/cli.js +69 -45
- package/dist/lib/helmChartWriter.js +16 -21
- package/dist/lib/rutter.js +39 -109
- package/dist/lib/templates/basic-chart.js +21 -0
- package/dist/lib/templates/subchart.js +46 -51
- package/dist/lib/templates/umbrella-chart.js +10 -55
- package/dist/lib/umbrellaRutter.js +4 -4
- package/dist/lib/utils/helmYamlSerializer.d.ts +8 -0
- package/dist/lib/utils/helmYamlSerializer.js +88 -0
- package/package.json +7 -8
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
|
|
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
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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(
|
|
175
|
-
console.log(`
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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'),
|
|
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`),
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
}
|
package/dist/lib/rutter.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { ApiObject, App, Chart, Testing } from 'cdk8s';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
144
|
-
|
|
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
|
|
147
|
-
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
155
|
-
if (!metadata
|
|
156
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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(
|
|
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':
|
|
209
|
-
'app.kubernetes.io/name': include
|
|
210
|
-
'app.kubernetes.io/instance':
|
|
211
|
-
'app.kubernetes.io/version':
|
|
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':
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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('
|
|
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.
|
|
135
|
-
apiVersion: networking.k8s.io/v1
|
|
136
|
-
kind: Ingress
|
|
137
|
-
metadata:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
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'),
|
|
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'),
|
|
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': '{{
|
|
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'),
|
|
170
|
-
const helpersTpl =
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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'),
|
|
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'),
|
|
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`),
|
|
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.
|
|
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
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
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",
|