package tools
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/sirupsen/logrus"
k8scontext "mcp-k8swizard/internal/context"
)
// KubectlTools handles kubectl command execution
type KubectlTools struct {
contextManager *k8scontext.Manager
}
// NewKubectlTools creates a new instance of KubectlTools
func NewKubectlTools() *KubectlTools {
return &KubectlTools{
contextManager: nil,
}
}
// NewKubectlToolsWithContext creates a new instance of KubectlTools with context manager
func NewKubectlToolsWithContext(cm *k8scontext.Manager) *KubectlTools {
return &KubectlTools{
contextManager: cm,
}
}
// getEffectiveContext returns the effective context to use
func (kt *KubectlTools) getEffectiveContext(providedContext string) string {
if providedContext != "" {
return providedContext
}
if kt.contextManager != nil {
return kt.contextManager.GetCurrentContext()
}
return ""
}
// HandleKubectlGet handles kubectl get commands
func (kt *KubectlTools) HandleKubectlGet(ctx context.Context, req *mcp.CallToolRequest, args KubectlGetArgs) (result *mcp.CallToolResult, data any, err error) {
// Add panic recovery
defer func() {
if r := recover(); r != nil {
logrus.Errorf("Panic in HandleKubectlGet: %v", r)
result = &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf("Internal error: %v", r)},
},
}
data = nil
err = nil
}
}()
logrus.Info("handleKubectlGet called")
logrus.Debugf("handleKubectlGet called with args: %+v", args)
// Get effective context
effectiveContext := kt.getEffectiveContext(args.Context)
args.Context = effectiveContext
// Build kubectl command
cmd := []string{"kubectl", "get", args.ResourceType}
if args.Namespace != "" {
cmd = append(cmd, "-n", args.Namespace)
}
if args.Name != "" {
cmd = append(cmd, args.Name)
}
if args.Output != "" {
cmd = append(cmd, "-o", args.Output)
}
if args.LabelSelector != "" {
cmd = append(cmd, "-l", args.LabelSelector)
}
if args.FieldSelector != "" {
cmd = append(cmd, "--field-selector", args.FieldSelector)
}
if args.Context != "" {
cmd = append(cmd, "--context", args.Context)
}
// Execute command
output, err := kt.ExecuteKubectlCommand(cmd)
if err != nil {
logrus.Errorf("kubectl get command failed: %v", err)
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf("Error executing kubectl get: %v", err)},
},
}, nil, nil
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: output},
},
}, nil, nil
}
// HandleKubectlDescribe handles kubectl describe commands
func (kt *KubectlTools) HandleKubectlDescribe(ctx context.Context, req *mcp.CallToolRequest, args KubectlDescribeArgs) (*mcp.CallToolResult, any, error) {
logrus.Info("handleKubectlDescribe called")
cmd := []string{"kubectl", "describe", args.ResourceType, args.Name}
if args.Namespace != "" {
cmd = append(cmd, "-n", args.Namespace)
}
if args.Context != "" {
cmd = append(cmd, "--context", args.Context)
}
output, err := kt.ExecuteKubectlCommand(cmd)
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf("Error executing kubectl describe: %v", err)},
},
}, nil, nil
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: output},
},
}, nil, nil
}
// HandleKubectlCreate handles kubectl create commands
func (kt *KubectlTools) HandleKubectlCreate(ctx context.Context, req *mcp.CallToolRequest, args KubectlCreateArgs) (*mcp.CallToolResult, any, error) {
logrus.Info("handleKubectlCreate called")
cmd := []string{"kubectl", "create", args.ResourceType, args.Name}
if args.Namespace != "" {
cmd = append(cmd, "-n", args.Namespace)
}
if args.Args != "" {
additionalArgs := strings.Fields(args.Args)
cmd = append(cmd, additionalArgs...)
}
if args.Context != "" {
cmd = append(cmd, "--context", args.Context)
}
output, err := kt.ExecuteKubectlCommand(cmd)
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf("Error executing kubectl create: %v", err)},
},
}, nil, nil
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: output},
},
}, nil, nil
}
// HandleKubectlApply handles kubectl apply commands
func (kt *KubectlTools) HandleKubectlApply(ctx context.Context, req *mcp.CallToolRequest, args KubectlApplyArgs) (*mcp.CallToolResult, any, error) {
logrus.Info("handleKubectlApply called")
// Create temporary file for manifest
tmpFile, err := os.CreateTemp("", "kubectl-apply-*.yaml")
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf("Error creating temporary file: %v", err)},
},
}, nil, nil
}
defer os.Remove(tmpFile.Name())
// Write manifest to file
if _, err := tmpFile.WriteString(args.Manifest); err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf("Error writing manifest to file: %v", err)},
},
}, nil, nil
}
tmpFile.Close()
// Build kubectl command
cmd := []string{"kubectl", "apply", "-f", tmpFile.Name()}
if args.Namespace != "" {
cmd = append(cmd, "-n", args.Namespace)
}
if args.Context != "" {
cmd = append(cmd, "--context", args.Context)
}
output, err := kt.ExecuteKubectlCommand(cmd)
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf("Error executing kubectl apply: %v", err)},
},
}, nil, nil
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: output},
},
}, nil, nil
}
// HandleKubectlDelete handles kubectl delete commands
func (kt *KubectlTools) HandleKubectlDelete(ctx context.Context, req *mcp.CallToolRequest, args KubectlDeleteArgs) (*mcp.CallToolResult, any, error) {
logrus.Info("handleKubectlDelete called")
cmd := []string{"kubectl", "delete", args.ResourceType, args.Name}
if args.Namespace != "" {
cmd = append(cmd, "-n", args.Namespace)
}
if args.Force {
cmd = append(cmd, "--force")
}
if args.GracePeriod > 0 {
cmd = append(cmd, "--grace-period", fmt.Sprintf("%d", args.GracePeriod))
}
if args.Context != "" {
cmd = append(cmd, "--context", args.Context)
}
output, err := kt.ExecuteKubectlCommand(cmd)
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf("Error executing kubectl delete: %v", err)},
},
}, nil, nil
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: output},
},
}, nil, nil
}
// HandleKubectlContext handles kubectl context management
func (kt *KubectlTools) HandleKubectlContext(ctx context.Context, req *mcp.CallToolRequest, args KubectlContextArgs) (*mcp.CallToolResult, any, error) {
logrus.Info("handleKubectlContext called")
cmd := []string{"kubectl", "config"}
switch args.Action {
case "list":
cmd = append(cmd, "get-contexts")
case "set":
if args.Context != "" {
cmd = append(cmd, "use-context", args.Context)
} else {
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: "Context name is required for set action"},
},
}, nil, nil
}
default: // get
cmd = append(cmd, "current-context")
}
output, err := kt.ExecuteKubectlCommand(cmd)
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf("Error executing kubectl context: %v", err)},
},
}, nil, nil
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: output},
},
}, nil, nil
}
// HandleKubectlScale handles kubectl scale commands
func (kt *KubectlTools) HandleKubectlScale(ctx context.Context, req *mcp.CallToolRequest, args KubectlScaleArgs) (*mcp.CallToolResult, any, error) {
logrus.Info("handleKubectlScale called")
cmd := []string{"kubectl", "scale", args.ResourceType, args.Name, fmt.Sprintf("--replicas=%d", args.Replicas)}
if args.Namespace != "" {
cmd = append(cmd, "-n", args.Namespace)
}
if args.Context != "" {
cmd = append(cmd, "--context", args.Context)
}
output, err := kt.ExecuteKubectlCommand(cmd)
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf("Error executing kubectl scale: %v", err)},
},
}, nil, nil
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: output},
},
}, nil, nil
}
// HandleKubectlPatch handles kubectl patch commands
func (kt *KubectlTools) HandleKubectlPatch(ctx context.Context, req *mcp.CallToolRequest, args KubectlPatchArgs) (*mcp.CallToolResult, any, error) {
logrus.Info("handleKubectlPatch called")
cmd := []string{"kubectl", "patch", args.ResourceType, args.Name}
if args.Namespace != "" {
cmd = append(cmd, "-n", args.Namespace)
}
if args.Type != "" {
cmd = append(cmd, "--type", args.Type)
}
cmd = append(cmd, "-p", args.Patch)
if args.Context != "" {
cmd = append(cmd, "--context", args.Context)
}
output, err := kt.ExecuteKubectlCommand(cmd)
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf("Error executing kubectl patch: %v", err)},
},
}, nil, nil
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: output},
},
}, nil, nil
}
// HandleKubectlRollout handles kubectl rollout commands
func (kt *KubectlTools) HandleKubectlRollout(ctx context.Context, req *mcp.CallToolRequest, args KubectlRolloutArgs) (*mcp.CallToolResult, any, error) {
logrus.Info("handleKubectlRollout called")
cmd := []string{"kubectl", "rollout", args.Action, args.ResourceType, args.Name}
if args.Namespace != "" {
cmd = append(cmd, "-n", args.Namespace)
}
if args.Revision > 0 && args.Action == "undo" {
cmd = append(cmd, "--to-revision", fmt.Sprintf("%d", args.Revision))
}
if args.Context != "" {
cmd = append(cmd, "--context", args.Context)
}
output, err := kt.ExecuteKubectlCommand(cmd)
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf("Error executing kubectl rollout: %v", err)},
},
}, nil, nil
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: output},
},
}, nil, nil
}
// HandleKubectlGeneric handles generic kubectl commands
func (kt *KubectlTools) HandleKubectlGeneric(ctx context.Context, req *mcp.CallToolRequest, args KubectlGenericArgs) (*mcp.CallToolResult, any, error) {
logrus.Info("handleKubectlGeneric called")
cmd := []string{"kubectl"}
// Split the command and append
commandParts := strings.Fields(args.Command)
cmd = append(cmd, commandParts...)
if args.Context != "" {
cmd = append(cmd, "--context", args.Context)
}
output, err := kt.ExecuteKubectlCommand(cmd)
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf("Error executing kubectl command: %v", err)},
},
}, nil, nil
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: output},
},
}, nil, nil
}
// HandleExplainResource handles kubectl explain commands
func (kt *KubectlTools) HandleExplainResource(ctx context.Context, req *mcp.CallToolRequest, args ExplainResourceArgs) (*mcp.CallToolResult, any, error) {
logrus.Info("handleExplainResource called")
cmd := []string{"kubectl", "explain", args.ResourceType}
if args.Field != "" {
cmd = append(cmd, args.Field)
}
if args.Context != "" {
cmd = append(cmd, "--context", args.Context)
}
output, err := kt.ExecuteKubectlCommand(cmd)
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf("Error executing kubectl explain: %v", err)},
},
}, nil, nil
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: output},
},
}, nil, nil
}
// HandleListApiResources handles kubectl api-resources commands
func (kt *KubectlTools) HandleListApiResources(ctx context.Context, req *mcp.CallToolRequest, args ListApiResourcesArgs) (*mcp.CallToolResult, any, error) {
logrus.Info("handleListApiResources called")
cmd := []string{"kubectl", "api-resources"}
if args.Context != "" {
cmd = append(cmd, "--context", args.Context)
}
output, err := kt.ExecuteKubectlCommand(cmd)
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: fmt.Sprintf("Error executing kubectl api-resources: %v", err)},
},
}, nil, nil
}
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: output},
},
}, nil, nil
}
// ExecuteKubectlCommand executes a kubectl command and returns the output
func (kt *KubectlTools) ExecuteKubectlCommand(cmd []string) (string, error) {
logrus.Infof("Executing command: %s", strings.Join(cmd, " "))
logrus.Debugf("Command details - Program: %s, Args: %v", cmd[0], cmd[1:])
// Create command context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
logrus.Debug("Created command context with 30 second timeout")
// Create the command
execCmd := exec.CommandContext(ctx, cmd[0], cmd[1:]...)
logrus.Debug("Created exec command")
// Set environment variables to ensure kubectl uses the correct kubeconfig
execCmd.Env = os.Environ()
logrus.Debugf("Set environment variables, total env vars: %d", len(execCmd.Env))
// Execute the command and capture output
logrus.Debug("Starting command execution")
output, err := execCmd.CombinedOutput()
if err != nil {
logrus.Errorf("Command failed: %v, output: %s", err, string(output))
logrus.Debugf("Command exit code: %d", execCmd.ProcessState.ExitCode())
return "", fmt.Errorf("command failed: %w\nOutput: %s", err, string(output))
}
logrus.Infof("Command completed successfully, output length: %d", len(output))
logrus.Debugf("Command output preview: %s", string(output[:min(100, len(output))]))
return string(output), nil
}
// min returns the minimum of two integers
func min(a, b int) int {
if a < b {
return a
}
return b
}