timonel 2.14.0-beta.1 → 3.0.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.
- package/CHANGELOG.md +371 -0
- package/README.md +202 -22
- package/SECURITY.md +25 -11
- package/dist/cli.js +54 -15
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/lib/helm.js +28 -1
- package/dist/lib/helmChartWriter.js +32 -11
- package/dist/lib/resources/baseResourceProvider.js +5 -0
- package/dist/lib/resources/cloud/aws/awsResources.js +2 -1
- package/dist/lib/resources/cloud/aws/karpenterResources.js +16 -2
- package/dist/lib/rutter.js +10 -8
- package/dist/lib/security.js +4 -4
- package/dist/lib/templates/flexible-subchart.js +14 -5
- package/dist/lib/templates/umbrella-chart.js +21 -10
- package/dist/lib/umbrellaRutter.js +19 -7
- package/dist/lib/utils/envVarsLoader.d.ts +19 -0
- package/dist/lib/utils/envVarsLoader.js +63 -0
- package/dist/lib/utils/helmControlStructures.d.ts +4 -5
- package/dist/lib/utils/helmControlStructures.js +12 -0
- package/dist/lib/utils/helmHelpers.js +10 -2
- package/dist/lib/utils/helmYamlSerializer.d.ts +2 -0
- package/dist/lib/utils/helmYamlSerializer.js +509 -23
- package/dist/lib/utils/logger.js +54 -39
- package/dist/lib/utils/valuesRef.d.ts +102 -0
- package/dist/lib/utils/valuesRef.js +273 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,18 +17,102 @@ directory.
|
|
|
17
17
|
|
|
18
18
|
## ✨ Key Features
|
|
19
19
|
|
|
20
|
+
### Core Capabilities
|
|
21
|
+
|
|
20
22
|
- **🔒 Type-safe API** with strict TypeScript and cdk8s constructs
|
|
21
|
-
- **🎯 Type-Safe Helm Helpers** with 9 composable template helpers (`helmIf`, `helmRange`,
|
|
22
|
-
`helmWith`, `helmInclude`, etc.)
|
|
23
23
|
- **🔧 Flexible resource creation** with built-in methods and `addManifest()` for custom resources
|
|
24
24
|
- **🌍 Multi-environment support** with automatic values files generation
|
|
25
25
|
- **☂️ Umbrella Charts** for managing multiple subcharts as a single unit
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
- **⚡ Minimal CLI** (`tl`) for scaffolding, synthesis, validation, and deployment
|
|
27
|
+
- **📦 Flexible subchart templates** supporting both cdk8s and cdk8s-plus-33
|
|
28
|
+
|
|
29
|
+
### Type-Safe Helm Helpers (v3.0+)
|
|
30
|
+
|
|
31
|
+
#### ValuesRef System (NEW in v3.0 - RECOMMENDED)
|
|
32
|
+
|
|
33
|
+
Type-safe proxy-based values references with full IDE support:
|
|
34
|
+
|
|
35
|
+
- Import: `import { valuesRef } from 'timonel'`
|
|
36
|
+
- **Comparison operators**: `eq`, `ne`, `gt`, `ge`, `lt`, `le`
|
|
37
|
+
- **Logical operators**: `not`, `and`, `or`
|
|
38
|
+
- **String functions**: `quote`, `upper`, `lower`, `trim`, `replace`, `contains`
|
|
39
|
+
- **Default values**: `default()`
|
|
40
|
+
- **Type checking**: `kindIs`, `hasKey`
|
|
41
|
+
- **YAML functions**: `toYaml`, `toJson`, `nindent`, `indent`
|
|
42
|
+
- **Field-level conditionals**: `v.if()`, `v.ifElse()` - Complex conditional logic
|
|
43
|
+
- **Range loops**: `v.range()` - Type-safe iteration
|
|
44
|
+
- **Context switching**: `v.with()` - Scoped value access
|
|
45
|
+
|
|
46
|
+
#### Composable Helpers
|
|
47
|
+
|
|
48
|
+
Template definition and inclusion helpers:
|
|
49
|
+
|
|
50
|
+
- `helmInclude`, `helmDefine`, `helmVar`, `helmBlock`, `helmComment`, `helmFragment`
|
|
51
|
+
- `template`, `include`, `quote`, `indent`
|
|
52
|
+
|
|
53
|
+
#### Value Reference Helpers
|
|
54
|
+
|
|
55
|
+
Useful string-based utilities (no ValuesRef equivalent):
|
|
56
|
+
|
|
57
|
+
- `requiredValuesRef` - Required value with validation
|
|
58
|
+
- `numberRef`, `boolRef`, `floatRef` - Type-cast references (int, bool, float64)
|
|
59
|
+
- `base64Ref` - Base64 encoding
|
|
60
|
+
|
|
61
|
+
#### Legacy Helpers (NOT RECOMMENDED)
|
|
62
|
+
|
|
63
|
+
**⚠️ Use ValuesRef system instead:**
|
|
64
|
+
|
|
65
|
+
- `valuesRef(path)` → use `v.path` (ValuesRef system)
|
|
66
|
+
- `stringRef()` → use `v.quote()` (ValuesRef system)
|
|
67
|
+
- `defaultRef()` → use `v.default()` (ValuesRef system)
|
|
68
|
+
- `jsonRef()` → use `v.toJson()` (ValuesRef system)
|
|
69
|
+
- `conditionalRef()` → use `v.if()` (ValuesRef system)
|
|
70
|
+
- `helmIf`, `helmIfSimple` → use `v.if()` (ValuesRef system)
|
|
71
|
+
- `helmRange` → use `v.range()` (ValuesRef system)
|
|
72
|
+
- `helmWith` → use `v.with()` (ValuesRef system)
|
|
73
|
+
- `helmIfElseIf` → use `v.if()` with nested conditions
|
|
74
|
+
|
|
75
|
+
### Enhanced Helm Helpers
|
|
76
|
+
|
|
77
|
+
- **Environment Helpers**: `envRef`, `envDefault`, `envRequired`, `envFromSecret`, `envFromConfigMap`
|
|
78
|
+
- **GitOps Helpers**: `gitBranch`, `gitCommit`, `gitTag`, `gitopsAnnotations`
|
|
79
|
+
- **Observability Helpers**: `prometheusAnnotations`, `datadogAnnotations`, `tracingAnnotations`
|
|
80
|
+
- **Validation Helpers**: `validateRequired`, `validatePattern`, `validateRange`, `validateEnum`
|
|
81
|
+
- **Standard Helpers**: 40+ built-in Helm helpers (chart.name, chart.fullname, chart.labels, etc.)
|
|
82
|
+
|
|
83
|
+
### Cloud Integrations
|
|
84
|
+
|
|
85
|
+
- **AWS Resources**:
|
|
86
|
+
- EBS/EFS StorageClass with encryption and performance options
|
|
87
|
+
- ALB Ingress with SSL/TLS and health checks
|
|
88
|
+
- IRSA ServiceAccount for pod-level IAM roles
|
|
89
|
+
- ECR integration
|
|
90
|
+
- Karpenter NodePool, NodeClaim, and EC2NodeClass
|
|
91
|
+
- **Karpenter Features**:
|
|
92
|
+
- Disruption budgets and consolidation policies
|
|
93
|
+
- Instance type selection and requirements
|
|
94
|
+
- Spot instance support
|
|
95
|
+
- Custom AMI and user data
|
|
96
|
+
|
|
97
|
+
### Security & Validation
|
|
98
|
+
|
|
99
|
+
- **🛡️ Security-first approach**:
|
|
100
|
+
- Input validation (CWE-20, CWE-22/23)
|
|
101
|
+
- Path traversal prevention
|
|
102
|
+
- Command injection prevention (CWE-78/77/88)
|
|
103
|
+
- Log injection protection (CWE-117)
|
|
104
|
+
- Code injection prevention (CWE-94)
|
|
105
|
+
- **NetworkPolicy support** for pod-level network isolation
|
|
106
|
+
- **Helm chart validation** with `validateHelmYaml`
|
|
107
|
+
- **SecurityUtils** for path validation and sanitization
|
|
108
|
+
|
|
109
|
+
### Developer Experience
|
|
110
|
+
|
|
111
|
+
- **Structured logging** with Pino (JSON format, performance tracking)
|
|
112
|
+
- **Environment variables loader** for external configuration
|
|
113
|
+
- **YAML serialization** with Helm template preservation
|
|
114
|
+
- **TypeScript strict mode** with all compiler checks enabled
|
|
115
|
+
- **Comprehensive error handling** with detailed messages
|
|
32
116
|
|
|
33
117
|
## 🚀 Quick Start
|
|
34
118
|
|
|
@@ -153,18 +237,114 @@ const umbrellaConfig = {
|
|
|
153
237
|
export const umbrella = new UmbrellaChartTemplate(umbrellaConfig);
|
|
154
238
|
```
|
|
155
239
|
|
|
156
|
-
### Type-Safe Helm Helpers
|
|
240
|
+
### Using Type-Safe Helm Helpers
|
|
241
|
+
|
|
242
|
+
#### ValuesRef System (Recommended)
|
|
243
|
+
|
|
244
|
+
The new ValuesRef system provides a type-safe, proxy-based approach to Helm values with full IDE support.
|
|
245
|
+
|
|
246
|
+
**⚠️ Important:** This is completely different from the legacy `valuesRef(path: string)` helper.
|
|
247
|
+
The new system uses generics and returns a proxy object with methods.
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
import { valuesRef } from 'timonel';
|
|
251
|
+
|
|
252
|
+
interface MyValues {
|
|
253
|
+
replicaCount: number;
|
|
254
|
+
image: { repository: string; tag: string };
|
|
255
|
+
autoscaling: { enabled: boolean; minReplicas: number };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const v = valuesRef<MyValues>();
|
|
259
|
+
|
|
260
|
+
// Type-safe value references with IDE autocomplete
|
|
261
|
+
const replicas = v.replicaCount; // {{ .Values.replicaCount }}
|
|
262
|
+
const imageTag = v.image.tag; // {{ .Values.image.tag }}
|
|
263
|
+
|
|
264
|
+
// Comparison operators
|
|
265
|
+
const isProd = v.environment.eq('production'); // eq .Values.environment "production"
|
|
266
|
+
const hasReplicas = v.replicaCount.gt(1); // gt .Values.replicaCount 1
|
|
267
|
+
|
|
268
|
+
// Logical operators
|
|
269
|
+
const notEnabled = v.autoscaling.enabled.not(); // not .Values.autoscaling.enabled
|
|
270
|
+
|
|
271
|
+
// String functions
|
|
272
|
+
const upperEnv = v.environment.upper(); // .Values.environment | upper
|
|
273
|
+
const quotedTag = v.image.tag.quote(); // .Values.image.tag | quote
|
|
274
|
+
|
|
275
|
+
// Default values
|
|
276
|
+
const port = v.port.default(8080); // {{ .Values.port | default 8080 }}
|
|
277
|
+
|
|
278
|
+
// Field-level conditionals
|
|
279
|
+
const deployment = {
|
|
280
|
+
spec: {
|
|
281
|
+
replicas: v.if(notEnabled, v.replicaCount), // Conditionally include field
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Range loops
|
|
286
|
+
const envVars = v.env.range((item, index) => ({
|
|
287
|
+
name: item.name,
|
|
288
|
+
value: item.value,
|
|
289
|
+
}));
|
|
290
|
+
|
|
291
|
+
// Context switching
|
|
292
|
+
const dbConfig = v.database.with((db) => ({
|
|
293
|
+
host: db.host,
|
|
294
|
+
port: db.port,
|
|
295
|
+
}));
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
#### Template Composition Helpers
|
|
299
|
+
|
|
300
|
+
Use these helpers for template definitions and inclusions:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import { helmInclude, helmDefine, helmFragment } from 'timonel';
|
|
304
|
+
|
|
305
|
+
// Template inclusion with pipe
|
|
306
|
+
const labels = helmInclude('chart.labels', '.', { pipe: 'nindent 4' });
|
|
307
|
+
|
|
308
|
+
// Define a named template
|
|
309
|
+
const myTemplate = helmDefine('myapp.config', {
|
|
310
|
+
key: 'value',
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Combine multiple constructs
|
|
314
|
+
const combined = helmFragment(helmInclude('chart.labels', '.'), { customKey: 'customValue' });
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
#### Legacy Flow Control (NOT RECOMMENDED)
|
|
318
|
+
|
|
319
|
+
**⚠️ These are legacy and NOT RECOMMENDED. Use ValuesRef system (v.if, v.range, v.with)
|
|
320
|
+
instead:**
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
// ❌ OLD WAY - string-based valuesRef (no type safety)
|
|
324
|
+
const oldRef = valuesRef('.Values.production');
|
|
325
|
+
|
|
326
|
+
// ✅ NEW WAY - ValuesRef system (type-safe)
|
|
327
|
+
const v = valuesRef<MyValues>();
|
|
328
|
+
const newRef = v.production; // {{ .Values.production }}
|
|
329
|
+
|
|
330
|
+
// ❌ OLD WAY - helmIf, helmRange, helmWith (string-based)
|
|
331
|
+
const config = helmIf('.Values.production', { replicas: 5 }, { replicas: 1 });
|
|
332
|
+
|
|
333
|
+
// ✅ NEW WAY - v.if(), v.range(), v.with() (type-safe)
|
|
334
|
+
const config = v.if(v.production, { replicas: 5 });
|
|
335
|
+
```
|
|
157
336
|
|
|
158
|
-
|
|
159
|
-
`helmRange`, `helmWith`, `helmInclude`, `helmDefine`, `helmVar`, `helmBlock`, `helmComment`, and
|
|
160
|
-
`helmFragment`.
|
|
337
|
+
**Why ValuesRef is Better:**
|
|
161
338
|
|
|
162
|
-
**
|
|
339
|
+
- ✅ **100% Type-Safe** - Catch errors at compile time with TypeScript generics
|
|
340
|
+
- ✅ **No Raw Strings** - Eliminate manual template interpolation and typos
|
|
341
|
+
- ✅ **Full IDE Support** - Autocomplete, type hints, and refactoring support
|
|
342
|
+
- ✅ **Proxy-based** - Chainable methods for complex logic
|
|
343
|
+
- ✅ **Composable** - Nest and combine operations naturally
|
|
344
|
+
- ❌ **Legacy helpers** - String-based, error-prone, no IDE support
|
|
163
345
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
- ✅ Composable - nest and combine helpers freely
|
|
167
|
-
- ✅ Full IDE Support - autocomplete and type hints
|
|
346
|
+
**Migration:** Replace `valuesRef(path)` with `v.path`. Replace `helmIf`, `helmRange`, `helmWith`
|
|
347
|
+
with `v.if()`, `v.range()`, `v.with()`.
|
|
168
348
|
|
|
169
349
|
**Learn more:** See the
|
|
170
350
|
[Type-Safe Helm Helpers Guide](https://github.com/KenkoGeek/timonel/wiki/Helm-Helpers-System) for
|
|
@@ -200,10 +380,10 @@ If you get `Error: Cannot find module 'cdk8s'` when running `tl umbrella synth`:
|
|
|
200
380
|
"version": "1.0.0",
|
|
201
381
|
"type": "module",
|
|
202
382
|
"dependencies": {
|
|
203
|
-
"cdk8s": "^2.70.
|
|
204
|
-
"cdk8s-plus-33": "^2.
|
|
205
|
-
"constructs": "^10.4.
|
|
206
|
-
"timonel": "^
|
|
383
|
+
"cdk8s": "^2.70.28",
|
|
384
|
+
"cdk8s-plus-33": "^2.4.6",
|
|
385
|
+
"constructs": "^10.4.3",
|
|
386
|
+
"timonel": "^3.0.0-beta.1"
|
|
207
387
|
},
|
|
208
388
|
"devDependencies": {
|
|
209
389
|
"@types/node": "^24.5.2",
|
|
@@ -238,7 +418,7 @@ MIT
|
|
|
238
418
|
[security-url]: SECURITY.md
|
|
239
419
|
[pnpm-badge]: https://img.shields.io/badge/pm-pnpm-ffd95a?logo=pnpm&logoColor=fff&labelColor=24292e
|
|
240
420
|
[pnpm-url]: https://pnpm.io/
|
|
241
|
-
[node-badge]: https://img.shields.io/badge/node-%3E%
|
|
421
|
+
[node-badge]: https://img.shields.io/badge/node-%3E%3D22-339933?logo=node.js&logoColor=fff
|
|
242
422
|
[node-url]: https://nodejs.org/
|
|
243
423
|
[ts-badge]: https://img.shields.io/badge/TypeScript-5.x-3178C6?logo=typescript&logoColor=fff
|
|
244
424
|
[ts-url]: https://www.typescriptlang.org/
|
package/SECURITY.md
CHANGED
|
@@ -36,13 +36,14 @@ We provide security updates for the following versions:
|
|
|
36
36
|
|
|
37
37
|
<!-- markdownlint-disable MD060 -->
|
|
38
38
|
|
|
39
|
-
| Version
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
| 2.
|
|
43
|
-
| 2.0-2.
|
|
44
|
-
|
|
|
45
|
-
|
|
|
39
|
+
| Version | Supported | Security Updates | End of Life |
|
|
40
|
+
| ------------ | -------------- | ---------------- | ----------- |
|
|
41
|
+
| 3.0.0-beta.1 | ✅ Current | Full support | TBD |
|
|
42
|
+
| 2.14.x | ✅ Supported | Full support | TBD |
|
|
43
|
+
| 2.8.0-2.13.x | ✅ Supported | Full support | 2025-06-30 |
|
|
44
|
+
| 2.0-2.7.x | ⚠️ Limited | Critical only | 2025-03-31 |
|
|
45
|
+
| 1.x.x | ❌ End of life | None | 2024-06-30 |
|
|
46
|
+
| < 1.0 | ❌ End of life | None | 2024-01-01 |
|
|
46
47
|
|
|
47
48
|
**Update policy:**
|
|
48
49
|
|
|
@@ -83,15 +84,28 @@ We provide security updates for the following versions:
|
|
|
83
84
|
### Security Features
|
|
84
85
|
|
|
85
86
|
- **Input validation**: Comprehensive path traversal prevention and sanitization via SecurityUtils
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
87
|
+
- CWE-22/23: Path traversal protection with URL-encoded sequence detection
|
|
88
|
+
- CWE-20: Proper input validation for CLI flags (--set, --env)
|
|
89
|
+
- Null byte injection prevention
|
|
90
|
+
- Reserved key validation in resource providers
|
|
91
|
+
- **Log injection protection**: All user inputs sanitized before logging (CWE-117)
|
|
92
|
+
- **Code injection prevention**: Strict validation for dynamic module loading (CWE-94)
|
|
93
|
+
- Regex escaping for dynamic pattern construction
|
|
94
|
+
- Function name validation before template generation
|
|
95
|
+
- **Command injection prevention**: Validated inputs for all shell commands (CWE-78/77/88)
|
|
96
|
+
- Release name validation (RFC 1123 subdomain)
|
|
97
|
+
- Namespace validation (Kubernetes naming rules)
|
|
98
|
+
- --set flag validation with support for nested paths and arrays
|
|
99
|
+
- **TypeScript strict mode**: Compile-time safety checks with all strict options enabled
|
|
89
100
|
- **No eval()**: Static code generation only
|
|
90
101
|
- **File system isolation**: Controlled output directory access with path validation
|
|
102
|
+
- Absolute path support with explicit allowAbsolute flag
|
|
103
|
+
- Base directory validation for all file operations
|
|
91
104
|
- **Helm template validation**: Input validation for all template functions
|
|
92
105
|
- **Karpenter security**: Secure node pool and scheduling configurations
|
|
93
106
|
- **Performance optimization**: Efficient algorithms preventing DoS via resource exhaustion
|
|
94
|
-
- **OWASP compliance**: Following secure coding guidelines (CWE-22, CWE-94, CWE-117
|
|
107
|
+
- **OWASP compliance**: Following secure coding guidelines (CWE-22, CWE-23, CWE-94, CWE-117,
|
|
108
|
+
CWE-78, CWE-77, CWE-88, CWE-20)
|
|
95
109
|
|
|
96
110
|
## Security Considerations for Users
|
|
97
111
|
|
package/dist/cli.js
CHANGED
|
@@ -142,8 +142,9 @@ async function cmdInit(name, silent = false) {
|
|
|
142
142
|
if (!SecurityUtils.isValidChartName(validName)) {
|
|
143
143
|
usageAndExit('Invalid chart name. Must be lowercase, start with a letter, and contain only letters, numbers, and dashes.');
|
|
144
144
|
}
|
|
145
|
-
const
|
|
146
|
-
const
|
|
145
|
+
const cwd = process.cwd();
|
|
146
|
+
const base = SecurityUtils.validatePath(path.join(cwd, validName), cwd);
|
|
147
|
+
const chartFile = SecurityUtils.validatePath(path.join(base, 'chart.ts'), cwd);
|
|
147
148
|
fs.mkdirSync(base, { recursive: true });
|
|
148
149
|
const { generateFlexibleSubchartTemplate } = await import('./lib/templates/flexible-subchart.js');
|
|
149
150
|
fs.writeFileSync(chartFile, generateFlexibleSubchartTemplate(validName));
|
|
@@ -152,19 +153,31 @@ async function cmdInit(name, silent = false) {
|
|
|
152
153
|
log(`Run 'tl synth ${validName}' to generate complete Helm chart`, silent);
|
|
153
154
|
}
|
|
154
155
|
async function cmdSynth(chartDirOrOutDir, flags, explicitOutDir) {
|
|
155
|
-
|
|
156
|
+
const cwd = process.cwd();
|
|
157
|
+
let chartDir = cwd;
|
|
156
158
|
let outDir;
|
|
157
|
-
if (chartDirOrOutDir
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
if (chartDirOrOutDir) {
|
|
160
|
+
const resolvedPath = path.resolve(chartDirOrOutDir);
|
|
161
|
+
const validatedPath = SecurityUtils.validatePath(resolvedPath, cwd, { allowAbsolute: true });
|
|
162
|
+
const chartTsPath = SecurityUtils.validatePath(path.join(validatedPath, 'chart.ts'), cwd, {
|
|
163
|
+
allowAbsolute: true,
|
|
164
|
+
});
|
|
165
|
+
if (fs.existsSync(chartTsPath)) {
|
|
166
|
+
chartDir = validatedPath;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
outDir = chartDirOrOutDir;
|
|
170
|
+
}
|
|
162
171
|
}
|
|
163
172
|
if (explicitOutDir) {
|
|
164
173
|
outDir = explicitOutDir;
|
|
165
174
|
}
|
|
166
|
-
const chartFile = path.join(chartDir, 'chart.ts')
|
|
167
|
-
|
|
175
|
+
const chartFile = SecurityUtils.validatePath(path.join(chartDir, 'chart.ts'), cwd, {
|
|
176
|
+
allowAbsolute: true,
|
|
177
|
+
});
|
|
178
|
+
const defaultOutDir = SecurityUtils.validatePath(path.join(chartDir, 'dist'), cwd, {
|
|
179
|
+
allowAbsolute: true,
|
|
180
|
+
});
|
|
168
181
|
if (!fs.existsSync(chartFile)) {
|
|
169
182
|
console.error('chart.ts not found. Run `tl init` first.');
|
|
170
183
|
process.exit(1);
|
|
@@ -177,7 +190,7 @@ async function cmdSynth(chartDirOrOutDir, flags, explicitOutDir) {
|
|
|
177
190
|
if (modifiedContent === originalContent) {
|
|
178
191
|
modifiedContent = originalContent.replace(/chart\.write\(['"][^'"]*['"]\)/, `chart.write('${safeOutDirLiteral}')`);
|
|
179
192
|
}
|
|
180
|
-
const tempChartFile = path.join(chartDir, '.timonel-temp-chart.ts');
|
|
193
|
+
const tempChartFile = SecurityUtils.validatePath(path.join(chartDir, '.timonel-temp-chart.ts'), cwd, { allowAbsolute: true });
|
|
181
194
|
fs.writeFileSync(tempChartFile, modifiedContent);
|
|
182
195
|
const wrapperScript = `
|
|
183
196
|
import { pathToFileURL } from 'url';
|
|
@@ -237,11 +250,34 @@ async function cmdValidate(flags) {
|
|
|
237
250
|
process.exit(result.status ?? 1);
|
|
238
251
|
}
|
|
239
252
|
}
|
|
253
|
+
function validateReleaseName(release) {
|
|
254
|
+
if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(release) || release.length > 53) {
|
|
255
|
+
console.error(`Invalid release name: ${SecurityUtils.sanitizeLogMessage(release)}`);
|
|
256
|
+
console.error('Release name must be lowercase alphanumeric with hyphens (max 53 chars)');
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function validateNamespace(namespace) {
|
|
261
|
+
if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(namespace) || namespace.length > 63) {
|
|
262
|
+
console.error(`Invalid namespace: ${SecurityUtils.sanitizeLogMessage(namespace)}`);
|
|
263
|
+
console.error('Namespace must be lowercase alphanumeric with hyphens (max 63 chars)');
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function validateSetFlag(setValue) {
|
|
268
|
+
if (!/^[a-zA-Z0-9._[\]-]+=.+$/.test(setValue)) {
|
|
269
|
+
console.error(`Invalid --set format: ${SecurityUtils.sanitizeLogMessage(setValue)}`);
|
|
270
|
+
console.error('Expected format: key=value');
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
240
274
|
async function cmdDeploy(release, namespace, flags) {
|
|
241
275
|
if (!release)
|
|
242
276
|
usageAndExit('Missing <release>');
|
|
277
|
+
validateReleaseName(release);
|
|
243
278
|
const args = ['upgrade', '--install', release, '.'];
|
|
244
279
|
if (namespace) {
|
|
280
|
+
validateNamespace(namespace);
|
|
245
281
|
args.push('--namespace', namespace);
|
|
246
282
|
}
|
|
247
283
|
if (flags?.env) {
|
|
@@ -256,6 +292,7 @@ async function cmdDeploy(release, namespace, flags) {
|
|
|
256
292
|
}
|
|
257
293
|
if (flags?.set) {
|
|
258
294
|
for (const setValue of flags.set) {
|
|
295
|
+
validateSetFlag(setValue);
|
|
259
296
|
args.push('--set', setValue);
|
|
260
297
|
}
|
|
261
298
|
}
|
|
@@ -465,7 +502,7 @@ async function cmdUmbrellaAdd(subchartPath, silent = false) {
|
|
|
465
502
|
fs.mkdirSync(subchartDir, { recursive: true });
|
|
466
503
|
const relativeSubchartPath = path.relative(chartsRoot, subchartDir) || subchartName;
|
|
467
504
|
const normalizedSubchartPath = relativeSubchartPath.split(path.sep).join('/');
|
|
468
|
-
const chartFile = path.join(subchartDir, 'chart.ts');
|
|
505
|
+
const chartFile = SecurityUtils.validatePath(path.join(subchartDir, 'chart.ts'), chartsRoot);
|
|
469
506
|
const { generateFlexibleSubchartTemplate } = await import('./lib/templates/flexible-subchart.js');
|
|
470
507
|
const subchartContent = generateFlexibleSubchartTemplate(subchartName);
|
|
471
508
|
fs.writeFileSync(chartFile, subchartContent);
|
|
@@ -479,8 +516,9 @@ async function cmdUmbrellaAdd(subchartPath, silent = false) {
|
|
|
479
516
|
log(`Subchart ${subchartName} added to umbrella at path '${normalizedSubchartPath}'`, silent);
|
|
480
517
|
}
|
|
481
518
|
async function cmdUmbrellaSynth(outDir, flags) {
|
|
482
|
-
const
|
|
483
|
-
const
|
|
519
|
+
const cwd = process.cwd();
|
|
520
|
+
const umbrellaFile = SecurityUtils.validatePath(path.join(cwd, UMBRELLA_FILE_NAME), cwd);
|
|
521
|
+
const defaultOutDir = SecurityUtils.validatePath(path.join(cwd, 'dist'), cwd);
|
|
484
522
|
if (!fs.existsSync(umbrellaFile)) {
|
|
485
523
|
console.error('umbrella.ts not found. Run `tl umbrella init` first.');
|
|
486
524
|
process.exit(1);
|
|
@@ -511,7 +549,8 @@ fs.mkdirSync(output, { recursive: true });
|
|
|
511
549
|
await Promise.resolve(runner(output, synthOptions));
|
|
512
550
|
console.log('Umbrella chart written to ' + output);
|
|
513
551
|
`;
|
|
514
|
-
const
|
|
552
|
+
const cwd = process.cwd();
|
|
553
|
+
const wrapperFile = SecurityUtils.validatePath(path.join(cwd, '.timonel-umbrella-wrapper.mjs'), cwd);
|
|
515
554
|
try {
|
|
516
555
|
fs.writeFileSync(wrapperFile, wrapperScript);
|
|
517
556
|
const result = spawnSync(process.execPath, [TSX_CLI_PATH, wrapperFile], {
|
package/dist/index.d.ts
CHANGED
|
@@ -12,5 +12,7 @@ export { DEFAULT_TERMINATION_GRACE_PERIOD, isValidDisruptionBudget, isValidKuber
|
|
|
12
12
|
export { AWS_HELPERS, createHelper, FILE_ACCESS_HELPERS, formatHelpers, generateHelpersTemplate, getDefaultHelpers, getHelpersByCategory, KUBERNETES_HELPERS, SPRIG_HELPERS, STANDARD_HELPERS, TEMPLATE_FUNCTION_HELPERS, type HelperDefinition, } from './lib/utils/helmHelpers.js';
|
|
13
13
|
export { createLogger, logger, LogLevel, TimonelLogger, type LogContext, type LoggerConfig, } from './lib/utils/logger.js';
|
|
14
14
|
export { UmbrellaRutter } from './lib/umbrellaRutter.js';
|
|
15
|
-
export { helmIf, helmRange, helmWith, helmInclude, helmDefine, helmVar, helmBlock, helmComment, helmFragment, createHelmExpression, isHelmConstruct, isHelmExpression, type HelmConstruct, type HelmExpression, type HelmContent, type HelmWhitespaceOptions, } from './lib/utils/helmControlStructures.js';
|
|
15
|
+
export { helmIf, helmIfSimple, helmRange, helmWith, helmInclude, helmDefine, helmVar, helmBlock, helmComment, helmFragment, createHelmExpression, isHelmConstruct, isHelmExpression, type HelmConstruct, type HelmExpression, type HelmContent, type HelmWhitespaceOptions, } from './lib/utils/helmControlStructures.js';
|
|
16
|
+
export { loadEnvVarsConfig, generateEnvVars, loadAndGenerateEnvVars, type EnvVarConfig, type LoadEnvVarsOptions, } from './lib/utils/envVarsLoader.js';
|
|
16
17
|
export { dumpHelmAwareYaml, validateHelmYaml, type HelmValidationError, type HelmValidationResult, } from './lib/utils/helmYamlSerializer.js';
|
|
18
|
+
export { valuesRef, isHelmValue, isHelmCondition, isHelmFieldConditional, isHelmRange, isHelmWith, serializeHelmValue, serializeHelmCondition, type HelmValue, type HelmCondition, type HelmFieldConditional, type HelmRange, type HelmWith, type HelmHelpers, } from './lib/utils/valuesRef.js';
|
package/dist/index.js
CHANGED
|
@@ -9,5 +9,7 @@ export { DEFAULT_TERMINATION_GRACE_PERIOD, isValidDisruptionBudget, isValidKuber
|
|
|
9
9
|
export { AWS_HELPERS, createHelper, FILE_ACCESS_HELPERS, formatHelpers, generateHelpersTemplate, getDefaultHelpers, getHelpersByCategory, KUBERNETES_HELPERS, SPRIG_HELPERS, STANDARD_HELPERS, TEMPLATE_FUNCTION_HELPERS, } from './lib/utils/helmHelpers.js';
|
|
10
10
|
export { createLogger, logger, LogLevel, TimonelLogger, } from './lib/utils/logger.js';
|
|
11
11
|
export { UmbrellaRutter } from './lib/umbrellaRutter.js';
|
|
12
|
-
export { helmIf, helmRange, helmWith, helmInclude, helmDefine, helmVar, helmBlock, helmComment, helmFragment, createHelmExpression, isHelmConstruct, isHelmExpression, } from './lib/utils/helmControlStructures.js';
|
|
12
|
+
export { helmIf, helmIfSimple, helmRange, helmWith, helmInclude, helmDefine, helmVar, helmBlock, helmComment, helmFragment, createHelmExpression, isHelmConstruct, isHelmExpression, } from './lib/utils/helmControlStructures.js';
|
|
13
|
+
export { loadEnvVarsConfig, generateEnvVars, loadAndGenerateEnvVars, } from './lib/utils/envVarsLoader.js';
|
|
13
14
|
export { dumpHelmAwareYaml, validateHelmYaml, } from './lib/utils/helmYamlSerializer.js';
|
|
15
|
+
export { valuesRef, isHelmValue, isHelmCondition, isHelmFieldConditional, isHelmRange, isHelmWith, serializeHelmValue, serializeHelmCondition, } from './lib/utils/valuesRef.js';
|
package/dist/lib/helm.js
CHANGED
|
@@ -36,6 +36,12 @@ export function indent(n, expr) {
|
|
|
36
36
|
.join('\n');
|
|
37
37
|
}
|
|
38
38
|
export function template(name, context = '.') {
|
|
39
|
+
if (!isValidHelmPath(name)) {
|
|
40
|
+
throw new Error(`Invalid template name: ${name}`);
|
|
41
|
+
}
|
|
42
|
+
if (!isValidHelmPath(context)) {
|
|
43
|
+
throw new Error(`Invalid template context: ${context}`);
|
|
44
|
+
}
|
|
39
45
|
return `{{ template "${name}" ${context} }}`;
|
|
40
46
|
}
|
|
41
47
|
export function include(name, context = '.') {
|
|
@@ -157,12 +163,33 @@ export function helmRange(collection, content, options = {}) {
|
|
|
157
163
|
if (!collection.startsWith('.')) {
|
|
158
164
|
throw new Error('Collection path must start with "." (e.g., ".Values.items")');
|
|
159
165
|
}
|
|
166
|
+
if (!isValidHelmPath(collection)) {
|
|
167
|
+
throw new Error(`Invalid collection path: ${collection}`);
|
|
168
|
+
}
|
|
160
169
|
const { keyValue = false, keyVar = '$key', valueVar = '$value', indexVar = '$index', itemVar = '$item', } = options;
|
|
170
|
+
const validateVarName = (varName, varType) => {
|
|
171
|
+
if (!varName.startsWith('$')) {
|
|
172
|
+
throw new Error(`${varType} must start with $ (e.g., $key, $value)`);
|
|
173
|
+
}
|
|
174
|
+
if (!/^\$[a-zA-Z_][a-zA-Z0-9_]*$/.test(varName)) {
|
|
175
|
+
throw new Error(`Invalid ${varType}: ${varName}`);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
if (keyValue) {
|
|
179
|
+
validateVarName(keyVar, 'keyVar');
|
|
180
|
+
validateVarName(valueVar, 'valueVar');
|
|
181
|
+
}
|
|
182
|
+
if (options.indexVar !== undefined) {
|
|
183
|
+
validateVarName(indexVar, 'indexVar');
|
|
184
|
+
}
|
|
185
|
+
if (options.itemVar !== undefined) {
|
|
186
|
+
validateVarName(itemVar, 'itemVar');
|
|
187
|
+
}
|
|
161
188
|
let rangeExpression;
|
|
162
189
|
if (keyValue) {
|
|
163
190
|
rangeExpression = `{{- range ${keyVar}, ${valueVar} := ${collection} }}`;
|
|
164
191
|
}
|
|
165
|
-
else if (options.indexVar && options.itemVar) {
|
|
192
|
+
else if (options.indexVar !== undefined && options.itemVar !== undefined) {
|
|
166
193
|
rangeExpression = `{{- range ${indexVar}, ${itemVar} := ${collection} }}`;
|
|
167
194
|
}
|
|
168
195
|
else {
|
|
@@ -2,7 +2,7 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { SecurityUtils } from './security.js';
|
|
4
4
|
import { createLogger } from './utils/logger.js';
|
|
5
|
-
import { dumpHelmAwareYaml } from './utils/helmYamlSerializer.js';
|
|
5
|
+
import { dumpHelmAwareYaml, postProcessFieldConditionals } from './utils/helmYamlSerializer.js';
|
|
6
6
|
export class HelmChartWriter {
|
|
7
7
|
static write(opts) {
|
|
8
8
|
const { outDir, meta, defaultValues = {}, envValues = {}, assets, helpersTpl, notesTpl, valuesSchema, logger: customLogger, } = opts;
|
|
@@ -35,7 +35,8 @@ export class HelmChartWriter {
|
|
|
35
35
|
timer();
|
|
36
36
|
}
|
|
37
37
|
static createDirectories(outDir) {
|
|
38
|
-
|
|
38
|
+
const templatesDir = SecurityUtils.validatePath(path.join(outDir, 'templates'), outDir);
|
|
39
|
+
fs.mkdirSync(templatesDir, { recursive: true });
|
|
39
40
|
}
|
|
40
41
|
static writeChartYaml(outDir, meta) {
|
|
41
42
|
const chartYaml = dumpHelmAwareYaml({
|
|
@@ -53,13 +54,16 @@ export class HelmChartWriter {
|
|
|
53
54
|
icon: meta.icon,
|
|
54
55
|
dependencies: meta.dependencies,
|
|
55
56
|
});
|
|
56
|
-
|
|
57
|
+
const chartYamlPath = SecurityUtils.validatePath(path.join(outDir, 'Chart.yaml'), outDir);
|
|
58
|
+
fs.writeFileSync(chartYamlPath, chartYaml);
|
|
57
59
|
}
|
|
58
60
|
static writeValuesFiles(outDir, defaultValues, envValues) {
|
|
59
|
-
|
|
61
|
+
const valuesPath = SecurityUtils.validatePath(path.join(outDir, 'values.yaml'), outDir);
|
|
62
|
+
fs.writeFileSync(valuesPath, dumpHelmAwareYaml(defaultValues));
|
|
60
63
|
for (const [env, values] of Object.entries(envValues)) {
|
|
61
64
|
const sanitizedEnv = SecurityUtils.sanitizeEnvironmentName(env);
|
|
62
|
-
|
|
65
|
+
const envValuesPath = SecurityUtils.validatePath(path.join(outDir, `values-${sanitizedEnv}.yaml`), outDir);
|
|
66
|
+
fs.writeFileSync(envValuesPath, dumpHelmAwareYaml(values));
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
static writeAssets(outDir, assets) {
|
|
@@ -77,20 +81,23 @@ export class HelmChartWriter {
|
|
|
77
81
|
.map((h) => [`{{- define "${h.name}" -}}`, h.template.trimEnd(), '{{- end }}', ''].join('\n'))
|
|
78
82
|
.join('\n');
|
|
79
83
|
}
|
|
80
|
-
|
|
84
|
+
const helpersPath = SecurityUtils.validatePath(path.join(outDir, 'templates', '_helpers.tpl'), outDir);
|
|
85
|
+
fs.writeFileSync(helpersPath, content);
|
|
81
86
|
}
|
|
82
87
|
static writeNotes(outDir, notesTpl) {
|
|
83
88
|
if (!notesTpl)
|
|
84
89
|
return;
|
|
85
|
-
|
|
90
|
+
const notesPath = SecurityUtils.validatePath(path.join(outDir, 'templates', 'NOTES.txt'), outDir);
|
|
91
|
+
fs.writeFileSync(notesPath, notesTpl.endsWith('\n') ? notesTpl : notesTpl + '\n');
|
|
86
92
|
}
|
|
87
93
|
static writeSchema(outDir, valuesSchema) {
|
|
88
94
|
if (!valuesSchema)
|
|
89
95
|
return;
|
|
90
|
-
|
|
96
|
+
const schemaPath = SecurityUtils.validatePath(path.join(outDir, 'values.schema.json'), outDir);
|
|
97
|
+
fs.writeFileSync(schemaPath, JSON.stringify(valuesSchema, null, 2) + '\n');
|
|
91
98
|
}
|
|
92
99
|
static writeHelmIgnore(outDir) {
|
|
93
|
-
const helmIgnorePath = path.join(outDir, '.helmignore');
|
|
100
|
+
const helmIgnorePath = SecurityUtils.validatePath(path.join(outDir, '.helmignore'), outDir);
|
|
94
101
|
if (!fs.existsSync(helmIgnorePath)) {
|
|
95
102
|
const helmIgnore = [
|
|
96
103
|
'# VCS',
|
|
@@ -149,19 +156,33 @@ function writeSingleAssetFile(outDir, targetDir, directorySegments, fileBaseName
|
|
|
149
156
|
const chartSubdir = path.join(outDir, targetDir, ...directorySegments);
|
|
150
157
|
SecurityUtils.validatePath(chartSubdir, outDir);
|
|
151
158
|
fs.mkdirSync(chartSubdir, { recursive: true });
|
|
159
|
+
if (fileBaseName.includes('..') || fileBaseName.includes('/') || fileBaseName.includes('\\')) {
|
|
160
|
+
throw new Error(`Invalid fileBaseName: ${fileBaseName}`);
|
|
161
|
+
}
|
|
152
162
|
const filename = `${fileBaseName}.yaml`;
|
|
163
|
+
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
|
|
164
|
+
throw new Error(`Invalid filename: ${filename}`);
|
|
165
|
+
}
|
|
153
166
|
const absolutePath = path.join(chartSubdir, filename);
|
|
154
167
|
SecurityUtils.validatePath(absolutePath, outDir);
|
|
155
|
-
|
|
168
|
+
const processedYaml = postProcessFieldConditionals(yaml);
|
|
169
|
+
fs.writeFileSync(absolutePath, processedYaml.endsWith('\n') ? processedYaml : `${processedYaml}\n`);
|
|
156
170
|
}
|
|
157
171
|
function writeMultipleAssetFiles(outDir, targetDir, directorySegments, fileBaseName, yaml) {
|
|
158
172
|
const chartSubdir = path.join(outDir, targetDir, ...directorySegments);
|
|
159
173
|
SecurityUtils.validatePath(chartSubdir, outDir);
|
|
160
174
|
fs.mkdirSync(chartSubdir, { recursive: true });
|
|
161
|
-
const
|
|
175
|
+
const processedYaml = postProcessFieldConditionals(yaml);
|
|
176
|
+
const parts = splitDocs(processedYaml);
|
|
162
177
|
parts.forEach((doc, index) => {
|
|
178
|
+
if (fileBaseName.includes('..') || fileBaseName.includes('/') || fileBaseName.includes('\\')) {
|
|
179
|
+
throw new Error(`Invalid fileBaseName: ${fileBaseName}`);
|
|
180
|
+
}
|
|
163
181
|
const suffix = parts.length > 1 ? `-${index + 1}` : '';
|
|
164
182
|
const filename = `${fileBaseName}${suffix}.yaml`;
|
|
183
|
+
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
|
|
184
|
+
throw new Error(`Invalid filename: ${filename}`);
|
|
185
|
+
}
|
|
165
186
|
const absolutePath = path.join(chartSubdir, filename);
|
|
166
187
|
SecurityUtils.validatePath(absolutePath, outDir);
|
|
167
188
|
fs.writeFileSync(absolutePath, doc.endsWith('\n') ? doc : `${doc}\n`);
|
|
@@ -48,6 +48,11 @@ export class BaseResourceProvider {
|
|
|
48
48
|
createRootLevelApiObject(name, apiVersion, kind, fields, labels, annotations) {
|
|
49
49
|
this.validateKubernetesName(name, kind);
|
|
50
50
|
this.validateLabels(labels, kind);
|
|
51
|
+
const reservedKeys = ['apiVersion', 'kind', 'metadata'];
|
|
52
|
+
const conflictingKeys = Object.keys(fields).filter((key) => reservedKeys.includes(key));
|
|
53
|
+
if (conflictingKeys.length > 0) {
|
|
54
|
+
throw new Error(`Fields object contains reserved keys: ${conflictingKeys.join(', ')}. These keys cannot be overridden.`);
|
|
55
|
+
}
|
|
51
56
|
return new ApiObject(this.chart, name, {
|
|
52
57
|
apiVersion,
|
|
53
58
|
kind,
|
|
@@ -120,7 +120,8 @@ export class AWSResources extends BaseResourceProvider {
|
|
|
120
120
|
throw new Error(`Ingress path "${path}" must start with /`);
|
|
121
121
|
}
|
|
122
122
|
if (normalizedPath.includes('../') || normalizedPath.includes('./')) {
|
|
123
|
-
|
|
123
|
+
const sanitizedPath = path.replace(/[\r\n]/g, '');
|
|
124
|
+
console.warn(`Warning: Ingress path "${sanitizedPath}" contains path traversal-like sequences`);
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
validatePathType(pathType) {
|