Skip to content
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
- name: Set up Helm
uses: azure/setup-helm@v4.2.0
with:
version: v3.17.0
version: v3.18.0

- name: Install unittest plugin
run: |
Expand Down
14 changes: 14 additions & 0 deletions internal/commands/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type CommandBuilder struct {
namespace string
context string
kubeconfig string
token string
output string
labels map[string]string
annotations map[string]string
Expand Down Expand Up @@ -120,6 +121,14 @@ func (cb *CommandBuilder) WithKubeconfig(kubeconfig string) *CommandBuilder {
return cb
}

// WithToken sets the authentication token for kubectl commands
func (cb *CommandBuilder) WithToken(token string) *CommandBuilder {
if token != "" {
cb.token = token
}
return cb
}

// WithOutput sets the output format
func (cb *CommandBuilder) WithOutput(output string) *CommandBuilder {
validOutputs := []string{"json", "yaml", "wide", "name", "custom-columns", "custom-columns-file", "go-template", "go-template-file", "jsonpath", "jsonpath-file"}
Expand Down Expand Up @@ -240,6 +249,11 @@ func (cb *CommandBuilder) Build() (string, []string, error) {
args = append(args, "--kubeconfig", cb.kubeconfig)
}

// Add token if specified
if cb.token != "" {
args = append(args, "--token", cb.token)
}

// Add output format
if cb.output != "" {
args = append(args, "--output", cb.output)
Expand Down
67 changes: 40 additions & 27 deletions pkg/k8s/k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"maps"
"math/rand"
"net/http"
"os"
"slices"
"strings"
Expand Down Expand Up @@ -37,8 +38,8 @@ func NewK8sToolWithConfig(kubeconfig string, llmModel llms.Model) *K8sTool {
}

// runKubectlCommandWithCacheInvalidation runs a kubectl command and invalidates cache if it's a modification operation
func (k *K8sTool) runKubectlCommandWithCacheInvalidation(ctx context.Context, args ...string) (*mcp.CallToolResult, error) {
result, err := k.runKubectlCommand(ctx, args...)
func (k *K8sTool) runKubectlCommandWithCacheInvalidation(ctx context.Context, headers http.Header, args ...string) (*mcp.CallToolResult, error) {
result, err := k.runKubectlCommand(ctx, headers, args...)

// If command succeeded and it's a modification command, invalidate cache
if err == nil && len(args) > 0 {
Expand Down Expand Up @@ -82,7 +83,7 @@ func (k *K8sTool) handleKubectlGetEnhanced(ctx context.Context, request mcp.Call
args = append(args, "-o", "json")
}

return k.runKubectlCommand(ctx, args...)
return k.runKubectlCommand(ctx, request.Header, args...)
}

// Get pod logs
Expand All @@ -106,7 +107,7 @@ func (k *K8sTool) handleKubectlLogsEnhanced(ctx context.Context, request mcp.Cal
args = append(args, "--tail", fmt.Sprintf("%d", tailLines))
}

return k.runKubectlCommand(ctx, args...)
return k.runKubectlCommand(ctx, request.Header, args...)
}

// Scale deployment
Expand All @@ -121,7 +122,7 @@ func (k *K8sTool) handleScaleDeployment(ctx context.Context, request mcp.CallToo

args := []string{"scale", "deployment", deploymentName, "--replicas", fmt.Sprintf("%d", replicas), "-n", namespace}

return k.runKubectlCommandWithCacheInvalidation(ctx, args...)
return k.runKubectlCommandWithCacheInvalidation(ctx, request.Header, args...)
}

// Patch resource
Expand Down Expand Up @@ -152,7 +153,7 @@ func (k *K8sTool) handlePatchResource(ctx context.Context, request mcp.CallToolR

args := []string{"patch", resourceType, resourceName, "-p", patch, "-n", namespace}

return k.runKubectlCommandWithCacheInvalidation(ctx, args...)
return k.runKubectlCommandWithCacheInvalidation(ctx, request.Header, args...)
}

// Apply manifest from content
Expand Down Expand Up @@ -197,7 +198,7 @@ func (k *K8sTool) handleApplyManifest(ctx context.Context, request mcp.CallToolR
return mcp.NewToolResultError(fmt.Sprintf("Failed to close temp file: %v", err)), nil
}

return k.runKubectlCommandWithCacheInvalidation(ctx, "apply", "-f", tmpFile.Name())
return k.runKubectlCommandWithCacheInvalidation(ctx, request.Header, "apply", "-f", tmpFile.Name())
}

// Delete resource
Expand All @@ -212,7 +213,7 @@ func (k *K8sTool) handleDeleteResource(ctx context.Context, request mcp.CallTool

args := []string{"delete", resourceType, resourceName, "-n", namespace}

return k.runKubectlCommandWithCacheInvalidation(ctx, args...)
return k.runKubectlCommandWithCacheInvalidation(ctx, request.Header, args...)
}

// Check service connectivity
Expand All @@ -227,23 +228,23 @@ func (k *K8sTool) handleCheckServiceConnectivity(ctx context.Context, request mc
// Create a temporary curl pod for connectivity check
podName := fmt.Sprintf("curl-test-%d", rand.Intn(10000))
defer func() {
_, _ = k.runKubectlCommand(ctx, "delete", "pod", podName, "-n", namespace, "--ignore-not-found")
_, _ = k.runKubectlCommand(ctx, request.Header, "delete", "pod", podName, "-n", namespace, "--ignore-not-found")
}()

// Create the curl pod
_, err := k.runKubectlCommand(ctx, "run", podName, "--image=curlimages/curl", "-n", namespace, "--restart=Never", "--", "sleep", "3600")
_, err := k.runKubectlCommand(ctx, request.Header, "run", podName, "--image=curlimages/curl", "-n", namespace, "--restart=Never", "--", "sleep", "3600")
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to create curl pod: %v", err)), nil
}

// Wait for pod to be ready
_, err = k.runKubectlCommandWithTimeout(ctx, 60*time.Second, "wait", "--for=condition=ready", "pod/"+podName, "-n", namespace)
_, err = k.runKubectlCommandWithTimeout(ctx, request.Header, 60*time.Second, "wait", "--for=condition=ready", "pod/"+podName, "-n", namespace)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to wait for curl pod: %v", err)), nil
}

// Execute kubectl command
return k.runKubectlCommand(ctx, "exec", podName, "-n", namespace, "--", "curl", "-s", serviceName)
return k.runKubectlCommand(ctx, request.Header, "exec", podName, "-n", namespace, "--", "curl", "-s", serviceName)
}

// Get cluster events
Expand All @@ -257,7 +258,7 @@ func (k *K8sTool) handleGetEvents(ctx context.Context, request mcp.CallToolReque
args = append(args, "--all-namespaces")
}

return k.runKubectlCommand(ctx, args...)
return k.runKubectlCommand(ctx, request.Header, args...)
}

