timonel 3.1.1 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +323 -695
  3. package/dist/cli.js +275 -15
  4. package/dist/index.d.ts +5 -1
  5. package/dist/index.js +12 -0
  6. package/dist/lib/helm.d.ts +471 -0
  7. package/dist/lib/helm.js +483 -0
  8. package/dist/lib/helmChartWriter.d.ts +157 -0
  9. package/dist/lib/helmChartWriter.js +171 -1
  10. package/dist/lib/policy/configurationLoader.d.ts +132 -0
  11. package/dist/lib/policy/configurationLoader.js +132 -0
  12. package/dist/lib/policy/errorContextGenerator.d.ts +89 -0
  13. package/dist/lib/policy/errorContextGenerator.js +99 -2
  14. package/dist/lib/policy/errors.d.ts +35 -0
  15. package/dist/lib/policy/errors.js +36 -0
  16. package/dist/lib/policy/index.d.ts +9 -0
  17. package/dist/lib/policy/index.js +16 -0
  18. package/dist/lib/policy/parallelExecutor.d.ts +90 -0
  19. package/dist/lib/policy/parallelExecutor.js +86 -3
  20. package/dist/lib/policy/pluginLoader.d.ts +92 -0
  21. package/dist/lib/policy/pluginLoader.js +92 -1
  22. package/dist/lib/policy/pluginRegistry.d.ts +54 -0
  23. package/dist/lib/policy/pluginRegistry.js +56 -0
  24. package/dist/lib/policy/policyEngine.d.ts +137 -0
  25. package/dist/lib/policy/policyEngine.js +191 -5
  26. package/dist/lib/policy/resultAggregator.d.ts +46 -0
  27. package/dist/lib/policy/resultAggregator.js +69 -1
  28. package/dist/lib/policy/resultFormatter.d.ts +88 -0
  29. package/dist/lib/policy/resultFormatter.js +101 -0
  30. package/dist/lib/policy/types.d.ts +136 -0
  31. package/dist/lib/policy/types.js +8 -0
  32. package/dist/lib/policy/validationCache.d.ts +146 -0
  33. package/dist/lib/policy/validationCache.js +142 -6
  34. package/dist/lib/resources/baseResourceProvider.d.ts +45 -0
  35. package/dist/lib/resources/baseResourceProvider.js +48 -1
  36. package/dist/lib/resources/cloud/aws/awsResources.d.ts +192 -0
  37. package/dist/lib/resources/cloud/aws/awsResources.js +163 -1
  38. package/dist/lib/resources/cloud/aws/karpenterResources.d.ts +131 -0
  39. package/dist/lib/resources/cloud/aws/karpenterResources.js +77 -0
  40. package/dist/lib/rutter.d.ts +381 -3
  41. package/dist/lib/rutter.js +439 -28
  42. package/dist/lib/security.d.ts +123 -0
  43. package/dist/lib/security.js +162 -4
  44. package/dist/lib/templates/flexible-subchart.d.ts +52 -0
  45. package/dist/lib/templates/flexible-subchart.js +70 -0
  46. package/dist/lib/templates/umbrella-chart.d.ts +27 -0
  47. package/dist/lib/templates/umbrella-chart.js +89 -0
  48. package/dist/lib/types.d.ts +26 -0
  49. package/dist/lib/umbrella.d.ts +23 -0
  50. package/dist/lib/umbrella.js +23 -0
  51. package/dist/lib/umbrellaRutter.d.ts +75 -0
  52. package/dist/lib/umbrellaRutter.js +82 -2
  53. package/dist/lib/utils/envVarsLoader.d.ts +49 -0
  54. package/dist/lib/utils/envVarsLoader.js +53 -0
  55. package/dist/lib/utils/helmConstructSerializer.d.ts +17 -0
  56. package/dist/lib/utils/helmConstructSerializer.js +22 -0
  57. package/dist/lib/utils/helmControlStructures.d.ts +194 -0
  58. package/dist/lib/utils/helmControlStructures.js +180 -0
  59. package/dist/lib/utils/helmHelpers/envHelpers.d.ts +13 -0
  60. package/dist/lib/utils/helmHelpers/envHelpers.js +13 -0
  61. package/dist/lib/utils/helmHelpers/gitopsHelpers.d.ts +13 -0
  62. package/dist/lib/utils/helmHelpers/gitopsHelpers.js +13 -0
  63. package/dist/lib/utils/helmHelpers/index.d.ts +74 -0
  64. package/dist/lib/utils/helmHelpers/index.js +85 -1
  65. package/dist/lib/utils/helmHelpers/observabilityHelpers.d.ts +13 -0
  66. package/dist/lib/utils/helmHelpers/observabilityHelpers.js +13 -0
  67. package/dist/lib/utils/helmHelpers/types.d.ts +23 -0
  68. package/dist/lib/utils/helmHelpers/types.js +4 -0
  69. package/dist/lib/utils/helmHelpers/validationHelpers.d.ts +13 -0
  70. package/dist/lib/utils/helmHelpers/validationHelpers.js +13 -0
  71. package/dist/lib/utils/helmHelpers.d.ts +62 -0
  72. package/dist/lib/utils/helmHelpers.js +77 -0
  73. package/dist/lib/utils/helmYamlSerializer.d.ts +77 -0
  74. package/dist/lib/utils/helmYamlSerializer.js +398 -21
  75. package/dist/lib/utils/logger.d.ts +153 -0
  76. package/dist/lib/utils/logger.js +170 -2
  77. package/dist/lib/utils/valuesRef.d.ts +181 -50
  78. package/dist/lib/utils/valuesRef.js +168 -170
  79. package/dist/lib/validation/inputValidator.d.ts +45 -0
  80. package/dist/lib/validation/inputValidator.js +67 -2
  81. package/dist/types/index.d.ts +34 -0
  82. package/dist/types/index.js +3 -0
  83. package/package.json +31 -38
