vcluster-yaml-mcp-server 0.1.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.
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Deterministic schema validation - validates user config against GitHub schema
3
+ * NO hardcoded schema structures - everything is dynamic
4
+ */
5
+
6
+ /**
7
+ * Validate user config against vCluster JSON schema
8
+ * @param {Object} userConfig - User's parsed YAML configuration
9
+ * @param {Object} schema - JSON schema from GitHub (chart/values.schema.json)
10
+ * @param {string} version - vCluster version being validated against
11
+ * @returns {Object} Validation result
12
+ */
13
+ export function validateConfigAgainstSchema(userConfig, schema, version) {
14
+ const errors = [];
15
+ const warnings = [];
16
+ const contextErrors = [];
17
+
18
+ // Extract all paths from user config
19
+ const userPaths = extractAllPaths(userConfig);
20
+
21
+ // Validate each path against the schema
22
+ for (const { path, value } of userPaths) {
23
+ const validation = validatePath(path, value, schema);
24
+
25
+ if (!validation.valid) {
26
+ errors.push({
27
+ path,
28
+ severity: 'error',
29
+ error: validation.error,
30
+ suggestion: validation.suggestion,
31
+ correct_alternatives: validation.alternatives || []
32
+ });
33
+ }
34
+
35
+ // If path is valid, check type and enum constraints
36
+ if (validation.valid && validation.schemaNode) {
37
+ const typeCheck = validateType(value, validation.schemaNode, path);
38
+ if (!typeCheck.valid) {
39
+ errors.push({
40
+ path,
41
+ severity: 'error',
42
+ error: typeCheck.error,
43
+ expected_type: typeCheck.expectedType,
44
+ actual_type: typeCheck.actualType,
45
+ fix: typeCheck.fix
46
+ });
47
+ }
48
+
49
+ // Check enum constraints
50
+ if (validation.schemaNode.enum) {
51
+ const enumCheck = validateEnum(value, validation.schemaNode.enum, path);
52
+ if (!enumCheck.valid) {
53
+ errors.push({
54
+ path,
55
+ severity: 'error',
56
+ error: enumCheck.error,
57
+ allowed_values: validation.schemaNode.enum,
58
+ actual_value: value
59
+ });
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ // Check for conflicting configurations
66
+ const conflicts = detectConflicts(userConfig);
67
+ contextErrors.push(...conflicts);
68
+
69
+ // Check required fields
70
+ const missing = checkRequiredFields(userConfig, schema);
71
+ errors.push(...missing);
72
+
73
+ const schema_valid = errors.length === 0;
74
+ const deploy_safe = errors.length === 0 && contextErrors.length === 0;
75
+
76
+ return {
77
+ syntax_valid: true,
78
+ schema_valid,
79
+ deploy_safe,
80
+ version,
81
+ errors,
82
+ warnings,
83
+ context_errors: contextErrors,
84
+ summary: generateSummary(errors, contextErrors, deploy_safe)
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Extract all paths from user config with values
90
+ */
91
+ function extractAllPaths(obj, prefix = '') {
92
+ const paths = [];
93
+
94
+ if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
95
+ for (const [key, value] of Object.entries(obj)) {
96
+ const path = prefix ? `${prefix}.${key}` : key;
97
+ paths.push({ path, value });
98
+
99
+ // Recurse for nested objects
100
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
101
+ paths.push(...extractAllPaths(value, path));
102
+ }
103
+ }
104
+ }
105
+
106
+ return paths;
107
+ }
108
+
109
+ /**
110
+ * Validate if a path exists in schema
111
+ */
112
+ function validatePath(path, value, schema) {
113
+ const parts = path.split('.');
114
+ let current = schema.properties || schema;
115
+ let schemaNode = null;
116
+
117
+ for (let i = 0; i < parts.length; i++) {
118
+ const part = parts[i];
119
+
120
+ if (!current || !current[part]) {
121
+ // Path doesn't exist in schema
122
+ return {
123
+ valid: false,
124
+ error: `Path does not exist in schema`,
125
+ suggestion: `Path '${path}' not found in schema`,
126
+ alternatives: findSimilarPaths(path, schema)
127
+ };
128
+ }
129
+
130
+ schemaNode = current[part];
131
+
132
+ // Navigate deeper
133
+ if (schemaNode.properties) {
134
+ current = schemaNode.properties;
135
+ } else if (schemaNode.additionalProperties) {
136
+ current = schemaNode.additionalProperties;
137
+ } else if (i < parts.length - 1) {
138
+ // Can't go deeper but we're not at the end
139
+ if (typeof value === 'object' && value !== null) {
140
+ return {
141
+ valid: false,
142
+ error: `Path '${parts.slice(0, i + 1).join('.')}' is a leaf node`,
143
+ suggestion: `Remove nested properties under '${parts.slice(0, i + 1).join('.')}'`
144
+ };
145
+ }
146
+ }
147
+ }
148
+
149
+ return {
150
+ valid: true,
151
+ schemaNode
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Find similar paths in schema for suggestions
157
+ */
158
+ function findSimilarPaths(targetPath, schema, maxSuggestions = 3) {
159
+ const allPaths = extractSchemaPaths(schema);
160
+ const targetParts = targetPath.toLowerCase().split('.');
161
+ const targetLast = targetParts[targetParts.length - 1];
162
+
163
+ const scored = allPaths.map(schemaPath => {
164
+ const parts = schemaPath.toLowerCase().split('.');
165
+ const last = parts[parts.length - 1];
166
+
167
+ let score = 0;
168
+ if (last === targetLast) score += 10;
169
+ if (last.includes(targetLast) || targetLast.includes(last)) score += 5;
170
+
171
+ // Shared prefix
172
+ const sharedPrefix = getSharedPrefix(targetParts, parts);
173
+ score += sharedPrefix * 2;
174
+
175
+ return { path: schemaPath, score };
176
+ });
177
+
178
+ return scored
179
+ .filter(s => s.score > 0)
180
+ .sort((a, b) => b.score - a.score)
181
+ .slice(0, maxSuggestions)
182
+ .map(s => s.path);
183
+ }
184
+
185
+ /**
186
+ * Extract all paths from schema
187
+ */
188
+ function extractSchemaPaths(schema, prefix = '') {
189
+ const paths = [];
190
+ const props = schema.properties || schema;
191
+
192
+ if (props && typeof props === 'object') {
193
+ for (const [key, value] of Object.entries(props)) {
194
+ const path = prefix ? `${prefix}.${key}` : key;
195
+ paths.push(path);
196
+
197
+ if (value && value.properties) {
198
+ paths.push(...extractSchemaPaths(value, path));
199
+ }
200
+ }
201
+ }
202
+
203
+ return paths;
204
+ }
205
+
206
+ /**
207
+ * Get shared prefix length
208
+ */
209
+ function getSharedPrefix(arr1, arr2) {
210
+ let count = 0;
211
+ const minLen = Math.min(arr1.length, arr2.length);
212
+
213
+ for (let i = 0; i < minLen; i++) {
214
+ if (arr1[i] === arr2[i]) count++;
215
+ else break;
216
+ }
217
+
218
+ return count;
219
+ }
220
+
221
+ /**
222
+ * Validate value type
223
+ */
224
+ function validateType(value, schemaNode, path) {
225
+ const expectedType = schemaNode.type;
226
+ const actualType = getValueType(value);
227
+
228
+ if (!expectedType) {
229
+ return { valid: true };
230
+ }
231
+
232
+ const expectedTypes = Array.isArray(expectedType) ? expectedType : [expectedType];
233
+
234
+ if (!expectedTypes.includes(actualType)) {
235
+ if (value === null && expectedTypes.includes('null')) {
236
+ return { valid: true };
237
+ }
238
+
239
+ return {
240
+ valid: false,
241
+ error: `Type mismatch`,
242
+ expectedType: expectedTypes.join(' or '),
243
+ actualType,
244
+ fix: `Change value to type: ${expectedTypes.join(' or ')}`
245
+ };
246
+ }
247
+
248
+ return { valid: true };
249
+ }
250
+
251
+ /**
252
+ * Get JavaScript type
253
+ */
254
+ function getValueType(value) {
255
+ if (value === null) return 'null';
256
+ if (Array.isArray(value)) return 'array';
257
+ if (typeof value === 'boolean') return 'boolean';
258
+ if (typeof value === 'number') return 'number';
259
+ if (typeof value === 'string') return 'string';
260
+ if (typeof value === 'object') return 'object';
261
+ return 'unknown';
262
+ }
263
+
264
+ /**
265
+ * Validate enum constraints
266
+ */
267
+ function validateEnum(value, allowedValues, path) {
268
+ if (!allowedValues.includes(value)) {
269
+ return {
270
+ valid: false,
271
+ error: `Value '${value}' not in allowed values: ${allowedValues.join(', ')}`
272
+ };
273
+ }
274
+
275
+ return { valid: true };
276
+ }
277
+
278
+ /**
279
+ * Detect conflicting configurations
280
+ */
281
+ function detectConflicts(userConfig) {
282
+ const conflicts = [];
283
+
284
+ // Check multiple distros enabled
285
+ if (userConfig.controlPlane?.distro) {
286
+ const distros = userConfig.controlPlane.distro;
287
+ const enabled = [];
288
+
289
+ if (distros.k3s?.enabled === true) enabled.push('k3s');
290
+ if (distros.k8s?.enabled === true) enabled.push('k8s');
291
+ if (distros.k0s?.enabled === true) enabled.push('k0s');
292
+ if (distros.eks?.enabled === true) enabled.push('eks');
293
+
294
+ if (enabled.length > 1) {
295
+ conflicts.push({
296
+ issue: 'Multiple distros enabled',
297
+ severity: 'error',
298
+ conflicting_paths: enabled.map(d => `controlPlane.distro.${d}.enabled: true`),
299
+ error: 'Only one distro can be enabled at a time',
300
+ fix: `Set only one distro to enabled: true. Currently: ${enabled.join(', ')}`
301
+ });
302
+ }
303
+ }
304
+
305
+ return conflicts;
306
+ }
307
+
308
+ /**
309
+ * Check required fields
310
+ */
311
+ function checkRequiredFields(userConfig, schema) {
312
+ const errors = [];
313
+ const required = schema.required || [];
314
+
315
+ for (const field of required) {
316
+ if (!(field in userConfig)) {
317
+ errors.push({
318
+ path: field,
319
+ severity: 'error',
320
+ error: `Required field '${field}' is missing`,
321
+ fix: `Add '${field}' to configuration`
322
+ });
323
+ }
324
+ }
325
+
326
+ return errors;
327
+ }
328
+
329
+ /**
330
+ * Generate summary
331
+ */
332
+ function generateSummary(errors, contextErrors, deploySafe) {
333
+ const errorCount = errors.length;
334
+ const contextErrorCount = contextErrors.length;
335
+ const totalIssues = errorCount + contextErrorCount;
336
+
337
+ if (deploySafe) {
338
+ return {
339
+ deploy_safe: true,
340
+ message: 'Configuration is valid and safe to deploy',
341
+ total_errors: 0
342
+ };
343
+ }
344
+
345
+ return {
346
+ deploy_safe: false,
347
+ message: `Configuration contains ${totalIssues} error(s) and will fail deployment. Fix required before creating vCluster.`,
348
+ total_errors: totalIssues,
349
+ severity: 'error'
350
+ };
351
+ }