UNPKG

@vfarcic/dot-ai

Version:

Universal Kubernetes application deployment agent with CLI and MCP interfaces

493 lines (492 loc) 21.1 kB
"use strict"; /** * Generate Manifests Tool - AI-driven manifest generation with validation loop */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.GENERATEMANIFESTS_TOOL_INPUT_SCHEMA = exports.GENERATEMANIFESTS_TOOL_DESCRIPTION = exports.GENERATEMANIFESTS_TOOL_NAME = void 0; exports.handleGenerateManifestsTool = handleGenerateManifestsTool; const zod_1 = require("zod"); const error_handling_1 = require("../core/error-handling"); const claude_1 = require("../core/claude"); const cluster_utils_1 = require("../core/cluster-utils"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const yaml = __importStar(require("js-yaml")); const child_process_1 = require("child_process"); const session_utils_1 = require("../core/session-utils"); const solution_utils_1 = require("../core/solution-utils"); // Tool metadata for direct MCP registration exports.GENERATEMANIFESTS_TOOL_NAME = 'generateManifests'; exports.GENERATEMANIFESTS_TOOL_DESCRIPTION = 'Generate final Kubernetes manifests from fully configured solution (ONLY after completing ALL stages: required, basic, advanced, and open)'; // Zod schema for MCP registration exports.GENERATEMANIFESTS_TOOL_INPUT_SCHEMA = { solutionId: zod_1.z.string().regex(/^sol_[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{6}_[a-f0-9]+$/).describe('The solution ID to generate manifests for (e.g., sol_2025-07-01T154349_1e1e242592ff)') }; /** * Load solution file and validate structure */ function loadSolutionFile(solutionId, sessionDir) { const solutionPath = path.join(sessionDir, `${solutionId}.json`); if (!fs.existsSync(solutionPath)) { throw new Error(`Solution file not found: ${solutionPath}. Available files: ${fs.readdirSync(sessionDir).filter(f => f.endsWith('.json')).join(', ')}`); } try { const content = fs.readFileSync(solutionPath, 'utf8'); const solution = JSON.parse(content); if (!solution.solutionId || !solution.questions) { throw new Error(`Invalid solution file structure: ${solutionId}. Missing required fields: solutionId or questions`); } return solution; } catch (error) { if (error instanceof SyntaxError) { throw new Error(`Invalid JSON in solution file: ${solutionId}`); } throw error; } } /** * Retrieve schemas for resources specified in the solution */ async function retrieveResourceSchemas(solution, dotAI, logger) { try { // Extract resource references from solution const resourceRefs = (solution.resources || []).map((resource) => ({ kind: resource.kind, apiVersion: resource.apiVersion, group: resource.group })); if (resourceRefs.length === 0) { logger.warn('No resources found in solution for schema retrieval'); return {}; } logger.info('Retrieving schemas for solution resources', { resourceCount: resourceRefs.length, resources: resourceRefs.map((r) => `${r.kind}@${r.apiVersion}`) }); const schemas = {}; // Retrieve schema for each resource for (const resourceRef of resourceRefs) { try { const resourceKey = `${resourceRef.kind}.${resourceRef.apiVersion}`; logger.debug('Retrieving schema', { resourceKey }); // Use discovery engine to explain the resource const explanation = await dotAI.discovery.explainResource(resourceRef.kind); schemas[resourceKey] = { kind: resourceRef.kind, apiVersion: resourceRef.apiVersion, schema: explanation, timestamp: new Date().toISOString() }; logger.debug('Schema retrieved successfully', { resourceKey, schemaLength: explanation.length }); } catch (error) { logger.error('Failed to retrieve schema for resource', error, { resource: resourceRef }); // Fail fast - if we can't get schemas, manifest generation will likely fail throw new Error(`Failed to retrieve schema for ${resourceRef.kind}: ${error instanceof Error ? error.message : String(error)}`); } } logger.info('All resource schemas retrieved successfully', { schemaCount: Object.keys(schemas).length }); return schemas; } catch (error) { logger.error('Schema retrieval failed', error); throw new Error(`Failed to retrieve resource schemas: ${error instanceof Error ? error.message : String(error)}`); } } /** * Validate YAML syntax */ function validateYamlSyntax(yamlContent) { try { yaml.loadAll(yamlContent); return { valid: true }; } catch (error) { return { valid: false, error: error instanceof Error ? error.message : 'Unknown YAML syntax error' }; } } /** * Run kubectl dry-run validation */ async function runKubectlDryRun(yamlPath) { return new Promise((resolve) => { const kubectl = (0, child_process_1.spawn)('kubectl', ['apply', '--dry-run=server', '-f', yamlPath], { stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; kubectl.stdout.on('data', (data) => { stdout += data.toString(); }); kubectl.stderr.on('data', (data) => { stderr += data.toString(); }); kubectl.on('close', (code) => { resolve({ success: code === 0, yamlSyntaxValid: true, // If we get here, YAML was parseable kubectlOutput: stderr || stdout, exitCode: code || 0, stderr, stdout }); }); kubectl.on('error', (error) => { resolve({ success: false, yamlSyntaxValid: true, error: `Failed to run kubectl: ${error.message}`, kubectlOutput: `kubectl command failed: ${error.message}`, exitCode: -1, stderr: error.message, stdout: '' }); }); }); } /** * Validate manifests using multi-layer approach */ async function validateManifests(yamlPath) { // First check if file exists if (!fs.existsSync(yamlPath)) { return { success: false, yamlSyntaxValid: false, error: `Manifest file not found: ${yamlPath}` }; } // Read YAML content const yamlContent = fs.readFileSync(yamlPath, 'utf8'); // 1. YAML syntax validation const syntaxCheck = validateYamlSyntax(yamlContent); if (!syntaxCheck.valid) { return { success: false, yamlSyntaxValid: false, error: `YAML syntax error: ${syntaxCheck.error}`, kubectlOutput: `YAML parsing failed: ${syntaxCheck.error}` }; } // 2. kubectl dry-run validation return await runKubectlDryRun(yamlPath); } /** * Generate manifests using AI with Claude integration */ async function generateManifestsWithAI(solution, dotAI, logger, errorContext, dotAiLabels) { // Load prompt template const promptPath = path.join(__dirname, '..', '..', 'prompts', 'manifest-generation.md'); const template = fs.readFileSync(promptPath, 'utf8'); // Retrieve schemas for solution resources const resourceSchemas = await retrieveResourceSchemas(solution, dotAI, logger); // Prepare template variables const solutionData = JSON.stringify(solution, null, 2); const previousAttempt = errorContext ? ` ### Generated Manifests: \`\`\`yaml ${errorContext.previousManifests} \`\`\` ` : 'None - this is the first attempt.'; const errorDetails = errorContext ? ` **Attempt**: ${errorContext.attempt} **YAML Syntax Valid**: ${errorContext.yamlSyntaxValid} **kubectl Output**: ${errorContext.kubectlOutput} **Exit Code**: ${errorContext.exitCode} **Error Details**: ${errorContext.stderr} ` : 'None - this is the first attempt.'; // Replace template variables const schemasData = JSON.stringify(resourceSchemas, null, 2); const labelsData = dotAiLabels ? JSON.stringify(dotAiLabels, null, 2) : '{}'; const aiPrompt = template .replace('{solution}', solutionData) .replace('{schemas}', schemasData) .replace('{previous_attempt}', previousAttempt) .replace('{error_details}', errorDetails) .replace('{labels}', labelsData); const isRetry = !!errorContext; logger.info('Generating manifests with AI', { isRetry, attempt: errorContext?.attempt, hasErrorContext: !!errorContext, solutionId: solution.solutionId }); // Initialize Claude integration const apiKey = process.env.ANTHROPIC_API_KEY || 'test-key'; const claudeIntegration = new claude_1.ClaudeIntegration(apiKey); // Send prompt to Claude const response = await claudeIntegration.sendMessage(aiPrompt); // Extract YAML content from response let manifestContent = response.content; // Try to extract YAML from code blocks if wrapped const yamlBlockMatch = manifestContent.match(/```(?:yaml|yml)?\s*([\s\S]*?)\s*```/); if (yamlBlockMatch) { manifestContent = yamlBlockMatch[1]; } // Clean up any leading/trailing whitespace manifestContent = manifestContent.trim(); logger.info('AI manifest generation completed', { manifestLength: manifestContent.length, isRetry, solutionId: solution.solutionId }); return manifestContent; } /** * Generate dot-ai application metadata ConfigMap */ function generateMetadataConfigMap(solution, userAnswers, logger) { const appName = userAnswers.name; const namespace = userAnswers.namespace || 'default'; const solutionId = solution.solutionId; const originalIntent = solution.intent; // Validate required fields (will throw if missing) const dotAiLabels = (0, solution_utils_1.addDotAiLabels)(undefined, userAnswers, solution); // Extract resource references from solution const resources = (solution.resources || []).map((resource) => ({ apiVersion: resource.apiVersion, kind: resource.kind, name: resource.name || appName, // Use app name as fallback namespace: resource.namespace || namespace })); // Create ConfigMap object const configMap = { apiVersion: 'v1', kind: 'ConfigMap', metadata: { name: (0, solution_utils_1.sanitizeKubernetesName)(`dot-ai-app-${appName}-${solutionId}`), namespace: namespace, labels: dotAiLabels, annotations: { 'dot-ai.io/original-intent': originalIntent } }, data: { 'deployment-info.yaml': yaml.dump({ appName, deployedAt: new Date().toISOString(), originalIntent, resources }) } }; try { return yaml.dump(configMap); } catch (error) { logger.error('Failed to generate YAML for ConfigMap', error, { configMap, appName, solutionId, namespace }); throw new Error(`ConfigMap YAML generation failed: ${error instanceof Error ? error.message : String(error)}`); } } /** * Direct MCP tool handler for generateManifests functionality */ async function handleGenerateManifestsTool(args, dotAI, logger, requestId) { return await error_handling_1.ErrorHandler.withErrorHandling(async () => { const maxAttempts = 10; logger.debug('Handling generateManifests request', { requestId, solutionId: args?.solutionId }); // Input validation is handled automatically by MCP SDK with Zod schema // args are already validated and typed when we reach this point // Get session directory from environment let sessionDir; try { sessionDir = (0, session_utils_1.getAndValidateSessionDirectory)(args, true); // requireWrite=true for manifest generation logger.debug('Session directory resolved and validated', { sessionDir }); } catch (error) { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.HIGH, error instanceof Error ? error.message : 'Session directory validation failed', { operation: 'session_directory_validation', component: 'GenerateManifestsTool', requestId, suggestedActions: [ 'Ensure session directory exists and is writable', 'Check directory permissions', 'Verify the directory path is correct', 'Verify DOT_AI_SESSION_DIR environment variable is correctly set' ] }); } // Ensure cluster connectivity before proceeding await (0, cluster_utils_1.ensureClusterConnection)(dotAI, logger, requestId, 'GenerateManifestsTool'); // Load solution file let solution; try { solution = loadSolutionFile(args.solutionId, sessionDir); logger.debug('Solution file loaded successfully', { solutionId: args.solutionId, hasQuestions: !!solution.questions, primaryResources: solution.resources }); } catch (error) { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.STORAGE, error_handling_1.ErrorSeverity.HIGH, error instanceof Error ? error.message : 'Failed to load solution file', { operation: 'solution_file_load', component: 'GenerateManifestsTool', requestId, input: { solutionId: args.solutionId, sessionDir }, suggestedActions: [ 'Check that the solution ID is correct', 'Verify the solution file exists in the session directory', 'Ensure the solution was fully configured with all stages complete', 'List available solution files in the session directory' ] }); } // Prepare file path for manifests const yamlPath = path.join(sessionDir, `${args.solutionId}.yaml`); // AI generation and validation loop let lastError; for (let attempt = 1; attempt <= maxAttempts; attempt++) { logger.info('AI manifest generation attempt', { attempt, maxAttempts, isRetry: attempt > 1, requestId }); try { // Extract user answers and generate required labels const userAnswers = (0, solution_utils_1.extractUserAnswers)(solution); const dotAiLabels = (0, solution_utils_1.addDotAiLabels)(undefined, userAnswers, solution); // Generate manifests with AI (including labels) const aiManifests = await generateManifestsWithAI(solution, dotAI, logger, lastError, dotAiLabels); // Generate metadata ConfigMap const metadataConfigMap = generateMetadataConfigMap(solution, userAnswers, logger); // Combine ConfigMap with AI-generated manifests const manifests = metadataConfigMap + '---\n' + aiManifests; // Save manifests to file fs.writeFileSync(yamlPath, manifests, 'utf8'); logger.info('Manifests saved to file', { yamlPath, attempt, requestId }); // Save a copy of this attempt for debugging const attemptPath = yamlPath.replace('.yaml', `_attempt_${attempt.toString().padStart(2, '0')}.yaml`); fs.writeFileSync(attemptPath, manifests, 'utf8'); logger.info('Saved manifest attempt for debugging', { attempt, attemptPath, requestId }); // Validate manifests const validation = await validateManifests(yamlPath); if (validation.success) { logger.info('Manifest validation successful', { attempt, yamlPath, requestId }); // Success! Return the validated manifests const response = { success: true, status: 'manifests_generated', solutionId: args.solutionId, manifests: manifests, yamlPath: yamlPath, validationAttempts: attempt, timestamp: new Date().toISOString() }; return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] }; } // Validation failed, prepare error context for next attempt lastError = { attempt, previousManifests: manifests, yamlSyntaxValid: validation.yamlSyntaxValid, kubectlOutput: validation.kubectlOutput, exitCode: validation.exitCode, stderr: validation.stderr, stdout: validation.stdout }; logger.warn('Manifest validation failed', { attempt, maxAttempts, yamlSyntaxValid: validation.yamlSyntaxValid, kubectlOutput: validation.kubectlOutput, requestId }); } catch (error) { logger.error('Error during manifest generation attempt', error); // Check if this is a validation error that should not be retried const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const isValidationError = errorMessage.includes('Application name is required') || errorMessage.includes('Application intent is required'); // If this is a validation error or the last attempt, throw the error immediately if (isValidationError || attempt === maxAttempts) { throw error; } // Prepare error context for retry lastError = { attempt, previousManifests: lastError?.previousManifests || '', yamlSyntaxValid: false, kubectlOutput: errorMessage, exitCode: -1, stderr: errorMessage, stdout: '' }; } } // If we reach here, all attempts failed throw new Error(`Failed to generate valid manifests after ${maxAttempts} attempts. Last error: ${lastError?.kubectlOutput}`); }, { operation: 'generate_manifests', component: 'GenerateManifestsTool', requestId, input: args }); }