Files
pulse/internal/ai/tools/kubernetes_control_test.go
T
2026-07-11 11:54:56 +01:00

828 lines
30 KiB
Go

package tools
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/agentcapabilities"
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
"github.com/rcourtman/pulse-go-rewrite/internal/ai/approval"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func newConfiguredKubernetesExecutor(snapshot models.StateSnapshot, mutate func(*ExecutorConfig)) *PulseToolExecutor {
adapter := unifiedresources.NewMonitorAdapter(nil)
adapter.PopulateFromSnapshot(snapshot)
// Autonomous dispatches fail closed when no audit store is wired
// (unknown remediation-lock state), so these routing/control tests
// always get an in-memory store.
cfg := ExecutorConfig{
UnifiedResourceProvider: adapter,
ActionAuditStore: unifiedresources.NewMemoryStore(),
}
if mutate != nil {
mutate(&cfg)
}
return NewPulseToolExecutor(cfg)
}
func TestValidateKubernetesResourceID(t *testing.T) {
tests := []struct {
name string
value string
wantErr bool
}{
{"valid simple", "nginx", false},
{"valid with dash", "my-app", false},
{"valid with dot", "my.app", false},
{"valid with numbers", "app123", false},
{"valid complex", "my-app-v1.2.3", false},
{"empty", "", true},
{"uppercase", "MyApp", true},
{"underscore", "my_app", true},
{"space", "my app", true},
{"special char", "my@app", true},
{"too long", string(make([]byte, 254)), true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateKubernetesResourceID(tt.value)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestFindAgentForKubernetesCluster(t *testing.T) {
t.Run("NoStateProvider", func(t *testing.T) {
exec := NewPulseToolExecutor(ExecutorConfig{})
agentID, cluster, err := exec.findAgentForKubernetesCluster("test")
assert.Error(t, err)
assert.Empty(t, agentID)
assert.Nil(t, cluster)
assert.Contains(t, err.Error(), "state not available")
})
t.Run("ClusterNotFound", func(t *testing.T) {
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1"},
},
}
exec := newConfiguredKubernetesExecutor(state, nil)
agentID, cluster, err := exec.findAgentForKubernetesCluster("nonexistent")
assert.Error(t, err)
assert.Empty(t, agentID)
assert.Nil(t, cluster)
assert.Contains(t, err.Error(), "not found")
})
t.Run("ClusterNoAgent", func(t *testing.T) {
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", AgentID: ""},
},
}
exec := newConfiguredKubernetesExecutor(state, nil)
agentID, cluster, err := exec.findAgentForKubernetesCluster("cluster-1")
assert.Error(t, err)
assert.Empty(t, agentID)
assert.Nil(t, cluster)
assert.Contains(t, err.Error(), "no agent configured")
})
t.Run("FoundByID", func(t *testing.T) {
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", AgentID: "agent-1"},
},
}
exec := newConfiguredKubernetesExecutor(state, nil)
agentID, cluster, err := exec.findAgentForKubernetesCluster("c1")
assert.NoError(t, err)
assert.Equal(t, "agent-1", agentID)
assert.NotNil(t, cluster)
assert.Equal(t, "cluster-1", cluster.Name)
})
t.Run("FoundByDisplayName", func(t *testing.T) {
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", DisplayName: "Production", AgentID: "agent-1"},
},
}
exec := newConfiguredKubernetesExecutor(state, nil)
agentID, _, err := exec.findAgentForKubernetesCluster("Production")
assert.NoError(t, err)
assert.Equal(t, "agent-1", agentID)
})
t.Run("FoundByCustomDisplayName", func(t *testing.T) {
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", CustomDisplayName: "My Cluster", AgentID: "agent-1"},
},
}
exec := newConfiguredKubernetesExecutor(state, nil)
agentID, _, err := exec.findAgentForKubernetesCluster("My Cluster")
assert.NoError(t, err)
assert.Equal(t, "agent-1", agentID)
})
t.Run("FoundWithUnifiedReadStateOnly", func(t *testing.T) {
snapshot := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", CustomDisplayName: "My Cluster", AgentID: "agent-1", Server: "https://k8s.example", Context: "prod"},
},
}
exec := newConfiguredKubernetesExecutor(snapshot, nil)
agentID, cluster, err := exec.findAgentForKubernetesCluster("My Cluster")
assert.NoError(t, err)
assert.Equal(t, "agent-1", agentID)
require.NotNil(t, cluster)
assert.NotEmpty(t, cluster.ID)
assert.Equal(t, "cluster-1", cluster.Name)
assert.Equal(t, "My Cluster", cluster.DisplayName)
assert.Equal(t, "https://k8s.example", cluster.Server)
assert.Equal(t, "prod", cluster.Context)
})
}
func TestExecuteKubernetesScale(t *testing.T) {
ctx := context.Background()
t.Run("MissingCluster", func(t *testing.T) {
exec := NewPulseToolExecutor(ExecutorConfig{StateProvider: &mockStateProvider{state: models.StateSnapshot{}}})
result, err := exec.executeKubernetesScale(ctx, map[string]interface{}{})
require.NoError(t, err)
assert.True(t, result.IsError)
assert.Contains(t, result.Content[0].Text, "cluster is required")
})
t.Run("MissingDeployment", func(t *testing.T) {
exec := NewPulseToolExecutor(ExecutorConfig{StateProvider: &mockStateProvider{state: models.StateSnapshot{}}})
result, err := exec.executeKubernetesScale(ctx, map[string]interface{}{
"cluster": "test",
})
require.NoError(t, err)
assert.True(t, result.IsError)
assert.Contains(t, result.Content[0].Text, "deployment is required")
})
t.Run("MissingReplicas", func(t *testing.T) {
exec := NewPulseToolExecutor(ExecutorConfig{StateProvider: &mockStateProvider{state: models.StateSnapshot{}}})
result, err := exec.executeKubernetesScale(ctx, map[string]interface{}{
"cluster": "test",
"deployment": "nginx",
})
require.NoError(t, err)
assert.True(t, result.IsError)
assert.Contains(t, result.Content[0].Text, "replicas is required")
})
t.Run("InvalidNamespace", func(t *testing.T) {
exec := NewPulseToolExecutor(ExecutorConfig{StateProvider: &mockStateProvider{state: models.StateSnapshot{}}})
result, err := exec.executeKubernetesScale(ctx, map[string]interface{}{
"cluster": "test",
"deployment": "nginx",
"replicas": 3,
"namespace": "Invalid_NS",
})
require.NoError(t, err)
assert.True(t, result.IsError)
assert.Contains(t, result.Content[0].Text, "invalid namespace")
})
t.Run("ReadOnlyMode", func(t *testing.T) {
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", AgentID: "agent-1"},
},
}
exec := newConfiguredKubernetesExecutor(state, func(cfg *ExecutorConfig) {
cfg.ControlLevel = ControlLevelReadOnly
})
result, err := exec.executeKubernetesScale(ctx, map[string]interface{}{
"cluster": "cluster-1",
"deployment": "nginx",
"replicas": 3,
})
require.NoError(t, err)
assert.Contains(t, result.Content[0].Text, "not available in read-only mode")
})
t.Run("ControlledRequiresApproval", func(t *testing.T) {
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", AgentID: "agent-1", DisplayName: "Cluster One"},
},
}
exec := newConfiguredKubernetesExecutor(state, func(cfg *ExecutorConfig) {
cfg.ControlLevel = ControlLevelControlled
})
result, err := exec.executeKubernetesScale(ctx, map[string]interface{}{
"cluster": "cluster-1",
"deployment": "nginx",
"replicas": 3,
})
require.NoError(t, err)
assert.Contains(t, result.Content[0].Text, "APPROVAL_REQUIRED")
assert.Contains(t, result.Content[0].Text, "scale")
})
t.Run("ExecuteSuccessVerified", func(t *testing.T) {
mockAgent := &mockAgentServer{
agents: []agentexec.ConnectedAgent{{AgentID: "agent-1", Hostname: "k8s-host"}},
}
mockAgent.On("ExecuteCommand", mock.Anything, "agent-1", mock.MatchedBy(func(cmd agentexec.ExecuteCommandPayload) bool {
return cmd.Command == "kubectl -n 'default' scale deployment 'nginx' --replicas=3" &&
cmd.TargetType == "agent"
})).Return(&agentexec.CommandResultPayload{
ExitCode: 0,
Stdout: "deployment.apps/nginx scaled",
}, nil)
// Read-after-write: the scale handler re-reads the deployment state.
mockAgent.On("ExecuteCommand", mock.Anything, "agent-1", mock.MatchedBy(func(cmd agentexec.ExecuteCommandPayload) bool {
return cmd.Command == "kubectl -n 'default' get deployment 'nginx' -o 'jsonpath={.spec.replicas} {.status.readyReplicas}'" &&
cmd.TargetType == "agent"
})).Return(&agentexec.CommandResultPayload{
ExitCode: 0,
Stdout: "3 3",
}, nil)
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", AgentID: "agent-1"},
},
}
exec := newConfiguredKubernetesExecutor(state, func(cfg *ExecutorConfig) {
cfg.AgentServer = mockAgent
cfg.ControlLevel = ControlLevelAutonomous
})
result, err := exec.executeKubernetesScale(ctx, map[string]interface{}{
"cluster": "cluster-1",
"deployment": "nginx",
"replicas": 3,
})
require.NoError(t, err)
require.False(t, result.IsError)
var resp map[string]interface{}
require.NoError(t, json.Unmarshal([]byte(result.Content[0].Text), &resp))
assert.Equal(t, true, resp["success"])
assert.Equal(t, "nginx", resp["deployment"])
verification, ok := resp["verification"].(map[string]interface{})
require.True(t, ok, "expected verification in response")
assert.Equal(t, true, verification["confirmed"])
assert.Equal(t, "kubectl_get", verification["method"])
observed, ok := verification["observed"].(map[string]interface{})
require.True(t, ok, "expected observed state in verification")
assert.Equal(t, float64(3), observed["replicas"])
assert.Equal(t, float64(3), observed["ready_replicas"])
mockAgent.AssertExpectations(t)
})
t.Run("ExecuteSuccessVerificationMismatch", func(t *testing.T) {
origSleep := kubernetesVerifySleepFn
kubernetesVerifySleepFn = func(context.Context, time.Duration) error { return nil }
t.Cleanup(func() { kubernetesVerifySleepFn = origSleep })
mockAgent := &mockAgentServer{
agents: []agentexec.ConnectedAgent{{AgentID: "agent-1", Hostname: "k8s-host"}},
}
mockAgent.On("ExecuteCommand", mock.Anything, "agent-1", mock.MatchedBy(func(cmd agentexec.ExecuteCommandPayload) bool {
return cmd.Command == "kubectl -n 'default' scale deployment 'nginx' --replicas=3"
})).Return(&agentexec.CommandResultPayload{
ExitCode: 0,
Stdout: "deployment.apps/nginx scaled",
}, nil).Once()
// The re-read keeps observing a different desired replica count.
mockAgent.On("ExecuteCommand", mock.Anything, "agent-1", mock.MatchedBy(func(cmd agentexec.ExecuteCommandPayload) bool {
return cmd.Command == "kubectl -n 'default' get deployment 'nginx' -o 'jsonpath={.spec.replicas} {.status.readyReplicas}'"
})).Return(&agentexec.CommandResultPayload{
ExitCode: 0,
Stdout: "1 1",
}, nil).Times(kubernetesVerifyMaxAttempts)
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", AgentID: "agent-1"},
},
}
exec := newConfiguredKubernetesExecutor(state, func(cfg *ExecutorConfig) {
cfg.AgentServer = mockAgent
cfg.ControlLevel = ControlLevelAutonomous
})
result, err := exec.executeKubernetesScale(ctx, map[string]interface{}{
"cluster": "cluster-1",
"deployment": "nginx",
"replicas": 3,
})
require.NoError(t, err)
var resp map[string]interface{}
require.NoError(t, json.Unmarshal([]byte(result.Content[0].Text), &resp))
verification, ok := resp["verification"].(map[string]interface{})
require.True(t, ok, "expected verification in response")
assert.Equal(t, false, verification["confirmed"])
observed, ok := verification["observed"].(map[string]interface{})
require.True(t, ok, "expected observed state in verification")
assert.Equal(t, float64(1), observed["replicas"])
mockAgent.AssertExpectations(t)
})
t.Run("ExecuteSuccessVerificationReadError", func(t *testing.T) {
mockAgent := &mockAgentServer{
agents: []agentexec.ConnectedAgent{{AgentID: "agent-1", Hostname: "k8s-host"}},
}
mockAgent.On("ExecuteCommand", mock.Anything, "agent-1", mock.MatchedBy(func(cmd agentexec.ExecuteCommandPayload) bool {
return cmd.Command == "kubectl -n 'default' scale deployment 'nginx' --replicas=3"
})).Return(&agentexec.CommandResultPayload{
ExitCode: 0,
Stdout: "deployment.apps/nginx scaled",
}, nil).Once()
mockAgent.On("ExecuteCommand", mock.Anything, "agent-1", mock.MatchedBy(func(cmd agentexec.ExecuteCommandPayload) bool {
return cmd.Command == "kubectl -n 'default' get deployment 'nginx' -o 'jsonpath={.spec.replicas} {.status.readyReplicas}'"
})).Return((*agentexec.CommandResultPayload)(nil), assert.AnError).Once()
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", AgentID: "agent-1"},
},
}
exec := newConfiguredKubernetesExecutor(state, func(cfg *ExecutorConfig) {
cfg.AgentServer = mockAgent
cfg.ControlLevel = ControlLevelAutonomous
})
result, err := exec.executeKubernetesScale(ctx, map[string]interface{}{
"cluster": "cluster-1",
"deployment": "nginx",
"replicas": 3,
})
require.NoError(t, err)
var resp map[string]interface{}
require.NoError(t, json.Unmarshal([]byte(result.Content[0].Text), &resp))
// The scale itself succeeded; verification must report unconfirmed, not error out.
assert.Equal(t, true, resp["success"])
verification, ok := resp["verification"].(map[string]interface{})
require.True(t, ok, "expected verification in response")
assert.Equal(t, false, verification["confirmed"])
assert.NotEmpty(t, verification["note"])
mockAgent.AssertExpectations(t)
})
t.Run("ExecuteFailureNoVerification", func(t *testing.T) {
mockAgent := &mockAgentServer{
agents: []agentexec.ConnectedAgent{{AgentID: "agent-1", Hostname: "k8s-host"}},
}
mockAgent.On("ExecuteCommand", mock.Anything, "agent-1", mock.MatchedBy(func(cmd agentexec.ExecuteCommandPayload) bool {
return cmd.Command == "kubectl -n 'default' scale deployment 'nginx' --replicas=3"
})).Return(&agentexec.CommandResultPayload{
ExitCode: 1,
Stderr: "Error from server (NotFound): deployments.apps \"nginx\" not found",
}, nil).Once()
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", AgentID: "agent-1"},
},
}
exec := newConfiguredKubernetesExecutor(state, func(cfg *ExecutorConfig) {
cfg.AgentServer = mockAgent
cfg.ControlLevel = ControlLevelAutonomous
})
result, err := exec.executeKubernetesScale(ctx, map[string]interface{}{
"cluster": "cluster-1",
"deployment": "nginx",
"replicas": 3,
})
require.NoError(t, err)
assert.Contains(t, result.Content[0].Text, "kubectl command failed")
mockAgent.AssertExpectations(t)
})
}
func TestExecuteKubernetesRestart(t *testing.T) {
ctx := context.Background()
t.Run("MissingDeployment", func(t *testing.T) {
exec := NewPulseToolExecutor(ExecutorConfig{StateProvider: &mockStateProvider{state: models.StateSnapshot{}}})
result, err := exec.executeKubernetesRestart(ctx, map[string]interface{}{
"cluster": "test",
})
require.NoError(t, err)
assert.True(t, result.IsError)
assert.Contains(t, result.Content[0].Text, "deployment is required")
})
t.Run("ExecuteSuccess", func(t *testing.T) {
mockAgent := &mockAgentServer{
agents: []agentexec.ConnectedAgent{{AgentID: "agent-1", Hostname: "k8s-host"}},
}
mockAgent.On("ExecuteCommand", mock.Anything, "agent-1", mock.MatchedBy(func(cmd agentexec.ExecuteCommandPayload) bool {
return cmd.Command == "kubectl -n 'default' rollout restart deployment/'nginx'"
})).Return(&agentexec.CommandResultPayload{
ExitCode: 0,
Stdout: "deployment.apps/nginx restarted",
}, nil)
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", AgentID: "agent-1"},
},
}
exec := newConfiguredKubernetesExecutor(state, func(cfg *ExecutorConfig) {
cfg.AgentServer = mockAgent
cfg.ControlLevel = ControlLevelAutonomous
})
result, err := exec.executeKubernetesRestart(ctx, map[string]interface{}{
"cluster": "cluster-1",
"deployment": "nginx",
})
require.NoError(t, err)
assert.Contains(t, result.Content[0].Text, "Successfully initiated rollout restart")
mockAgent.AssertExpectations(t)
})
}
func TestExecuteKubernetesDeletePod(t *testing.T) {
ctx := context.Background()
t.Run("MissingPod", func(t *testing.T) {
exec := NewPulseToolExecutor(ExecutorConfig{StateProvider: &mockStateProvider{state: models.StateSnapshot{}}})
result, err := exec.executeKubernetesDeletePod(ctx, map[string]interface{}{
"cluster": "test",
})
require.NoError(t, err)
assert.True(t, result.IsError)
assert.Contains(t, result.Content[0].Text, "pod is required")
})
t.Run("ExecuteSuccess", func(t *testing.T) {
mockAgent := &mockAgentServer{
agents: []agentexec.ConnectedAgent{{AgentID: "agent-1", Hostname: "k8s-host"}},
}
mockAgent.On("ExecuteCommand", mock.Anything, "agent-1", mock.MatchedBy(func(cmd agentexec.ExecuteCommandPayload) bool {
return cmd.Command == "kubectl -n 'default' delete pod 'nginx-abc123'"
})).Return(&agentexec.CommandResultPayload{
ExitCode: 0,
Stdout: "pod \"nginx-abc123\" deleted",
}, nil)
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", AgentID: "agent-1"},
},
}
exec := newConfiguredKubernetesExecutor(state, func(cfg *ExecutorConfig) {
cfg.AgentServer = mockAgent
cfg.ControlLevel = ControlLevelAutonomous
})
result, err := exec.executeKubernetesDeletePod(ctx, map[string]interface{}{
"cluster": "cluster-1",
"pod": "nginx-abc123",
})
require.NoError(t, err)
assert.Contains(t, result.Content[0].Text, "Successfully deleted pod")
mockAgent.AssertExpectations(t)
})
}
func TestExecuteKubernetesExec(t *testing.T) {
ctx := context.Background()
t.Run("MissingCommand", func(t *testing.T) {
exec := NewPulseToolExecutor(ExecutorConfig{StateProvider: &mockStateProvider{state: models.StateSnapshot{}}})
result, err := exec.executeKubernetesExec(ctx, map[string]interface{}{
"cluster": "test",
"pod": "nginx",
})
require.NoError(t, err)
assert.True(t, result.IsError)
assert.Contains(t, result.Content[0].Text, "command is required")
})
t.Run("ExecuteWithoutContainer", func(t *testing.T) {
mockAgent := &mockAgentServer{
agents: []agentexec.ConnectedAgent{{AgentID: "agent-1", Hostname: "k8s-host"}},
}
mockAgent.On("ExecuteCommand", mock.Anything, "agent-1", mock.MatchedBy(func(cmd agentexec.ExecuteCommandPayload) bool {
return cmd.Command == "kubectl -n 'default' exec 'nginx-pod' -- sh -c 'cat /etc/nginx/nginx.conf'"
})).Return(&agentexec.CommandResultPayload{
ExitCode: 0,
Stdout: "server { listen 80; }",
}, nil)
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", AgentID: "agent-1"},
},
}
exec := newConfiguredKubernetesExecutor(state, func(cfg *ExecutorConfig) {
cfg.AgentServer = mockAgent
cfg.ControlLevel = ControlLevelAutonomous
})
result, err := exec.executeKubernetesExec(ctx, map[string]interface{}{
"cluster": "cluster-1",
"pod": "nginx-pod",
"command": "cat /etc/nginx/nginx.conf",
})
require.NoError(t, err)
assert.Contains(t, result.Content[0].Text, "Command executed")
assert.Contains(t, result.Content[0].Text, "server { listen 80; }")
mockAgent.AssertExpectations(t)
})
t.Run("ExecuteWithContainer", func(t *testing.T) {
mockAgent := &mockAgentServer{
agents: []agentexec.ConnectedAgent{{AgentID: "agent-1", Hostname: "k8s-host"}},
}
mockAgent.On("ExecuteCommand", mock.Anything, "agent-1", mock.MatchedBy(func(cmd agentexec.ExecuteCommandPayload) bool {
return cmd.Command == "kubectl -n 'kube-system' exec 'coredns-pod' -c 'coredns' -- sh -c 'cat /etc/coredns/Corefile'"
})).Return(&agentexec.CommandResultPayload{
ExitCode: 0,
Stdout: ".:53 { forward . /etc/resolv.conf }",
}, nil)
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", AgentID: "agent-1"},
},
}
exec := newConfiguredKubernetesExecutor(state, func(cfg *ExecutorConfig) {
cfg.AgentServer = mockAgent
cfg.ControlLevel = ControlLevelAutonomous
})
result, err := exec.executeKubernetesExec(ctx, map[string]interface{}{
"cluster": "cluster-1",
"namespace": "kube-system",
"pod": "coredns-pod",
"container": "coredns",
"command": "cat /etc/coredns/Corefile",
})
require.NoError(t, err)
assert.Contains(t, result.Content[0].Text, "Command executed")
mockAgent.AssertExpectations(t)
})
t.Run("ExecEscapesMetacharacters", func(t *testing.T) {
mockAgent := &mockAgentServer{
agents: []agentexec.ConnectedAgent{{AgentID: "agent-1", Hostname: "k8s-host"}},
}
mockAgent.On("ExecuteCommand", mock.Anything, "agent-1", mock.MatchedBy(func(cmd agentexec.ExecuteCommandPayload) bool {
return cmd.Command == "kubectl -n 'default' exec 'nginx-pod' -- sh -c 'cat /etc/nginx/nginx.conf; id'"
})).Return(&agentexec.CommandResultPayload{
ExitCode: 0,
Stdout: "ok",
}, nil)
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", AgentID: "agent-1"},
},
}
exec := newConfiguredKubernetesExecutor(state, func(cfg *ExecutorConfig) {
cfg.AgentServer = mockAgent
cfg.ControlLevel = ControlLevelAutonomous
})
result, err := exec.executeKubernetesExec(ctx, map[string]interface{}{
"cluster": "cluster-1",
"pod": "nginx-pod",
"command": "cat /etc/nginx/nginx.conf; id",
})
require.NoError(t, err)
assert.Contains(t, result.Content[0].Text, "Command executed")
mockAgent.AssertExpectations(t)
})
t.Run("MismatchedApprovedIDDoesNotBypass", func(t *testing.T) {
store, err := approval.NewStore(approval.StoreConfig{
DataDir: t.TempDir(),
DisablePersistence: true,
})
require.NoError(t, err)
approval.SetStore(store)
defer approval.SetStore(nil)
req := &approval.ApprovalRequest{
ID: "approval-1",
Command: "kubectl -n default exec nginx-pod -- whoami",
TargetType: "kubernetes",
TargetID: "nginx-pod",
}
require.NoError(t, store.CreateApproval(req))
_, err = store.Approve("approval-1", "tester")
require.NoError(t, err)
mockAgent := &mockAgentServer{
agents: []agentexec.ConnectedAgent{{AgentID: "agent-1", Hostname: "k8s-host"}},
}
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", AgentID: "agent-1"},
},
}
exec := newConfiguredKubernetesExecutor(state, func(cfg *ExecutorConfig) {
cfg.AgentServer = mockAgent
cfg.ControlLevel = ControlLevelControlled
})
result, err := exec.executeKubernetesExec(ctx, agentcapabilities.WithApprovalArgument(map[string]interface{}{
"cluster": "cluster-1",
"pod": "nginx-pod",
"command": "cat /etc/nginx/nginx.conf",
}, "approval-1"))
require.NoError(t, err)
assert.Contains(t, result.Content[0].Text, "APPROVAL_REQUIRED")
mockAgent.AssertNotCalled(t, "ExecuteCommand", mock.Anything, mock.Anything, mock.Anything)
})
t.Run("MatchingApprovedIDCarriesAuthorizationToAgentBoundary", func(t *testing.T) {
store, err := approval.NewStore(approval.StoreConfig{
DataDir: t.TempDir(),
DisablePersistence: true,
})
require.NoError(t, err)
approval.SetStore(store)
defer approval.SetStore(nil)
kubeCmd := buildKubectlExecCommand("default", "nginx-pod", "", "cat /etc/nginx/nginx.conf")
req := &approval.ApprovalRequest{
ID: "approval-2",
Command: kubeCmd,
TargetType: "kubernetes",
TargetID: "c1:default:pod:nginx-pod",
}
require.NoError(t, store.CreateApproval(req))
_, err = store.Approve("approval-2", "tester")
require.NoError(t, err)
mockAgent := &mockAgentServer{
agents: []agentexec.ConnectedAgent{{AgentID: "agent-1", Hostname: "k8s-host"}},
}
mockAgent.On("ExecuteCommand", mock.Anything, "agent-1", mock.MatchedBy(func(cmd agentexec.ExecuteCommandPayload) bool {
return cmd.Command == kubeCmd
})).Return(&agentexec.CommandResultPayload{
ExitCode: 0,
Stdout: "ok",
}, nil)
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", AgentID: "agent-1"},
},
}
exec := newConfiguredKubernetesExecutor(state, func(cfg *ExecutorConfig) {
cfg.AgentServer = mockAgent
cfg.ControlLevel = ControlLevelControlled
})
result, err := exec.executeKubernetesExec(ctx, agentcapabilities.WithApprovalArgument(map[string]interface{}{
"cluster": "cluster-1",
"pod": "nginx-pod",
"command": "cat /etc/nginx/nginx.conf",
}, "approval-2"))
require.NoError(t, err)
assert.NotContains(t, result.Content[0].Text, "APPROVAL_REQUIRED")
mockAgent.AssertExpectations(t)
// The mock agent boundary does not consume. Production agentexec consumes
// atomically before grant minting and dispatch; that replay behavior is
// covered by the agentexec/API authorization tests.
result, err = exec.executeKubernetesExec(ctx, agentcapabilities.WithApprovalArgument(map[string]interface{}{
"cluster": "cluster-1",
"pod": "nginx-pod",
"command": "cat /etc/nginx/nginx.conf",
}, "approval-2"))
require.NoError(t, err)
assert.NotContains(t, result.Content[0].Text, "APPROVAL_REQUIRED")
})
}
func TestExecuteKubernetesLogs(t *testing.T) {
ctx := context.Background()
t.Run("MissingPod", func(t *testing.T) {
exec := NewPulseToolExecutor(ExecutorConfig{StateProvider: &mockStateProvider{state: models.StateSnapshot{}}})
result, err := exec.executeKubernetesLogs(ctx, map[string]interface{}{
"cluster": "test",
})
require.NoError(t, err)
assert.True(t, result.IsError)
assert.Contains(t, result.Content[0].Text, "pod is required")
})
t.Run("LogsNoApprovalNeeded", func(t *testing.T) {
// Logs should work even in controlled mode without approval
mockAgent := &mockAgentServer{
agents: []agentexec.ConnectedAgent{{AgentID: "agent-1", Hostname: "k8s-host"}},
}
mockAgent.On("ExecuteCommand", mock.Anything, "agent-1", mock.MatchedBy(func(cmd agentexec.ExecuteCommandPayload) bool {
return cmd.Command == "kubectl -n 'default' logs 'nginx-pod' --tail=50"
})).Return(&agentexec.CommandResultPayload{
ExitCode: 0,
Stdout: "2024-01-01 10:00:00 Request received\n2024-01-01 10:00:01 Response sent",
}, nil)
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", AgentID: "agent-1"},
},
}
exec := newConfiguredKubernetesExecutor(state, func(cfg *ExecutorConfig) {
cfg.AgentServer = mockAgent
cfg.ControlLevel = ControlLevelControlled
})
result, err := exec.executeKubernetesLogs(ctx, map[string]interface{}{
"cluster": "cluster-1",
"pod": "nginx-pod",
"lines": 50,
})
require.NoError(t, err)
// Should NOT require approval since logs is read-only
assert.NotContains(t, result.Content[0].Text, "APPROVAL_REQUIRED")
assert.Contains(t, result.Content[0].Text, "Logs from pod")
mockAgent.AssertExpectations(t)
})
t.Run("LogsWithContainer", func(t *testing.T) {
mockAgent := &mockAgentServer{
agents: []agentexec.ConnectedAgent{{AgentID: "agent-1", Hostname: "k8s-host"}},
}
mockAgent.On("ExecuteCommand", mock.Anything, "agent-1", mock.MatchedBy(func(cmd agentexec.ExecuteCommandPayload) bool {
return cmd.Command == "kubectl -n 'default' logs 'nginx-pod' -c 'sidecar' --tail=100"
})).Return(&agentexec.CommandResultPayload{
ExitCode: 0,
Stdout: "Sidecar logs here",
}, nil)
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", AgentID: "agent-1"},
},
}
exec := newConfiguredKubernetesExecutor(state, func(cfg *ExecutorConfig) {
cfg.AgentServer = mockAgent
cfg.ControlLevel = ControlLevelAutonomous
})
result, err := exec.executeKubernetesLogs(ctx, map[string]interface{}{
"cluster": "cluster-1",
"pod": "nginx-pod",
"container": "sidecar",
})
require.NoError(t, err)
assert.Contains(t, result.Content[0].Text, "Logs from pod")
mockAgent.AssertExpectations(t)
})
t.Run("EmptyLogs", func(t *testing.T) {
mockAgent := &mockAgentServer{
agents: []agentexec.ConnectedAgent{{AgentID: "agent-1", Hostname: "k8s-host"}},
}
mockAgent.On("ExecuteCommand", mock.Anything, "agent-1", mock.Anything).Return(&agentexec.CommandResultPayload{
ExitCode: 0,
Stdout: "",
}, nil)
state := models.StateSnapshot{
KubernetesClusters: []models.KubernetesCluster{
{ID: "c1", Name: "cluster-1", AgentID: "agent-1"},
},
}
exec := newConfiguredKubernetesExecutor(state, func(cfg *ExecutorConfig) {
cfg.AgentServer = mockAgent
cfg.ControlLevel = ControlLevelAutonomous
})
result, err := exec.executeKubernetesLogs(ctx, map[string]interface{}{
"cluster": "cluster-1",
"pod": "nginx-pod",
})
require.NoError(t, err)
assert.Contains(t, result.Content[0].Text, "No logs found")
mockAgent.AssertExpectations(t)
})
}
func TestFormatKubernetesApprovalNeeded(t *testing.T) {
result := formatKubernetesApprovalNeeded("scale", "nginx", "default", "production", "kubectl scale...", "approval-123")
assert.Contains(t, result, "APPROVAL_REQUIRED")
assert.Contains(t, result, "scale")
assert.Contains(t, result, "nginx")
assert.Contains(t, result, "default")
assert.Contains(t, result, "production")
assert.Contains(t, result, "approval-123")
}