@@ -1,12 +1,27 @@
1
+ /**
2
+ * Validation Result Cache Implementation
3
+ *
4
+ * This module provides caching functionality for policy validation results to optimize
5
+ * performance for repeated manifest validations. It implements cache invalidation
6
+ * strategies and optimizations for large chart processing.
7
+ *
8
+ * @since 3.0.0
9
+ */
1
10
  import { createLogger } from '../utils/logger.js';
11
+ /**
12
+ * Default cache configuration
13
+ */
2
14
  const DEFAULT_CACHE_OPTIONS = {
3
15
  maxEntries: 1000,
4
- maxAge: 30 * 60 * 1000,
16
+ maxAge: 30 * 60 * 1000, // 30 minutes
5
17
  enableCompression: false,
6
18
  evictionStrategy: 'lru',
7
19
  cacheFailures: true,
8
- minExecutionTimeToCache: 10,
20
+ minExecutionTimeToCache: 10, // 10ms
9
21
  };
22
+ /**
23
+ * Validation result cache implementation
24
+ */
10
25
  export class ValidationCache {
11
26
  constructor(options = {}) {
12
27
  this.cache = new Map();
@@ -24,8 +39,15 @@ export class ValidationCache {
24
39
  options: this.options,
25
40
  operation: 'cache_init',
26
41
  });
42
+ // Set up periodic cleanup
27
43
  this.startPeriodicCleanup();
28
44
  }
45
+ /**
46
+ * Gets a cached validation result if available
47
+ * @param manifestHash - Hash of the manifests
48
+ * @param pluginHash - Hash of the plugin configuration
49
+ * @returns Cached result or undefined if not found
50
+ */
29
51
  get(manifestHash, pluginHash) {
30
52
  const cacheKey = this.generateCacheKey(manifestHash, pluginHash);
31
53
  const entry = this.cache.get(cacheKey);
@@ -39,6 +61,7 @@ export class ValidationCache {
39
61
  });
40
62
  return undefined;
41
63
  }
64
+ // Check if entry has expired
42
65
  if (this.isExpired(entry)) {
43
66
  this.cache.delete(cacheKey);
44
67
  this.stats.misses++;
@@ -51,6 +74,7 @@ export class ValidationCache {
51
74
  });
52
75
  return undefined;
53
76
  }
77
+ // Update access statistics
54
78
  entry.accessCount++;
55
79
  entry.lastAccessed = Date.now();
56
80
  this.stats.hits++;
@@ -64,7 +88,14 @@ export class ValidationCache {
64
88
  });
65
89
  return entry.result;
66
90
  }
