timonel 2.9.1 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,9 @@
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';
6
+ import { createFlexibleSubchart } from './flexible-subchart.js';
5
7
  export function generateUmbrellaChart(name) {
6
8
  return `import { App } from 'cdk8s';
7
9
  import { UmbrellaChart } from 'timonel';
@@ -91,7 +93,87 @@ export class UmbrellaChartTemplate extends Chart {
91
93
  },
92
94
  });
93
95
  }
94
- _addSubchart(_subchart, _index) {
96
+ _addSubchart(subchart, index) {
97
+ try {
98
+ let _subchartInstance;
99
+ const { name: _name, version: _version, description: _description, ...subchartProps } = subchart;
100
+ const flexibleSubchart = createFlexibleSubchart(this, `${subchart.name}-${index}`, {
101
+ name: subchart.name,
102
+ version: subchart.version || '1.0.0',
103
+ description: subchart.description || `${subchart.name} subchart`,
104
+ ...subchartProps,
105
+ });
106
+ if (typeof subchart.chart === 'function') {
107
+ try {
108
+ const subchartInstance = subchart.chart(this, `${subchart.name}-${index}`);
109
+ if (subchartInstance.node.children) {
110
+ subchartInstance.node.children.forEach((child) => {
111
+ if (child instanceof Chart) {
112
+ flexibleSubchart.addConstruct(child);
113
+ }
114
+ });
115
+ }
116
+ }
117
+ catch {
118
+ const subchartInstance = subchart.chart();
119
+ if (subchartInstance.node.children) {
120
+ subchartInstance.node.children.forEach((child) => {
121
+ if (child instanceof Chart) {
122
+ flexibleSubchart.addConstruct(child);
123
+ }
124
+ });
125
+ }
126
+ }
127
+ }
128
+ else if (subchart.chart && typeof subchart.chart === 'object') {
129
+ if (subchart.chart.constructor && subchart.chart.constructor.name !== 'Object') {
130
+ flexibleSubchart.addConstruct(subchart.chart);
131
+ }
132
+ else {
133
+ flexibleSubchart.addManifest(subchart.chart);
134
+ }
135
+ }
136
+ console.log(`Added flexible subchart: ${subchart.name}`);
137
+ }
138
+ catch (_error) {
139
+ console.warn(`Failed to add subchart ${subchart.name}:`, _error);
140
+ const _fallbackSubchart = createFlexibleSubchart(this, `${subchart.name}-fallback-${index}`, {
141
+ name: subchart.name,
142
+ version: subchart.version || '1.0.0',
143
+ description: `${subchart.name} subchart (fallback)`,
144
+ });
145
+ console.log(`Created fallback subchart: ${subchart.name}`);
146
+ }
147
+ }
148
+ _createBasicSubchart(subchartName, subchartDir) {
149
+ const subchartYaml = {
150
+ apiVersion: 'v2',
151
+ name: subchartName,
152
+ description: `${subchartName} subchart`,
153
+ type: 'application',
154
+ version: '1.0.0',
155
+ appVersion: '1.0.0',
156
+ };
157
+ writeFileSync(join(subchartDir, 'Chart.yaml'), dumpHelmAwareYaml(subchartYaml));
158
+ const subchartValues = {
159
+ enabled: true,
160
+ replicas: 1,
161
+ image: {
162
+ repository: 'nginx',
163
+ tag: 'latest',
164
+ },
165
+ };
166
+ writeFileSync(join(subchartDir, 'values.yaml'), dumpHelmAwareYaml(subchartValues));
167
+ const templatesDir = join(subchartDir, 'templates');
168
+ if (!existsSync(templatesDir)) {
169
+ mkdirSync(templatesDir, { recursive: true });
170
+ }
171
+ const helpersTpl = generateHelpersTemplate('aws', undefined, {
172
+ includeKubernetes: true,
173
+ includeSprig: true,
174
+ });
175
+ writeFileSync(join(templatesDir, '_helpers.tpl'), helpersTpl);
176
+ console.log(`Created basic Helm chart structure for subchart: ${subchartName}`);
95
177
  }
96
178
  writeHelmChart(outputDir) {
97
179
  if (!existsSync(outputDir)) {
@@ -110,7 +192,7 @@ export class UmbrellaChartTemplate extends Chart {
110
192
  repository: 'file://./charts/' + subchart.name,
111
193
  })) || [],
112
194
  };
113
- writeFileSync(join(outputDir, 'Chart.yaml'), YAML.stringify(chartYaml));
195
+ writeFileSync(join(outputDir, 'Chart.yaml'), dumpHelmAwareYaml(chartYaml));
114
196
  const valuesYaml = {
115
197
  global: {
116
198
  namespace: this.config.name,
@@ -122,7 +204,7 @@ export class UmbrellaChartTemplate extends Chart {
122
204
  },
123
205
  ]) || []),
124
206
  };
125
- writeFileSync(join(outputDir, 'values.yaml'), YAML.stringify(valuesYaml));
207
+ writeFileSync(join(outputDir, 'values.yaml'), dumpHelmAwareYaml(valuesYaml));
126
208
  const chartsDir = join(outputDir, 'charts');
127
209
  if (!existsSync(chartsDir)) {
128
210
  mkdirSync(chartsDir, { recursive: true });
@@ -132,21 +214,50 @@ export class UmbrellaChartTemplate extends Chart {
132
214
  if (!existsSync(subchartDir)) {
133
215
  mkdirSync(subchartDir, { recursive: true });
134
216
  }
135
- let subchartInstance = null;
217
+ const { name: _name, version: _version, description: _description, ...subchartProps } = subchart;
218
+ const flexibleSubchart = createFlexibleSubchart(this, `${subchart.name}-chart`, {
219
+ name: subchart.name,
220
+ version: subchart.version || '1.0.0',
221
+ description: subchart.description || `${subchart.name} subchart`,
222
+ ...subchartProps,
223
+ });
136
224
  if (typeof subchart.chart === 'function') {
137
225
  try {
138
- subchartInstance = subchart.chart();
226
+ const chartInstance = subchart.chart(this, `${subchart.name}-chart`);
227
+ if (chartInstance && typeof chartInstance.writeHelmChart === 'function') {
228
+ chartInstance.writeHelmChart(subchartDir);
229
+ }
230
+ else {
231
+ flexibleSubchart.writeHelmChart(subchartDir);
232
+ }
139
233
  }
140
- catch (error) {
141
- console.warn(`Failed to create subchart ${subchart.name}:`, error);
142
- return;
234
+ catch {
235
+ try {
236
+ const chartInstance = subchart.chart();
237
+ if (chartInstance && typeof chartInstance.writeHelmChart === 'function') {
238
+ chartInstance.writeHelmChart(subchartDir);
239
+ }
240
+ else {
241
+ flexibleSubchart.writeHelmChart(subchartDir);
242
+ }
243
+ }
244
+ catch (fallbackError) {
245
+ console.warn(`Failed to create subchart ${subchart.name}:`, fallbackError);
246
+ flexibleSubchart.writeHelmChart(subchartDir);
247
+ }
143
248
  }
144
249
  }
145
- else {
146
- subchartInstance = subchart.chart;
250
+ else if (subchart.chart && typeof subchart.chart === 'object') {
251
+ if (subchart.chart.constructor && subchart.chart.constructor.name !== 'Object') {
252
+ flexibleSubchart.addConstruct(subchart.chart);
253
+ }
254
+ else {
255
+ flexibleSubchart.addManifest(subchart.chart);
256
+ }
257
+ flexibleSubchart.writeHelmChart(subchartDir);
147
258
  }
148
- if (subchartInstance && typeof subchartInstance.writeHelmChart === 'function') {
149
- subchartInstance.writeHelmChart(subchartDir);
259
+ else {
260
+ flexibleSubchart.writeHelmChart(subchartDir);
150
261
  }
151
262
  });
152
263
  const templatesDir = join(outputDir, 'templates');
@@ -159,64 +270,18 @@ export class UmbrellaChartTemplate extends Chart {
159
270
  metadata: {
160
271
  name: '{{ .Values.global.namespace | default .Release.Name }}',
161
272
  labels: {
162
- 'app.kubernetes.io/name': '{{ include "chart.name" . }}',
273
+ 'app.kubernetes.io/name': '{{ .Chart.Name }}',
163
274
  'app.kubernetes.io/instance': '{{ .Release.Name }}',
164
275
  'app.kubernetes.io/version': '{{ .Chart.AppVersion }}',
165
276
  'app.kubernetes.io/managed-by': '{{ .Release.Service }}',
166
277
  },
167
278
  },
168
279
  };
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
- `;
280
+ writeFileSync(join(templatesDir, 'namespace.yaml'), dumpHelmAwareYaml(namespaceYaml));
281
+ const helpersTpl = generateHelpersTemplate('aws', undefined, {
282
+ includeKubernetes: true,
283
+ includeSprig: true,
284
+ });
220
285
  writeFileSync(join(templatesDir, '_helpers.tpl'), helpersTpl);
221
286
  }
