Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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