typekro 0.2.2 → 0.3.1
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/README.md +4 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/core/composition/imperative.d.ts.map +1 -1
- package/dist/core/composition/imperative.js +15 -2
- package/dist/core/composition/imperative.js.map +1 -1
- package/dist/core/composition/typekro-runtime/typekro-runtime.d.ts.map +1 -1
- package/dist/core/composition/typekro-runtime/typekro-runtime.js +24 -25
- package/dist/core/composition/typekro-runtime/typekro-runtime.js.map +1 -1
- package/dist/core/dependencies/type-guards.d.ts.map +1 -1
- package/dist/core/dependencies/type-guards.js +7 -2
- package/dist/core/dependencies/type-guards.js.map +1 -1
- package/dist/core/deployment/engine.d.ts +13 -1
- package/dist/core/deployment/engine.d.ts.map +1 -1
- package/dist/core/deployment/engine.js +48 -3
- package/dist/core/deployment/engine.js.map +1 -1
- package/dist/core/errors.d.ts +85 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +135 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/evaluation/cel-optimizer.d.ts.map +1 -1
- package/dist/core/evaluation/cel-optimizer.js +7 -13
- package/dist/core/evaluation/cel-optimizer.js.map +1 -1
- package/dist/core/expressions/analyzer.d.ts +584 -0
- package/dist/core/expressions/analyzer.d.ts.map +1 -0
- package/dist/core/expressions/analyzer.js +2956 -0
- package/dist/core/expressions/analyzer.js.map +1 -0
- package/dist/core/expressions/cache.d.ts +136 -0
- package/dist/core/expressions/cache.d.ts.map +1 -0
- package/dist/core/expressions/cache.js +347 -0
- package/dist/core/expressions/cache.js.map +1 -0
- package/dist/core/expressions/cel-conversion-engine.d.ts +126 -0
- package/dist/core/expressions/cel-conversion-engine.d.ts.map +1 -0
- package/dist/core/expressions/cel-conversion-engine.js +293 -0
- package/dist/core/expressions/cel-conversion-engine.js.map +1 -0
- package/dist/core/expressions/compile-time-validation.d.ts +270 -0
- package/dist/core/expressions/compile-time-validation.d.ts.map +1 -0
- package/dist/core/expressions/compile-time-validation.js +506 -0
- package/dist/core/expressions/compile-time-validation.js.map +1 -0
- package/dist/core/expressions/composition-integration.d.ts +315 -0
- package/dist/core/expressions/composition-integration.d.ts.map +1 -0
- package/dist/core/expressions/composition-integration.js +936 -0
- package/dist/core/expressions/composition-integration.js.map +1 -0
- package/dist/core/expressions/conditional-expression-processor.d.ts +154 -0
- package/dist/core/expressions/conditional-expression-processor.d.ts.map +1 -0
- package/dist/core/expressions/conditional-expression-processor.js +479 -0
- package/dist/core/expressions/conditional-expression-processor.js.map +1 -0
- package/dist/core/expressions/conditional-integration.d.ts +133 -0
- package/dist/core/expressions/conditional-integration.d.ts.map +1 -0
- package/dist/core/expressions/conditional-integration.js +293 -0
- package/dist/core/expressions/conditional-integration.js.map +1 -0
- package/dist/core/expressions/conditional-validation.d.ts +181 -0
- package/dist/core/expressions/conditional-validation.d.ts.map +1 -0
- package/dist/core/expressions/conditional-validation.js +460 -0
- package/dist/core/expressions/conditional-validation.js.map +1 -0
- package/dist/core/expressions/context-aware-generator.d.ts +127 -0
- package/dist/core/expressions/context-aware-generator.d.ts.map +1 -0
- package/dist/core/expressions/context-aware-generator.js +500 -0
- package/dist/core/expressions/context-aware-generator.js.map +1 -0
- package/dist/core/expressions/context-detector.d.ts +148 -0
- package/dist/core/expressions/context-detector.d.ts.map +1 -0
- package/dist/core/expressions/context-detector.js +546 -0
- package/dist/core/expressions/context-detector.js.map +1 -0
- package/dist/core/expressions/context-switcher.d.ts +185 -0
- package/dist/core/expressions/context-switcher.d.ts.map +1 -0
- package/dist/core/expressions/context-switcher.js +515 -0
- package/dist/core/expressions/context-switcher.js.map +1 -0
- package/dist/core/expressions/context-validator.d.ts +176 -0
- package/dist/core/expressions/context-validator.d.ts.map +1 -0
- package/dist/core/expressions/context-validator.js +452 -0
- package/dist/core/expressions/context-validator.js.map +1 -0
- package/dist/core/expressions/custom-context-manager.d.ts +194 -0
- package/dist/core/expressions/custom-context-manager.d.ts.map +1 -0
- package/dist/core/expressions/custom-context-manager.js +390 -0
- package/dist/core/expressions/custom-context-manager.js.map +1 -0
- package/dist/core/expressions/expression-proxy.d.ts +80 -0
- package/dist/core/expressions/expression-proxy.d.ts.map +1 -0
- package/dist/core/expressions/expression-proxy.js +227 -0
- package/dist/core/expressions/expression-proxy.js.map +1 -0
- package/dist/core/expressions/factory-integration.d.ts +132 -0
- package/dist/core/expressions/factory-integration.d.ts.map +1 -0
- package/dist/core/expressions/factory-integration.js +327 -0
- package/dist/core/expressions/factory-integration.js.map +1 -0
- package/dist/core/expressions/factory-pattern-handler.d.ts +88 -0
- package/dist/core/expressions/factory-pattern-handler.d.ts.map +1 -0
- package/dist/core/expressions/factory-pattern-handler.js +336 -0
- package/dist/core/expressions/factory-pattern-handler.js.map +1 -0
- package/dist/core/expressions/field-hydration-processor.d.ts +188 -0
- package/dist/core/expressions/field-hydration-processor.d.ts.map +1 -0
- package/dist/core/expressions/field-hydration-processor.js +562 -0
- package/dist/core/expressions/field-hydration-processor.js.map +1 -0
- package/dist/core/expressions/imperative-analyzer.d.ts +21 -0
- package/dist/core/expressions/imperative-analyzer.d.ts.map +1 -0
- package/dist/core/expressions/imperative-analyzer.js +457 -0
- package/dist/core/expressions/imperative-analyzer.js.map +1 -0
- package/dist/core/expressions/index.d.ts +54 -0
- package/dist/core/expressions/index.d.ts.map +1 -0
- package/dist/core/expressions/index.js +50 -0
- package/dist/core/expressions/index.js.map +1 -0
- package/dist/core/expressions/lazy-analysis.d.ts +1128 -0
- package/dist/core/expressions/lazy-analysis.d.ts.map +1 -0
- package/dist/core/expressions/lazy-analysis.js +2443 -0
- package/dist/core/expressions/lazy-analysis.js.map +1 -0
- package/dist/core/expressions/magic-assignable-analyzer.d.ts +123 -0
- package/dist/core/expressions/magic-assignable-analyzer.d.ts.map +1 -0
- package/dist/core/expressions/magic-assignable-analyzer.js +352 -0
- package/dist/core/expressions/magic-assignable-analyzer.js.map +1 -0
- package/dist/core/expressions/magic-proxy-analyzer.d.ts +206 -0
- package/dist/core/expressions/magic-proxy-analyzer.d.ts.map +1 -0
- package/dist/core/expressions/magic-proxy-analyzer.js +639 -0
- package/dist/core/expressions/magic-proxy-analyzer.js.map +1 -0
- package/dist/core/expressions/magic-proxy-detector.d.ts +154 -0
- package/dist/core/expressions/magic-proxy-detector.d.ts.map +1 -0
- package/dist/core/expressions/magic-proxy-detector.js +242 -0
- package/dist/core/expressions/magic-proxy-detector.js.map +1 -0
- package/dist/core/expressions/migration-helpers.d.ts +133 -0
- package/dist/core/expressions/migration-helpers.d.ts.map +1 -0
- package/dist/core/expressions/migration-helpers.js +443 -0
- package/dist/core/expressions/migration-helpers.js.map +1 -0
- package/dist/core/expressions/optionality-handler.d.ts +503 -0
- package/dist/core/expressions/optionality-handler.d.ts.map +1 -0
- package/dist/core/expressions/optionality-handler.js +1306 -0
- package/dist/core/expressions/optionality-handler.js.map +1 -0
- package/dist/core/expressions/readiness-integration.d.ts +119 -0
- package/dist/core/expressions/readiness-integration.d.ts.map +1 -0
- package/dist/core/expressions/readiness-integration.js +386 -0
- package/dist/core/expressions/readiness-integration.js.map +1 -0
- package/dist/core/expressions/resource-analyzer.d.ts +486 -0
- package/dist/core/expressions/resource-analyzer.d.ts.map +1 -0
- package/dist/core/expressions/resource-analyzer.js +1086 -0
- package/dist/core/expressions/resource-analyzer.js.map +1 -0
- package/dist/core/expressions/resource-validation.d.ts +187 -0
- package/dist/core/expressions/resource-validation.d.ts.map +1 -0
- package/dist/core/expressions/resource-validation.js +552 -0
- package/dist/core/expressions/resource-validation.js.map +1 -0
- package/dist/core/expressions/runtime-error-mapper.d.ts +138 -0
- package/dist/core/expressions/runtime-error-mapper.d.ts.map +1 -0
- package/dist/core/expressions/runtime-error-mapper.js +412 -0
- package/dist/core/expressions/runtime-error-mapper.js.map +1 -0
- package/dist/core/expressions/source-map.d.ts +168 -0
- package/dist/core/expressions/source-map.d.ts.map +1 -0
- package/dist/core/expressions/source-map.js +350 -0
- package/dist/core/expressions/source-map.js.map +1 -0
- package/dist/core/expressions/status-builder-analyzer.d.ts +353 -0
- package/dist/core/expressions/status-builder-analyzer.d.ts.map +1 -0
- package/dist/core/expressions/status-builder-analyzer.js +1311 -0
- package/dist/core/expressions/status-builder-analyzer.js.map +1 -0
- package/dist/core/expressions/type-inference.d.ts +184 -0
- package/dist/core/expressions/type-inference.d.ts.map +1 -0
- package/dist/core/expressions/type-inference.js +838 -0
- package/dist/core/expressions/type-inference.js.map +1 -0
- package/dist/core/expressions/type-safety.d.ts +203 -0
- package/dist/core/expressions/type-safety.d.ts.map +1 -0
- package/dist/core/expressions/type-safety.js +442 -0
- package/dist/core/expressions/type-safety.js.map +1 -0
- package/dist/core/expressions/types.d.ts +282 -0
- package/dist/core/expressions/types.d.ts.map +1 -0
- package/dist/core/expressions/types.js +8 -0
- package/dist/core/expressions/types.js.map +1 -0
- package/dist/core/kubernetes/client-provider.js +2 -2
- package/dist/core/kubernetes/client-provider.js.map +1 -1
- package/dist/core/references/cel.d.ts +13 -1
- package/dist/core/references/cel.d.ts.map +1 -1
- package/dist/core/references/cel.js +50 -0
- package/dist/core/references/cel.js.map +1 -1
- package/dist/core/references/schema-proxy.d.ts.map +1 -1
- package/dist/core/references/schema-proxy.js +13 -8
- package/dist/core/references/schema-proxy.js.map +1 -1
- package/dist/core/serialization/core.d.ts.map +1 -1
- package/dist/core/serialization/core.js +573 -9
- package/dist/core/serialization/core.js.map +1 -1
- package/dist/core/types/deployment.d.ts +7 -0
- package/dist/core/types/deployment.d.ts.map +1 -1
- package/dist/core/types/deployment.js +7 -0
- package/dist/core/types/deployment.js.map +1 -1
- package/dist/core/types/index.d.ts +1 -0
- package/dist/core/types/index.d.ts.map +1 -1
- package/dist/core/types/index.js.map +1 -1
- package/dist/core/validation/cel-validator.d.ts.map +1 -1
- package/dist/core/validation/cel-validator.js +4 -8
- package/dist/core/validation/cel-validator.js.map +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +1 -1
- package/dist/core.js.map +1 -1
- package/dist/factories/helm/helm-release.d.ts.map +1 -1
- package/dist/factories/helm/helm-release.js +0 -5
- package/dist/factories/helm/helm-release.js.map +1 -1
- package/dist/factories/helm/types.d.ts +1 -1
- package/dist/factories/helm/types.d.ts.map +1 -1
- package/dist/factories/index.js +1 -0
- package/dist/factories/index.js.map +1 -1
- package/dist/factories/shared.d.ts.map +1 -1
- package/dist/factories/shared.js +21 -1
- package/dist/factories/shared.js.map +1 -1
- package/dist/factories/simple/index.d.ts +2 -2
- package/dist/factories/simple/index.d.ts.map +1 -1
- package/dist/factories/simple/workloads/deployment.d.ts +3 -3
- package/dist/factories/simple/workloads/deployment.d.ts.map +1 -1
- package/dist/factories/simple/workloads/deployment.js +37 -11
- package/dist/factories/simple/workloads/deployment.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/type-guards.d.ts +6 -0
- package/dist/utils/type-guards.d.ts.map +1 -1
- package/dist/utils/type-guards.js +25 -2
- package/dist/utils/type-guards.js.map +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,1086 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Builder Integration for JavaScript to CEL Expression Conversion
|
|
3
|
+
*
|
|
4
|
+
* This module provides integration with TypeKro's existing KubernetesRef and magic proxy systems
|
|
5
|
+
* for analyzing resource configurations that contain JavaScript expressions with KubernetesRef objects.
|
|
6
|
+
*
|
|
7
|
+
* The analyzer detects when resource builders use expressions that depend on other resources
|
|
8
|
+
* and converts them to appropriate CEL expressions based on the factory pattern.
|
|
9
|
+
*/
|
|
10
|
+
import { containsKubernetesRefs, isKubernetesRef } from '../../utils/type-guards.js';
|
|
11
|
+
import { ConversionError } from '../errors.js';
|
|
12
|
+
import { JavaScriptToCelAnalyzer, } from './analyzer.js';
|
|
13
|
+
import { SourceMapBuilder } from './source-map.js';
|
|
14
|
+
/**
|
|
15
|
+
* Analyzes resource configurations for JavaScript expressions containing KubernetesRef objects
|
|
16
|
+
*/
|
|
17
|
+
export class ResourceAnalyzer {
|
|
18
|
+
analyzer;
|
|
19
|
+
dependencyTracker;
|
|
20
|
+
typeValidator;
|
|
21
|
+
constructor() {
|
|
22
|
+
this.analyzer = new JavaScriptToCelAnalyzer();
|
|
23
|
+
this.dependencyTracker = new DependencyTracker();
|
|
24
|
+
this.typeValidator = new ResourceTypeValidator();
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Analyze a resource configuration for KubernetesRef-dependent expressions
|
|
28
|
+
* This is the main entry point for resource builder integration
|
|
29
|
+
*/
|
|
30
|
+
analyzeResourceConfig(resourceId, config, context) {
|
|
31
|
+
const result = {
|
|
32
|
+
originalConfig: config,
|
|
33
|
+
processedConfig: {},
|
|
34
|
+
dependencies: [],
|
|
35
|
+
convertedFields: [],
|
|
36
|
+
errors: [],
|
|
37
|
+
requiresConversion: false,
|
|
38
|
+
circularDependencies: [],
|
|
39
|
+
typeValidationResults: []
|
|
40
|
+
};
|
|
41
|
+
try {
|
|
42
|
+
// Deep analyze the configuration object
|
|
43
|
+
result.processedConfig = this.analyzeConfigObject(config, '', context, result);
|
|
44
|
+
// Track dependencies automatically
|
|
45
|
+
const _dependencyInfos = this.dependencyTracker.trackDependencies(resourceId, result.dependencies, result.convertedFields, {
|
|
46
|
+
trackSchemaDependencies: true,
|
|
47
|
+
trackResourceDependencies: true,
|
|
48
|
+
trackExternalDependencies: true,
|
|
49
|
+
detectCircularDependencies: true,
|
|
50
|
+
computeDeploymentOrder: true
|
|
51
|
+
});
|
|
52
|
+
// Get circular dependencies from tracker
|
|
53
|
+
result.circularDependencies = this.dependencyTracker.getDependencyGraph().circularChains;
|
|
54
|
+
// Validate resource types if requested
|
|
55
|
+
if (context.validateResourceTypes) {
|
|
56
|
+
result.typeValidationResults = this.validateResourceTypes(result.dependencies, context);
|
|
57
|
+
}
|
|
58
|
+
// Set overall conversion flag
|
|
59
|
+
result.requiresConversion = result.convertedFields.length > 0;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
result.errors.push(new ConversionError(`Failed to analyze resource config: ${error instanceof Error ? error.message : String(error)}`, JSON.stringify(config), 'unknown'));
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Recursively analyze a configuration object for KubernetesRef objects
|
|
68
|
+
*/
|
|
69
|
+
analyzeConfigObject(obj, fieldPath, context, result) {
|
|
70
|
+
if (obj === null || obj === undefined) {
|
|
71
|
+
return obj;
|
|
72
|
+
}
|
|
73
|
+
// Check if this value contains KubernetesRef objects
|
|
74
|
+
if (!containsKubernetesRefs(obj)) {
|
|
75
|
+
// No KubernetesRef objects - return as-is for performance
|
|
76
|
+
return obj;
|
|
77
|
+
}
|
|
78
|
+
// Handle direct KubernetesRef objects
|
|
79
|
+
if (isKubernetesRef(obj)) {
|
|
80
|
+
return this.convertKubernetesRefInConfig(obj, fieldPath, context, result);
|
|
81
|
+
}
|
|
82
|
+
// Handle arrays
|
|
83
|
+
if (Array.isArray(obj)) {
|
|
84
|
+
return obj.map((item, index) => this.analyzeConfigObject(item, `${fieldPath}[${index}]`, context, result));
|
|
85
|
+
}
|
|
86
|
+
// Handle objects
|
|
87
|
+
if (typeof obj === 'object') {
|
|
88
|
+
const processedObj = {};
|
|
89
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
90
|
+
const currentFieldPath = fieldPath ? `${fieldPath}.${key}` : key;
|
|
91
|
+
processedObj[key] = this.analyzeConfigObject(value, currentFieldPath, context, result);
|
|
92
|
+
}
|
|
93
|
+
return processedObj;
|
|
94
|
+
}
|
|
95
|
+
// Handle primitive values that might be expressions
|
|
96
|
+
if (typeof obj === 'string' && this.looksLikeExpression(obj)) {
|
|
97
|
+
return this.analyzeStringExpression(obj, fieldPath, context, result);
|
|
98
|
+
}
|
|
99
|
+
return obj;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Convert a KubernetesRef object in a resource configuration
|
|
103
|
+
*/
|
|
104
|
+
convertKubernetesRefInConfig(ref, fieldPath, context, result) {
|
|
105
|
+
try {
|
|
106
|
+
// Track this dependency
|
|
107
|
+
result.dependencies.push(ref);
|
|
108
|
+
result.convertedFields.push(fieldPath);
|
|
109
|
+
// Create analysis context for this KubernetesRef
|
|
110
|
+
const analysisContext = {
|
|
111
|
+
type: 'resource',
|
|
112
|
+
availableReferences: context.availableResources || {},
|
|
113
|
+
factoryType: context.factoryType,
|
|
114
|
+
sourceMap: new SourceMapBuilder(),
|
|
115
|
+
dependencies: [],
|
|
116
|
+
...(context.schemaProxy && { schemaProxy: context.schemaProxy })
|
|
117
|
+
};
|
|
118
|
+
// Convert the KubernetesRef to CEL
|
|
119
|
+
const celExpression = this.analyzer.convertKubernetesRefToCel(ref, analysisContext);
|
|
120
|
+
// Add any additional dependencies found during conversion
|
|
121
|
+
result.dependencies.push(...(analysisContext.dependencies || []));
|
|
122
|
+
return celExpression;
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
const conversionError = new ConversionError(`Failed to convert KubernetesRef at ${fieldPath}: ${error instanceof Error ? error.message : String(error)}`, `${ref.resourceId}.${ref.fieldPath}`, 'member-access');
|
|
126
|
+
result.errors.push(conversionError);
|
|
127
|
+
// Return the original ref on error
|
|
128
|
+
return ref;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Analyze string expressions that might contain KubernetesRef objects
|
|
133
|
+
*/
|
|
134
|
+
analyzeStringExpression(expression, fieldPath, context, result) {
|
|
135
|
+
try {
|
|
136
|
+
// Create analysis context
|
|
137
|
+
const analysisContext = {
|
|
138
|
+
type: 'resource',
|
|
139
|
+
availableReferences: context.availableResources || {},
|
|
140
|
+
factoryType: context.factoryType,
|
|
141
|
+
sourceMap: new SourceMapBuilder(),
|
|
142
|
+
dependencies: [],
|
|
143
|
+
sourceText: expression,
|
|
144
|
+
...(context.schemaProxy && { schemaProxy: context.schemaProxy })
|
|
145
|
+
};
|
|
146
|
+
// Analyze the expression
|
|
147
|
+
const conversionResult = this.analyzer.analyzeExpression(expression, analysisContext);
|
|
148
|
+
if (conversionResult.valid && conversionResult.requiresConversion) {
|
|
149
|
+
// Track dependencies and converted fields
|
|
150
|
+
result.dependencies.push(...conversionResult.dependencies);
|
|
151
|
+
result.convertedFields.push(fieldPath);
|
|
152
|
+
result.errors.push(...conversionResult.errors);
|
|
153
|
+
return conversionResult.celExpression;
|
|
154
|
+
}
|
|
155
|
+
// No conversion needed or failed - return original
|
|
156
|
+
result.errors.push(...conversionResult.errors);
|
|
157
|
+
return expression;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
const conversionError = new ConversionError(`Failed to analyze expression at ${fieldPath}: ${error instanceof Error ? error.message : String(error)}`, expression, 'javascript');
|
|
161
|
+
result.errors.push(conversionError);
|
|
162
|
+
return expression;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Check if a string looks like a JavaScript expression
|
|
167
|
+
*/
|
|
168
|
+
looksLikeExpression(str) {
|
|
169
|
+
// Look for common expression patterns
|
|
170
|
+
return (str.includes('${') || // Template literals
|
|
171
|
+
str.includes('?.') || // Optional chaining
|
|
172
|
+
str.includes('||') || // Logical OR
|
|
173
|
+
str.includes('&&') || // Logical AND
|
|
174
|
+
str.includes('??') || // Nullish coalescing
|
|
175
|
+
/\w+\.\w+/.test(str) // Property access
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Validate resource types for KubernetesRef objects
|
|
180
|
+
* This implements requirement 4.5: resource type validation
|
|
181
|
+
*/
|
|
182
|
+
validateResourceTypes(dependencies, context) {
|
|
183
|
+
const validationContext = {
|
|
184
|
+
strictTypeChecking: context.validateResourceTypes || false,
|
|
185
|
+
...(context.availableResources && { availableResources: context.availableResources }),
|
|
186
|
+
...(context.schemaProxy && { schemaProxy: context.schemaProxy })
|
|
187
|
+
};
|
|
188
|
+
return dependencies.map(dep => this.typeValidator.validateKubernetesRef(dep, validationContext));
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get dependency information for a resource
|
|
192
|
+
*/
|
|
193
|
+
getDependencyInfo(resourceId) {
|
|
194
|
+
return this.dependencyTracker.getDependencies(resourceId);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Get the full dependency graph
|
|
198
|
+
*/
|
|
199
|
+
getDependencyGraph() {
|
|
200
|
+
return this.dependencyTracker.getDependencyGraph();
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Get resources that depend on a specific resource
|
|
204
|
+
*/
|
|
205
|
+
getDependents(resourceId) {
|
|
206
|
+
return this.dependencyTracker.getDependents(resourceId);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Check if there are circular dependencies
|
|
210
|
+
*/
|
|
211
|
+
hasCircularDependencies() {
|
|
212
|
+
return this.dependencyTracker.hasCircularDependencies();
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Get the recommended deployment order
|
|
216
|
+
*/
|
|
217
|
+
getDeploymentOrder() {
|
|
218
|
+
return this.dependencyTracker.getDeploymentOrder();
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Perform advanced circular dependency analysis
|
|
222
|
+
* This provides detailed analysis and recommendations for resolving circular dependencies
|
|
223
|
+
*/
|
|
224
|
+
analyzeCircularDependencies() {
|
|
225
|
+
return this.dependencyTracker.detectCircularDependencyChains();
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Get the resource type validator
|
|
229
|
+
*/
|
|
230
|
+
getTypeValidator() {
|
|
231
|
+
return this.typeValidator;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Register a custom schema validator
|
|
235
|
+
*/
|
|
236
|
+
registerSchemaValidator(schemaType, validator) {
|
|
237
|
+
this.typeValidator.registerSchemaValidator(schemaType, validator);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Register a custom resource type
|
|
241
|
+
*/
|
|
242
|
+
registerResourceType(name, typeInfo) {
|
|
243
|
+
this.typeValidator.registerResourceType(name, typeInfo);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Validate a single KubernetesRef for type correctness
|
|
247
|
+
*/
|
|
248
|
+
validateKubernetesRef(ref, context) {
|
|
249
|
+
return this.typeValidator.validateKubernetesRef(ref, context);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Convenience function for analyzing resource configurations
|
|
254
|
+
* This is the main entry point for resource builder integration
|
|
255
|
+
*/
|
|
256
|
+
export function analyzeResourceConfig(resourceId, config, context) {
|
|
257
|
+
const analyzer = new ResourceAnalyzer();
|
|
258
|
+
const fullContext = {
|
|
259
|
+
...context,
|
|
260
|
+
resourceId,
|
|
261
|
+
resourceConfig: config
|
|
262
|
+
};
|
|
263
|
+
return analyzer.analyzeResourceConfig(resourceId, config, fullContext);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Analyze resource configurations for factory function integration with KubernetesRef detection
|
|
267
|
+
* This function integrates with existing factory functions to detect and convert
|
|
268
|
+
* JavaScript expressions that contain KubernetesRef objects from the magic proxy system
|
|
269
|
+
*/
|
|
270
|
+
export function analyzeFactoryResourceConfig(resourceId, config, availableResources, schemaProxy, factoryType = 'kro') {
|
|
271
|
+
const context = {
|
|
272
|
+
type: 'resource',
|
|
273
|
+
resourceId,
|
|
274
|
+
resourceConfig: config,
|
|
275
|
+
availableReferences: availableResources,
|
|
276
|
+
availableResources,
|
|
277
|
+
factoryType,
|
|
278
|
+
validateResourceTypes: true,
|
|
279
|
+
sourceMap: new SourceMapBuilder(),
|
|
280
|
+
...(schemaProxy && { schemaProxy })
|
|
281
|
+
};
|
|
282
|
+
return analyzeResourceConfig(resourceId, config, context);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Automatic dependency tracker for KubernetesRef objects in expressions
|
|
286
|
+
* This implements requirement 4.2: automatic dependency tracking
|
|
287
|
+
*/
|
|
288
|
+
export class DependencyTracker {
|
|
289
|
+
dependencyGraph;
|
|
290
|
+
constructor() {
|
|
291
|
+
this.dependencyGraph = {
|
|
292
|
+
dependencies: new Map(),
|
|
293
|
+
dependents: new Map(),
|
|
294
|
+
circularChains: [],
|
|
295
|
+
deploymentOrder: []
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Track dependencies for a resource configuration
|
|
300
|
+
*/
|
|
301
|
+
trackDependencies(resourceId, dependencies, fieldPaths, options = {}) {
|
|
302
|
+
const dependencyInfos = [];
|
|
303
|
+
for (let i = 0; i < dependencies.length; i++) {
|
|
304
|
+
const dep = dependencies[i];
|
|
305
|
+
if (!dep)
|
|
306
|
+
continue;
|
|
307
|
+
const fieldPath = fieldPaths[i] || `unknown[${i}]`;
|
|
308
|
+
const dependencyInfo = this.createDependencyInfo(dep, fieldPath, options);
|
|
309
|
+
dependencyInfos.push(dependencyInfo);
|
|
310
|
+
// Add to dependency graph
|
|
311
|
+
this.addToDependencyGraph(resourceId, dependencyInfo);
|
|
312
|
+
}
|
|
313
|
+
// Update dependency graph computations
|
|
314
|
+
if (options.detectCircularDependencies) {
|
|
315
|
+
this.detectCircularDependencies();
|
|
316
|
+
}
|
|
317
|
+
if (options.computeDeploymentOrder) {
|
|
318
|
+
this.computeDeploymentOrder();
|
|
319
|
+
}
|
|
320
|
+
return dependencyInfos;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Create dependency information for a KubernetesRef
|
|
324
|
+
*/
|
|
325
|
+
createDependencyInfo(ref, fieldPath, options) {
|
|
326
|
+
const dependencyType = this.determineDependencyType(ref);
|
|
327
|
+
// Skip tracking based on options
|
|
328
|
+
if (dependencyType === 'schema' && options.trackSchemaDependencies === false) {
|
|
329
|
+
return this.createSkippedDependencyInfo(ref, fieldPath, dependencyType);
|
|
330
|
+
}
|
|
331
|
+
if (dependencyType === 'resource' && options.trackResourceDependencies === false) {
|
|
332
|
+
return this.createSkippedDependencyInfo(ref, fieldPath, dependencyType);
|
|
333
|
+
}
|
|
334
|
+
if (dependencyType === 'external' && options.trackExternalDependencies === false) {
|
|
335
|
+
return this.createSkippedDependencyInfo(ref, fieldPath, dependencyType);
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
reference: ref,
|
|
339
|
+
fieldPath,
|
|
340
|
+
dependencyType,
|
|
341
|
+
required: this.isDependencyRequired(ref, fieldPath),
|
|
342
|
+
expectedType: ref._type ? String(ref._type) : 'unknown',
|
|
343
|
+
metadata: {
|
|
344
|
+
affectsReadiness: this.affectsReadiness(ref, fieldPath),
|
|
345
|
+
conditional: this.isConditional(fieldPath),
|
|
346
|
+
expressionContext: this.getExpressionContext(fieldPath)
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Create a skipped dependency info (for disabled tracking)
|
|
352
|
+
*/
|
|
353
|
+
createSkippedDependencyInfo(ref, fieldPath, dependencyType) {
|
|
354
|
+
return {
|
|
355
|
+
reference: ref,
|
|
356
|
+
fieldPath,
|
|
357
|
+
dependencyType,
|
|
358
|
+
required: false,
|
|
359
|
+
expectedType: 'skipped',
|
|
360
|
+
metadata: {
|
|
361
|
+
affectsReadiness: false,
|
|
362
|
+
conditional: false,
|
|
363
|
+
expressionContext: 'skipped'
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Determine the type of dependency
|
|
369
|
+
*/
|
|
370
|
+
determineDependencyType(ref) {
|
|
371
|
+
if (ref.resourceId === '__schema__') {
|
|
372
|
+
return 'schema';
|
|
373
|
+
}
|
|
374
|
+
// Check if it's a known resource type
|
|
375
|
+
if (ref.resourceId.match(/^[a-z][a-z0-9-]*$/)) {
|
|
376
|
+
return 'resource';
|
|
377
|
+
}
|
|
378
|
+
return 'external';
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Determine if a dependency is required
|
|
382
|
+
*/
|
|
383
|
+
isDependencyRequired(ref, fieldPath) {
|
|
384
|
+
// Schema dependencies are generally required
|
|
385
|
+
if (ref.resourceId === '__schema__') {
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
// Dependencies in required fields are required
|
|
389
|
+
if (this.isRequiredField(fieldPath)) {
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
// Dependencies in conditional expressions may not be required
|
|
393
|
+
if (this.isConditional(fieldPath)) {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
// Default to required for safety
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Check if a field path represents a required field
|
|
401
|
+
*/
|
|
402
|
+
isRequiredField(fieldPath) {
|
|
403
|
+
// Common required fields
|
|
404
|
+
const requiredFields = ['name', 'image', 'namespace'];
|
|
405
|
+
return requiredFields.some(field => fieldPath.includes(field));
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Check if a dependency affects resource readiness
|
|
409
|
+
*/
|
|
410
|
+
affectsReadiness(ref, fieldPath) {
|
|
411
|
+
// Status field dependencies typically affect readiness
|
|
412
|
+
if (ref.fieldPath.startsWith('status.')) {
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
// Dependencies in readiness-related fields
|
|
416
|
+
const readinessFields = ['ready', 'available', 'replicas', 'conditions'];
|
|
417
|
+
return readinessFields.some(field => fieldPath.includes(field) || ref.fieldPath.includes(field));
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Check if a field path is in a conditional context
|
|
421
|
+
*/
|
|
422
|
+
isConditional(fieldPath) {
|
|
423
|
+
// Look for conditional patterns in field path
|
|
424
|
+
return fieldPath.includes('?') ||
|
|
425
|
+
fieldPath.includes('||') ||
|
|
426
|
+
fieldPath.includes('&&') ||
|
|
427
|
+
fieldPath.includes('??');
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Get expression context for a field path
|
|
431
|
+
*/
|
|
432
|
+
getExpressionContext(fieldPath) {
|
|
433
|
+
// Extract the top-level field context
|
|
434
|
+
const parts = fieldPath.split('.');
|
|
435
|
+
if (parts.length > 0 && parts[0]) {
|
|
436
|
+
return parts[0];
|
|
437
|
+
}
|
|
438
|
+
return 'unknown';
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Add dependency info to the dependency graph
|
|
442
|
+
*/
|
|
443
|
+
addToDependencyGraph(resourceId, dependencyInfo) {
|
|
444
|
+
// Add to dependencies map
|
|
445
|
+
if (!this.dependencyGraph.dependencies.has(resourceId)) {
|
|
446
|
+
this.dependencyGraph.dependencies.set(resourceId, []);
|
|
447
|
+
}
|
|
448
|
+
this.dependencyGraph.dependencies.get(resourceId)?.push(dependencyInfo);
|
|
449
|
+
// Add to dependents map (reverse mapping)
|
|
450
|
+
const dependentResourceId = dependencyInfo.reference.resourceId;
|
|
451
|
+
if (dependentResourceId !== '__schema__') {
|
|
452
|
+
if (!this.dependencyGraph.dependents.has(dependentResourceId)) {
|
|
453
|
+
this.dependencyGraph.dependents.set(dependentResourceId, []);
|
|
454
|
+
}
|
|
455
|
+
const dependents = this.dependencyGraph.dependents.get(dependentResourceId);
|
|
456
|
+
if (!dependents.includes(resourceId)) {
|
|
457
|
+
dependents.push(resourceId);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Detect circular dependencies in the dependency graph
|
|
463
|
+
*/
|
|
464
|
+
detectCircularDependencies() {
|
|
465
|
+
this.dependencyGraph.circularChains = [];
|
|
466
|
+
const visited = new Set();
|
|
467
|
+
const recursionStack = new Set();
|
|
468
|
+
// Check each resource for cycles
|
|
469
|
+
for (const resourceId of this.dependencyGraph.dependencies.keys()) {
|
|
470
|
+
if (!visited.has(resourceId)) {
|
|
471
|
+
this.detectCyclesFromResource(resourceId, [], visited, recursionStack);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Detect cycles starting from a specific resource
|
|
477
|
+
*/
|
|
478
|
+
detectCyclesFromResource(resourceId, path, visited, recursionStack) {
|
|
479
|
+
if (recursionStack.has(resourceId)) {
|
|
480
|
+
// Found a cycle
|
|
481
|
+
const cycleStart = path.indexOf(resourceId);
|
|
482
|
+
if (cycleStart >= 0) {
|
|
483
|
+
const cycle = path.slice(cycleStart).concat([resourceId]);
|
|
484
|
+
this.dependencyGraph.circularChains.push(cycle);
|
|
485
|
+
}
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (visited.has(resourceId)) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
visited.add(resourceId);
|
|
492
|
+
recursionStack.add(resourceId);
|
|
493
|
+
// Follow dependencies
|
|
494
|
+
const dependencies = this.dependencyGraph.dependencies.get(resourceId) || [];
|
|
495
|
+
for (const dep of dependencies) {
|
|
496
|
+
if (dep.reference.resourceId !== '__schema__') {
|
|
497
|
+
this.detectCyclesFromResource(dep.reference.resourceId, [...path, resourceId], visited, recursionStack);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
recursionStack.delete(resourceId);
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Compute deployment order using topological sort
|
|
504
|
+
*/
|
|
505
|
+
computeDeploymentOrder() {
|
|
506
|
+
const inDegree = new Map();
|
|
507
|
+
const adjList = new Map();
|
|
508
|
+
// Initialize in-degree and adjacency list
|
|
509
|
+
for (const [resourceId, dependencies] of this.dependencyGraph.dependencies) {
|
|
510
|
+
if (!inDegree.has(resourceId)) {
|
|
511
|
+
inDegree.set(resourceId, 0);
|
|
512
|
+
}
|
|
513
|
+
if (!adjList.has(resourceId)) {
|
|
514
|
+
adjList.set(resourceId, []);
|
|
515
|
+
}
|
|
516
|
+
for (const dep of dependencies) {
|
|
517
|
+
if (dep.reference.resourceId !== '__schema__') {
|
|
518
|
+
const depResourceId = dep.reference.resourceId;
|
|
519
|
+
if (!inDegree.has(depResourceId)) {
|
|
520
|
+
inDegree.set(depResourceId, 0);
|
|
521
|
+
}
|
|
522
|
+
if (!adjList.has(depResourceId)) {
|
|
523
|
+
adjList.set(depResourceId, []);
|
|
524
|
+
}
|
|
525
|
+
// Add edge from dependency to dependent
|
|
526
|
+
adjList.get(depResourceId)?.push(resourceId);
|
|
527
|
+
inDegree.set(resourceId, inDegree.get(resourceId) + 1);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// Kahn's algorithm for topological sort
|
|
532
|
+
const queue = [];
|
|
533
|
+
const result = [];
|
|
534
|
+
// Find all resources with no dependencies
|
|
535
|
+
for (const [resourceId, degree] of inDegree) {
|
|
536
|
+
if (degree === 0) {
|
|
537
|
+
queue.push(resourceId);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
while (queue.length > 0) {
|
|
541
|
+
const current = queue.shift();
|
|
542
|
+
result.push(current);
|
|
543
|
+
// Process all dependents
|
|
544
|
+
const dependents = adjList.get(current) || [];
|
|
545
|
+
for (const dependent of dependents) {
|
|
546
|
+
const newDegree = inDegree.get(dependent) - 1;
|
|
547
|
+
inDegree.set(dependent, newDegree);
|
|
548
|
+
if (newDegree === 0) {
|
|
549
|
+
queue.push(dependent);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
this.dependencyGraph.deploymentOrder = result;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Get the current dependency graph
|
|
557
|
+
*/
|
|
558
|
+
getDependencyGraph() {
|
|
559
|
+
return { ...this.dependencyGraph };
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Get dependencies for a specific resource
|
|
563
|
+
*/
|
|
564
|
+
getDependencies(resourceId) {
|
|
565
|
+
return this.dependencyGraph.dependencies.get(resourceId) || [];
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Get resources that depend on a specific resource
|
|
569
|
+
*/
|
|
570
|
+
getDependents(resourceId) {
|
|
571
|
+
return this.dependencyGraph.dependents.get(resourceId) || [];
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Check if there are circular dependencies
|
|
575
|
+
*/
|
|
576
|
+
hasCircularDependencies() {
|
|
577
|
+
return this.dependencyGraph.circularChains.length > 0;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Get the deployment order
|
|
581
|
+
*/
|
|
582
|
+
getDeploymentOrder() {
|
|
583
|
+
return [...this.dependencyGraph.deploymentOrder];
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Reset the dependency graph
|
|
587
|
+
*/
|
|
588
|
+
reset() {
|
|
589
|
+
this.dependencyGraph = {
|
|
590
|
+
dependencies: new Map(),
|
|
591
|
+
dependents: new Map(),
|
|
592
|
+
circularChains: [],
|
|
593
|
+
deploymentOrder: []
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Advanced circular dependency detection with detailed chain analysis
|
|
598
|
+
* This provides more sophisticated analysis of KubernetesRef chains
|
|
599
|
+
*/
|
|
600
|
+
detectCircularDependencyChains() {
|
|
601
|
+
const analysis = {
|
|
602
|
+
hasCircularDependencies: false,
|
|
603
|
+
circularChains: [],
|
|
604
|
+
chainAnalysis: [],
|
|
605
|
+
recommendations: []
|
|
606
|
+
};
|
|
607
|
+
// Use Tarjan's strongly connected components algorithm for better cycle detection
|
|
608
|
+
const tarjanResult = this.findStronglyConnectedComponents();
|
|
609
|
+
for (const component of tarjanResult.components) {
|
|
610
|
+
if (component.length > 1) {
|
|
611
|
+
// This is a circular dependency
|
|
612
|
+
analysis.hasCircularDependencies = true;
|
|
613
|
+
analysis.circularChains.push(component);
|
|
614
|
+
// Analyze the chain
|
|
615
|
+
const chainAnalysis = this.analyzeCircularChain(component);
|
|
616
|
+
analysis.chainAnalysis.push(chainAnalysis);
|
|
617
|
+
// Generate recommendations
|
|
618
|
+
const recommendations = this.generateCircularDependencyRecommendations(chainAnalysis);
|
|
619
|
+
analysis.recommendations.push(...recommendations);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return analysis;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Find strongly connected components using Tarjan's algorithm
|
|
626
|
+
*/
|
|
627
|
+
findStronglyConnectedComponents() {
|
|
628
|
+
const index = new Map();
|
|
629
|
+
const lowLink = new Map();
|
|
630
|
+
const onStack = new Set();
|
|
631
|
+
const stack = [];
|
|
632
|
+
const components = [];
|
|
633
|
+
let currentIndex = 0;
|
|
634
|
+
const strongConnect = (resourceId) => {
|
|
635
|
+
index.set(resourceId, currentIndex);
|
|
636
|
+
lowLink.set(resourceId, currentIndex);
|
|
637
|
+
currentIndex++;
|
|
638
|
+
stack.push(resourceId);
|
|
639
|
+
onStack.add(resourceId);
|
|
640
|
+
// Get dependencies for this resource
|
|
641
|
+
const dependencies = this.dependencyGraph.dependencies.get(resourceId) || [];
|
|
642
|
+
for (const dep of dependencies) {
|
|
643
|
+
const depResourceId = dep.reference.resourceId;
|
|
644
|
+
if (depResourceId === '__schema__')
|
|
645
|
+
continue; // Skip schema references
|
|
646
|
+
if (!index.has(depResourceId)) {
|
|
647
|
+
strongConnect(depResourceId);
|
|
648
|
+
lowLink.set(resourceId, Math.min(lowLink.get(resourceId), lowLink.get(depResourceId)));
|
|
649
|
+
}
|
|
650
|
+
else if (onStack.has(depResourceId)) {
|
|
651
|
+
lowLink.set(resourceId, Math.min(lowLink.get(resourceId), index.get(depResourceId)));
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// If resourceId is a root node, pop the stack and create a component
|
|
655
|
+
if (lowLink.get(resourceId) === index.get(resourceId)) {
|
|
656
|
+
const component = [];
|
|
657
|
+
let w;
|
|
658
|
+
do {
|
|
659
|
+
w = stack.pop();
|
|
660
|
+
onStack.delete(w);
|
|
661
|
+
component.push(w);
|
|
662
|
+
} while (w !== resourceId);
|
|
663
|
+
components.push(component);
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
// Run algorithm on all unvisited nodes
|
|
667
|
+
for (const resourceId of this.dependencyGraph.dependencies.keys()) {
|
|
668
|
+
if (!index.has(resourceId)) {
|
|
669
|
+
strongConnect(resourceId);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return { components };
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Analyze a circular dependency chain
|
|
676
|
+
*/
|
|
677
|
+
analyzeCircularChain(chain) {
|
|
678
|
+
const analysis = {
|
|
679
|
+
chain,
|
|
680
|
+
chainLength: chain.length,
|
|
681
|
+
severity: this.calculateChainSeverity(chain),
|
|
682
|
+
breakPoints: this.findPotentialBreakPoints(chain),
|
|
683
|
+
affectedFields: this.getAffectedFields(chain),
|
|
684
|
+
riskLevel: 'medium'
|
|
685
|
+
};
|
|
686
|
+
// Determine risk level
|
|
687
|
+
if (analysis.severity > 0.8 || analysis.chainLength > 5) {
|
|
688
|
+
analysis.riskLevel = 'high';
|
|
689
|
+
}
|
|
690
|
+
else if (analysis.severity < 0.3 && analysis.chainLength <= 2) {
|
|
691
|
+
analysis.riskLevel = 'low';
|
|
692
|
+
}
|
|
693
|
+
return analysis;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Calculate the severity of a circular dependency chain
|
|
697
|
+
*/
|
|
698
|
+
calculateChainSeverity(chain) {
|
|
699
|
+
let severity = 0;
|
|
700
|
+
let totalDependencies = 0;
|
|
701
|
+
for (const resourceId of chain) {
|
|
702
|
+
const dependencies = this.dependencyGraph.dependencies.get(resourceId) || [];
|
|
703
|
+
totalDependencies += dependencies.length;
|
|
704
|
+
// Increase severity for required dependencies
|
|
705
|
+
const requiredDeps = dependencies.filter(dep => dep.required);
|
|
706
|
+
severity += requiredDeps.length * 0.3;
|
|
707
|
+
// Increase severity for readiness-affecting dependencies
|
|
708
|
+
const readinessDeps = dependencies.filter(dep => dep.metadata?.affectsReadiness);
|
|
709
|
+
severity += readinessDeps.length * 0.2;
|
|
710
|
+
}
|
|
711
|
+
// Normalize by chain length and total dependencies
|
|
712
|
+
return Math.min(severity / (chain.length * Math.max(totalDependencies, 1)), 1);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Find potential break points in a circular chain
|
|
716
|
+
*/
|
|
717
|
+
findPotentialBreakPoints(chain) {
|
|
718
|
+
const breakPoints = [];
|
|
719
|
+
for (const resourceId of chain) {
|
|
720
|
+
const dependencies = this.dependencyGraph.dependencies.get(resourceId) || [];
|
|
721
|
+
// Look for optional dependencies that could be break points
|
|
722
|
+
const optionalDeps = dependencies.filter(dep => !dep.required);
|
|
723
|
+
if (optionalDeps.length > 0) {
|
|
724
|
+
breakPoints.push(resourceId);
|
|
725
|
+
}
|
|
726
|
+
// Look for conditional dependencies
|
|
727
|
+
const conditionalDeps = dependencies.filter(dep => dep.metadata?.conditional);
|
|
728
|
+
if (conditionalDeps.length > 0) {
|
|
729
|
+
breakPoints.push(resourceId);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return [...new Set(breakPoints)]; // Remove duplicates
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Get affected fields for a circular chain
|
|
736
|
+
*/
|
|
737
|
+
getAffectedFields(chain) {
|
|
738
|
+
const affectedFields = [];
|
|
739
|
+
for (const resourceId of chain) {
|
|
740
|
+
const dependencies = this.dependencyGraph.dependencies.get(resourceId) || [];
|
|
741
|
+
for (const dep of dependencies) {
|
|
742
|
+
if (chain.includes(dep.reference.resourceId)) {
|
|
743
|
+
affectedFields.push(`${resourceId}.${dep.fieldPath}`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return affectedFields;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Generate recommendations for resolving circular dependencies
|
|
751
|
+
*/
|
|
752
|
+
generateCircularDependencyRecommendations(chainAnalysis) {
|
|
753
|
+
const recommendations = [];
|
|
754
|
+
// Recommend breaking at optional dependencies
|
|
755
|
+
if (chainAnalysis.breakPoints.length > 0) {
|
|
756
|
+
recommendations.push({
|
|
757
|
+
type: 'break-optional-dependency',
|
|
758
|
+
description: `Consider making dependencies optional at: ${chainAnalysis.breakPoints.join(', ')}`,
|
|
759
|
+
severity: 'medium',
|
|
760
|
+
affectedResources: chainAnalysis.breakPoints,
|
|
761
|
+
implementation: 'Use conditional expressions or default values for these dependencies'
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
// Recommend refactoring for high-severity chains
|
|
765
|
+
if (chainAnalysis.severity > 0.7) {
|
|
766
|
+
recommendations.push({
|
|
767
|
+
type: 'refactor-architecture',
|
|
768
|
+
description: 'Consider refactoring the resource architecture to eliminate circular dependencies',
|
|
769
|
+
severity: 'high',
|
|
770
|
+
affectedResources: chainAnalysis.chain,
|
|
771
|
+
implementation: 'Extract shared dependencies into separate resources or use event-driven patterns'
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
// Recommend using external configuration for long chains
|
|
775
|
+
if (chainAnalysis.chainLength > 4) {
|
|
776
|
+
recommendations.push({
|
|
777
|
+
type: 'external-configuration',
|
|
778
|
+
description: 'Consider using external configuration (ConfigMaps, Secrets) to break the dependency chain',
|
|
779
|
+
severity: 'medium',
|
|
780
|
+
affectedResources: chainAnalysis.chain,
|
|
781
|
+
implementation: 'Move configuration values to ConfigMaps and reference them instead of cross-resource dependencies'
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
return recommendations;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Comprehensive resource type validator for KubernetesRef objects
|
|
789
|
+
* This implements requirement 4.5: resource type validation
|
|
790
|
+
*/
|
|
791
|
+
export class ResourceTypeValidator {
|
|
792
|
+
knownResourceTypes;
|
|
793
|
+
schemaValidators;
|
|
794
|
+
constructor() {
|
|
795
|
+
this.knownResourceTypes = new Map();
|
|
796
|
+
this.schemaValidators = new Map();
|
|
797
|
+
this.initializeKnownTypes();
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Validate a KubernetesRef for type correctness
|
|
801
|
+
*/
|
|
802
|
+
validateKubernetesRef(ref, context) {
|
|
803
|
+
const result = {
|
|
804
|
+
fieldPath: `${ref.resourceId}.${ref.fieldPath}`,
|
|
805
|
+
reference: ref,
|
|
806
|
+
valid: true,
|
|
807
|
+
expectedType: ref._type ? String(ref._type) : 'unknown'
|
|
808
|
+
};
|
|
809
|
+
try {
|
|
810
|
+
if (ref.resourceId === '__schema__') {
|
|
811
|
+
return this.validateSchemaRef(ref, context, result);
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
return this.validateResourceRef(ref, context, result);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
catch (error) {
|
|
818
|
+
result.valid = false;
|
|
819
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
820
|
+
return result;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Validate a schema reference
|
|
825
|
+
*/
|
|
826
|
+
validateSchemaRef(ref, context, result) {
|
|
827
|
+
if (!context.schemaProxy) {
|
|
828
|
+
result.valid = false;
|
|
829
|
+
result.error = 'Schema proxy not available for validation';
|
|
830
|
+
return result;
|
|
831
|
+
}
|
|
832
|
+
// Validate field path structure
|
|
833
|
+
const pathValidation = this.validateSchemaFieldPath(ref.fieldPath);
|
|
834
|
+
if (!pathValidation.valid) {
|
|
835
|
+
result.valid = false;
|
|
836
|
+
result.error = pathValidation.error || 'Validation failed';
|
|
837
|
+
return result;
|
|
838
|
+
}
|
|
839
|
+
// Validate against schema if available
|
|
840
|
+
const schemaValidator = this.schemaValidators.get(context.schemaType || 'default');
|
|
841
|
+
if (schemaValidator) {
|
|
842
|
+
const schemaValidation = schemaValidator.validateField(ref.fieldPath, ref._type);
|
|
843
|
+
if (!schemaValidation.valid) {
|
|
844
|
+
result.valid = false;
|
|
845
|
+
result.error = schemaValidation.error || 'Schema validation failed';
|
|
846
|
+
result.actualType = schemaValidation.actualType || 'unknown';
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return result;
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Validate a resource reference
|
|
853
|
+
*/
|
|
854
|
+
validateResourceRef(ref, context, result) {
|
|
855
|
+
// Check if resource exists
|
|
856
|
+
const resource = context.availableResources?.[ref.resourceId];
|
|
857
|
+
if (!resource) {
|
|
858
|
+
result.valid = false;
|
|
859
|
+
result.error = `Resource '${ref.resourceId}' not found`;
|
|
860
|
+
return result;
|
|
861
|
+
}
|
|
862
|
+
// Validate field path structure
|
|
863
|
+
const pathValidation = this.validateResourceFieldPath(ref.fieldPath, resource);
|
|
864
|
+
if (!pathValidation.valid) {
|
|
865
|
+
result.valid = false;
|
|
866
|
+
result.error = pathValidation.error || 'Path validation failed';
|
|
867
|
+
result.actualType = pathValidation.actualType || 'unknown';
|
|
868
|
+
return result;
|
|
869
|
+
}
|
|
870
|
+
// Validate type compatibility
|
|
871
|
+
const typeValidation = this.validateTypeCompatibility(ref, resource);
|
|
872
|
+
if (!typeValidation.valid) {
|
|
873
|
+
result.valid = false;
|
|
874
|
+
result.error = typeValidation.error || 'Type validation failed';
|
|
875
|
+
result.actualType = typeValidation.actualType || 'unknown';
|
|
876
|
+
}
|
|
877
|
+
return result;
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Validate schema field path structure
|
|
881
|
+
*/
|
|
882
|
+
validateSchemaFieldPath(fieldPath) {
|
|
883
|
+
const parts = fieldPath.split('.');
|
|
884
|
+
if (parts.length < 2) {
|
|
885
|
+
return {
|
|
886
|
+
valid: false,
|
|
887
|
+
error: 'Schema field path must have at least 2 parts (e.g., spec.name)'
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
const rootField = parts[0];
|
|
891
|
+
if (rootField !== 'spec' && rootField !== 'status') {
|
|
892
|
+
return {
|
|
893
|
+
valid: false,
|
|
894
|
+
error: `Schema field path must start with 'spec' or 'status', got '${rootField}'`
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
return { valid: true };
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Validate resource field path structure
|
|
901
|
+
*/
|
|
902
|
+
validateResourceFieldPath(fieldPath, resource) {
|
|
903
|
+
const parts = fieldPath.split('.');
|
|
904
|
+
if (parts.length === 0) {
|
|
905
|
+
return {
|
|
906
|
+
valid: false,
|
|
907
|
+
error: 'Field path cannot be empty'
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
const rootField = parts[0];
|
|
911
|
+
const validRootFields = ['metadata', 'spec', 'status'];
|
|
912
|
+
if (!rootField || !validRootFields.includes(rootField)) {
|
|
913
|
+
return {
|
|
914
|
+
valid: false,
|
|
915
|
+
error: `Invalid root field '${rootField || 'undefined'}'. Must be one of: ${validRootFields.join(', ')}`
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
// Validate specific field patterns
|
|
919
|
+
const patternValidation = this.validateFieldPattern(fieldPath);
|
|
920
|
+
if (!patternValidation.valid) {
|
|
921
|
+
return patternValidation;
|
|
922
|
+
}
|
|
923
|
+
// Try to infer actual type from resource
|
|
924
|
+
const actualType = this.inferFieldType(fieldPath, resource);
|
|
925
|
+
return {
|
|
926
|
+
valid: true,
|
|
927
|
+
actualType: actualType || 'unknown'
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Validate field patterns against known Kubernetes patterns
|
|
932
|
+
*/
|
|
933
|
+
validateFieldPattern(fieldPath) {
|
|
934
|
+
// Known valid patterns
|
|
935
|
+
const validPatterns = [
|
|
936
|
+
// Metadata fields
|
|
937
|
+
{ pattern: /^metadata\.(name|namespace|uid|resourceVersion|generation)$/, type: 'string' },
|
|
938
|
+
{ pattern: /^metadata\.labels\..+$/, type: 'string' },
|
|
939
|
+
{ pattern: /^metadata\.annotations\..+$/, type: 'string' },
|
|
940
|
+
// Common spec fields
|
|
941
|
+
{ pattern: /^spec\.replicas$/, type: 'number' },
|
|
942
|
+
{ pattern: /^spec\.selector\.matchLabels\..+$/, type: 'string' },
|
|
943
|
+
// Common status fields
|
|
944
|
+
{ pattern: /^status\.ready$/, type: 'boolean' },
|
|
945
|
+
{ pattern: /^status\.(replicas|readyReplicas|availableReplicas|unavailableReplicas)$/, type: 'number' },
|
|
946
|
+
{ pattern: /^status\.conditions\[\d+\]\.(type|status|reason|message)$/, type: 'string' },
|
|
947
|
+
{ pattern: /^status\.conditions\[\d+\]\.lastTransitionTime$/, type: 'string' },
|
|
948
|
+
{ pattern: /^status\.loadBalancer\.ingress\[\d+\]\.(ip|hostname)$/, type: 'string' },
|
|
949
|
+
{ pattern: /^status\.(podIP|hostIP)$/, type: 'string' },
|
|
950
|
+
{ pattern: /^status\.phase$/, type: 'string' },
|
|
951
|
+
// Generic patterns
|
|
952
|
+
{ pattern: /^spec\./, type: 'unknown' },
|
|
953
|
+
{ pattern: /^status\./, type: 'unknown' }
|
|
954
|
+
];
|
|
955
|
+
const matchingPattern = validPatterns.find(p => p.pattern.test(fieldPath));
|
|
956
|
+
if (!matchingPattern) {
|
|
957
|
+
return {
|
|
958
|
+
valid: false,
|
|
959
|
+
error: `Field path '${fieldPath}' does not match any known Kubernetes field patterns`
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
return {
|
|
963
|
+
valid: true,
|
|
964
|
+
actualType: matchingPattern.type
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Validate type compatibility between expected and actual types
|
|
969
|
+
*/
|
|
970
|
+
validateTypeCompatibility(ref, resource) {
|
|
971
|
+
if (!ref._type) {
|
|
972
|
+
// No expected type specified, assume compatible
|
|
973
|
+
return { valid: true };
|
|
974
|
+
}
|
|
975
|
+
const expectedType = String(ref._type);
|
|
976
|
+
const actualType = this.inferFieldType(ref.fieldPath, resource);
|
|
977
|
+
if (!actualType || actualType === 'unknown') {
|
|
978
|
+
// Cannot determine actual type, assume compatible
|
|
979
|
+
return { valid: true };
|
|
980
|
+
}
|
|
981
|
+
// Check type compatibility
|
|
982
|
+
const compatible = this.areTypesCompatible(expectedType, actualType);
|
|
983
|
+
if (!compatible) {
|
|
984
|
+
return {
|
|
985
|
+
valid: false,
|
|
986
|
+
error: `Type mismatch: expected '${expectedType}' but field has type '${actualType}'`,
|
|
987
|
+
actualType
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
return { valid: true, actualType };
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Check if two types are compatible
|
|
994
|
+
*/
|
|
995
|
+
areTypesCompatible(expectedType, actualType) {
|
|
996
|
+
// Exact match
|
|
997
|
+
if (expectedType === actualType) {
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
// Compatible type mappings
|
|
1001
|
+
const compatibleTypes = {
|
|
1002
|
+
'string': ['string', 'unknown'],
|
|
1003
|
+
'number': ['number', 'integer', 'float', 'unknown'],
|
|
1004
|
+
'boolean': ['boolean', 'unknown'],
|
|
1005
|
+
'object': ['object', 'unknown'],
|
|
1006
|
+
'array': ['array', 'unknown'],
|
|
1007
|
+
'unknown': ['string', 'number', 'boolean', 'object', 'array', 'unknown']
|
|
1008
|
+
};
|
|
1009
|
+
const compatibleWithExpected = compatibleTypes[expectedType] || [];
|
|
1010
|
+
return compatibleWithExpected.includes(actualType);
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Infer the type of a field from a resource
|
|
1014
|
+
*/
|
|
1015
|
+
inferFieldType(fieldPath, _resource) {
|
|
1016
|
+
// Try to infer type from field path patterns
|
|
1017
|
+
if (fieldPath.includes('replicas') || fieldPath.includes('generation') || fieldPath.includes('port')) {
|
|
1018
|
+
return 'number';
|
|
1019
|
+
}
|
|
1020
|
+
if (fieldPath.includes('ready') || fieldPath.includes('enabled')) {
|
|
1021
|
+
return 'boolean';
|
|
1022
|
+
}
|
|
1023
|
+
if (fieldPath.includes('name') || fieldPath.includes('namespace') || fieldPath.includes('ip') || fieldPath.includes('phase')) {
|
|
1024
|
+
return 'string';
|
|
1025
|
+
}
|
|
1026
|
+
if (fieldPath.includes('labels') || fieldPath.includes('annotations') || fieldPath.includes('selector')) {
|
|
1027
|
+
return 'object';
|
|
1028
|
+
}
|
|
1029
|
+
if (fieldPath.includes('conditions') || fieldPath.includes('ingress')) {
|
|
1030
|
+
return 'array';
|
|
1031
|
+
}
|
|
1032
|
+
return 'unknown';
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Initialize known resource types
|
|
1036
|
+
*/
|
|
1037
|
+
initializeKnownTypes() {
|
|
1038
|
+
// Common Kubernetes resource types
|
|
1039
|
+
this.knownResourceTypes.set('Deployment', {
|
|
1040
|
+
apiVersion: 'apps/v1',
|
|
1041
|
+
kind: 'Deployment',
|
|
1042
|
+
commonFields: {
|
|
1043
|
+
'metadata.name': 'string',
|
|
1044
|
+
'metadata.namespace': 'string',
|
|
1045
|
+
'spec.replicas': 'number',
|
|
1046
|
+
'status.readyReplicas': 'number',
|
|
1047
|
+
'status.availableReplicas': 'number'
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
this.knownResourceTypes.set('Service', {
|
|
1051
|
+
apiVersion: 'v1',
|
|
1052
|
+
kind: 'Service',
|
|
1053
|
+
commonFields: {
|
|
1054
|
+
'metadata.name': 'string',
|
|
1055
|
+
'metadata.namespace': 'string',
|
|
1056
|
+
'spec.type': 'string',
|
|
1057
|
+
'spec.ports': 'array',
|
|
1058
|
+
'status.loadBalancer.ingress': 'array'
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
this.knownResourceTypes.set('Pod', {
|
|
1062
|
+
apiVersion: 'v1',
|
|
1063
|
+
kind: 'Pod',
|
|
1064
|
+
commonFields: {
|
|
1065
|
+
'metadata.name': 'string',
|
|
1066
|
+
'metadata.namespace': 'string',
|
|
1067
|
+
'status.phase': 'string',
|
|
1068
|
+
'status.podIP': 'string',
|
|
1069
|
+
'status.hostIP': 'string'
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Register a custom schema validator
|
|
1075
|
+
*/
|
|
1076
|
+
registerSchemaValidator(schemaType, validator) {
|
|
1077
|
+
this.schemaValidators.set(schemaType, validator);
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Register a custom resource type
|
|
1081
|
+
*/
|
|
1082
|
+
registerResourceType(name, typeInfo) {
|
|
1083
|
+
this.knownResourceTypes.set(name, typeInfo);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
//# sourceMappingURL=resource-analyzer.js.map
|