222
287
  }
@@ -3,6 +3,9 @@ export interface ChartProps {
3
3
  name: string;
4
4
  version: string;
5
5
  description: string;
6
+ namespace?: string;
7
+ labels?: Record<string, string>;
8
+ annotations?: Record<string, string>;
6
9
  services?: Array<{
7
10
  name: string;
8
11
  port: number;
@@ -10,8 +13,12 @@ export interface ChartProps {
10
13
  }>;
11
14
  subcharts?: Array<{
12
15
  name: string;
13
- chart: Chart | (() => Chart);
16
+ chart: Chart | ((scope: Chart, id: string) => Chart) | unknown;
17
+ version?: string;
18
+ enabled?: boolean;
19
+ [key: string]: unknown;
14
20
  }>;
21
+ [key: string]: unknown;
15
22
  }
16
23
  export interface SubchartProps {
17
24
  name: string;
@@ -1,7 +1,7 @@
1
1
  import { writeFileSync, mkdirSync } from 'fs';
2
2
  import { join } from 'path';
3
- import YAML from 'yaml';
4
3
  import { SecurityUtils } from './security.js';
4
+ import { dumpHelmAwareYaml } from './utils/helmYamlSerializer.js';
5
5
  export class UmbrellaRutter {
6
6
  constructor(props) {
7
7
  this.validateMetadata(props.meta);
@@ -54,7 +54,7 @@ export class UmbrellaRutter {
54
54
  };
55
55
  }),
56
56
  };
57
- writeFileSync(join(outDir, 'Chart.yaml'), YAML.stringify(chartYaml));
57
+ writeFileSync(join(outDir, 'Chart.yaml'), dumpHelmAwareYaml(chartYaml));
58
58
  }
