UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

237 lines (212 loc) 8.96 kB
import { z } from 'zod'; import { ToolBase, ToolConfig, ToolHandler, ToolResponse } from '../ToolBase'; import { KubernetesToolContext } from './types'; export class KubernetesTool extends ToolBase { readonly config: ToolConfig = { name: 'kubernetes_api_request', shortDescription: 'Make requests to the Kubernetes API', description: `Make requests to the Kubernetes API server to fetch, create, update or delete resources. RESOURCE UPDATE GUIDELINES: - For UPDATE/MODIFY/CHANGE operations: Use PUT method with ONLY the specific fields to change - Provide patch objects that will be merged with existing resources - Use null values to remove fields (e.g., {"spec": {"livenessProbe": null}}) - The system automatically merges patches with current resources before making PUT requests LOG HANDLING FOR MULTI-CONTAINER PODS: - When a user asks for logs from a pod, ALWAYS first check the pod specification to determine the number of containers - If the pod has only one container, directly fetch logs: kubernetes_api_request(url="/api/v1/namespaces/default/pods/pod-name/log", method="GET") - If the pod has multiple containers, you MUST ask the user which container they want logs from - List the available container names and ask the user to specify - Once the user specifies a container, fetch logs with the container parameter: kubernetes_api_request(url="/api/v1/namespaces/default/pods/pod-name/log?container=container-name", method="GET") - NEVER attempt to fetch logs from a multi-container pod without specifying the container name - Examples of user requests that specify containers: - "get logs from pod-name container nginx" → kubernetes_api_request(url="/api/v1/namespaces/default/pods/pod-name/log?container=nginx", method="GET") - "show logs for container sidecar in pod-name" → kubernetes_api_request(url="/api/v1/namespaces/default/pods/pod-name/log?container=sidecar", method="GET") - If you encounter an error about "container name must be specified", the error handler will automatically provide guidance to the user - IMPORTANT: Parse user requests carefully to detect if they're specifying a container name in their request: - Look for patterns like "container [name]", "from container [name]", "[pod-name] [container-name]" - If user specifies a container name, use it directly in the log URL - If user doesn't specify a container and the pod has multiple containers, fetch pod details first to list available containers`, schema: z.object({ url: z .string() .describe('URL to request, e.g., /api/v1/pods or /api/v1/namespaces/default/pods/pod-name'), method: z .string() .describe( 'HTTP method: GET, POST, PUT, DELETE. Use PUT for updating specific fields of existing resources.' ), body: z .string() .optional() .describe( `Optional HTTP Request body: - For PUT: Provide ONLY the fields to change as a JSON patch (e.g., {"spec": {"replicas": 3}}) - Use null to remove fields (e.g., {"spec": {"livenessProbe": null}}) - For POST: Provide the complete resource definition - The system will automatically merge PUT patches with existing resources` ), }), }; private context: KubernetesToolContext | null = null; constructor() { super(); } /** * Set the UI context that allows the tool to interact with the modal's state */ setContext(context: KubernetesToolContext): void { this.context = context; } /** * Check if the tool has a context configured */ hasContext(): boolean { return this.context !== null; } /** * Check if the provided context is different from the current one * We do a shallow comparison of key properties to avoid unnecessary reconfigurations */ isContextDifferent(newContext: KubernetesToolContext): boolean { if (!this.context) { return true; // No context set yet, so it's different } const clustersChanged = JSON.stringify(this.context.selectedClusters) !== JSON.stringify(newContext.selectedClusters); // For UI and callbacks, we only care if they're significantly different // Since these objects are recreated on each render, we'll be less strict // and only reconfigure if clusters changed or if context was null before return clustersChanged; } handler: ToolHandler = async ( { url, method, body }, toolCallId, pendingPrompt ): Promise<ToolResponse> => { if (!this.context) { throw new Error('Kubernetes tool context not configured'); } // For GET requests, we can execute them immediately using the API helper if (method.toUpperCase() === 'GET') { try { const apiResponse = await this.context.callbacks.handleActualApiRequest( url, method, body || '', () => {}, // No-op onClose for GET requests this.context.aiManager, // Use aiManager from context '', // No resource info needed for GET requests undefined, // No target cluster specified undefined // No failure callback for GET requests (they're read-only) ); // Tag the history entry pushed by handleActualApiRequest with the toolCallId // so processToolResponses can pick it up and send it to the LLM for analysis. if (this.context.aiManager?.history) { const history = this.context.aiManager.history; for (let i = history.length - 1; i >= 0; i--) { if (history[i].role === 'tool' && !history[i].toolCallId) { history[i].toolCallId = toolCallId; history[i].name = 'kubernetes_api_request'; break; } } } // The handleActualApiRequest already adds to history (now tagged with toolCallId) return { content: typeof apiResponse === 'string' ? apiResponse : JSON.stringify(apiResponse), shouldAddToHistory: false, // Already added by handleActualApiRequest shouldProcessFollowUp: true, metadata: { method: method.toUpperCase(), url, body, success: true, }, }; } catch (error) { const errorContent = JSON.stringify({ error: true, message: `Error executing GET request: ${error.message}`, request: { method: method.toUpperCase(), url: url, body: body || null, }, }); return { content: errorContent, shouldAddToHistory: true, shouldProcessFollowUp: false, metadata: { method: method.toUpperCase(), url, body, error: error.message, }, }; } } // For non-GET requests, trigger the confirmation dialog this.context.callbacks.setApiRequest({ url, method, body, toolCallId, pendingPrompt, }); this.context.callbacks.setShowApiConfirmation(true); const content = JSON.stringify({ status: 'pending_confirmation', message: `This ${method.toUpperCase()} request requires confirmation before proceeding.`, request: { method: method.toUpperCase(), url: url, body: body || null, }, // Include full resource info for PATCH requests fullResource: method.toUpperCase() === 'PATCH' && body, }); return { content, shouldAddToHistory: false, // Don't add confirmation requests to history shouldProcessFollowUp: false, // Don't process follow-up for confirmation requests metadata: { requiresConfirmation: true, method: method.toUpperCase(), url, body, }, }; }; /** * Handle the API confirmation - this would be called by the confirmation dialog */ async handleApiConfirmation(body: string, resourceInfo: any): Promise<void> { if (!this.context || !this.context.ui.apiRequest) return; const { url, method } = this.context.ui.apiRequest; // Clear the request state this.context.callbacks.setApiRequest(null); // Use the existing API request handler await this.context.callbacks.handleActualApiRequest( url, method, body, this.handleApiDialogClose.bind(this), this.context.aiManager, // Use aiManager from context resourceInfo, undefined, // No target cluster specified undefined // No failure callback needed here as it's handled by the main flow ); } /** * Handle closing the API dialog */ handleApiDialogClose(): void { if (!this.context) return; this.context.callbacks.setShowApiConfirmation(false); this.context.callbacks.setApiRequest(null); this.context.callbacks.setApiResponse(null); this.context.callbacks.setApiRequestError(null); this.context.callbacks.setApiLoading(false); } }