timonel 2.8.0 → 2.8.3
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 +12 -0
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +319 -191
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -6
- package/dist/lib/helm.d.ts +0 -472
- package/dist/lib/helm.js +0 -481
- package/dist/lib/helmChartWriter.d.ts +0 -178
- package/dist/lib/helmChartWriter.js +0 -180
- package/dist/lib/resources/baseResourceProvider.d.ts +0 -46
- package/dist/lib/resources/baseResourceProvider.js +1 -47
- package/dist/lib/resources/cloud/aws/awsResources.d.ts +0 -144
- package/dist/lib/resources/cloud/aws/awsResources.js +1 -163
- package/dist/lib/resources/cloud/aws/karpenterResources.d.ts +0 -132
- package/dist/lib/resources/cloud/aws/karpenterResources.js +0 -75
- package/dist/lib/rutter.d.ts +0 -324
- package/dist/lib/rutter.js +8 -352
- package/dist/lib/security.d.ts +0 -119
- package/dist/lib/security.js +4 -160
- package/dist/lib/umbrella.d.ts +0 -24
- package/dist/lib/umbrella.js +0 -24
- package/dist/lib/umbrellaRutter.d.ts +0 -71
- package/dist/lib/umbrellaRutter.js +2 -82
- package/dist/lib/utils/helmHelpers.d.ts +0 -39
- package/dist/lib/utils/helmHelpers.js +0 -32
- package/package.json +1 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/helm.d.ts.map +0 -1
- package/dist/lib/helm.js.map +0 -1
- package/dist/lib/helmChartWriter.d.ts.map +0 -1
- package/dist/lib/helmChartWriter.js.map +0 -1
- package/dist/lib/resources/baseResourceProvider.d.ts.map +0 -1
- package/dist/lib/resources/baseResourceProvider.js.map +0 -1
- package/dist/lib/resources/cloud/aws/awsResources.d.ts.map +0 -1
- package/dist/lib/resources/cloud/aws/awsResources.js.map +0 -1
- package/dist/lib/resources/cloud/aws/karpenterResources.d.ts.map +0 -1
- package/dist/lib/resources/cloud/aws/karpenterResources.js.map +0 -1
- package/dist/lib/rutter.d.ts.map +0 -1
- package/dist/lib/rutter.js.map +0 -1
- package/dist/lib/security.d.ts.map +0 -1
- package/dist/lib/security.js.map +0 -1
- package/dist/lib/umbrella.d.ts.map +0 -1
- package/dist/lib/umbrella.js.map +0 -1
- package/dist/lib/umbrellaRutter.d.ts.map +0 -1
- package/dist/lib/umbrellaRutter.js.map +0 -1
- package/dist/lib/utils/helmHelpers.d.ts.map +0 -1
- package/dist/lib/utils/helmHelpers.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import { spawnSync, execSync } from 'child_process';
|
|
3
|
+
import { mkdirSync, existsSync, writeFileSync, rmSync, unlinkSync } from 'fs';
|
|
4
|
+
import { join, dirname, isAbsolute, resolve } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
3
6
|
import * as fs from 'fs';
|
|
4
7
|
import * as path from 'path';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
8
|
import { SecurityUtils } from './lib/security.js';
|
|
7
|
-
// ES modules equivalent of __dirname
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
-
const __dirname =
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
10
11
|
const HELM_INSTALL_URL = 'https://helm.sh/docs/intro/install/';
|
|
11
12
|
const HELM_ENV_VAR_MSG = 'Or set HELM_BIN environment variable to helm binary path.';
|
|
12
13
|
function usageAndExit(msg) {
|
|
@@ -50,42 +51,36 @@ function usageAndExit(msg) {
|
|
|
50
51
|
async function cmdInit(name, silent = false) {
|
|
51
52
|
if (!name)
|
|
52
53
|
usageAndExit('Missing <chart-name>');
|
|
53
|
-
|
|
54
|
-
if (!SecurityUtils.isValidChartName(
|
|
54
|
+
const validName = name;
|
|
55
|
+
if (!SecurityUtils.isValidChartName(validName)) {
|
|
55
56
|
usageAndExit('Invalid chart name: must be RFC 1123 compliant (lowercase alphanumeric with hyphens)');
|
|
56
57
|
}
|
|
57
|
-
const sanitizedName =
|
|
58
|
-
const base = SecurityUtils.validatePath(
|
|
59
|
-
const file =
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
|
|
63
|
-
if (fs.existsSync(file)) {
|
|
58
|
+
const sanitizedName = validName;
|
|
59
|
+
const base = SecurityUtils.validatePath(join(process.cwd(), 'charts', sanitizedName), process.cwd());
|
|
60
|
+
const file = join(base, 'chart.ts');
|
|
61
|
+
mkdirSync(base, { recursive: true });
|
|
62
|
+
if (existsSync(file)) {
|
|
64
63
|
console.error(`File already exists: ${file}`);
|
|
65
64
|
process.exit(1);
|
|
66
65
|
}
|
|
67
|
-
|
|
68
|
-
fs.writeFileSync(file, exampleChartTs(name));
|
|
66
|
+
writeFileSync(file, exampleChartTs(validName));
|
|
69
67
|
log(SecurityUtils.sanitizeLogMessage(`Scaffold created at ${file}`), silent);
|
|
70
68
|
}
|
|
71
69
|
async function cmdValidate(projectDir, silent = false) {
|
|
72
70
|
if (!projectDir)
|
|
73
71
|
usageAndExit('Missing <projectDir>');
|
|
74
72
|
const proj = projectDir;
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
|
|
81
|
-
if (!fs.existsSync(chartTs)) {
|
|
73
|
+
const basePath = isAbsolute(proj) ? dirname(proj) : process.cwd();
|
|
74
|
+
const chartTs = isAbsolute(proj)
|
|
75
|
+
? SecurityUtils.validatePath(join(proj, 'chart.ts'), basePath)
|
|
76
|
+
: SecurityUtils.validatePath(join(process.cwd(), proj, 'chart.ts'), process.cwd());
|
|
77
|
+
if (!existsSync(chartTs)) {
|
|
82
78
|
console.error(SecurityUtils.sanitizeLogMessage(`chart.ts not found at ${chartTs}`));
|
|
83
79
|
process.exit(1);
|
|
84
80
|
}
|
|
85
|
-
// Check if helm is available
|
|
86
81
|
const helm = process.env['HELM_BIN'] || 'helm';
|
|
87
82
|
try {
|
|
88
|
-
|
|
83
|
+
execSync(`${helm} version --short`, { stdio: 'ignore' });
|
|
89
84
|
}
|
|
90
85
|
catch {
|
|
91
86
|
console.error('✗ Helm not found. Install Helm to enable chart validation.');
|
|
@@ -93,12 +88,10 @@ async function cmdValidate(projectDir, silent = false) {
|
|
|
93
88
|
console.error(` ${HELM_ENV_VAR_MSG}`);
|
|
94
89
|
process.exit(1);
|
|
95
90
|
}
|
|
96
|
-
const tempDir =
|
|
91
|
+
const tempDir = join(process.cwd(), '.timonel-validate');
|
|
97
92
|
try {
|
|
98
|
-
// Generate chart to temp directory for validation
|
|
99
93
|
await cmdSynth(projectDir, tempDir, { dryRun: false, silent: true, set: {} });
|
|
100
|
-
|
|
101
|
-
const lintResult = cp.spawnSync(helm, ['lint', tempDir], {
|
|
94
|
+
const lintResult = spawnSync(helm, ['lint', tempDir], {
|
|
102
95
|
stdio: silent ? 'pipe' : 'inherit',
|
|
103
96
|
encoding: 'utf8',
|
|
104
97
|
});
|
|
@@ -117,19 +110,11 @@ async function cmdValidate(projectDir, silent = false) {
|
|
|
117
110
|
process.exit(1);
|
|
118
111
|
}
|
|
119
112
|
finally {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
113
|
+
if (existsSync(tempDir)) {
|
|
114
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
123
115
|
}
|
|
124
116
|
}
|
|
125
117
|
}
|
|
126
|
-
/**
|
|
127
|
-
* Execute TypeScript chart using tsx with proper error handling.
|
|
128
|
-
* @param resolvedPath - Resolved path to the TypeScript file
|
|
129
|
-
* @param outDir - Output directory for the chart
|
|
130
|
-
* @param flags - CLI flags
|
|
131
|
-
* @since 2.7.3
|
|
132
|
-
*/
|
|
133
118
|
async function executeTypeScriptChart(resolvedPath, outDir, flags) {
|
|
134
119
|
const wrapperScript = `
|
|
135
120
|
import { pathToFileURL } from 'url';
|
|
@@ -156,10 +141,10 @@ fs.mkdirSync(outDir, { recursive: true });
|
|
|
156
141
|
await Promise.resolve(runner(outDir));
|
|
157
142
|
console.log('Chart written to ' + outDir);
|
|
158
143
|
`;
|
|
159
|
-
const wrapperFile =
|
|
144
|
+
const wrapperFile = join(process.cwd(), '.timonel-wrapper.mjs');
|
|
160
145
|
try {
|
|
161
|
-
|
|
162
|
-
const result =
|
|
146
|
+
writeFileSync(wrapperFile, wrapperScript);
|
|
147
|
+
const result = spawnSync('npx', ['tsx', wrapperFile], {
|
|
163
148
|
stdio: flags?.silent ? 'pipe' : 'inherit',
|
|
164
149
|
encoding: 'utf8',
|
|
165
150
|
});
|
|
@@ -171,9 +156,8 @@ console.log('Chart written to ' + outDir);
|
|
|
171
156
|
}
|
|
172
157
|
}
|
|
173
158
|
finally {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
fs.unlinkSync(wrapperFile);
|
|
159
|
+
if (existsSync(wrapperFile)) {
|
|
160
|
+
unlinkSync(wrapperFile);
|
|
177
161
|
}
|
|
178
162
|
}
|
|
179
163
|
}
|
|
@@ -181,24 +165,20 @@ async function cmdSynth(projectDir, out, flags) {
|
|
|
181
165
|
if (!projectDir)
|
|
182
166
|
usageAndExit('Missing <projectDir>');
|
|
183
167
|
const proj = projectDir;
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
|
|
190
|
-
if (!fs.existsSync(chartTs)) {
|
|
168
|
+
const basePath = isAbsolute(proj) ? dirname(proj) : process.cwd();
|
|
169
|
+
const chartTs = isAbsolute(proj)
|
|
170
|
+
? SecurityUtils.validatePath(join(proj, 'chart.ts'), basePath)
|
|
171
|
+
: SecurityUtils.validatePath(join(process.cwd(), proj, 'chart.ts'), process.cwd());
|
|
172
|
+
if (!existsSync(chartTs)) {
|
|
191
173
|
console.error(SecurityUtils.sanitizeLogMessage(`chart.ts not found at ${chartTs}`));
|
|
192
174
|
process.exit(1);
|
|
193
175
|
}
|
|
194
|
-
// Validate TypeScript file extension
|
|
195
176
|
if (!SecurityUtils.isValidTypeScriptFile(chartTs)) {
|
|
196
177
|
console.error('Security: Only TypeScript files (.ts) are allowed');
|
|
197
178
|
process.exit(1);
|
|
198
179
|
}
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
const outDir = out || path.join(path.dirname(chartTs), 'dist');
|
|
180
|
+
const resolvedPath = resolve(chartTs);
|
|
181
|
+
const outDir = out || join(dirname(chartTs), 'dist');
|
|
202
182
|
if (flags?.dryRun) {
|
|
203
183
|
log(`[DRY RUN] Would write chart to ${outDir}`, flags.silent);
|
|
204
184
|
return;
|
|
@@ -215,15 +195,17 @@ async function cmdSynth(projectDir, out, flags) {
|
|
|
215
195
|
async function cmdDiff(projectDir, chartDir, silent = false) {
|
|
216
196
|
if (!projectDir || !chartDir)
|
|
217
197
|
usageAndExit('Missing <projectDir> or <chartDir>');
|
|
218
|
-
const
|
|
198
|
+
const validProjectDir = projectDir;
|
|
199
|
+
const validChartDir = chartDir;
|
|
200
|
+
const tempDir = join(process.cwd(), '.timonel-temp');
|
|
219
201
|
try {
|
|
220
|
-
|
|
221
|
-
await cmdSynth(projectDir, tempDir);
|
|
222
|
-
// Compare with existing chart
|
|
202
|
+
await cmdSynth(validProjectDir, tempDir);
|
|
223
203
|
const diffCmd = process.platform === 'win32' ? 'fc' : 'diff';
|
|
224
|
-
const args = process.platform === 'win32'
|
|
204
|
+
const args = process.platform === 'win32'
|
|
205
|
+
? ['/N', validChartDir, tempDir]
|
|
206
|
+
: ['-r', validChartDir, tempDir];
|
|
225
207
|
log(`> ${diffCmd} ${args.join(' ')}`, silent);
|
|
226
|
-
const res =
|
|
208
|
+
const res = spawnSync(diffCmd, args, { stdio: 'inherit' });
|
|
227
209
|
if (res.status === 0) {
|
|
228
210
|
log('✓ No differences found', silent);
|
|
229
211
|
}
|
|
@@ -233,19 +215,19 @@ async function cmdDiff(projectDir, chartDir, silent = false) {
|
|
|
233
215
|
}
|
|
234
216
|
}
|
|
235
217
|
finally {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
218
|
+
if (existsSync(tempDir)) {
|
|
219
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
239
220
|
}
|
|
240
221
|
}
|
|
241
222
|
}
|
|
242
223
|
async function cmdDeploy(projectDir, releaseName, flags) {
|
|
243
224
|
if (!projectDir || !releaseName)
|
|
244
225
|
usageAndExit('Missing <projectDir> or <release>');
|
|
245
|
-
|
|
226
|
+
const validProjectDir = projectDir;
|
|
227
|
+
const validReleaseName = releaseName;
|
|
246
228
|
const helm = process.env['HELM_BIN'] || 'helm';
|
|
247
229
|
try {
|
|
248
|
-
|
|
230
|
+
execSync(`${helm} version --short`, { stdio: 'ignore' });
|
|
249
231
|
}
|
|
250
232
|
catch {
|
|
251
233
|
console.error('✗ Helm not found. Install Helm to enable deployment.');
|
|
@@ -253,21 +235,16 @@ async function cmdDeploy(projectDir, releaseName, flags) {
|
|
|
253
235
|
console.error(` ${HELM_ENV_VAR_MSG}`);
|
|
254
236
|
process.exit(1);
|
|
255
237
|
}
|
|
256
|
-
const tempDir =
|
|
238
|
+
const tempDir = join(process.cwd(), '.timonel-deploy');
|
|
257
239
|
try {
|
|
258
|
-
|
|
259
|
-
await cmdSynth(projectDir, tempDir, flags);
|
|
260
|
-
// Check if release exists
|
|
240
|
+
await cmdSynth(validProjectDir, tempDir, flags);
|
|
261
241
|
const checkCmd = `${helm} status ${releaseName}`;
|
|
262
|
-
const releaseExists =
|
|
263
|
-
// Build helm command
|
|
242
|
+
const releaseExists = spawnSync('sh', ['-c', checkCmd], { stdio: 'ignore' }).status === 0;
|
|
264
243
|
const action = releaseExists ? 'upgrade' : 'install';
|
|
265
|
-
const args = [action,
|
|
266
|
-
// Add environment-specific values if specified
|
|
244
|
+
const args = [action, validReleaseName, tempDir];
|
|
267
245
|
if (flags?.env) {
|
|
268
|
-
const valuesFile =
|
|
269
|
-
|
|
270
|
-
if (fs.existsSync(valuesFile)) {
|
|
246
|
+
const valuesFile = join(tempDir, `values-${flags.env}.yaml`);
|
|
247
|
+
if (existsSync(valuesFile)) {
|
|
271
248
|
args.push('-f', valuesFile);
|
|
272
249
|
}
|
|
273
250
|
}
|
|
@@ -275,16 +252,15 @@ async function cmdDeploy(projectDir, releaseName, flags) {
|
|
|
275
252
|
args.push('--dry-run');
|
|
276
253
|
}
|
|
277
254
|
log(`> ${helm} ${args.join(' ')}`, flags?.silent);
|
|
278
|
-
const res =
|
|
255
|
+
const res = spawnSync(helm, args, { stdio: 'inherit' });
|
|
279
256
|
if (res.status !== 0) {
|
|
280
257
|
process.exit(res.status ?? 1);
|
|
281
258
|
}
|
|
282
|
-
log(`✓ ${action === 'install' ? 'Deployed' : 'Updated'} release ${
|
|
259
|
+
log(`✓ ${action === 'install' ? 'Deployed' : 'Updated'} release ${validReleaseName}`, flags?.silent);
|
|
283
260
|
}
|
|
284
261
|
finally {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
262
|
+
if (existsSync(tempDir)) {
|
|
263
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
288
264
|
}
|
|
289
265
|
}
|
|
290
266
|
}
|
|
@@ -307,65 +283,99 @@ async function cmdUmbrella(subcommand, args, flags) {
|
|
|
307
283
|
}
|
|
308
284
|
}
|
|
309
285
|
const UMBRELLA_CONFIG_FILE = 'umbrella.config.json';
|
|
286
|
+
const UMBRELLA_FILE_NAME = 'umbrella.ts';
|
|
310
287
|
async function cmdUmbrellaInit(name, silent = false) {
|
|
311
288
|
const MISSING_NAME_MSG = 'Missing umbrella chart name';
|
|
312
289
|
if (!name)
|
|
313
290
|
usageAndExit(MISSING_NAME_MSG);
|
|
314
|
-
const
|
|
315
|
-
const
|
|
291
|
+
const validName = name;
|
|
292
|
+
const base = path.join(process.cwd(), validName);
|
|
293
|
+
const umbrellaFile = path.join(base, UMBRELLA_FILE_NAME);
|
|
316
294
|
const configFile = path.join(base, UMBRELLA_CONFIG_FILE);
|
|
317
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
|
|
318
295
|
fs.mkdirSync(base, { recursive: true });
|
|
319
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
|
|
320
296
|
fs.mkdirSync(path.join(base, 'charts'), { recursive: true });
|
|
321
|
-
|
|
322
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
|
|
323
|
-
fs.writeFileSync(umbrellaFile, exampleUmbrellaTs(name));
|
|
324
|
-
// Create config file
|
|
297
|
+
fs.writeFileSync(umbrellaFile, exampleUmbrellaTs(validName));
|
|
325
298
|
const config = {
|
|
326
|
-
name,
|
|
299
|
+
name: validName,
|
|
327
300
|
version: '0.1.0',
|
|
328
301
|
description: `${name} umbrella chart`,
|
|
329
302
|
subcharts: [],
|
|
330
303
|
};
|
|
331
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
|
|
332
304
|
fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
|
|
333
305
|
log(`Umbrella chart structure created at ${base}`, silent);
|
|
334
306
|
log(`Add subcharts with: tl umbrella add <subchart-name>`, silent);
|
|
335
307
|
}
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
if (!
|
|
308
|
+
function updateUmbrellaTs(subchartPath, subchartName) {
|
|
309
|
+
const umbrellaFile = path.join(process.cwd(), UMBRELLA_FILE_NAME);
|
|
310
|
+
if (!fs.existsSync(umbrellaFile)) {
|
|
311
|
+
console.error('umbrella.ts not found.');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
let content = fs.readFileSync(umbrellaFile, 'utf8');
|
|
315
|
+
const chartName = subchartName || path.basename(subchartPath);
|
|
316
|
+
const camelCaseName = chartName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
317
|
+
const importStatement = `import { rutter as ${camelCaseName} } from './charts/${subchartPath}/chart';`;
|
|
318
|
+
const importRegex = /import\s+.*?from\s+['"].*?['"];?/g;
|
|
319
|
+
const imports = content.match(importRegex) || [];
|
|
320
|
+
if (imports.length > 0) {
|
|
321
|
+
const lastImport = imports[imports.length - 1];
|
|
322
|
+
if (!lastImport) {
|
|
323
|
+
throw new Error('No valid import statements found');
|
|
324
|
+
}
|
|
325
|
+
const lastImportIndex = content.lastIndexOf(lastImport);
|
|
326
|
+
const insertIndex = lastImportIndex + lastImport.length;
|
|
327
|
+
if (!content.includes(importStatement)) {
|
|
328
|
+
content = content.slice(0, insertIndex) + '\n' + importStatement + content.slice(insertIndex);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const subchartsRegex = /subcharts:\s*\[([\s\S]*?)\]/;
|
|
332
|
+
const match = content.match(subchartsRegex);
|
|
333
|
+
if (match) {
|
|
334
|
+
const subchartsContent = match[1];
|
|
335
|
+
if (subchartsContent === undefined) {
|
|
336
|
+
throw new Error('Unable to parse subcharts content');
|
|
337
|
+
}
|
|
338
|
+
if (!subchartsContent.includes(`name: '${chartName}'`)) {
|
|
339
|
+
let newSubchartsContent;
|
|
340
|
+
const subchartEntry = `{ name: '${chartName}', rutter: ${camelCaseName} }`;
|
|
341
|
+
const hasExistingEntries = /\{\s*name:\s*['"]/.test(subchartsContent);
|
|
342
|
+
if (!hasExistingEntries) {
|
|
343
|
+
newSubchartsContent = `\n // Add your subcharts here:\n ${subchartEntry},\n `;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
const trimmedContent = subchartsContent.trimEnd();
|
|
347
|
+
newSubchartsContent = trimmedContent + `,\n ${subchartEntry}`;
|
|
348
|
+
}
|
|
349
|
+
content = content.replace(subchartsRegex, `subcharts: [${newSubchartsContent}\n ]`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
fs.writeFileSync(umbrellaFile, content);
|
|
353
|
+
}
|
|
354
|
+
async function cmdUmbrellaAdd(subchartPath, silent = false) {
|
|
355
|
+
const MISSING_SUBCHART_MSG = 'Missing subchart name or path';
|
|
356
|
+
if (!subchartPath)
|
|
339
357
|
usageAndExit(MISSING_SUBCHART_MSG);
|
|
358
|
+
const validSubchartPath = subchartPath;
|
|
340
359
|
const configFile = path.join(process.cwd(), UMBRELLA_CONFIG_FILE);
|
|
341
360
|
if (!fs.existsSync(configFile)) {
|
|
342
361
|
console.error(`${UMBRELLA_CONFIG_FILE} not found. Run \`tl umbrella init\` first.`);
|
|
343
362
|
process.exit(1);
|
|
344
363
|
}
|
|
345
364
|
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
346
|
-
|
|
347
|
-
const subchartDir = path.join(process.cwd(), 'charts',
|
|
348
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
|
|
365
|
+
const subchartName = path.basename(validSubchartPath);
|
|
366
|
+
const subchartDir = path.join(process.cwd(), 'charts', validSubchartPath);
|
|
349
367
|
fs.mkdirSync(subchartDir, { recursive: true });
|
|
350
368
|
const chartFile = path.join(subchartDir, 'chart.ts');
|
|
351
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
|
|
352
369
|
fs.writeFileSync(chartFile, exampleSubchartTs(subchartName));
|
|
353
|
-
// Update config
|
|
354
370
|
config.subcharts.push({
|
|
355
371
|
name: subchartName,
|
|
356
372
|
version: '0.1.0',
|
|
357
|
-
path: `./charts/${
|
|
373
|
+
path: `./charts/${validSubchartPath}/chart.ts`,
|
|
358
374
|
});
|
|
359
375
|
fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
|
|
360
|
-
|
|
376
|
+
updateUmbrellaTs(validSubchartPath, subchartName);
|
|
377
|
+
log(`Subchart ${subchartName} added to umbrella at path '${validSubchartPath}'`, silent);
|
|
361
378
|
}
|
|
362
|
-
/**
|
|
363
|
-
* Execute TypeScript umbrella chart using tsx with proper error handling.
|
|
364
|
-
* @param resolvedPath - Resolved path to the TypeScript file
|
|
365
|
-
* @param outDir - Output directory for the chart
|
|
366
|
-
* @param flags - CLI flags
|
|
367
|
-
* @since 2.7.3
|
|
368
|
-
*/
|
|
369
379
|
async function executeTypeScriptUmbrella(resolvedPath, outDir, flags) {
|
|
370
380
|
const wrapperScript = `
|
|
371
381
|
import { pathToFileURL } from 'url';
|
|
@@ -386,7 +396,7 @@ console.log('Umbrella chart written to ' + output);
|
|
|
386
396
|
const wrapperFile = path.join(process.cwd(), '.timonel-umbrella-wrapper.mjs');
|
|
387
397
|
try {
|
|
388
398
|
fs.writeFileSync(wrapperFile, wrapperScript);
|
|
389
|
-
const result =
|
|
399
|
+
const result = spawnSync('npx', ['tsx', wrapperFile], {
|
|
390
400
|
stdio: flags?.silent ? 'pipe' : 'inherit',
|
|
391
401
|
encoding: 'utf8',
|
|
392
402
|
});
|
|
@@ -398,7 +408,6 @@ console.log('Umbrella chart written to ' + output);
|
|
|
398
408
|
}
|
|
399
409
|
}
|
|
400
410
|
finally {
|
|
401
|
-
// Cleanup wrapper file
|
|
402
411
|
if (fs.existsSync(wrapperFile)) {
|
|
403
412
|
fs.unlinkSync(wrapperFile);
|
|
404
413
|
}
|
|
@@ -410,17 +419,15 @@ async function cmdUmbrellaSynth(outDir, flags) {
|
|
|
410
419
|
console.error(`${UMBRELLA_CONFIG_FILE} not found. Run \`tl umbrella init\` first.`);
|
|
411
420
|
process.exit(1);
|
|
412
421
|
}
|
|
413
|
-
const umbrellaFile = path.join(process.cwd(),
|
|
422
|
+
const umbrellaFile = path.join(process.cwd(), UMBRELLA_FILE_NAME);
|
|
414
423
|
if (!fs.existsSync(umbrellaFile)) {
|
|
415
424
|
console.error('umbrella.ts not found.');
|
|
416
425
|
process.exit(1);
|
|
417
426
|
}
|
|
418
|
-
// Validate TypeScript file extension
|
|
419
427
|
if (!SecurityUtils.isValidTypeScriptFile(umbrellaFile)) {
|
|
420
428
|
console.error('Security: Only TypeScript files (.ts) are allowed');
|
|
421
429
|
process.exit(1);
|
|
422
430
|
}
|
|
423
|
-
// Validate path to prevent traversal attacks
|
|
424
431
|
const validatedPath = SecurityUtils.validatePath(umbrellaFile, process.cwd());
|
|
425
432
|
const resolvedPath = path.resolve(validatedPath);
|
|
426
433
|
const output = outDir || path.join(process.cwd(), 'dist');
|
|
@@ -440,20 +447,21 @@ async function cmdUmbrellaSynth(outDir, flags) {
|
|
|
440
447
|
async function cmdPackage(chartDir, out, silent = false) {
|
|
441
448
|
if (!chartDir)
|
|
442
449
|
usageAndExit('Missing <chartDir>');
|
|
443
|
-
const
|
|
450
|
+
const validChartDir = chartDir;
|
|
451
|
+
const src = path.isAbsolute(validChartDir)
|
|
452
|
+
? validChartDir
|
|
453
|
+
: path.join(process.cwd(), validChartDir);
|
|
444
454
|
const chartYaml = path.join(src, 'Chart.yaml');
|
|
445
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
|
|
446
455
|
if (!fs.existsSync(chartYaml)) {
|
|
447
456
|
console.error(`Chart.yaml not found in ${src}`);
|
|
448
457
|
process.exit(1);
|
|
449
458
|
}
|
|
450
459
|
const outDir = out ? (path.isAbsolute(out) ? out : path.join(process.cwd(), out)) : src;
|
|
451
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool needs dynamic paths
|
|
452
460
|
fs.mkdirSync(outDir, { recursive: true });
|
|
453
461
|
const helm = process.env['HELM_BIN'] || 'helm';
|
|
454
462
|
const args = ['package', src, '-d', outDir];
|
|
455
463
|
log(`> ${helm} ${args.join(' ')}`, silent);
|
|
456
|
-
const res =
|
|
464
|
+
const res = spawnSync(helm, args, { stdio: 'inherit' });
|
|
457
465
|
if (res.error) {
|
|
458
466
|
console.error(`✗ Failed to execute Helm: ${res.error.message}`);
|
|
459
467
|
console.error(` Install: ${HELM_INSTALL_URL}`);
|
|
@@ -505,18 +513,11 @@ export default function run(outDir: string) {
|
|
|
505
513
|
}
|
|
506
514
|
`;
|
|
507
515
|
}
|
|
508
|
-
/**
|
|
509
|
-
* Generate TypeScript code for a subchart with correct package imports.
|
|
510
|
-
* Uses published package imports instead of relative development paths.
|
|
511
|
-
* @param name - The name of the subchart
|
|
512
|
-
* @returns TypeScript code string for the subchart
|
|
513
|
-
* @since 2.7.3
|
|
514
|
-
*/
|
|
515
516
|
function exampleSubchartTs(name) {
|
|
516
517
|
return `import { Rutter } from 'timonel';
|
|
517
|
-
import { valuesRef } from 'timonel/lib/helm';
|
|
518
|
+
import { valuesRef, helm } from 'timonel/lib/helm';
|
|
518
519
|
|
|
519
|
-
//
|
|
520
|
+
// Create a new Rutter instance
|
|
520
521
|
const rutter = new Rutter({
|
|
521
522
|
meta: {
|
|
522
523
|
name: '${name}',
|
|
@@ -531,19 +532,73 @@ const rutter = new Rutter({
|
|
|
531
532
|
},
|
|
532
533
|
});
|
|
533
534
|
|
|
534
|
-
// Add
|
|
535
|
-
rutter.
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
535
|
+
// Add Deployment using addManifest with native Kubernetes object
|
|
536
|
+
rutter.addManifest({
|
|
537
|
+
apiVersion: 'apps/v1',
|
|
538
|
+
kind: 'Deployment',
|
|
539
|
+
metadata: {
|
|
540
|
+
name: '${name}',
|
|
541
|
+
labels: {
|
|
542
|
+
app: '${name}'
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
spec: {
|
|
546
|
+
replicas: valuesRef('replicas'),
|
|
547
|
+
selector: {
|
|
548
|
+
matchLabels: {
|
|
549
|
+
app: '${name}'
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
template: {
|
|
553
|
+
metadata: {
|
|
554
|
+
labels: {
|
|
555
|
+
app: '${name}'
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
spec: {
|
|
559
|
+
containers: [
|
|
560
|
+
{
|
|
561
|
+
name: '${name}',
|
|
562
|
+
image: String(valuesRef('image.repository')) + ':' + String(valuesRef('image.tag')),
|
|
563
|
+
ports: [
|
|
564
|
+
{
|
|
565
|
+
name: 'http',
|
|
566
|
+
containerPort: 80,
|
|
567
|
+
protocol: 'TCP'
|
|
568
|
+
}
|
|
569
|
+
]
|
|
570
|
+
}
|
|
571
|
+
]
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}, '${name}-deployment');
|
|
541
576
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
577
|
+
// Add Service using addManifest with native Kubernetes object
|
|
578
|
+
rutter.addManifest({
|
|
579
|
+
apiVersion: 'v1',
|
|
580
|
+
kind: 'Service',
|
|
581
|
+
metadata: {
|
|
582
|
+
name: '${name}',
|
|
583
|
+
labels: {
|
|
584
|
+
app: '${name}'
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
spec: {
|
|
588
|
+
type: 'ClusterIP',
|
|
589
|
+
ports: [
|
|
590
|
+
{
|
|
591
|
+
port: valuesRef('service.port'),
|
|
592
|
+
targetPort: 'http',
|
|
593
|
+
protocol: 'TCP',
|
|
594
|
+
name: 'http'
|
|
595
|
+
}
|
|
596
|
+
],
|
|
597
|
+
selector: {
|
|
598
|
+
app: '${name}'
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}, '${name}-service');
|
|
547
602
|
|
|
548
603
|
export default function run(outDir: string) {
|
|
549
604
|
rutter.write(outDir);
|
|
@@ -553,13 +608,6 @@ export default function run(outDir: string) {
|
|
|
553
608
|
export { rutter };
|
|
554
609
|
`;
|
|
555
610
|
}
|
|
556
|
-
/**
|
|
557
|
-
* Generate TypeScript code for a main chart with correct package imports.
|
|
558
|
-
* Uses published package imports instead of relative development paths.
|
|
559
|
-
* @param name - The name of the chart
|
|
560
|
-
* @returns TypeScript code string for the chart
|
|
561
|
-
* @since 2.7.3
|
|
562
|
-
*/
|
|
563
611
|
function exampleChartTs(name) {
|
|
564
612
|
return `import { Rutter } from 'timonel';
|
|
565
613
|
import { valuesRef, helm } from 'timonel/lib/helm';
|
|
@@ -577,6 +625,7 @@ const rutter = new Rutter({
|
|
|
577
625
|
replicas: 1,
|
|
578
626
|
service: { port: 80 },
|
|
579
627
|
ingress: { enabled: false, host: 'example.com', className: 'nginx' },
|
|
628
|
+
namespace: { create: false, name: '${name}' },
|
|
580
629
|
},
|
|
581
630
|
envValues: {
|
|
582
631
|
dev: { replicas: 1 },
|
|
@@ -584,54 +633,140 @@ const rutter = new Rutter({
|
|
|
584
633
|
},
|
|
585
634
|
});
|
|
586
635
|
|
|
587
|
-
//
|
|
588
|
-
rutter.
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
636
|
+
// Add Deployment using addManifest with native Kubernetes object
|
|
637
|
+
rutter.addManifest({
|
|
638
|
+
apiVersion: 'apps/v1',
|
|
639
|
+
kind: 'Deployment',
|
|
640
|
+
metadata: {
|
|
641
|
+
name: '${name}',
|
|
642
|
+
labels: {
|
|
643
|
+
app: '${name}'
|
|
644
|
+
}
|
|
596
645
|
},
|
|
597
|
-
|
|
646
|
+
spec: {
|
|
647
|
+
replicas: valuesRef('replicas'),
|
|
648
|
+
selector: {
|
|
649
|
+
matchLabels: {
|
|
650
|
+
app: '${name}'
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
template: {
|
|
654
|
+
metadata: {
|
|
655
|
+
labels: {
|
|
656
|
+
app: '${name}'
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
spec: {
|
|
660
|
+
containers: [
|
|
661
|
+
{
|
|
662
|
+
name: '${name}',
|
|
663
|
+
image: String(valuesRef('image.repository')) + ':' + String(valuesRef('image.tag')),
|
|
664
|
+
ports: [
|
|
665
|
+
{
|
|
666
|
+
name: 'http',
|
|
667
|
+
containerPort: 80,
|
|
668
|
+
protocol: 'TCP'
|
|
669
|
+
}
|
|
670
|
+
],
|
|
671
|
+
env: [
|
|
672
|
+
{
|
|
673
|
+
name: 'APP_NAME',
|
|
674
|
+
value: '${name}'
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
name: 'RELEASE',
|
|
678
|
+
value: helm.releaseName
|
|
679
|
+
}
|
|
680
|
+
]
|
|
681
|
+
}
|
|
682
|
+
]
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}, '${name}-deployment');
|
|
598
687
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
}
|
|
688
|
+
// Add Service using addManifest with native Kubernetes object
|
|
689
|
+
rutter.addManifest({
|
|
690
|
+
apiVersion: 'v1',
|
|
691
|
+
kind: 'Service',
|
|
692
|
+
metadata: {
|
|
693
|
+
name: '${name}',
|
|
694
|
+
labels: {
|
|
695
|
+
app: '${name}'
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
spec: {
|
|
699
|
+
type: 'ClusterIP',
|
|
700
|
+
ports: [
|
|
701
|
+
{
|
|
702
|
+
port: valuesRef('service.port'),
|
|
703
|
+
targetPort: 'http',
|
|
704
|
+
protocol: 'TCP',
|
|
705
|
+
name: 'http'
|
|
706
|
+
}
|
|
707
|
+
],
|
|
708
|
+
selector: {
|
|
709
|
+
app: '${name}'
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}, '${name}-service');
|
|
605
713
|
|
|
606
|
-
//
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
714
|
+
// Add Ingress using addConditionalManifest with native Kubernetes object
|
|
715
|
+
rutter.addConditionalManifest({
|
|
716
|
+
apiVersion: 'networking.k8s.io/v1',
|
|
717
|
+
kind: 'Ingress',
|
|
718
|
+
metadata: {
|
|
719
|
+
name: '${name}',
|
|
720
|
+
labels: {
|
|
721
|
+
app: '${name}'
|
|
722
|
+
}
|
|
723
|
+
},
|
|
724
|
+
spec: {
|
|
725
|
+
ingressClassName: String(valuesRef('ingress.className')),
|
|
726
|
+
rules: [
|
|
727
|
+
{
|
|
728
|
+
host: String(valuesRef('ingress.host')),
|
|
729
|
+
http: {
|
|
730
|
+
paths: [
|
|
731
|
+
{
|
|
732
|
+
path: '/',
|
|
733
|
+
pathType: 'Prefix',
|
|
734
|
+
backend: {
|
|
735
|
+
service: {
|
|
736
|
+
name: '${name}',
|
|
737
|
+
port: {
|
|
738
|
+
number: valuesRef('service.port')
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
]
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
]
|
|
747
|
+
}
|
|
748
|
+
}, valuesRef('ingress.enabled'), '${name}-ingress');
|
|
749
|
+
|
|
750
|
+
// Add Namespace using addConditionalManifest
|
|
751
|
+
rutter.addConditionalManifest({
|
|
752
|
+
apiVersion: 'v1',
|
|
753
|
+
kind: 'Namespace',
|
|
754
|
+
metadata: {
|
|
755
|
+
name: String(valuesRef('namespace.name')),
|
|
756
|
+
labels: {
|
|
757
|
+
app: '${name}',
|
|
758
|
+
'app.kubernetes.io/name': '${name}',
|
|
759
|
+
'app.kubernetes.io/instance': helm.releaseName
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}, valuesRef('namespace.create'), '${name}-namespace');
|
|
626
763
|
|
|
627
764
|
export default function run(outDir: string) {
|
|
628
765
|
rutter.write(outDir);
|
|
629
766
|
}
|
|
630
767
|
`;
|
|
631
768
|
}
|
|
632
|
-
// eslint-disable-next-line sonarjs/cognitive-complexity -- CLI argument parsing requires multiple conditions
|
|
633
769
|
function parseArgs() {
|
|
634
|
-
/* eslint-disable security/detect-object-injection */
|
|
635
770
|
const argv = process.argv.slice(2);
|
|
636
771
|
const flags = { dryRun: false, silent: false, set: {} };
|
|
637
772
|
const args = [];
|
|
@@ -679,14 +814,8 @@ function parseArgs() {
|
|
|
679
814
|
}
|
|
680
815
|
}
|
|
681
816
|
}
|
|
682
|
-
/* eslint-enable security/detect-object-injection */
|
|
683
817
|
return { cmd, args, flags };
|
|
684
818
|
}
|
|
685
|
-
/**
|
|
686
|
-
* Display the current version of Timonel CLI.
|
|
687
|
-
* Reads version from package.json using ES modules compatible path resolution.
|
|
688
|
-
* @since 2.7.3
|
|
689
|
-
*/
|
|
690
819
|
function showVersion() {
|
|
691
820
|
const packagePath = path.join(__dirname, '..', 'package.json');
|
|
692
821
|
try {
|
|
@@ -734,4 +863,3 @@ main().catch((err) => {
|
|
|
734
863
|
console.error(err);
|
|
735
864
|
process.exit(1);
|
|
736
865
|
});
|
|
737
|
-
//# sourceMappingURL=cli.js.map
|