91
+ /**
92
+ * Stores a validation result in the cache
93
+ * @param manifestHash - Hash of the manifests
94
+ * @param pluginHash - Hash of the plugin configuration
95
+ * @param result - Validation result to cache
96
+ */
67
97
  set(manifestHash, pluginHash, result) {
98
+ // Check if we should cache this result
68
99
  if (!this.shouldCache(result)) {
69
100
  this.logger.debug('Skipping cache storage', {
70
101
  manifestHash: manifestHash.substring(0, 8),
@@ -76,6 +107,7 @@ export class ValidationCache {
76
107
  return;
77
108
  }
78
109
  const cacheKey = this.generateCacheKey(manifestHash, pluginHash);
110
+ // Check if we need to evict entries
79
111
  if (this.cache.size >= this.options.maxEntries) {
80
112
  this.evictEntries();
81
113
  }
@@ -98,12 +130,21 @@ export class ValidationCache {
98
130
  operation: 'cache_store',
99
131
  });
100
132
  }
133
+ /**
134
+ * Invalidates cache entries based on criteria
135
+ * @param criteria - Invalidation criteria
136
+ */
101
137
  invalidate(criteria) {
102
138
  if (criteria.all) {
103
139
  return this.invalidateAll();
104
140
  }
105
141
  return this.invalidateSelective(criteria);
106
142
  }
143
+ /**
144
+ * Invalidates all cache entries
145
+ * @returns Number of invalidated entries
146
+ * @private
147
+ */
107
148
  invalidateAll() {
108
149
  const invalidatedCount = this.cache.size;
109
150
  this.cache.clear();
@@ -114,6 +155,12 @@ export class ValidationCache {
114
155
  this.updateStats();
115
156
  return invalidatedCount;
116
157
  }
158
+ /**
159
+ * Invalidates cache entries based on selective criteria
160
+ * @param criteria - Invalidation criteria
161
+ * @returns Number of invalidated entries
162
+ * @private
163
+ */
117
164
  invalidateSelective(criteria) {
118
165
  const keysToDelete = [];
119
166
  for (const [key, entry] of this.cache.entries()) {
@@ -132,6 +179,13 @@ export class ValidationCache {
132
179
  this.updateStats();
133
180
  return keysToDelete.length;
134
181
  }
182
+ /**
183
+ * Determines if a cache entry should be invalidated
184
+ * @param entry - Cache entry to check
185
+ * @param criteria - Invalidation criteria
186
+ * @returns True if entry should be invalidated
187
+ * @private
188
+ */
135
189
  shouldInvalidateEntry(entry, criteria) {
136
190
  if (criteria.manifestHash && entry.manifestHash === criteria.manifestHash) {
137
191
  return true;
@@ -144,9 +198,16 @@ export class ValidationCache {
144
198
  }
145
199
  return false;
146
200
  }
201
+ /**
202
+ * Gets current cache statistics
203
+ * @returns Cache statistics
204
+ */
147
205
  getStats() {
148
206
  return { ...this.stats };
149
207
  }
208
+ /**
209
+ * Clears all cache statistics
210
+ */
150
211
  clearStats() {
151
212
  this.stats = {
152
213
  hits: 0,
@@ -160,23 +221,48 @@ export class ValidationCache {
160
221
  operation: 'cache_stats_clear',
161
222
  });
162
223
  }
224
+ /**
225
+ * Generates a cache key from manifest and plugin hashes
226
+ * @param manifestHash - Hash of the manifests
227
+ * @param pluginHash - Hash of the plugin configuration
228
+ * @returns Cache key string
229
+ * @private
230
+ */
163
231
  generateCacheKey(manifestHash, pluginHash) {
164
232
  return `${manifestHash}:${pluginHash}`;
165
233
  }
234
+ /**
235
+ * Checks if a cache entry has expired
236
+ * @param entry - Cache entry to check
237
+ * @returns True if expired
238
+ * @private
239
+ */
166
240
  isExpired(entry) {
167
241
  return Date.now() - entry.timestamp > this.options.maxAge;
168
242
  }
243
+ /**
244
+ * Determines if a validation result should be cached
245
+ * @param result - Validation result
246
+ * @returns True if should be cached
247
+ * @private
248
+ */
169
249
  shouldCache(result) {
250
+ // Don't cache if execution time is too short
170
251
  if (result.metadata.executionTime < this.options.minExecutionTimeToCache) {
171
252
  return false;
172
253
  }
254
+ // Don't cache failures if disabled
173
255
  if (!this.options.cacheFailures && !result.valid) {
174
256
  return false;
175
257
  }
176
258
  return true;
177
259
  }
260
+ /**
261
+ * Evicts cache entries based on the configured strategy
262
+ * @private
263
+ */
178
264
  evictEntries() {
179
- const entriesToEvict = Math.max(1, Math.floor(this.options.maxEntries * 0.1));
265
+ const entriesToEvict = Math.max(1, Math.floor(this.options.maxEntries * 0.1)); // Evict 10%
180
266
  switch (this.options.evictionStrategy) {
181
267
  case 'lru':
182
268
  this.evictLRU(entriesToEvict);
@@ -195,6 +281,11 @@ export class ValidationCache {
195
281
  operation: 'cache_evict',
196
282
  });
197
283
  }
284
+ /**
285
+ * Evicts least recently used entries
286
+ * @param count - Number of entries to evict
287
+ * @private
288
+ */
198
289
  evictLRU(count) {
199
290
  const entries = Array.from(this.cache.entries())
200
291
  .sort(([, a], [, b]) => a.lastAccessed - b.lastAccessed)
@@ -203,6 +294,11 @@ export class ValidationCache {
203
294
  this.cache.delete(key);
204
295
  }
205
296
  }
297
+ /**
298
+ * Evicts least frequently used entries
299
+ * @param count - Number of entries to evict
300
+ * @private
301
+ */
206
302
  evictLFU(count) {
207
303
  const entries = Array.from(this.cache.entries())
208
304
  .sort(([, a], [, b]) => a.accessCount - b.accessCount)
@@ -211,6 +307,11 @@ export class ValidationCache {
211
307
  this.cache.delete(key);
212
308
  }
213
309
  }
310
+ /**
311
+ * Evicts oldest entries (time to live)
312
+ * @param count - Number of entries to evict
313
+ * @private
314
+ */
214
315
  evictTTL(count) {
215
316
  const entries = Array.from(this.cache.entries())
216
317
  .sort(([, a], [, b]) => a.timestamp - b.timestamp)
@@ -219,6 +320,10 @@ export class ValidationCache {
219
320
  this.cache.delete(key);
220
321
  }
221
322
  }
323
+ /**
324
+ * Updates cache statistics
325
+ * @private
326
+ */
222
327
  updateStats() {
223
328
  const totalRequests = this.stats.hits + this.stats.misses;
224
329
  this.stats.entries = this.cache.size;
@@ -232,22 +337,36 @@ export class ValidationCache {
232
337
  this.stats.averageAccessCount = 0;
233
338
  }
234
339
  }
340
+ /**
341
+ * Estimates memory usage of the cache
342
+ * @returns Estimated memory usage in bytes
343
+ * @private
344
+ */
235
345
  estimateMemoryUsage() {
236
346
  let totalSize = 0;
237
347
  for (const entry of this.cache.values()) {
238
- totalSize += JSON.stringify(entry.result).length * 2;
348
+ // Rough estimation of entry size
349
+ totalSize += JSON.stringify(entry.result).length * 2; // UTF-16 encoding
239
350
  totalSize += entry.manifestHash.length * 2;
240
351
  totalSize += entry.pluginHash.length * 2;
241
- totalSize += 64;
352
+ totalSize += 64; // Overhead for timestamps and counters
242
353
  }
243
354
  return totalSize;
244
355
  }
356
+ /**
357
+ * Starts periodic cleanup of expired entries
358
+ * @private
359
+ */
245
360
  startPeriodicCleanup() {
246
- const cleanupInterval = Math.min(this.options.maxAge / 4, 5 * 60 * 1000);
361
+ const cleanupInterval = Math.min(this.options.maxAge / 4, 5 * 60 * 1000); // Every 5 minutes or 1/4 of maxAge
247
362
  globalThis.setInterval(() => {
248
363
  this.cleanupExpiredEntries();
249
364
  }, cleanupInterval);
250
365
  }
366
+ /**
367
+ * Removes expired entries from the cache
368
+ * @private
369
+ */
251
370
  cleanupExpiredEntries() {
252
371
  const keysToDelete = [];
253
372
  const now = Date.now();
@@ -269,10 +388,21 @@ export class ValidationCache {
269
388
  }
270
389
  }
271
390
  }
391
+ /**
392
+ * Utility function to generate a hash from manifests
393
+ * @param manifests - Array of manifest objects
394
+ * @returns Hash string
395
+ */
272
396
  export function generateManifestHash(manifests) {
273
397
  const manifestString = JSON.stringify(manifests, Object.keys(manifests).sort());
274
398
  return hashString(manifestString);
275
399
  }
400
+ /**
401
+ * Utility function to generate a hash from plugin configuration
402
+ * @param plugins - Array of plugin names
403
+ * @param pluginConfigs - Plugin configurations
404
+ * @returns Hash string
405
+ */
276
406
  export function generatePluginHash(plugins, pluginConfigs) {
277
407
  const configString = JSON.stringify({
278
408
  plugins: plugins.sort(),
@@ -280,6 +410,12 @@ export function generatePluginHash(plugins, pluginConfigs) {
280
410
  }, Object.keys({ plugins: plugins.sort(), configs: pluginConfigs }).sort());
281
411
  return hashString(configString);
282
412
  }
413
+ /**
414
+ * Simple string hashing function (djb2 algorithm)
415
+ * @param str - String to hash
416
+ * @returns Hash string
417
+ * @private
418
+ */
283
419
  function hashString(str) {
284
420
  let hash = 5381;
285
421
  for (let i = 0; i < str.length; i++) {
@@ -1,13 +1,58 @@
1
1
  import { ApiObject } from 'cdk8s';
2
2
  import type { Chart } from 'cdk8s';
3
+ /**
4
+ * Base interface for all resource providers
5
+ * Provides common functionality and ensures consistent patterns across providers
6
+ *
7
+ * @since 2.8.0+
8
+ */
3
9
  export interface IResourceProvider {
10
+ /** Reference to the CDK8s chart */
4
11
  readonly chart: Chart;
5
12
  }
13
+ /**
14
+ * Abstract base class for resource providers
15
+ * Implements common functionality and validation patterns
16
+ *
17
+ * @since 2.4.0
18
+ */
6
19
  export declare abstract class BaseResourceProvider implements IResourceProvider {
7
20
  readonly chart: Chart;
8
21
  constructor(chart: Chart);
22
+ /**
23
+ * Validates that a name follows Kubernetes naming conventions
24
+ * @param name - Resource name to validate
25
+ * @param resourceType - Type of resource for error messages
26
+ * @throws Error if name is invalid
27
+ */
9
28
  protected validateKubernetesName(name: string, resourceType: string): void;
29
+ /**
30
+ * Validates that labels follow Kubernetes conventions
31
+ * @param labels - Labels to validate
32
+ * @param resourceType - Type of resource for error messages
33
+ */
10
34
  protected validateLabels(labels: Record<string, string> | undefined, resourceType: string): void;
35
+ /**
36
+ * Creates a standardized ApiObject with common metadata
37
+ * @param name - Resource name
38
+ * @param apiVersion - Kubernetes API version
39
+ * @param kind - Kubernetes resource kind
40
+ * @param spec - Resource specification
41
+ * @param labels - Optional labels
42
+ * @param annotations - Optional annotations
43
+ * @returns Created ApiObject
44
+ */
11
45
  protected createApiObject(name: string, apiVersion: string, kind: string, spec: Record<string, unknown>, labels?: Record<string, string>, annotations?: Record<string, string>): ApiObject;
46
+ /**
47
+ * Creates an ApiObject for resources that don't use spec wrapper (like StorageClass)
48
+ * @param name - Resource name
49
+ * @param apiVersion - API version
50
+ * @param kind - Resource kind
51
+ * @param fields - Root-level fields for the resource
52
+ * @param labels - Optional labels
53
+ * @param annotations - Optional annotations
54
+ * @returns Created ApiObject
55
+ * @since 2.8.0+
56
+ */
12
57
  protected createRootLevelApiObject(name: string, apiVersion: string, kind: string, fields: Record<string, unknown>, labels?: Record<string, string>, annotations?: Record<string, string>): ApiObject;
13
58
  }
@@ -1,36 +1,70 @@
1
1
  import { ApiObject } from 'cdk8s';
2
+ /**
3
+ * Abstract base class for resource providers
4
+ * Implements common functionality and validation patterns
5
+ *
6
+ * @since 2.4.0
7
+ */
2
8
  export class BaseResourceProvider {
3
9
  constructor(chart) {
4
10
  this.chart = chart;
5
11
  }
12
+ /**
13
+ * Validates that a name follows Kubernetes naming conventions
14
+ * @param name - Resource name to validate
15
+ * @param resourceType - Type of resource for error messages
16
+ * @throws Error if name is invalid
17
+ */
6
18
  validateKubernetesName(name, resourceType) {
7
19
  if (!name || typeof name !== 'string') {
8
20
  throw new Error(`${resourceType} name must be a non-empty string`);
9
21
  }
22
+ // Kubernetes name validation (RFC 1123 subdomain)
23
+ // eslint-disable-next-line security/detect-unsafe-regex -- Simple character class regex is safe
10
24
  const nameRegex = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/;
11
25
  if (!nameRegex.test(name) || name.length > 63) {
12
26
  throw new Error(`${resourceType} name '${name}' must be a valid Kubernetes name (lowercase alphanumeric and hyphens, max 63 chars)`);
13
27
  }
14
28
  }
29
+ /**
30
+ * Validates that labels follow Kubernetes conventions
31
+ * @param labels - Labels to validate
32
+ * @param resourceType - Type of resource for error messages
33
+ */
15
34
  validateLabels(labels, resourceType) {
16
35
  if (!labels)
17
36
  return;
18
37
  Object.entries(labels).forEach(([key, value]) => {
38
+ // Validate label key
19
39
  if (key.length > 253) {
20
40
  throw new Error(`${resourceType} label key '${key}' exceeds 253 characters`);
21
41
  }
42
+ // Validate label value
22
43
  if (value.length > 63) {
23
44
  throw new Error(`${resourceType} label value '${value}' exceeds 63 characters`);
24
45
  }
46
+ // Skip validation for Helm template values
25
47
  if (value.includes('{{') && value.includes('}}')) {
26
48
  return;
27
49
  }
50
+ // Basic character validation
51
+ // eslint-disable-next-line security/detect-unsafe-regex -- Simple character class regex is safe
28
52
  const labelRegex = /^[a-zA-Z0-9]([-a-zA-Z0-9_.]*[a-zA-Z0-9])?$/;
29
53
  if (!labelRegex.test(value)) {
30
54
  throw new Error(`${resourceType} label value '${value}' contains invalid characters`);
31
55
  }
32
56
  });
33
57
  }
58
+ /**
59
+ * Creates a standardized ApiObject with common metadata
60
+ * @param name - Resource name
61
+ * @param apiVersion - Kubernetes API version
62
+ * @param kind - Kubernetes resource kind
63
+ * @param spec - Resource specification
64
+ * @param labels - Optional labels
65
+ * @param annotations - Optional annotations
66
+ * @returns Created ApiObject
67
+ */
34
68
  createApiObject(name, apiVersion, kind, spec, labels, annotations) {
35
69
  this.validateKubernetesName(name, kind);
36
70
  this.validateLabels(labels, kind);
@@ -45,9 +79,21 @@ export class BaseResourceProvider {
45
79
  spec,
46
80
  });
47
81
  }
82
+ /**
83
+ * Creates an ApiObject for resources that don't use spec wrapper (like StorageClass)
84
+ * @param name - Resource name
85
+ * @param apiVersion - API version
86
+ * @param kind - Resource kind
87
+ * @param fields - Root-level fields for the resource
88
+ * @param labels - Optional labels
89
+ * @param annotations - Optional annotations
90
+ * @returns Created ApiObject
91
+ * @since 2.8.0+
92
+ */
48
93
  createRootLevelApiObject(name, apiVersion, kind, fields, labels, annotations) {
49
94
  this.validateKubernetesName(name, kind);
50
95
  this.validateLabels(labels, kind);
96
+ // Validate that fields doesn't contain reserved keys
51
97
  const reservedKeys = ['apiVersion', 'kind', 'metadata'];
52
98
  const conflictingKeys = Object.keys(fields).filter((key) => reservedKeys.includes(key));
53
99
  if (conflictingKeys.length > 0) {
@@ -61,7 +107,8 @@ export class BaseResourceProvider {
61
107
  ...(labels && Object.keys(labels).length > 0 ? { labels } : {}),
62
108
  ...(annotations && Object.keys(annotations).length > 0 ? { annotations } : {}),
63
109
  },
64
- ...fields,
110
+ // Safe: fields validated above to not contain reserved keys (apiVersion, kind, metadata)
111
+ ...fields, // Fields go at root level, not under spec
65
112
  });
66
113
  }
67
114
  }