59
59
  writeParentValues(outDir) {
60
60
  const values = {
@@ -68,12 +68,12 @@ export class UmbrellaRutter {
68
68
  };
69
69
  }
70
70
  }
71
- writeFileSync(join(outDir, 'values.yaml'), YAML.stringify(values));
71
+ writeFileSync(join(outDir, 'values.yaml'), dumpHelmAwareYaml(values));
72
72
  if (this.props.envValues) {
73
73
  for (const [env, envVals] of Object.entries(this.props.envValues)) {
74
74
  const sanitizedEnv = SecurityUtils.sanitizeEnvironmentName(env);
75
75
  const envValues = this.deepMerge(values, envVals);
76
- writeFileSync(join(outDir, `values-${sanitizedEnv}.yaml`), YAML.stringify(envValues));
76
+ writeFileSync(join(outDir, `values-${sanitizedEnv}.yaml`), dumpHelmAwareYaml(envValues));
77
77
  }
78
78
  }
79
79
  }
@@ -0,0 +1,8 @@
1
+ import * as jsYaml from 'js-yaml';
2
+ export declare function dumpHelmAwareYaml(obj: unknown, options?: jsYaml.DumpOptions): string;
3
+ export declare function stringify(obj: unknown, options?: {
4
+ lineWidth?: number;
5
+ doubleQuotedAsJSON?: boolean;
6
+ simpleKeys?: boolean;
7
+ }): string;
8
+ export declare function validateHelmExpressions(yaml: string): boolean;
@@ -0,0 +1,88 @@
1
+ import * as jsYaml from 'js-yaml';
2
+ const HELM_EXPRESSION_PATTERNS = [
3
+ /\{\{\s*[^}]+\s*\}\}/g,
4
+ /\{\{\s*include\s+[^}]+\s*\}\}/g,
5
+ /\{\{\s*if\s+[^}]+\s*\}\}/g,
6
+ /\{\{\s*else\s*\}\}/g,
7
+ /\{\{\s*end\s*\}\}/g,
8
+ /\{\{\s*range\s+[^}]+\s*\}\}/g,
9
+ /\{\{\s*with\s+[^}]+\s*\}\}/g,
10
+ /\{\{\s*\$[^}]+\s*\}\}/g,
11
+ /\{\{\s*[^}]*\|\s*[^}]+\s*\}\}/g,
12
+ ];
13
+ function createHelmExpression(value) {
14
+ return {
15
+ __helmExpression: true,
16
+ value,
17
+ };
18
+ }
19
+ function isHelmExpression(value) {
20
+ return (typeof value === 'object' &&
21
+ value !== null &&
22
+ '__helmExpression' in value &&
23
+ value.__helmExpression === true);
24
+ }
25
+ function containsHelmExpression(str) {
26
+ return HELM_EXPRESSION_PATTERNS.some((pattern) => pattern.test(str));
27
+ }
28
+ function preprocessHelmExpressions(obj) {
29
+ if (typeof obj === 'string') {
30
+ if (containsHelmExpression(obj)) {
31
+ return createHelmExpression(obj);
32
+ }
33
+ return obj;
34
+ }
35
+ if (Array.isArray(obj)) {
36
+ return obj.map(preprocessHelmExpressions);
37
+ }
38
+ if (obj && typeof obj === 'object') {
39
+ const result = {};
40
+ for (const [key, value] of Object.entries(obj)) {
41
+ Object.defineProperty(result, key, {
42
+ value: preprocessHelmExpressions(value),
43
+ writable: true,
44
+ enumerable: true,
45
+ configurable: true,
46
+ });
47
+ }
48
+ return result;
49
+ }
50
+ return obj;
51
+ }
52
+ function helmAwareReplacer(key, value) {
53
+ if (isHelmExpression(value)) {
54
+ return value.value;
55
+ }
56
+ return value;
57
+ }
58
+ export function dumpHelmAwareYaml(obj, options = {}) {
59
+ const preprocessed = preprocessHelmExpressions(obj);
60
+ const helmOptions = {
61
+ forceQuotes: false,
62
+ lineWidth: options.lineWidth ?? 0,
63
+ flowLevel: options.flowLevel ?? -1,
64
+ replacer: helmAwareReplacer,
65
+ ...options,
66
+ };
67
+ let yamlOutput = jsYaml.dump(preprocessed, helmOptions);
68
+ yamlOutput = postProcessHelmExpressions(yamlOutput);
69
+ return yamlOutput;
70
+ }
71
+ function postProcessHelmExpressions(yaml) {
72
+ let processed = yaml;
73
+ processed = processed.replace(/'(\{\{[^}]*\}\})'/g, '$1');
74
+ processed = processed.replace(/"(\{\{[^}]*\}\})"/g, '$1');
75
+ processed = processed.replace(/\\(\{\{[^}]*\}\})/g, '$1');
76
+ return processed;
77
+ }
78
+ export function stringify(obj, options = {}) {
79
+ const jsYamlOptions = {
80
+ lineWidth: options.lineWidth ?? 0,
81
+ quotingType: options.doubleQuotedAsJSON ? '"' : "'",
82
+ };
83
+ return dumpHelmAwareYaml(obj, jsYamlOptions);
84
+ }
85
+ export function validateHelmExpressions(yaml) {
86
+ const quotedHelmExpressions = [/'(\{\{[^}]*\}\})'/g, /"(\{\{[^}]*\}\})"/g];
87
+ return !quotedHelmExpressions.some((pattern) => pattern.test(yaml));
88
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "timonel",
3
3
  "type": "module",
4
- "version": "2.9.1",
4
+ "version": "2.10.0",
5
5
  "description": "Timonel: programmatic Helm chart generator using cdk8s (TypeScript)",
6
6
  "bin": {
7
7
  "timonel": "dist/cli.js",
@@ -81,27 +81,29 @@
81
81
  ]
82
82
  },
