mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-29 03:50:18 +00:00
839 lines
27 KiB
Go
839 lines
27 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
)
|
|
|
|
// registerKubernetesTools registers the pulse_kubernetes tool
|
|
func (e *PulseToolExecutor) registerKubernetesTools() {
|
|
e.registry.Register(RegisteredTool{
|
|
Definition: Tool{
|
|
Name: "pulse_kubernetes",
|
|
Description: `Query and control Kubernetes clusters, nodes, pods, and deployments. Query: clusters, nodes, pods, deployments. Control: scale, restart, delete_pod, exec, logs.`,
|
|
InputSchema: InputSchema{
|
|
Type: "object",
|
|
Properties: map[string]PropertySchema{
|
|
"type": {
|
|
Type: "string",
|
|
Description: "Operation type",
|
|
Enum: []string{"clusters", "nodes", "pods", "deployments", "scale", "restart", "delete_pod", "exec", "logs"},
|
|
},
|
|
"cluster": {
|
|
Type: "string",
|
|
Description: "Cluster name or ID",
|
|
},
|
|
"namespace": {
|
|
Type: "string",
|
|
Description: "Kubernetes namespace (default: 'default')",
|
|
},
|
|
"deployment": {
|
|
Type: "string",
|
|
Description: "Deployment name (for scale, restart)",
|
|
},
|
|
"pod": {
|
|
Type: "string",
|
|
Description: "Pod name (for delete_pod, exec, logs)",
|
|
},
|
|
"container": {
|
|
Type: "string",
|
|
Description: "Container name (for exec, logs - uses first container if omitted)",
|
|
},
|
|
"command": {
|
|
Type: "string",
|
|
Description: "Command to execute (for exec)",
|
|
},
|
|
"replicas": {
|
|
Type: "integer",
|
|
Description: "Desired replica count (for scale)",
|
|
},
|
|
"lines": {
|
|
Type: "integer",
|
|
Description: "Number of log lines to return (for logs, default: 100)",
|
|
},
|
|
"status": {
|
|
Type: "string",
|
|
Description: "Filter by pod phase: Running, Pending, Failed, Succeeded (for pods)",
|
|
},
|
|
"limit": {
|
|
Type: "integer",
|
|
Description: "Maximum number of results (default: 100)",
|
|
},
|
|
"offset": {
|
|
Type: "integer",
|
|
Description: "Number of results to skip",
|
|
},
|
|
},
|
|
Required: []string{"type"},
|
|
},
|
|
},
|
|
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
|
|
return exec.executeKubernetes(ctx, args)
|
|
},
|
|
})
|
|
}
|
|
|
|
// executeKubernetes routes to the appropriate kubernetes handler based on type
|
|
func (e *PulseToolExecutor) executeKubernetes(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
|
|
resourceType, _ := args["type"].(string)
|
|
switch resourceType {
|
|
case "clusters":
|
|
return e.executeGetKubernetesClusters(ctx)
|
|
case "nodes":
|
|
return e.executeGetKubernetesNodes(ctx, args)
|
|
case "pods":
|
|
return e.executeGetKubernetesPods(ctx, args)
|
|
case "deployments":
|
|
return e.executeGetKubernetesDeployments(ctx, args)
|
|
// Control operations
|
|
case "scale":
|
|
return e.executeKubernetesScale(ctx, args)
|
|
case "restart":
|
|
return e.executeKubernetesRestart(ctx, args)
|
|
case "delete_pod":
|
|
return e.executeKubernetesDeletePod(ctx, args)
|
|
case "exec":
|
|
return e.executeKubernetesExec(ctx, args)
|
|
case "logs":
|
|
return e.executeKubernetesLogs(ctx, args)
|
|
default:
|
|
return NewErrorResult(fmt.Errorf("unknown type: %s. Use: clusters, nodes, pods, deployments, scale, restart, delete_pod, exec, logs", resourceType)), nil
|
|
}
|
|
}
|
|
|
|
func (e *PulseToolExecutor) executeGetKubernetesClusters(_ context.Context) (CallToolResult, error) {
|
|
if e.stateProvider == nil {
|
|
return NewTextResult("State provider not available."), nil
|
|
}
|
|
|
|
state := e.stateProvider.GetState()
|
|
|
|
if len(state.KubernetesClusters) == 0 {
|
|
return NewTextResult("No Kubernetes clusters found. Kubernetes monitoring may not be configured."), nil
|
|
}
|
|
|
|
var clusters []KubernetesClusterSummary
|
|
for _, c := range state.KubernetesClusters {
|
|
readyNodes := 0
|
|
for _, node := range c.Nodes {
|
|
if node.Ready {
|
|
readyNodes++
|
|
}
|
|
}
|
|
|
|
displayName := c.DisplayName
|
|
if c.CustomDisplayName != "" {
|
|
displayName = c.CustomDisplayName
|
|
}
|
|
|
|
clusters = append(clusters, KubernetesClusterSummary{
|
|
ID: c.ID,
|
|
Name: c.Name,
|
|
DisplayName: displayName,
|
|
Server: c.Server,
|
|
Version: c.Version,
|
|
Status: c.Status,
|
|
NodeCount: len(c.Nodes),
|
|
PodCount: len(c.Pods),
|
|
DeploymentCount: len(c.Deployments),
|
|
ReadyNodes: readyNodes,
|
|
})
|
|
}
|
|
|
|
response := KubernetesClustersResponse{
|
|
Clusters: clusters,
|
|
Total: len(clusters),
|
|
}
|
|
|
|
return NewJSONResult(response), nil
|
|
}
|
|
|
|
func (e *PulseToolExecutor) executeGetKubernetesNodes(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
|
|
if e.stateProvider == nil {
|
|
return NewTextResult("State provider not available."), nil
|
|
}
|
|
|
|
clusterArg, _ := args["cluster"].(string)
|
|
if clusterArg == "" {
|
|
return NewErrorResult(fmt.Errorf("cluster is required")), nil
|
|
}
|
|
|
|
state := e.stateProvider.GetState()
|
|
|
|
// Find the cluster (also match CustomDisplayName)
|
|
var cluster *KubernetesClusterSummary
|
|
for _, c := range state.KubernetesClusters {
|
|
if c.ID == clusterArg || c.Name == clusterArg || c.DisplayName == clusterArg || c.CustomDisplayName == clusterArg {
|
|
displayName := c.DisplayName
|
|
if c.CustomDisplayName != "" {
|
|
displayName = c.CustomDisplayName
|
|
}
|
|
cluster = &KubernetesClusterSummary{
|
|
ID: c.ID,
|
|
Name: c.Name,
|
|
DisplayName: displayName,
|
|
}
|
|
|
|
var nodes []KubernetesNodeSummary
|
|
for _, node := range c.Nodes {
|
|
nodes = append(nodes, KubernetesNodeSummary{
|
|
UID: node.UID,
|
|
Name: node.Name,
|
|
Ready: node.Ready,
|
|
Unschedulable: node.Unschedulable,
|
|
Roles: node.Roles,
|
|
KubeletVersion: node.KubeletVersion,
|
|
ContainerRuntimeVersion: node.ContainerRuntimeVersion,
|
|
OSImage: node.OSImage,
|
|
Architecture: node.Architecture,
|
|
CapacityCPU: node.CapacityCPU,
|
|
CapacityMemoryBytes: node.CapacityMemoryBytes,
|
|
CapacityPods: node.CapacityPods,
|
|
AllocatableCPU: node.AllocCPU,
|
|
AllocatableMemoryBytes: node.AllocMemoryBytes,
|
|
AllocatablePods: node.AllocPods,
|
|
})
|
|
}
|
|
|
|
response := KubernetesNodesResponse{
|
|
Cluster: cluster.DisplayName,
|
|
Nodes: nodes,
|
|
Total: len(nodes),
|
|
}
|
|
if response.Nodes == nil {
|
|
response.Nodes = []KubernetesNodeSummary{}
|
|
}
|
|
return NewJSONResult(response), nil
|
|
}
|
|
}
|
|
|
|
return NewTextResult(fmt.Sprintf("Kubernetes cluster '%s' not found.", clusterArg)), nil
|
|
}
|
|
|
|
func (e *PulseToolExecutor) executeGetKubernetesPods(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
|
|
if e.stateProvider == nil {
|
|
return NewTextResult("State provider not available."), nil
|
|
}
|
|
|
|
clusterArg, _ := args["cluster"].(string)
|
|
if clusterArg == "" {
|
|
return NewErrorResult(fmt.Errorf("cluster is required")), nil
|
|
}
|
|
|
|
namespaceFilter, _ := args["namespace"].(string)
|
|
statusFilter, _ := args["status"].(string)
|
|
limit := intArg(args, "limit", 100)
|
|
offset := intArg(args, "offset", 0)
|
|
|
|
state := e.stateProvider.GetState()
|
|
|
|
// Find the cluster (also match CustomDisplayName)
|
|
for _, c := range state.KubernetesClusters {
|
|
if c.ID == clusterArg || c.Name == clusterArg || c.DisplayName == clusterArg || c.CustomDisplayName == clusterArg {
|
|
displayName := c.DisplayName
|
|
if c.CustomDisplayName != "" {
|
|
displayName = c.CustomDisplayName
|
|
}
|
|
|
|
var pods []KubernetesPodSummary
|
|
totalPods := 0
|
|
filteredCount := 0
|
|
|
|
for _, pod := range c.Pods {
|
|
// Apply filters
|
|
if namespaceFilter != "" && pod.Namespace != namespaceFilter {
|
|
continue
|
|
}
|
|
if statusFilter != "" && !strings.EqualFold(pod.Phase, statusFilter) {
|
|
continue
|
|
}
|
|
|
|
filteredCount++
|
|
|
|
// Apply pagination
|
|
if totalPods < offset {
|
|
totalPods++
|
|
continue
|
|
}
|
|
if len(pods) >= limit {
|
|
totalPods++
|
|
continue
|
|
}
|
|
|
|
var containers []KubernetesPodContainerSummary
|
|
for _, container := range pod.Containers {
|
|
containers = append(containers, KubernetesPodContainerSummary{
|
|
Name: container.Name,
|
|
Ready: container.Ready,
|
|
State: container.State,
|
|
RestartCount: container.RestartCount,
|
|
Reason: container.Reason,
|
|
})
|
|
}
|
|
|
|
pods = append(pods, KubernetesPodSummary{
|
|
UID: pod.UID,
|
|
Name: pod.Name,
|
|
Namespace: pod.Namespace,
|
|
NodeName: pod.NodeName,
|
|
Phase: pod.Phase,
|
|
Reason: pod.Reason,
|
|
Restarts: pod.Restarts,
|
|
QoSClass: pod.QoSClass,
|
|
OwnerKind: pod.OwnerKind,
|
|
OwnerName: pod.OwnerName,
|
|
Containers: containers,
|
|
})
|
|
totalPods++
|
|
}
|
|
|
|
response := KubernetesPodsResponse{
|
|
Cluster: displayName,
|
|
Pods: pods,
|
|
Total: len(c.Pods),
|
|
Filtered: filteredCount,
|
|
}
|
|
if response.Pods == nil {
|
|
response.Pods = []KubernetesPodSummary{}
|
|
}
|
|
return NewJSONResult(response), nil
|
|
}
|
|
}
|
|
|
|
return NewTextResult(fmt.Sprintf("Kubernetes cluster '%s' not found.", clusterArg)), nil
|
|
}
|
|
|
|
func (e *PulseToolExecutor) executeGetKubernetesDeployments(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
|
|
if e.stateProvider == nil {
|
|
return NewTextResult("State provider not available."), nil
|
|
}
|
|
|
|
clusterArg, _ := args["cluster"].(string)
|
|
if clusterArg == "" {
|
|
return NewErrorResult(fmt.Errorf("cluster is required")), nil
|
|
}
|
|
|
|
namespaceFilter, _ := args["namespace"].(string)
|
|
limit := intArg(args, "limit", 100)
|
|
offset := intArg(args, "offset", 0)
|
|
|
|
state := e.stateProvider.GetState()
|
|
|
|
// Find the cluster (also match CustomDisplayName)
|
|
for _, c := range state.KubernetesClusters {
|
|
if c.ID == clusterArg || c.Name == clusterArg || c.DisplayName == clusterArg || c.CustomDisplayName == clusterArg {
|
|
displayName := c.DisplayName
|
|
if c.CustomDisplayName != "" {
|
|
displayName = c.CustomDisplayName
|
|
}
|
|
|
|
var deployments []KubernetesDeploymentSummary
|
|
filteredCount := 0
|
|
count := 0
|
|
|
|
for _, dep := range c.Deployments {
|
|
// Apply namespace filter
|
|
if namespaceFilter != "" && dep.Namespace != namespaceFilter {
|
|
continue
|
|
}
|
|
|
|
filteredCount++
|
|
|
|
// Apply pagination
|
|
if count < offset {
|
|
count++
|
|
continue
|
|
}
|
|
if len(deployments) >= limit {
|
|
count++
|
|
continue
|
|
}
|
|
|
|
deployments = append(deployments, KubernetesDeploymentSummary{
|
|
UID: dep.UID,
|
|
Name: dep.Name,
|
|
Namespace: dep.Namespace,
|
|
DesiredReplicas: dep.DesiredReplicas,
|
|
ReadyReplicas: dep.ReadyReplicas,
|
|
AvailableReplicas: dep.AvailableReplicas,
|
|
UpdatedReplicas: dep.UpdatedReplicas,
|
|
})
|
|
count++
|
|
}
|
|
|
|
response := KubernetesDeploymentsResponse{
|
|
Cluster: displayName,
|
|
Deployments: deployments,
|
|
Total: len(c.Deployments),
|
|
Filtered: filteredCount,
|
|
}
|
|
if response.Deployments == nil {
|
|
response.Deployments = []KubernetesDeploymentSummary{}
|
|
}
|
|
return NewJSONResult(response), nil
|
|
}
|
|
}
|
|
|
|
return NewTextResult(fmt.Sprintf("Kubernetes cluster '%s' not found.", clusterArg)), nil
|
|
}
|
|
|
|
// ========== Kubernetes Control Operations ==========
|
|
|
|
// findAgentForKubernetesCluster finds the agent for a Kubernetes cluster
|
|
func (e *PulseToolExecutor) findAgentForKubernetesCluster(clusterArg string) (string, *models.KubernetesCluster, error) {
|
|
if e.stateProvider == nil {
|
|
return "", nil, fmt.Errorf("state provider not available")
|
|
}
|
|
|
|
state := e.stateProvider.GetState()
|
|
|
|
for i := range state.KubernetesClusters {
|
|
c := &state.KubernetesClusters[i]
|
|
if c.ID == clusterArg || c.Name == clusterArg || c.DisplayName == clusterArg || c.CustomDisplayName == clusterArg {
|
|
if c.AgentID == "" {
|
|
return "", nil, fmt.Errorf("cluster '%s' has no agent configured - kubectl commands cannot be executed", clusterArg)
|
|
}
|
|
return c.AgentID, c, nil
|
|
}
|
|
}
|
|
return "", nil, fmt.Errorf("kubernetes cluster '%s' not found", clusterArg)
|
|
}
|
|
|
|
// validateKubernetesResourceID validates a Kubernetes resource identifier (namespace, pod, deployment, container names)
|
|
func validateKubernetesResourceID(value string) error {
|
|
if value == "" {
|
|
return fmt.Errorf("value cannot be empty")
|
|
}
|
|
// Kubernetes resource names must be valid DNS subdomains: lowercase, alphanumeric, '-' and '.'
|
|
// Max 253 characters
|
|
if len(value) > 253 {
|
|
return fmt.Errorf("value too long (max 253 characters)")
|
|
}
|
|
for _, c := range value {
|
|
if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.') {
|
|
return fmt.Errorf("invalid character '%c' in resource name", c)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// executeKubernetesScale scales a deployment
|
|
func (e *PulseToolExecutor) executeKubernetesScale(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
|
|
clusterArg, _ := args["cluster"].(string)
|
|
namespace, _ := args["namespace"].(string)
|
|
deployment, _ := args["deployment"].(string)
|
|
replicas := intArg(args, "replicas", -1)
|
|
|
|
if clusterArg == "" {
|
|
return NewErrorResult(fmt.Errorf("cluster is required")), nil
|
|
}
|
|
if deployment == "" {
|
|
return NewErrorResult(fmt.Errorf("deployment is required")), nil
|
|
}
|
|
if replicas < 0 {
|
|
return NewErrorResult(fmt.Errorf("replicas is required and must be >= 0")), nil
|
|
}
|
|
if namespace == "" {
|
|
namespace = "default"
|
|
}
|
|
|
|
// Validate identifiers
|
|
if err := validateKubernetesResourceID(namespace); err != nil {
|
|
return NewErrorResult(fmt.Errorf("invalid namespace: %w", err)), nil
|
|
}
|
|
if err := validateKubernetesResourceID(deployment); err != nil {
|
|
return NewErrorResult(fmt.Errorf("invalid deployment: %w", err)), nil
|
|
}
|
|
|
|
// Check control level
|
|
if e.controlLevel == ControlLevelReadOnly {
|
|
return NewTextResult("Kubernetes control operations are not available in read-only mode."), nil
|
|
}
|
|
|
|
agentID, cluster, err := e.findAgentForKubernetesCluster(clusterArg)
|
|
if err != nil {
|
|
return NewTextResult(err.Error()), nil
|
|
}
|
|
|
|
// Check if pre-approved
|
|
preApproved := isPreApproved(args)
|
|
|
|
// Build command
|
|
command := fmt.Sprintf("kubectl -n %s scale deployment %s --replicas=%d", namespace, deployment, replicas)
|
|
|
|
// Request approval if needed
|
|
if !preApproved && !e.isAutonomous && e.controlLevel == ControlLevelControlled {
|
|
displayName := cluster.DisplayName
|
|
if cluster.CustomDisplayName != "" {
|
|
displayName = cluster.CustomDisplayName
|
|
}
|
|
approvalID := createApprovalRecord(command, "kubernetes", deployment, displayName, fmt.Sprintf("Scale deployment %s to %d replicas", deployment, replicas))
|
|
return NewTextResult(formatKubernetesApprovalNeeded("scale", deployment, namespace, displayName, command, approvalID)), nil
|
|
}
|
|
|
|
if e.agentServer == nil {
|
|
return NewErrorResult(fmt.Errorf("no agent server available")), nil
|
|
}
|
|
|
|
result, err := e.agentServer.ExecuteCommand(ctx, agentID, agentexec.ExecuteCommandPayload{
|
|
Command: command,
|
|
TargetType: "host",
|
|
TargetID: "",
|
|
})
|
|
if err != nil {
|
|
return NewErrorResult(fmt.Errorf("failed to execute kubectl: %w", err)), nil
|
|
}
|
|
|
|
output := result.Stdout
|
|
if result.Stderr != "" {
|
|
output += "\n" + result.Stderr
|
|
}
|
|
|
|
if result.ExitCode == 0 {
|
|
return NewTextResult(fmt.Sprintf("✓ Successfully scaled deployment '%s' to %d replicas in namespace '%s'. Action complete - no verification needed.\n%s", deployment, replicas, namespace, output)), nil
|
|
}
|
|
|
|
return NewTextResult(fmt.Sprintf("kubectl command failed (exit code %d):\n%s", result.ExitCode, output)), nil
|
|
}
|
|
|
|
// executeKubernetesRestart restarts a deployment via rollout restart
|
|
func (e *PulseToolExecutor) executeKubernetesRestart(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
|
|
clusterArg, _ := args["cluster"].(string)
|
|
namespace, _ := args["namespace"].(string)
|
|
deployment, _ := args["deployment"].(string)
|
|
|
|
if clusterArg == "" {
|
|
return NewErrorResult(fmt.Errorf("cluster is required")), nil
|
|
}
|
|
if deployment == "" {
|
|
return NewErrorResult(fmt.Errorf("deployment is required")), nil
|
|
}
|
|
if namespace == "" {
|
|
namespace = "default"
|
|
}
|
|
|
|
// Validate identifiers
|
|
if err := validateKubernetesResourceID(namespace); err != nil {
|
|
return NewErrorResult(fmt.Errorf("invalid namespace: %w", err)), nil
|
|
}
|
|
if err := validateKubernetesResourceID(deployment); err != nil {
|
|
return NewErrorResult(fmt.Errorf("invalid deployment: %w", err)), nil
|
|
}
|
|
|
|
// Check control level
|
|
if e.controlLevel == ControlLevelReadOnly {
|
|
return NewTextResult("Kubernetes control operations are not available in read-only mode."), nil
|
|
}
|
|
|
|
agentID, cluster, err := e.findAgentForKubernetesCluster(clusterArg)
|
|
if err != nil {
|
|
return NewTextResult(err.Error()), nil
|
|
}
|
|
|
|
// Check if pre-approved
|
|
preApproved := isPreApproved(args)
|
|
|
|
// Build command
|
|
command := fmt.Sprintf("kubectl -n %s rollout restart deployment/%s", namespace, deployment)
|
|
|
|
// Request approval if needed
|
|
if !preApproved && !e.isAutonomous && e.controlLevel == ControlLevelControlled {
|
|
displayName := cluster.DisplayName
|
|
if cluster.CustomDisplayName != "" {
|
|
displayName = cluster.CustomDisplayName
|
|
}
|
|
approvalID := createApprovalRecord(command, "kubernetes", deployment, displayName, fmt.Sprintf("Restart deployment %s", deployment))
|
|
return NewTextResult(formatKubernetesApprovalNeeded("restart", deployment, namespace, displayName, command, approvalID)), nil
|
|
}
|
|
|
|
if e.agentServer == nil {
|
|
return NewErrorResult(fmt.Errorf("no agent server available")), nil
|
|
}
|
|
|
|
result, err := e.agentServer.ExecuteCommand(ctx, agentID, agentexec.ExecuteCommandPayload{
|
|
Command: command,
|
|
TargetType: "host",
|
|
TargetID: "",
|
|
})
|
|
if err != nil {
|
|
return NewErrorResult(fmt.Errorf("failed to execute kubectl: %w", err)), nil
|
|
}
|
|
|
|
output := result.Stdout
|
|
if result.Stderr != "" {
|
|
output += "\n" + result.Stderr
|
|
}
|
|
|
|
if result.ExitCode == 0 {
|
|
return NewTextResult(fmt.Sprintf("✓ Successfully initiated rollout restart for deployment '%s' in namespace '%s'. Action complete - pods will restart gradually.\n%s", deployment, namespace, output)), nil
|
|
}
|
|
|
|
return NewTextResult(fmt.Sprintf("kubectl command failed (exit code %d):\n%s", result.ExitCode, output)), nil
|
|
}
|
|
|
|
// executeKubernetesDeletePod deletes a pod
|
|
func (e *PulseToolExecutor) executeKubernetesDeletePod(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
|
|
clusterArg, _ := args["cluster"].(string)
|
|
namespace, _ := args["namespace"].(string)
|
|
pod, _ := args["pod"].(string)
|
|
|
|
if clusterArg == "" {
|
|
return NewErrorResult(fmt.Errorf("cluster is required")), nil
|
|
}
|
|
if pod == "" {
|
|
return NewErrorResult(fmt.Errorf("pod is required")), nil
|
|
}
|
|
if namespace == "" {
|
|
namespace = "default"
|
|
}
|
|
|
|
// Validate identifiers
|
|
if err := validateKubernetesResourceID(namespace); err != nil {
|
|
return NewErrorResult(fmt.Errorf("invalid namespace: %w", err)), nil
|
|
}
|
|
if err := validateKubernetesResourceID(pod); err != nil {
|
|
return NewErrorResult(fmt.Errorf("invalid pod: %w", err)), nil
|
|
}
|
|
|
|
// Check control level
|
|
if e.controlLevel == ControlLevelReadOnly {
|
|
return NewTextResult("Kubernetes control operations are not available in read-only mode."), nil
|
|
}
|
|
|
|
agentID, cluster, err := e.findAgentForKubernetesCluster(clusterArg)
|
|
if err != nil {
|
|
return NewTextResult(err.Error()), nil
|
|
}
|
|
|
|
// Check if pre-approved
|
|
preApproved := isPreApproved(args)
|
|
|
|
// Build command
|
|
command := fmt.Sprintf("kubectl -n %s delete pod %s", namespace, pod)
|
|
|
|
// Request approval if needed
|
|
if !preApproved && !e.isAutonomous && e.controlLevel == ControlLevelControlled {
|
|
displayName := cluster.DisplayName
|
|
if cluster.CustomDisplayName != "" {
|
|
displayName = cluster.CustomDisplayName
|
|
}
|
|
approvalID := createApprovalRecord(command, "kubernetes", pod, displayName, fmt.Sprintf("Delete pod %s", pod))
|
|
return NewTextResult(formatKubernetesApprovalNeeded("delete_pod", pod, namespace, displayName, command, approvalID)), nil
|
|
}
|
|
|
|
if e.agentServer == nil {
|
|
return NewErrorResult(fmt.Errorf("no agent server available")), nil
|
|
}
|
|
|
|
result, err := e.agentServer.ExecuteCommand(ctx, agentID, agentexec.ExecuteCommandPayload{
|
|
Command: command,
|
|
TargetType: "host",
|
|
TargetID: "",
|
|
})
|
|
if err != nil {
|
|
return NewErrorResult(fmt.Errorf("failed to execute kubectl: %w", err)), nil
|
|
}
|
|
|
|
output := result.Stdout
|
|
if result.Stderr != "" {
|
|
output += "\n" + result.Stderr
|
|
}
|
|
|
|
if result.ExitCode == 0 {
|
|
return NewTextResult(fmt.Sprintf("✓ Successfully deleted pod '%s' in namespace '%s'. If managed by a controller, a new pod will be created.\n%s", pod, namespace, output)), nil
|
|
}
|
|
|
|
return NewTextResult(fmt.Sprintf("kubectl command failed (exit code %d):\n%s", result.ExitCode, output)), nil
|
|
}
|
|
|
|
// executeKubernetesExec executes a command inside a pod
|
|
func (e *PulseToolExecutor) executeKubernetesExec(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
|
|
clusterArg, _ := args["cluster"].(string)
|
|
namespace, _ := args["namespace"].(string)
|
|
pod, _ := args["pod"].(string)
|
|
container, _ := args["container"].(string)
|
|
command, _ := args["command"].(string)
|
|
|
|
if clusterArg == "" {
|
|
return NewErrorResult(fmt.Errorf("cluster is required")), nil
|
|
}
|
|
if pod == "" {
|
|
return NewErrorResult(fmt.Errorf("pod is required")), nil
|
|
}
|
|
if command == "" {
|
|
return NewErrorResult(fmt.Errorf("command is required")), nil
|
|
}
|
|
if namespace == "" {
|
|
namespace = "default"
|
|
}
|
|
|
|
// Validate identifiers
|
|
if err := validateKubernetesResourceID(namespace); err != nil {
|
|
return NewErrorResult(fmt.Errorf("invalid namespace: %w", err)), nil
|
|
}
|
|
if err := validateKubernetesResourceID(pod); err != nil {
|
|
return NewErrorResult(fmt.Errorf("invalid pod: %w", err)), nil
|
|
}
|
|
if container != "" {
|
|
if err := validateKubernetesResourceID(container); err != nil {
|
|
return NewErrorResult(fmt.Errorf("invalid container: %w", err)), nil
|
|
}
|
|
}
|
|
|
|
// Check control level
|
|
if e.controlLevel == ControlLevelReadOnly {
|
|
return NewTextResult("Kubernetes control operations are not available in read-only mode."), nil
|
|
}
|
|
|
|
agentID, cluster, err := e.findAgentForKubernetesCluster(clusterArg)
|
|
if err != nil {
|
|
return NewTextResult(err.Error()), nil
|
|
}
|
|
|
|
// Check if pre-approved
|
|
preApproved := isPreApproved(args)
|
|
|
|
// Build kubectl command
|
|
var kubectlCmd string
|
|
if container != "" {
|
|
kubectlCmd = fmt.Sprintf("kubectl -n %s exec %s -c %s -- %s", namespace, pod, container, command)
|
|
} else {
|
|
kubectlCmd = fmt.Sprintf("kubectl -n %s exec %s -- %s", namespace, pod, command)
|
|
}
|
|
|
|
// Request approval if needed
|
|
if !preApproved && !e.isAutonomous && e.controlLevel == ControlLevelControlled {
|
|
displayName := cluster.DisplayName
|
|
if cluster.CustomDisplayName != "" {
|
|
displayName = cluster.CustomDisplayName
|
|
}
|
|
approvalID := createApprovalRecord(kubectlCmd, "kubernetes", pod, displayName, fmt.Sprintf("Execute command in pod %s", pod))
|
|
return NewTextResult(formatKubernetesApprovalNeeded("exec", pod, namespace, displayName, kubectlCmd, approvalID)), nil
|
|
}
|
|
|
|
if e.agentServer == nil {
|
|
return NewErrorResult(fmt.Errorf("no agent server available")), nil
|
|
}
|
|
|
|
result, err := e.agentServer.ExecuteCommand(ctx, agentID, agentexec.ExecuteCommandPayload{
|
|
Command: kubectlCmd,
|
|
TargetType: "host",
|
|
TargetID: "",
|
|
})
|
|
if err != nil {
|
|
return NewErrorResult(fmt.Errorf("failed to execute kubectl: %w", err)), nil
|
|
}
|
|
|
|
output := result.Stdout
|
|
if result.Stderr != "" {
|
|
output += "\n" + result.Stderr
|
|
}
|
|
|
|
// Always show output explicitly to prevent LLM hallucination
|
|
if result.ExitCode == 0 {
|
|
if output == "" {
|
|
return NewTextResult(fmt.Sprintf("Command executed in pod '%s' (exit code 0).\n\nOutput:\n(no output)", pod)), nil
|
|
}
|
|
return NewTextResult(fmt.Sprintf("Command executed in pod '%s' (exit code 0).\n\nOutput:\n%s", pod, output)), nil
|
|
}
|
|
|
|
if output == "" {
|
|
return NewTextResult(fmt.Sprintf("Command in pod '%s' exited with code %d.\n\nOutput:\n(no output)", pod, result.ExitCode)), nil
|
|
}
|
|
return NewTextResult(fmt.Sprintf("Command in pod '%s' exited with code %d.\n\nOutput:\n%s", pod, result.ExitCode, output)), nil
|
|
}
|
|
|
|
// executeKubernetesLogs retrieves pod logs
|
|
func (e *PulseToolExecutor) executeKubernetesLogs(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
|
|
clusterArg, _ := args["cluster"].(string)
|
|
namespace, _ := args["namespace"].(string)
|
|
pod, _ := args["pod"].(string)
|
|
container, _ := args["container"].(string)
|
|
lines := intArg(args, "lines", 100)
|
|
|
|
if clusterArg == "" {
|
|
return NewErrorResult(fmt.Errorf("cluster is required")), nil
|
|
}
|
|
if pod == "" {
|
|
return NewErrorResult(fmt.Errorf("pod is required")), nil
|
|
}
|
|
if namespace == "" {
|
|
namespace = "default"
|
|
}
|
|
|
|
// Validate identifiers
|
|
if err := validateKubernetesResourceID(namespace); err != nil {
|
|
return NewErrorResult(fmt.Errorf("invalid namespace: %w", err)), nil
|
|
}
|
|
if err := validateKubernetesResourceID(pod); err != nil {
|
|
return NewErrorResult(fmt.Errorf("invalid pod: %w", err)), nil
|
|
}
|
|
if container != "" {
|
|
if err := validateKubernetesResourceID(container); err != nil {
|
|
return NewErrorResult(fmt.Errorf("invalid container: %w", err)), nil
|
|
}
|
|
}
|
|
|
|
// Logs is a read operation, but still requires a connected agent
|
|
agentID, _, err := e.findAgentForKubernetesCluster(clusterArg)
|
|
if err != nil {
|
|
return NewTextResult(err.Error()), nil
|
|
}
|
|
|
|
// Build kubectl command - logs is read-only so no approval needed
|
|
var kubectlCmd string
|
|
if container != "" {
|
|
kubectlCmd = fmt.Sprintf("kubectl -n %s logs %s -c %s --tail=%d", namespace, pod, container, lines)
|
|
} else {
|
|
kubectlCmd = fmt.Sprintf("kubectl -n %s logs %s --tail=%d", namespace, pod, lines)
|
|
}
|
|
|
|
if e.agentServer == nil {
|
|
return NewErrorResult(fmt.Errorf("no agent server available")), nil
|
|
}
|
|
|
|
result, err := e.agentServer.ExecuteCommand(ctx, agentID, agentexec.ExecuteCommandPayload{
|
|
Command: kubectlCmd,
|
|
TargetType: "host",
|
|
TargetID: "",
|
|
})
|
|
if err != nil {
|
|
return NewErrorResult(fmt.Errorf("failed to execute kubectl: %w", err)), nil
|
|
}
|
|
|
|
output := result.Stdout
|
|
if result.Stderr != "" && result.ExitCode != 0 {
|
|
output += "\n" + result.Stderr
|
|
}
|
|
|
|
if result.ExitCode == 0 {
|
|
if output == "" {
|
|
return NewTextResult(fmt.Sprintf("No logs found for pod '%s' in namespace '%s'", pod, namespace)), nil
|
|
}
|
|
return NewTextResult(fmt.Sprintf("Logs from pod '%s' (last %d lines):\n%s", pod, lines, output)), nil
|
|
}
|
|
|
|
return NewTextResult(fmt.Sprintf("kubectl logs failed (exit code %d):\n%s", result.ExitCode, output)), nil
|
|
}
|
|
|
|
// formatKubernetesApprovalNeeded formats an approval-required response for Kubernetes operations
|
|
func formatKubernetesApprovalNeeded(action, resource, namespace, cluster, command, approvalID string) string {
|
|
payload := map[string]interface{}{
|
|
"type": "approval_required",
|
|
"approval_id": approvalID,
|
|
"action": action,
|
|
"resource": resource,
|
|
"namespace": namespace,
|
|
"cluster": cluster,
|
|
"command": command,
|
|
"how_to_approve": "Click the approval button in the chat to execute this action.",
|
|
"do_not_retry": true,
|
|
}
|
|
b, _ := json.Marshal(payload)
|
|
return "APPROVAL_REQUIRED: " + string(b)
|
|
}
|