// Execute command in pod
Expand Down Expand Up @@ -287,12 +288,12 @@ func (k *K8sTool) handleExecCommand(ctx context.Context, request mcp.CallToolReq

args := []string{"exec", podName, "-n", namespace, "--", command}

return k.runKubectlCommand(ctx, args...)
return k.runKubectlCommand(ctx, request.Header, args...)
}

// Get available API resources
func (k *K8sTool) handleGetAvailableAPIResources(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return k.runKubectlCommand(ctx, "api-resources")
return k.runKubectlCommand(ctx, request.Header, "api-resources")
}

// Kubectl describe tool
Expand All @@ -310,7 +311,7 @@ func (k *K8sTool) handleKubectlDescribeTool(ctx context.Context, request mcp.Cal
args = append(args, "-n", namespace)
}

return k.runKubectlCommand(ctx, args...)
return k.runKubectlCommand(ctx, request.Header, args...)
}

// Rollout operations
Expand All @@ -329,12 +330,12 @@ func (k *K8sTool) handleRollout(ctx context.Context, request mcp.CallToolRequest
args = append(args, "-n", namespace)
}

return k.runKubectlCommand(ctx, args...)
return k.runKubectlCommand(ctx, request.Header, args...)
}

// Get cluster configuration
func (k *K8sTool) handleGetClusterConfiguration(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return k.runKubectlCommand(ctx, "config", "view", "-o", "json")
return k.runKubectlCommand(ctx, request.Header, "config", "view", "-o", "json")
}

// Remove annotation
Expand All @@ -353,7 +354,7 @@ func (k *K8sTool) handleRemoveAnnotation(ctx context.Context, request mcp.CallTo
args = append(args, "-n", namespace)
}

