timonel 2.8.1 → 2.9.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cli.d.ts +0 -1
  3. package/dist/cli.js +274 -634
  4. package/dist/index.d.ts +4 -6
  5. package/dist/index.js +4 -7
  6. package/dist/lib/helm.d.ts +0 -472
  7. package/dist/lib/helm.js +0 -481
  8. package/dist/lib/helmChartWriter.d.ts +0 -178
  9. package/dist/lib/helmChartWriter.js +27 -180
  10. package/dist/lib/resources/baseResourceProvider.d.ts +0 -46
  11. package/dist/lib/resources/baseResourceProvider.js +1 -47
  12. package/dist/lib/resources/cloud/aws/awsResources.d.ts +0 -144
  13. package/dist/lib/resources/cloud/aws/awsResources.js +1 -163
  14. package/dist/lib/resources/cloud/aws/karpenterResources.d.ts +0 -132
  15. package/dist/lib/resources/cloud/aws/karpenterResources.js +0 -75
  16. package/dist/lib/rutter.d.ts +28 -340
  17. package/dist/lib/rutter.js +291 -371
  18. package/dist/lib/security.d.ts +0 -119
  19. package/dist/lib/security.js +4 -160
  20. package/dist/lib/templates/basic-chart.d.ts +19 -0
  21. package/dist/lib/templates/basic-chart.js +189 -0
  22. package/dist/lib/templates/subchart.d.ts +27 -0
  23. package/dist/lib/templates/subchart.js +224 -0
  24. package/dist/lib/templates/umbrella-chart.d.ts +13 -0
  25. package/dist/lib/templates/umbrella-chart.js +222 -0
  26. package/dist/lib/types.d.ts +20 -0
  27. package/dist/lib/types.js +1 -0
  28. package/dist/lib/umbrella.d.ts +0 -24
  29. package/dist/lib/umbrella.js +0 -24
  30. package/dist/lib/umbrellaRutter.d.ts +0 -71
  31. package/dist/lib/umbrellaRutter.js +2 -82
  32. package/dist/lib/utils/helmHelpers.d.ts +18 -41
  33. package/dist/lib/utils/helmHelpers.js +246 -35
  34. package/package.json +9 -7
  35. package/README.md +0 -74
  36. package/dist/cli.d.ts.map +0 -1
  37. package/dist/cli.js.map +0 -1
  38. package/dist/index.d.ts.map +0 -1
  39. package/dist/index.js.map +0 -1
  40. package/dist/lib/helm.d.ts.map +0 -1
  41. package/dist/lib/helm.js.map +0 -1
  42. package/dist/lib/helmChartWriter.d.ts.map +0 -1
  43. package/dist/lib/helmChartWriter.js.map +0 -1
  44. package/dist/lib/resources/baseResourceProvider.d.ts.map +0 -1
  45. package/dist/lib/resources/baseResourceProvider.js.map +0 -1
  46. package/dist/lib/resources/cloud/aws/awsResources.d.ts.map +0 -1
  47. package/dist/lib/resources/cloud/aws/awsResources.js.map +0 -1
  48. package/dist/lib/resources/cloud/aws/karpenterResources.d.ts.map +0 -1
  49. package/dist/lib/resources/cloud/aws/karpenterResources.js.map +0 -1
  50. package/dist/lib/rutter.d.ts.map +0 -1
  51. package/dist/lib/rutter.js.map +0 -1
  52. package/dist/lib/security.d.ts.map +0 -1
  53. package/dist/lib/security.js.map +0 -1
  54. package/dist/lib/umbrella.d.ts.map +0 -1
  55. package/dist/lib/umbrella.js.map +0 -1
  56. package/dist/lib/umbrellaRutter.d.ts.map +0 -1
  57. package/dist/lib/umbrellaRutter.js.map +0 -1
  58. package/dist/lib/utils/helmHelpers.d.ts.map +0 -1
  59. package/dist/lib/utils/helmHelpers.js.map +0 -1
@@ -1,304 +1,59 @@
1
1
  import { ApiObject, App, Chart, Testing } from 'cdk8s';
2
+ import Handlebars from 'handlebars';
2
3
  import YAML from 'yaml';
3
4
  import { helm, include } from './helm.js';
4
5
  import { HelmChartWriter } from './helmChartWriter.js';
5
6
  import { AWSResources } from './resources/cloud/aws/awsResources.js';
6
7
  import { KarpenterResources } from './resources/cloud/aws/karpenterResources.js';
7
8
  import { generateHelpersTemplate } from './utils/helmHelpers.js';
