diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 017cfba..f6d841c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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: | diff --git a/internal/commands/builder.go b/internal/commands/builder.go index 3bced94..3a3dacf 100644 --- a/internal/commands/builder.go +++ b/internal/commands/builder.go @@ -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 @@ -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"} @@ -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) diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go index c8e9085..3344b14 100644 --- a/pkg/k8s/k8s.go +++ b/pkg/k8s/k8s.go @@ -6,6 +6,7 @@ import ( "fmt" "maps" "math/rand" + "net/http" "os" "slices" "strings" @@ -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 { @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 { @@ -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) @@ -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 } @@ -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 } diff --git a/pkg/k8s/k8s_test.go b/pkg/k8s/k8s_test.go index e373066..57a350c 100644 --- a/pkg/k8s/k8s_test.go +++ b/pkg/k8s/k8s_test.go @@ -2,6 +2,7 @@ package k8s import ( "context" + "net/http" "testing" "github.com/kagent-dev/tools/internal/cmd" @@ -32,6 +33,21 @@ func getResultText(result *mcp.CallToolResult) string { return "" } +// Helper function to create an http.Header with Bearer token authorization +func headerWithBearerToken(token string) http.Header { + h := http.Header{} + h.Set("Authorization", "Bearer "+token) + return h +} + +// Helper function to create a CallToolRequest with Bearer token +func requestWithBearerToken(token string, args map[string]interface{}) mcp.CallToolRequest { + req := mcp.CallToolRequest{} + req.Header = headerWithBearerToken(token) + req.Params.Arguments = args + return req +} + func TestHandleGetAvailableAPIResources(t *testing.T) { ctx := context.Background() @@ -1063,3 +1079,441 @@ users: assert.Contains(t, resultText, "clusters") }) } + +// Tests for Bearer token passing to kubectl commands +func TestBearerTokenPassthrough(t *testing.T) { + ctx := context.Background() + + t.Run("get resources with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `NAME READY STATUS RESTARTS AGE` + mock.AddCommandString("kubectl", []string{"get", "pods", "-o", "wide", "--token", "test-token-123"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := requestWithBearerToken("test-token-123", map[string]interface{}{"resource_type": "pods"}) + result, err := k8sTool.handleKubectlGetEnhanced(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + // Verify the command was executed with the token + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Equal(t, "kubectl", callLog[0].Command) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "test-token-123") + }) + + t.Run("scale deployment with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment scaled` + mock.AddCommandString("kubectl", []string{"scale", "deployment", "test-deployment", "--replicas", "5", "-n", "default", "--token", "my-auth-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := requestWithBearerToken("my-auth-token", map[string]interface{}{ + "name": "test-deployment", + "replicas": float64(5), + }) + + result, err := k8sTool.handleScaleDeployment(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + // Verify the command was executed with the token + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "my-auth-token") + }) + + t.Run("get pod logs with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `log line 1 +log line 2` + mock.AddCommandString("kubectl", []string{"logs", "test-pod", "-n", "default", "--tail", "50", "--token", "logs-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := requestWithBearerToken("logs-token", map[string]interface{}{"pod_name": "test-pod"}) + result, err := k8sTool.handleKubectlLogsEnhanced(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "logs-token") + }) + + t.Run("delete resource with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment deleted` + mock.AddCommandString("kubectl", []string{"delete", "deployment", "test-deployment", "-n", "default", "--token", "delete-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := requestWithBearerToken("delete-token", map[string]interface{}{ + "resource_type": "deployment", + "resource_name": "test-deployment", + }) + + result, err := k8sTool.handleDeleteResource(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "delete-token") + }) + + t.Run("patch resource with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment patched` + mock.AddCommandString("kubectl", []string{"patch", "deployment", "test-deployment", "-p", `{"spec":{"replicas":5}}`, "-n", "default", "--token", "patch-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := requestWithBearerToken("patch-token", map[string]interface{}{ + "resource_type": "deployment", + "resource_name": "test-deployment", + "patch": `{"spec":{"replicas":5}}`, + }) + + result, err := k8sTool.handlePatchResource(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "patch-token") + }) + + t.Run("describe resource with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `Name: test-deployment` + mock.AddCommandString("kubectl", []string{"describe", "deployment", "test-deployment", "-n", "default", "--token", "describe-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := requestWithBearerToken("describe-token", map[string]interface{}{ + "resource_type": "deployment", + "resource_name": "test-deployment", + "namespace": "default", + }) + + result, err := k8sTool.handleKubectlDescribeTool(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "describe-token") + }) + + t.Run("rollout with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `deployment.apps/myapp restarted` + mock.AddCommandString("kubectl", []string{"rollout", "restart", "deployment/myapp", "-n", "default", "--token", "rollout-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := requestWithBearerToken("rollout-token", map[string]interface{}{ + "action": "restart", + "resource_type": "deployment", + "resource_name": "myapp", + "namespace": "default", + }) + + result, err := k8sTool.handleRollout(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "rollout-token") + }) + + t.Run("get events with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `{"items": []}` + mock.AddCommandString("kubectl", []string{"get", "events", "-o", "json", "--all-namespaces", "--token", "events-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := requestWithBearerToken("events-token", nil) + result, err := k8sTool.handleGetEvents(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "events-token") + }) + + t.Run("exec command with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `total 8` + mock.AddCommandString("kubectl", []string{"exec", "mypod", "-n", "default", "--", "ls -la", "--token", "exec-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := requestWithBearerToken("exec-token", map[string]interface{}{ + "pod_name": "mypod", + "namespace": "default", + "command": "ls -la", + }) + + result, err := k8sTool.handleExecCommand(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "exec-token") + }) + + t.Run("annotate resource with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment annotated` + mock.AddCommandString("kubectl", []string{"annotate", "deployment", "test-deployment", "key1=value1", "--token", "annotate-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := requestWithBearerToken("annotate-token", map[string]interface{}{ + "resource_type": "deployment", + "resource_name": "test-deployment", + "annotations": "key1=value1", + }) + + result, err := k8sTool.handleAnnotateResource(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "annotate-token") + }) + + t.Run("label resource with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment labeled` + mock.AddCommandString("kubectl", []string{"label", "deployment", "test-deployment", "env=prod", "--token", "label-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := requestWithBearerToken("label-token", map[string]interface{}{ + "resource_type": "deployment", + "resource_name": "test-deployment", + "labels": "env=prod", + }) + + result, err := k8sTool.handleLabelResource(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "label-token") + }) + + t.Run("api resources with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `NAME SHORTNAMES APIVERSION NAMESPACED KIND` + mock.AddCommandString("kubectl", []string{"api-resources", "--token", "api-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := requestWithBearerToken("api-token", nil) + result, err := k8sTool.handleGetAvailableAPIResources(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "api-token") + }) + + t.Run("cluster configuration with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `{"current-context": "default"}` + mock.AddCommandString("kubectl", []string{"config", "view", "-o", "json", "--token", "config-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := requestWithBearerToken("config-token", nil) + result, err := k8sTool.handleGetClusterConfiguration(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "config-token") + }) + + t.Run("remove annotation with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment annotated` + mock.AddCommandString("kubectl", []string{"annotate", "deployment", "test-deployment", "key1-", "--token", "remove-anno-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := requestWithBearerToken("remove-anno-token", map[string]interface{}{ + "resource_type": "deployment", + "resource_name": "test-deployment", + "annotation_key": "key1", + }) + + result, err := k8sTool.handleRemoveAnnotation(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "remove-anno-token") + }) + + t.Run("remove label with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment labeled` + mock.AddCommandString("kubectl", []string{"label", "deployment", "test-deployment", "env-", "--token", "remove-label-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := requestWithBearerToken("remove-label-token", map[string]interface{}{ + "resource_type": "deployment", + "resource_name": "test-deployment", + "label_key": "env", + }) + + result, err := k8sTool.handleRemoveLabel(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "remove-label-token") + }) + + t.Run("create resource from URL with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `deployment.apps/test-deployment created` + mock.AddCommandString("kubectl", []string{"create", "-f", "https://example.com/manifest.yaml", "-n", "default", "--token", "url-token"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := requestWithBearerToken("url-token", map[string]interface{}{ + "url": "https://example.com/manifest.yaml", + "namespace": "default", + }) + + result, err := k8sTool.handleCreateResourceFromURL(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "url-token") + }) + + t.Run("apply manifest with bearer token", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + manifest := `apiVersion: v1 +kind: Pod +metadata: + name: test-pod` + expectedOutput := `pod/test-pod created` + // Use partial matcher since temp file name is dynamic + mock.AddPartialMatcherString("kubectl", []string{"apply", "-f"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := requestWithBearerToken("apply-token", map[string]interface{}{ + "manifest": manifest, + }) + + result, err := k8sTool.handleApplyManifest(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.Contains(t, callLog[0].Args, "--token") + assert.Contains(t, callLog[0].Args, "apply-token") + }) + + t.Run("no token when authorization header missing", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `NAME READY STATUS RESTARTS AGE` + // No --token in expected args since no Authorization header + mock.AddCommandString("kubectl", []string{"get", "pods", "-o", "wide"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := mcp.CallToolRequest{} + req.Params.Arguments = map[string]interface{}{"resource_type": "pods"} + // No Header set on request + result, err := k8sTool.handleKubectlGetEnhanced(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + // Verify no --token was added + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.NotContains(t, callLog[0].Args, "--token") + }) + + t.Run("no token when authorization header is not bearer", func(t *testing.T) { + mock := cmd.NewMockShellExecutor() + expectedOutput := `NAME READY STATUS RESTARTS AGE` + // No --token in expected args since Authorization is not Bearer + mock.AddCommandString("kubectl", []string{"get", "pods", "-o", "wide"}, expectedOutput, nil) + ctx := cmd.WithShellExecutor(ctx, mock) + + k8sTool := newTestK8sTool() + req := mcp.CallToolRequest{} + req.Header = http.Header{} + req.Header.Set("Authorization", "Basic dXNlcjpwYXNz") + req.Params.Arguments = map[string]interface{}{"resource_type": "pods"} + result, err := k8sTool.handleKubectlGetEnhanced(ctx, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsError) + + // Verify no --token was added + callLog := mock.GetCallLog() + require.Len(t, callLog, 1) + assert.NotContains(t, callLog[0].Args, "--token") + }) +}