return k.runKubectlCommand(ctx, args...)
return k.runKubectlCommand(ctx, request.Header, args...)
}

// Remove label
Expand All @@ -372,7 +373,7 @@ func (k *K8sTool) handleRemoveLabel(ctx context.Context, request mcp.CallToolReq
args = append(args, "-n", namespace)
}

return k.runKubectlCommand(ctx, args...)
return k.runKubectlCommand(ctx, request.Header, args...)
}

// Annotate resource
Expand All @@ -393,7 +394,7 @@ func (k *K8sTool) handleAnnotateResource(ctx context.Context, request mcp.CallTo
args = append(args, "-n", namespace)
}

return k.runKubectlCommand(ctx, args...)
return k.runKubectlCommand(ctx, request.Header, args...)
}

// Label resource
Expand All @@ -414,7 +415,7 @@ func (k *K8sTool) handleLabelResource(ctx context.Context, request mcp.CallToolR
args = append(args, "-n", namespace)
}

return k.runKubectlCommand(ctx, args...)
return k.runKubectlCommand(ctx, request.Header, args...)
}

// Create resource from URL
Expand All @@ -431,7 +432,7 @@ func (k *K8sTool) handleCreateResourceFromURL(ctx context.Context, request mcp.C
args = append(args, "-n", namespace)
}

return k.runKubectlCommand(ctx, args...)
return k.runKubectlCommand(ctx, request.Header, args...)
}

// Resource generation embeddings
Expand Down Expand Up @@ -528,11 +529,22 @@ func (k *K8sTool) handleGenerateResource(ctx context.Context, request mcp.CallTo
return mcp.NewToolResultText(responseText), nil
}

// extractBearerToken extracts the Bearer token from the Authorization header
func extractBearerToken(headers http.Header) string {
if auth := headers.Get("Authorization"); auth != "" {
if strings.HasPrefix(auth, "Bearer ") {
return strings.TrimPrefix(auth, "Bearer ")
}
}
return ""
}

// runKubectlCommand is a helper function to execute kubectl commands
func (k *K8sTool) runKubectlCommand(ctx context.Context, args ...string) (*mcp.CallToolResult, error) {
func (k *K8sTool) runKubectlCommand(ctx context.Context, headers http.Header, args ...string) (*mcp.CallToolResult, error) {
output, err := commands.NewCommandBuilder("kubectl").
WithArgs(args...).
WithKubeconfig(k.kubeconfig).
WithToken(extractBearerToken(headers)).
Execute(ctx)

if err != nil {
Expand All @@ -543,10 +555,11 @@ func (k *K8sTool) runKubectlCommand(ctx context.Context, args ...string) (*mcp.C
}

// runKubectlCommandWithTimeout is a helper function to execute kubectl commands with a timeout
func (k *K8sTool) runKubectlCommandWithTimeout(ctx context.Context, timeout time.Duration, args ...string) (*mcp.CallToolResult, error) {
func (k *K8sTool) runKubectlCommandWithTimeout(ctx context.Context, headers http.Header, timeout time.Duration, args ...string) (*mcp.CallToolResult, error) {
output, err := commands.NewCommandBuilder("kubectl").
WithArgs(args...).
WithKubeconfig(k.kubeconfig).
WithToken(extractBearerToken(headers)).
WithTimeout(timeout).
Execute(ctx)

Expand Down Expand Up @@ -694,7 +707,7 @@ func RegisterTools(s *server.MCPServer, llm llms.Model, kubeconfig string) {
}
tmpFile.Close()

result, err := k8sTool.runKubectlCommand(ctx, "create", "-f", tmpFile.Name())
result, err := k8sTool.runKubectlCommand(ctx, request.Header, "create", "-f", tmpFile.Name())
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Create command failed: %v", err)), nil
}
Expand Down Expand Up @@ -727,7 +740,7 @@ func RegisterTools(s *server.MCPServer, llm llms.Model, kubeconfig string) {
args = append(args, "-n", namespace)
}

result, err := k8sTool.runKubectlCommand(ctx, args...)
result, err := k8sTool.runKubectlCommand(ctx, request.Header, args...)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Get YAML command failed: %v", err)), nil
}
Expand Down
Loading