8
- /**
9
- * Rutter class with modular architecture
10
- * Provides a clean API while delegating to specialized resource providers
11
- * Maintains full backward compatibility with the original Rutter class
12
- *
13
- * @since 2.4.0
14
- */
15
9
  export class Rutter {
16
10
  constructor(props) {
17
11
  this.assets = [];
18
- this.props = props;
19
- this.meta = props.meta;
20
12
  this.defaultValues = props.defaultValues ?? {};
21
13
  this.envValues = props.envValues ?? {};
22
- // Create CDK8s infrastructure
14
+ this.meta = props.meta;
15
+ this.props = props;
23
16
  this.app = new App();
24
17
  this.chart = new Chart(this.app, props.meta.name, {
25
18
  ...props.chartProps,
26
19
  ...(props.namespace ? { namespace: props.namespace } : {}),
27
20
  });
28
- // Initialize resource providers
29
21
  this.awsResources = new AWSResources(this.chart);
30
22
  this.karpenterResources = new KarpenterResources(this.chart);
31
23
  }
32
- // AWS Resources
33
- /**
34
- * Creates an AWS EBS StorageClass
35
- * @param spec - EBS StorageClass specification
36
- * @returns Created StorageClass ApiObject
37
- *
38
- * @example
39
- * ```typescript
40
- * rutter.addAWSEBSStorageClass({
41
- * name: 'fast-ebs',
42
- * volumeType: 'gp3',
43
- * encrypted: true,
44
- * allowVolumeExpansion: true
45
- * });
46
- * ```
47
- *
48
- * @since 1.0.0
49
- */
50
24
  addAWSEBSStorageClass(spec) {
51
25
  return this.awsResources.addEBSStorageClass(spec);
52
26
  }
53
- /**
54
- * Creates an AWS EFS StorageClass
55
- * @param spec - EFS StorageClass specification
56
- * @returns Created StorageClass ApiObject
57
- *
58
- * @example
59
- * ```typescript
60
- * rutter.addAWSEFSStorageClass({
61
- * name: 'efs-storage',
62
- * fileSystemId: 'fs-12345678',
63
- * directoryPerms: '0755'
64
- * });
65
- * ```
66
- *
67
- * @since 1.0.0
68
- */
69
27
  addAWSEFSStorageClass(spec) {
70
28
  return this.awsResources.addEFSStorageClass(spec);
71
29
  }
72
- /**
73
- * Creates an AWS IRSA ServiceAccount
74
- * @param spec - IRSA ServiceAccount specification
75
- * @returns Created ServiceAccount ApiObject
76
- *
77
- * @example
78
- * ```typescript
79
- * rutter.addAWSIRSAServiceAccount({
80
- * name: 'my-service-account',
81
- * roleArn: 'arn:aws:iam::ACCOUNT_ID:role/MyRole'
82
- * });
83
- * ```
84
- *
85
- * @since 1.0.0
86
- */
87
30
  addAWSIRSAServiceAccount(spec) {
88
31
  return this.awsResources.addIRSAServiceAccount(spec);
89
32
  }
90
- /**
91
- * Creates an AWS ALB Ingress
92
- * @param spec - ALB Ingress specification
93
- * @returns Created Ingress ApiObject
94
- *
95
- * @example
96
- * ```typescript
97
- * rutter.addAWSALBIngress({
98
- * name: 'web-ingress',
99
- * rules: [{
100
- * paths: [{
101
- * path: '/',
102
- * pathType: 'Prefix',
103
- * backend: { service: { name: 'web-service', port: { number: 80 } } }
104
- * }]
105
- * }],
106
- * scheme: 'internet-facing'
107
- * });
108
- * ```
109
- *
110
- * @since 1.0.0
111
- */
112
33
  addAWSALBIngress(spec) {
113
34
  return this.awsResources.addALBIngress(spec);
114
35
  }
115
- // AWS Karpenter Resources
116
- /**
117
- * Creates a Karpenter NodePool resource
118
- * @param spec - NodePool specification
119
- * @returns Created NodePool ApiObject
120
- *
121
- * @example
122
- * ```typescript
123
- * rutter.addKarpenterNodePool({
124
- * name: 'default-nodepool',
125
- * template: {
126
- * spec: {
127
- * nodeClassRef: {
128
- * apiVersion: 'karpenter.k8s.aws/v1beta1',
129
- * kind: 'EC2NodeClass',
130
- * name: 'default'
131
- * }
132
- * }
133
- * }
134
- * });
135
- * ```
136
- *
137
- * @since 2.7.0
138
- */
139
36
  addKarpenterNodePool(spec) {
140
37
  return this.karpenterResources.addKarpenterNodePool(spec);
141
38
  }
142
- /**
143
- * Creates a Karpenter NodeClaim resource
144
- * @param spec - NodeClaim specification
145
- * @returns Created NodeClaim ApiObject
146
- *
147
- * @example
148
- * ```typescript
149
- * rutter.addKarpenterNodeClaim({
150
- * name: 'my-node-claim',
151
- * nodeClassRef: {
152
- * apiVersion: 'karpenter.k8s.aws/v1beta1',
153
- * kind: 'EC2NodeClass',
154
- * name: 'default'
155
- * }
156
- * });
157
- * ```
158
- *
159
- * @since 2.7.0
160
- */
161
39
  addKarpenterNodeClaim(spec) {
162
40
  return this.karpenterResources.addKarpenterNodeClaim(spec);
163
41
  }
164
- /**
165
- * Creates a Karpenter EC2NodeClass resource
166
- * @param spec - EC2NodeClass specification
167
- * @returns Created EC2NodeClass ApiObject
168
- *
169
- * @example
170
- * ```typescript
171
- * rutter.addKarpenterEC2NodeClass({
172
- * name: 'default',
173
- * amiFamily: 'AL2',
174
- * subnetSelectorTerms: [{ tags: { 'karpenter.sh/discovery': 'my-cluster' } }],
175
- * securityGroupSelectorTerms: [{ tags: { 'karpenter.sh/discovery': 'my-cluster' } }]
176
- * });
177
- * ```
178
- *
179
- * @since 2.7.0
180
- */
181
42
  addKarpenterEC2NodeClass(spec) {
182
43
  return this.karpenterResources.addKarpenterEC2NodeClass(spec);
183
44
  }
184
- /**
185
- * Creates a Karpenter NodePool with optimized disruption settings for cost efficiency
186
- * @param spec - NodePool specification with disruption optimization
187
- * @returns Created NodePool ApiObject
188
- *
189
- * @example
190
- * ```typescript
191
- * rutter.addKarpenterNodePoolWithDisruption({
192
- * name: 'cost-optimized',
193
- * nodeClassRef: {
194
- * apiVersion: 'karpenter.k8s.aws/v1beta1',
195
- * kind: 'EC2NodeClass',
196
- * name: 'default'
197
- * },
198
- * consolidationPolicy: 'WhenEmptyOrUnderutilized',
199
- * consolidateAfter: '30s',
200
- * expireAfter: '24h',
201
- * disruptionBudgets: [
202
- * { nodes: '20%' },
203
- * { nodes: '0', schedule: '0 9 * * 1-5', duration: '8h' }
204
- * ]
205
- * });
206
- * ```
207
- *
208
- * @since 2.7.1
209
- */
210
45
  addKarpenterNodePoolWithDisruption(spec) {
211
46
  return this.karpenterResources.addKarpenterNodePoolWithDisruption(spec);
212
47
  }
213
- /**
214
- * Creates a Karpenter NodePool with scheduling constraints and priorities
215
- * @param spec - NodePool specification with scheduling optimization
216
- * @returns Created NodePool ApiObject
217
- *
218
- * @example
219
- * ```typescript
220
- * rutter.addKarpenterNodePoolWithScheduling({
221
- * name: 'gpu-workloads',
222
- * nodeClassRef: {
223
- * apiVersion: 'karpenter.k8s.aws/v1beta1',
224
- * kind: 'EC2NodeClass',
225
- * name: 'gpu-nodeclass'
226
- * },
227
- * requirements: [
228
- * { key: 'node.kubernetes.io/instance-type', operator: 'In', values: ['g4dn.xlarge', 'g4dn.2xlarge'] }
229
- * ],
230
- * taints: [
231
- * { key: 'nvidia.com/gpu', effect: 'NoSchedule' }
232
- * ],
233
- * terminationGracePeriod: '60s',
234
- * weight: 100
235
- * });
236
- * ```
237
- *
238
- * @since 2.7.1
239
- */
240
48
  addKarpenterNodePoolWithScheduling(spec) {
241
49
  return this.karpenterResources.addKarpenterNodePoolWithScheduling(spec);
242
50
  }
243
- // AWS ECR ServiceAccount
244
- /**
245
- * Creates a ServiceAccount with ECR access annotations
246
- * @param spec - ECR ServiceAccount specification
247
- * @returns Created ServiceAccount ApiObject
248
- *
249
- * @example
250
- * ```typescript
251
- * rutter.addAWSECRServiceAccount({
252
- * name: 'ecr-service-account',
253
- * roleArn: 'arn:aws:iam::ACCOUNT_ID:role/ECRAccessRole'
254
- * });
255
- * ```
256
- *
257
- * @since 2.7.0
258
- */
259
51
  addAWSECRServiceAccount(spec) {
260
52
  return this.awsResources.addECRServiceAccount(spec);
261
53
  }
262
- // Utility methods
263
- /**
264
- * Adds a Kubernetes manifest to the chart using CDK8S
265
- *
266
- * This method allows you to add any Kubernetes manifest (CRDs, custom resources, etc.)
267
- * either as YAML strings or as objects. The manifest will be processed using CDK8S
268
- * for better type safety and validation.
269
- *
270
- * @param yamlOrObject - Kubernetes manifest as YAML string or object
271
- * @param id - Unique identifier for the manifest (required for multiple manifests)
272
- *
273
- * @example
274
- * ```typescript
275
- * // Using YAML string for CRD
276
- * rutter.addManifest(`
277
- * apiVersion: apiextensions.k8s.io/v1
278
- * kind: CustomResourceDefinition
279
- * metadata:
280
- * name: example.com
281
- * spec:
282
- * group: example.com
283
- * versions: []
284
- * `, 'my-crd');
285
- *
286
- * // Using object for any Kubernetes resource
287
- * rutter.addManifest({
288
- * apiVersion: 'v1',
289
- * kind: 'ConfigMap',
290
- * metadata: { name: 'my-config' },
291
- * data: { key: 'value' }
292
- * }, 'my-configmap');
293
- * ```
294
- *
295
- * @since 3.0.0
296
- * @note Consider using cdk8s-plus constructs for better type safety when available
297
- */
298
54
  addManifest(yamlOrObject, id) {
299
55
  let manifestObject;
300
56
  if (typeof yamlOrObject === 'string') {
301
- // Parse YAML string to object
302
57
  try {
303
58
  manifestObject = YAML.parse(yamlOrObject);
304
59
  }
@@ -307,15 +62,12 @@ export class Rutter {
307
62
  }
308
63
  }
309
64
  else if (typeof yamlOrObject === 'object' && yamlOrObject !== null) {
310
- // Use object directly
311
65
  manifestObject = yamlOrObject;
312
66
  }
313
67
  else {
314
68
  throw new Error('addManifest() requires either a YAML string or an object');
315
69
  }
316
- // Validate basic Kubernetes manifest structure
317
70
  this.validateManifestStructure(manifestObject);
318
- // Create CDK8S ApiObject from the manifest
319
71
  return new ApiObject(this.chart, id, {
320
72
  apiVersion: manifestObject['apiVersion'],
321
73
  kind: manifestObject['kind'],
@@ -323,55 +75,57 @@ export class Rutter {
323
75
  spec: manifestObject['spec'],
324
76
  });
325
77
  }
326
- /**
327
- * Adds a Kubernetes manifest wrapped in a Helm conditional
328
- *
329
- * This method creates a manifest that will only be rendered if the specified
330
- * condition evaluates to true. The condition is checked against .Values in Helm.
331
- *
332
- * @param manifestObject - JavaScript object representing the Kubernetes manifest
333
- * @param condition - Path to the condition value in .Values (e.g., 'createNamespace')
334
- * @param id - Unique identifier for this manifest within the chart
335
- * @returns The created ApiObject instance
336
- * @throws {Error} If the manifest structure is incorrect or condition path is invalid
337
- *
338
- * @example
339
- * ```typescript
340
- * // Add a namespace that's only created if .Values.createNamespace is true
341
- * rutter.addConditionalManifest(
342
- * {
343
- * apiVersion: 'v1',
344
- * kind: 'Namespace',
345
- * metadata: {
346
- * name: valuesRef('namespace'),
347
- * labels: {
348
- * environment: conditionalRef('environment', 'environment')
349
- * }
350
- * }
351
- * },
352
- * 'createNamespace',
353
- * 'namespace'
354
- * );
355
- * ```
356
- *
357
- * @since 2.7.4
358
- */
78
+ addTemplateManifest(yamlTemplate, id) {
79
+ const templateAsset = {
80
+ id,
81
+ yaml: yamlTemplate,
82
+ target: 'templates',
83
+ };
84
+ this.assets.push(templateAsset);
85
+ return new ApiObject(this.chart, id, {
86
+ apiVersion: 'v1',
87
+ kind: 'ConfigMap',
88
+ metadata: { name: id },
89
+ });
90
+ }
359
91
  addConditionalManifest(manifestObject, condition, id) {
360
- // Validate condition path
361
92
  if (!condition || typeof condition !== 'string') {
362
93
  throw new Error('Condition must be a non-empty string');
363
94
  }
364
- // Validate basic Kubernetes manifest structure
365
95
  this.validateManifestStructure(manifestObject);
366
- // Store the manifest as a conditional asset that will be processed during write
367
- const conditionalAsset = {
368
- id,
369
- yaml: `{{- if .Values.${condition} }}\n${YAML.stringify(manifestObject)}{{- end }}`,
370
- target: 'templates',
371
- };
372
- this.assets.push(conditionalAsset);
373
- // Create a placeholder CDK8S ApiObject for consistency
374
- return new ApiObject(this.chart, id, {
96
+ let helmCondition;
97
+ if (condition.startsWith('{{') && condition.endsWith('}}')) {
98
+ helmCondition = condition.slice(2, -2).trim();
99
+ }
100
+ else {
101
+ helmCondition = `.Values.${condition}`;
102
+ }
103
+ try {
104
+ const yamlContent = YAML.stringify(manifestObject, {
105
+ 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
+ });
118
+ const conditionalAsset = {
119
+ id,
120
+ yaml: conditionalYaml,
121
+ target: 'templates',
122
+ };
123
+ this.assets.push(conditionalAsset);
124
+ }
125
+ catch (error) {
126
+ throw new Error(`Failed to compile Handlebars template for manifest '${id}': ${error instanceof Error ? error.message : 'Unknown error'}`);
127
+ }
128
+ return new ApiObject(this.chart, `${id}-placeholder`, {
375
129
  apiVersion: manifestObject['apiVersion'],
376
130
  kind: manifestObject['kind'],
377
131
  metadata: {
@@ -379,17 +133,12 @@ export class Rutter {
379
133
  annotations: {
380
134
  ...(manifestObject['metadata']?.['annotations'] || {}),
381
135
  'timonel.sh/conditional': condition,
136
+ 'timonel.sh/placeholder': 'true',
382
137
  },
383
138
  },
384
139
  spec: manifestObject['spec'],
385
140
  });
386
141
  }
387
- /**
388
- * Validates the structure of a Kubernetes manifest object
389
- * @param manifest - The manifest object to validate
390
- * @private
391
- * @since 3.0.0
392
- */
393
142
  validateManifestStructure(manifest) {
394
143
  if (!manifest['apiVersion'] || typeof manifest['apiVersion'] !== 'string') {
395
144
  throw new Error('Kubernetes manifest must have a valid apiVersion');
@@ -406,7 +155,6 @@ export class Rutter {
406
155
  if (!metadata['name'] || typeof metadata['name'] !== 'string') {
407
156
  throw new Error('Kubernetes manifest metadata must have a valid name');
408
157
  }
409
- // Additional validation for specific resource types
410
158
  const kind = manifest['kind'];
411
159
  if (kind === 'CustomResourceDefinition') {
412
160
  if (!manifest['spec'] || typeof manifest['spec'] !== 'object' || manifest['spec'] === null) {
@@ -421,59 +169,35 @@ export class Rutter {
421
169
  }
422
170
  }
423
171
  }
424
- /**
425
- * Gets chart metadata
426
- * @returns Chart metadata
427
- *
428
- * @since 1.0.0
429
- */
430
172
  getMeta() {
431
173
  return { ...this.meta };
432
174
  }
433
- /**
434
- * Gets default values
435
- * @returns Default values object
436
- *
437
- * @since 1.0.0
438
- */
439
175
  getDefaultValues() {
440
176
  return { ...this.defaultValues };
441
177
  }
442
- /**
443
- * Gets environment-specific values
444
- * @returns Environment values object
445
- *
446
- * @since 1.0.0
447
- */
448
178
  getEnvValues() {
449
179
  return { ...this.envValues };
450
180
  }
451
- /**
452
- * Gets all assets (CRDs, etc.)
453
- * @returns Array of assets
454
- *
455
- * @since 1.0.0
456
- */
457
181
  getAssets() {
458
182
  return [...this.assets];
459
183
  }
460
- /**
461
- * Converts the chart to SynthAsset array for HelmChartWriter
462
- * @returns Array of synthesized assets
463
- *
464
- * @since 2.4.0
465
- */
466
184
  toSynthArray() {
467
- // Get ApiObject IDs before synthesis
185
+ console.log('🔍 toSynthArray called');
468
186
  const apiObjectIds = [];
469
187
  for (const child of this.chart.node.children) {
470
- if (child instanceof ApiObject) {
188
+ if (child instanceof ApiObject && !child.node.id.endsWith('-placeholder')) {
471
189
  apiObjectIds.push(child.node.id);
472
190
  }
473
191
  }
474
- // Use cdk8s Testing.synth to obtain manifest objects
475
- const manifestObjs = Testing.synth(this.chart);
476
- // Enforce common labels best-practice on all rendered objects
192
+ const allManifestObjs = Testing.synth(this.chart);
193
+ const manifestObjs = allManifestObjs.filter((obj) => {
194
+ if (obj && typeof obj === 'object') {
195
+ const o = obj;
196
+ const annotations = o.metadata?.annotations || {};
197
+ return annotations['timonel.sh/placeholder'] !== 'true';
198
+ }
199
+ return true;
200
+ });
477
201
  const enriched = manifestObjs.map((obj) => {
478
202
  if (obj && typeof obj === 'object') {
479
203
  const o = obj;
@@ -488,10 +212,8 @@ export class Rutter {
488
212
  'app.kubernetes.io/managed-by': '{{ .Release.Service }}',
489
213
  'app.kubernetes.io/part-of': helm.chartName,
490
214
  };
491
- // Apply defaults only if not already set
492
215
  for (const [key, value] of Object.entries(defaults)) {
493
216
  if (!(key in labels)) {
494
- // eslint-disable-next-line security/detect-object-injection -- Safe: controlled label assignment
495
217
  labels[key] = value;
496
218
  }
497
219
  }
@@ -499,8 +221,8 @@ export class Rutter {
499
221
  return obj;
500
222
  });
501
223
  const synthAssets = [];
224
+ console.log('🔍 singleManifestFile:', this.props.singleManifestFile);
502
225
  if (this.props.singleManifestFile) {
503
- // Combine all resources into single manifest
504
226
  const combinedYaml = enriched
505
227
  .map((obj) => this.processHelmTemplates(YAML.stringify(obj).trim()))
506
228
  .filter(Boolean)
@@ -509,47 +231,245 @@ export class Rutter {
509
231
  synthAssets.push({ id: manifestId, yaml: combinedYaml });
510
232
  }
511
233
  else {
512
- // Create separate files for each resource (default behavior)
513
234
  enriched.forEach((obj, index) => {
235
+ const apiObjectId = apiObjectIds[index];
236
+ const manifestId = apiObjectId || `manifest-${index + 1}`;
237
+ console.log(`🔍 Processing asset ${manifestId} (index ${index})`);
514
238
  const yaml = this.processHelmTemplates(YAML.stringify(obj).trim());
515
239
  if (yaml) {
516
- // Use descriptive name from ApiObject ID if available, otherwise fallback to generic name
517
- // eslint-disable-next-line security/detect-object-injection
518
- const apiObjectId = apiObjectIds[index];
519
- const manifestId = apiObjectId || `manifest-${index + 1}`;
520
240
  synthAssets.push({ id: manifestId, yaml });
521
241
  }
522
242
  });
523
243
  }
524
- // Add any additional assets (CRDs, etc.)
525
244
  this.assets.forEach((asset) => {
526
245
  synthAssets.push({ id: asset.id, yaml: asset.yaml });
527
246
  });
528
247
  return synthAssets;
529
248
  }
530
- /**
531
- * Process YAML content to remove quotes from Helm templates
532
- * This fixes the issue where YAML.stringify adds quotes around Helm template expressions
533
- * Removes quotes from all Helm template expressions
534
- */
535
249
  processHelmTemplates(yaml) {
536
- // Remove quotes from Helm template expressions
537
- // Match patterns like "{{ .Values.replicas | int }}", "{{ .Release.Name }}", etc.
538
- // Handle both key-value pairs and standalone values
539
- return yaml
540
- .replace(/": "\{\{([^}]+)\}\}"/g, ': {{ $1 }}')
541
- .replace(/: "\{\{([^}]+)\}\}"/g, ': {{ $1 }}')
542
- .replace(/"\{\{([^}]+)\}\}"/g, '{{ $1 }}')
543
- .replace(/": "\.nan"/g, ': .nan');
544
- }
545
- /**
546
- * Writes the Helm chart to the specified output directory
547
- * @param outDir - Output directory path
548
- *
549
- * @since 1.0.0
550
- */
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
+ let processed = this.fixCharacterMappingIssues(yaml);
257
+ processed = this.applyHelmTemplateReplacements(processed);
258
+ console.log('✅ processHelmTemplates completed');
259
+ return processed;
260
+ }
261
+ fixCharacterMappingIssues(yaml) {
262
+ const lines = yaml.split('\n');
263
+ const fixedLines = [];
264
+ let i = 0;
265
+ while (i < lines.length) {
266
+ const line = lines[i];
267
+ if (!line) {
268
+ i++;
269
+ continue;
270
+ }
271
+ const charKeyMatch = line.match(/^(\s*)"(\d+)":\s*(.+)$/);
272
+ if (charKeyMatch && charKeyMatch[2] && parseInt(charKeyMatch[2]) === 0) {
273
+ const indent = charKeyMatch[1] || '';
274
+ const result = this.processCharacterMapping(lines, i, indent);
275
+ if (result.reconstructed) {
276
+ fixedLines.push(result.reconstructed);
277
+ i = result.nextIndex;
278
+ continue;
279
+ }
280
+ }
281
+ if (line) {
282
+ fixedLines.push(line);
283
+ }
284
+ i++;
285
+ }
286
+ return fixedLines.join('\n');
287
+ }
288
+ processCharacterMapping(lines, startIndex, indent) {
289
+ const charMappings = [];
290
+ let j = startIndex;
291
+ while (j < lines.length) {
292
+ const currentLine = lines[j];
293
+ if (!currentLine) {
294
+ break;
295
+ }
296
+ const currentMatch = currentLine.match(/^(\s*)"(\d+)":\s*(.+)$/);
297
+ if (currentMatch && currentMatch[1] === indent && currentMatch[2] && currentMatch[3]) {
298
+ const index = parseInt(currentMatch[2]);
299
+ let char = currentMatch[3];
300
+ if (char && char.startsWith('"') && char.endsWith('"')) {
301
+ char = char.slice(1, -1);
302
+ }
303
+ charMappings.push({ index, char });
304
+ j++;
305
+ }
306
+ else {
307
+ break;
308
+ }
309
+ }
310
+ if (charMappings.length > 3) {
311
+ charMappings.sort((a, b) => a.index - b.index);
312
+ const reconstructed = charMappings.map((m) => m.char).join('');
313
+ if (reconstructed.includes('{{') && reconstructed.includes('}}')) {
314
+ return { reconstructed: `${indent}${reconstructed}`, nextIndex: j };
315
+ }
316
+ }
317
+ return { nextIndex: j };
318
+ }
319
+ 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
+ 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
+ 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
+ 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
+ processed = this.cleanupArtifacts(processed);
349
+ if (processed.includes('number:')) {
350
+ console.log('DEBUG: After cleanupArtifacts:', processed.split('\n').filter((line) => line.includes('number:')));
351
+ }
352
+ return processed;
353
+ }
354
+ fixIncludeStatements(processed) {
355
+ processed = processed.replace(/\{\{ include \\"([^"]+)\\" \. \| nindent (\d+) \}\}/g, '{{ include "$1" . | nindent $2 }}');
356
+ processed = processed.replace(/\{\{ include \\"([^"]+)\\" \. \}\}/g, '{{ include "$1" . }}');
357
+ return processed;
358
+ }
359
+ fixFunctionCalls(processed) {
360
+ return processed.replace(/\{\{ (\w+) \\"([^"]*)\\" ([^}]*) \}\}/g, '{{ $1 "$2" $3 }}');
361
+ }
362
+ fixChartReferences(processed) {
363
+ return processed.replace(/\{\{ \.Chart\.(\w+) \| replace \\"([^"]*)\\" \\"([^"]*)\\" ([^}]*) \}\}/g, '{{ .Chart.$1 | replace "$2" "$3" $4 }}');
364
+ }
365
+ removeHelmExpressionQuotes(processed) {
366
+ 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
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\|\s*(?:int|float|bool|number)[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
372
+ processed = processed.replace(/^(\s*)(port):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
373
+ processed = processed.replace(/^(\s*)(port\.number):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
374
+ processed = processed.replace(/^(\s*)(service\.port):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
375
+ processed = processed.replace(/^(\s*)(backend\.service\.port\.number):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
376
+ processed = processed.replace(/^(\s*)(spec\.port):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
377
+ 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
+ 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
+ processed = processed.replace(/^(\s*)(replicas):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
396
+ processed = processed.replace(/^(\s*)(targetPort):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
397
+ processed = processed.replace(/^(\s*)(nodePort):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
398
+ processed = processed.replace(/^(\s*)(containerPort):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
399
+ processed = processed.replace(/^(\s*)(hostPort):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
400
+ processed = processed.replace(/^(\s*)(weight):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
401
+ processed = processed.replace(/^(\s*)(priority):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
402
+ processed = processed.replace(/^(\s*)(timeoutSeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
403
+ processed = processed.replace(/^(\s*)(periodSeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
404
+ processed = processed.replace(/^(\s*)(successThreshold):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
405
+ processed = processed.replace(/^(\s*)(failureThreshold):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
406
+ processed = processed.replace(/^(\s*)(initialDelaySeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
407
+ processed = processed.replace(/^(\s*)(terminationGracePeriodSeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
408
+ processed = processed.replace(/^(\s*)(activeDeadlineSeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
409
+ processed = processed.replace(/^(\s*)(backoffLimit):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
410
+ processed = processed.replace(/^(\s*)(parallelism):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
411
+ processed = processed.replace(/^(\s*)(completions):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
412
+ processed = processed.replace(/^(\s*)(revisionHistoryLimit):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
413
+ processed = processed.replace(/^(\s*)(progressDeadlineSeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
414
+ processed = processed.replace(/^(\s*)(minReadySeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
415
+ processed = processed.replace(/^(\s*)(maxUnavailable):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
416
+ processed = processed.replace(/^(\s*)(maxSurge):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
417
+ processed = processed.replace(/^(\s*)(cpu):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
418
+ processed = processed.replace(/^(\s*)(memory):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
419
+ processed = processed.replace(/^(\s*)(enabled):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
420
+ processed = processed.replace(/^(\s*)(create):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
421
+ processed = processed.replace(/^(\s*)(allow):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
422
+ processed = processed.replace(/^(\s*)(disable):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
423
+ processed = processed.replace(/^(\s*)(force):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
424
+ processed = processed.replace(/^(\s*)(required):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
425
+ processed = processed.replace(/^(\s*)(optional):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
426
+ processed = processed.replace(/^(\s*)(readOnly):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
427
+ processed = processed.replace(/^(\s*)(runAsNonRoot):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
428
+ processed = processed.replace(/^(\s*)(privileged):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
429
+ processed = processed.replace(/^(\s*)(allowPrivilegeEscalation):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
430
+ processed = processed.replace(/^(\s*)(readOnlyRootFilesystem):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
431
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*true[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
432
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*false[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
433
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\d+[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
434
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\beq\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
435
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\bne\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
436
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\blt\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
437
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\ble\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
438
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\bgt\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
439
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\bge\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
440
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\band\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
441
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\bor\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
442
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\bnot\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
443
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*[+][^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
444
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*[-][^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
445
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*[*][^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
446
+ processed = processed.replace(/:\s*"([^"]*\{\{[^}]+\}\}[^"]*)"/g, ': $1');
447
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"(true|false)"$/gm, '$1$2: $3');
448
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"(-?\d+)"$/gm, '$1$2: $3');
449
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"(-?\d+\.\d+)"$/gm, '$1$2: $3');
450
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"(-?\d+\.?\d*[eE][+-]?\d+)"$/gm, '$1$2: $3');
451
+ processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
452
+ processed = processed.replace(/"\{\{([^}]*)\}\}"/g, '{{$1}}');
453
+ processed = processed.replace(/^(\s*)-\s*"\{\{([^}]*)\}\}"/gm, '$1- {{$2}}');
454
+ return processed;
455
+ }
456
+ fixConditionalBlocks(processed) {
457
+ return processed.replace(/"\{\{-\s*(if|with|range)([^}]*)\}\}([^"]*)\{\{-\s*end\s*\}\}"/g, '{{- $1$2}}$3{{- end }}');
458
+ }
459
+ fixPipeExpressions(processed) {
460
+ return processed.replace(/"\{\{([^}]*\|[^}]*)\}\}"/g, '{{$1}}');
461
+ }
462
+ fixNestedQuotes(processed) {
463
+ return processed.replace(/\{\{([^}]*)\\"([^"]*)\\"([^}]*)\}\}/g, '{{$1"$2"$3}}');
464
+ }
465
+ cleanupArtifacts(processed) {
466
+ processed = processed.replace(/": "\.nan"/g, ': .nan');
467
+ processed = processed.replace(/: \{\{([^}]+)\}\}$/gm, ': {{$1}}');
468
+ return processed;
469
+ }
551
470
  write(outDir) {
552
- // Generate helpers template
471
+ console.log('🚀 Rutter.write called with outDir:', outDir);
472
+ console.log('📊 Assets count:', this.assets.length);
553
473
  let helpersContent;
554
474
  if (this.props.helpersTpl) {
555
475
  if (typeof this.props.helpersTpl === 'string') {
@@ -567,19 +487,19 @@ ${helper.template}
567
487
  }
568
488
  }
569
489
  else {
570
- // Auto-generate standard helpers
571
490
  helpersContent = generateHelpersTemplate(this.props.cloudProvider);
572
491
  }
492
+ const synthAssets = this.toSynthArray();
493
+ console.log('📦 Generated synth assets:', synthAssets.length);
573
494
  HelmChartWriter.write({
574
495
  outDir,
575
496
  meta: this.meta,
576
497
  defaultValues: this.defaultValues,
577
498
  envValues: this.envValues,
578
- assets: this.toSynthArray(),
499
+ assets: synthAssets,
579
500
  helpersTpl: helpersContent,
580
501
  });
502
+ console.log('✅ Rutter.write completed');
581
503
  }
582
504
  }
583
- // Constants for labels
584
505
  Rutter.HELPER_NAME = 'chart.name';
585
- //# sourceMappingURL=rutter.js.map