83
83
  "dependencies": {
84
- "cdk8s": "^2.70.11",
85
- "cdk8s-plus-33": "^2.3.5",
84
+ "cdk8s": "^2.70.15",
85
+ "cdk8s-plus-33": "^2.3.6",
86
86
  "constructs": "^10.4.2",
87
87
  "handlebars": "^4.7.8",
88
- "timonel": "^2.9.0",
88
+ "js-yaml": "^4.1.0",
89
+ "timonel": "^2.9.2",
89
90
  "ts-node": "^10.9.2",
90
91
  "yaml": "^2.8.1"
91
92
  },
92
93
  "devDependencies": {
93
94
  "@commitlint/cli": "^19.8.1",
94
95
  "@commitlint/config-conventional": "^19.8.1",
95
- "@eslint/js": "^9.35.0",
96
+ "@eslint/js": "^9.36.0",
96
97
  "@semantic-release/changelog": "^6.0.3",
97
98
  "@semantic-release/exec": "^7.1.0",
98
99
  "@semantic-release/git": "^10.0.1",
99
- "@types/node": "^24.3.0",
100
- "@typescript-eslint/eslint-plugin": "^8.42.0",
101
- "@typescript-eslint/parser": "^8.42.0",
102
- "@vitest/coverage-v8": "^2.1.9",
100
+ "@types/js-yaml": "^4.0.9",
101
+ "@types/node": "^24.5.2",
102
+ "@typescript-eslint/eslint-plugin": "^8.44.0",
103
+ "@typescript-eslint/parser": "^8.44.0",
104
+ "@vitest/coverage-v8": "^3.2.4",
103
105
  "conventional-changelog-cli": "^5.0.0",
104
- "eslint": "^9.35.0",
106
+ "eslint": "^9.36.0",
105
107
  "eslint-config-prettier": "^10.1.8",
106
108
  "eslint-import-resolver-typescript": "^4.4.4",
107
109
  "eslint-plugin-import": "^2.32.0",
@@ -113,10 +115,10 @@
113
115
  "markdownlint": "^0.38.0",
114
116
  "markdownlint-cli": "^0.45.0",
115
117
  "prettier": "^3.6.2",
116
- "semantic-release": "^24.2.7",
118
+ "semantic-release": "^24.2.9",
117
119
  "tsx": "^4.20.5",
118
120
  "typescript": "^5.9.2",
119
- "vitest": "^2.1.9"
121
+ "vitest": "^3.2.4"
120
122
  },
121
123
  "files": [
122
124
  "dist",