Files
Pulse/internal/dockeragent/swarm_coverage_test.go
rcourtman ed78509f92 Fix flaky tests and improve coverage across alerts, api, and config packages
- Fix deadlock and race conditions in internal/alerts
- Add comprehensive error path tests for internal/config
- Fix 401 handling in internal/api
- Fix Docker Swarm task filtering test logic
2026-01-03 18:36:17 +00:00

715 lines
20 KiB
Go

package dockeragent
import (
"context"
"errors"
"testing"
"time"
swarmtypes "github.com/docker/docker/api/types/swarm"
systemtypes "github.com/docker/docker/api/types/system"
agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker"
)
func TestResolvedSwarmScope(t *testing.T) {
info := systemtypes.Info{
Swarm: swarmtypes.Info{
ControlAvailable: true,
},
}
agent := &Agent{cfg: Config{SwarmScope: swarmScopeAuto}}
if got := agent.resolvedSwarmScope(info); got != swarmScopeCluster {
t.Fatalf("expected cluster scope, got %q", got)
}
agent.cfg.SwarmScope = swarmScopeNode
if got := agent.resolvedSwarmScope(info); got != swarmScopeNode {
t.Fatalf("expected node scope, got %q", got)
}
info.Swarm.ControlAvailable = false
agent.cfg.SwarmScope = swarmScopeAuto
if got := agent.resolvedSwarmScope(info); got != swarmScopeNode {
t.Fatalf("expected node scope, got %q", got)
}
agent.cfg.SwarmScope = "unknown"
if got := agent.resolvedSwarmScope(info); got != swarmScopeNode {
t.Fatalf("expected fallback node scope, got %q", got)
}
}
func TestMapSwarmService(t *testing.T) {
now := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)
updateAt := now.Add(2 * time.Minute)
svc := &swarmtypes.Service{
ID: "svc1",
Spec: swarmtypes.ServiceSpec{
Annotations: swarmtypes.Annotations{
Name: "web",
Labels: map[string]string{
"com.docker.stack.namespace": "stack",
},
},
Mode: swarmtypes.ServiceMode{Replicated: &swarmtypes.ReplicatedService{}},
TaskTemplate: swarmtypes.TaskSpec{
ContainerSpec: &swarmtypes.ContainerSpec{
Image: "nginx:latest",
},
},
},
ServiceStatus: &swarmtypes.ServiceStatus{
DesiredTasks: 3,
RunningTasks: 2,
CompletedTasks: 1,
},
UpdateStatus: &swarmtypes.UpdateStatus{
State: swarmtypes.UpdateStateCompleted,
Message: "done",
CompletedAt: &updateAt,
},
Endpoint: swarmtypes.Endpoint{
Ports: []swarmtypes.PortConfig{
{Name: "http", Protocol: swarmtypes.PortConfigProtocolTCP, TargetPort: 80, PublishedPort: 8080, PublishMode: swarmtypes.PortConfigPublishModeIngress},
},
},
Meta: swarmtypes.Meta{
CreatedAt: now,
UpdatedAt: updateAt,
},
}
got := mapSwarmService(svc)
if got.ID != "svc1" || got.Name != "web" || got.Mode == "" {
t.Fatalf("unexpected service mapping: %+v", got)
}
if got.Stack != "stack" {
t.Fatalf("expected stack label to be mapped, got %q", got.Stack)
}
if got.Image != "nginx:latest" {
t.Fatalf("expected image to be mapped, got %q", got.Image)
}
if got.DesiredTasks != 3 || got.RunningTasks != 2 || got.CompletedTasks != 1 {
t.Fatalf("unexpected task counts: %+v", got)
}
if got.UpdateStatus == nil || got.UpdateStatus.State != string(swarmtypes.UpdateStateCompleted) {
t.Fatalf("expected update status to be mapped, got %+v", got.UpdateStatus)
}
if got.EndpointPorts == nil || len(got.EndpointPorts) != 1 {
t.Fatalf("expected endpoint ports to be mapped")
}
if got.CreatedAt == nil || got.UpdatedAt == nil {
t.Fatalf("expected timestamps to be mapped")
}
}
func TestMapSwarmTask(t *testing.T) {
now := time.Date(2024, 2, 3, 4, 5, 6, 0, time.UTC)
containerStart := now.Add(-2 * time.Minute)
containerFinish := now.Add(-time.Minute)
containers := map[string]agentsdocker.Container{
"container-full": {
ID: "container-full",
Name: "web.1",
StartedAt: &containerStart,
FinishedAt: func() *time.Time {
val := containerFinish
return &val
}(),
},
}
t.Run("running task", func(t *testing.T) {
task := &swarmtypes.Task{
ID: "task1",
ServiceID: "svc1",
Slot: 1,
NodeID: "node1",
Status: swarmtypes.TaskStatus{
State: swarmtypes.TaskStateRunning,
Timestamp: now,
ContainerStatus: &swarmtypes.ContainerStatus{
ContainerID: "container-full",
},
},
Meta: swarmtypes.Meta{
CreatedAt: now.Add(-time.Minute),
},
}
svc := &swarmtypes.Service{Spec: swarmtypes.ServiceSpec{Annotations: swarmtypes.Annotations{Name: "web"}}}
got := mapSwarmTask(task, svc, containers)
if got.ServiceName != "web" {
t.Fatalf("expected service name, got %q", got.ServiceName)
}
if got.ContainerName != "web.1" {
t.Fatalf("expected container name, got %q", got.ContainerName)
}
if got.StartedAt == nil || !got.StartedAt.Equal(now) {
t.Fatalf("expected started at timestamp from task")
}
})
t.Run("completed task uses container timestamps", func(t *testing.T) {
task := &swarmtypes.Task{
ID: "task2",
ServiceID: "svc2",
Status: swarmtypes.TaskStatus{
State: swarmtypes.TaskStateComplete,
ContainerStatus: &swarmtypes.ContainerStatus{
ContainerID: "container-full",
},
},
Meta: swarmtypes.Meta{
CreatedAt: now.Add(-time.Hour),
},
}
got := mapSwarmTask(task, nil, containers)
if got.CompletedAt == nil || !got.CompletedAt.Equal(containerFinish) {
t.Fatalf("expected completed at from container timestamp")
}
if got.StartedAt == nil || !got.StartedAt.Equal(containerStart) {
t.Fatalf("expected started at from container timestamp")
}
})
t.Run("updated at and completed timestamp", func(t *testing.T) {
updated := now.Add(time.Minute)
task := &swarmtypes.Task{
ID: "task3",
ServiceID: "svc3",
Status: swarmtypes.TaskStatus{
State: swarmtypes.TaskStateFailed,
Timestamp: now,
},
Meta: swarmtypes.Meta{
CreatedAt: now.Add(-time.Hour),
UpdatedAt: updated,
},
}
svc := &swarmtypes.Service{Spec: swarmtypes.ServiceSpec{Annotations: swarmtypes.Annotations{Name: "api"}}}
got := mapSwarmTask(task, svc, nil)
if got.UpdatedAt == nil || !got.UpdatedAt.Equal(updated) {
t.Fatalf("expected updated at to be set")
}
if got.CompletedAt == nil || !got.CompletedAt.Equal(now) {
t.Fatalf("expected completed at from task timestamp")
}
})
}
func TestCollectSwarmDataFromManager(t *testing.T) {
agent := &Agent{
docker: &fakeDockerClient{
serviceListFn: func(_ context.Context, _ swarmtypes.ServiceListOptions) ([]swarmtypes.Service, error) {
return []swarmtypes.Service{
{ID: "svc1", Spec: swarmtypes.ServiceSpec{Annotations: swarmtypes.Annotations{Name: "alpha"}}},
{ID: "svc2", Spec: swarmtypes.ServiceSpec{Annotations: swarmtypes.Annotations{Name: "beta"}}},
}, nil
},
taskListFn: func(_ context.Context, opts swarmtypes.TaskListOptions) ([]swarmtypes.Task, error) {
if got := opts.Filters.Get("node"); len(got) != 1 || got[0] != "node1" {
t.Fatalf("expected node filter to include node1, got %v", got)
}
return []swarmtypes.Task{
{ID: "task1", ServiceID: "svc1", DesiredState: swarmtypes.TaskStateRunning, Status: swarmtypes.TaskStatus{State: swarmtypes.TaskStateRunning}},
}, nil
},
},
}
info := systemtypes.Info{
Swarm: swarmtypes.Info{
NodeID: "node1",
},
}
services, tasks, err := agent.collectSwarmDataFromManager(context.Background(), info, swarmScopeNode, nil, true, true)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(tasks) != 1 {
t.Fatalf("expected 1 task, got %d", len(tasks))
}
if len(services) != 1 {
t.Fatalf("expected filtered services, got %d", len(services))
}
t.Run("task list error", func(t *testing.T) {
agent := &Agent{
docker: &fakeDockerClient{
serviceListFn: func(_ context.Context, _ swarmtypes.ServiceListOptions) ([]swarmtypes.Service, error) {
return []swarmtypes.Service{
{ID: "svc1", Spec: swarmtypes.ServiceSpec{Annotations: swarmtypes.Annotations{Name: "alpha"}}},
}, nil
},
taskListFn: func(_ context.Context, _ swarmtypes.TaskListOptions) ([]swarmtypes.Task, error) {
return nil, errors.New("task failed")
},
},
}
info := systemtypes.Info{
Swarm: swarmtypes.Info{
NodeID: "node1",
},
}
services, tasks, err := agent.collectSwarmDataFromManager(context.Background(), info, swarmScopeNode, nil, true, true)
if err == nil {
t.Fatal("expected error")
}
if len(services) != 1 || tasks != nil {
t.Fatalf("expected services only on task error")
}
})
}
func TestCollectSwarmData(t *testing.T) {
t.Run("unsupported swarm returns nils", func(t *testing.T) {
agent := &Agent{supportsSwarm: false}
services, tasks, info := agent.collectSwarmData(context.Background(), systemtypes.Info{}, nil)
if services != nil || tasks != nil || info != nil {
t.Fatal("expected nil outputs when swarm unsupported")
}
})
t.Run("empty swarm info returns nil", func(t *testing.T) {
agent := &Agent{supportsSwarm: true}
services, tasks, info := agent.collectSwarmData(context.Background(), systemtypes.Info{}, nil)
if services != nil || tasks != nil || info != nil {
t.Fatal("expected nil outputs for empty swarm info")
}
})
t.Run("inactive swarm returns info only", func(t *testing.T) {
agent := &Agent{supportsSwarm: true, cfg: Config{SwarmScope: swarmScopeNode}}
info := systemtypes.Info{
Swarm: swarmtypes.Info{
NodeID: "node1",
LocalNodeState: swarmtypes.LocalNodeStatePending,
},
}
services, tasks, swarmInfo := agent.collectSwarmData(context.Background(), info, nil)
if services != nil || tasks != nil {
t.Fatal("expected nil services/tasks for inactive swarm")
}
if swarmInfo == nil || swarmInfo.NodeID != "node1" {
t.Fatal("expected swarm info to be returned")
}
})
t.Run("manager error falls back to containers", func(t *testing.T) {
agent := &Agent{
supportsSwarm: true,
cfg: Config{
IncludeServices: true,
IncludeTasks: true,
SwarmScope: swarmScopeAuto,
},
docker: &fakeDockerClient{
serviceListFn: func(context.Context, swarmtypes.ServiceListOptions) ([]swarmtypes.Service, error) {
return nil, errors.New("boom")
},
},
}
containers := []agentsdocker.Container{
{
ID: "container1",
Name: "web.1",
Image: "nginx:latest",
State: "running",
Labels: map[string]string{
"com.docker.swarm.service.id": "svc1",
"com.docker.swarm.service.name": "web",
"com.docker.swarm.task.id": "task1",
"com.docker.swarm.task.slot": "1",
"com.docker.swarm.task.message": "ok",
"com.docker.swarm.task.error": "",
"com.docker.stack.namespace": "stack",
"com.docker.swarm.node.id": "node1",
"com.docker.swarm.node.name": "node1",
"com.docker.swarm.task.desired-state": "running",
},
},
}
info := systemtypes.Info{
Swarm: swarmtypes.Info{
NodeID: "node1",
ControlAvailable: true,
LocalNodeState: swarmtypes.LocalNodeStateActive,
},
}
services, tasks, swarmInfo := agent.collectSwarmData(context.Background(), info, containers)
if len(tasks) != 1 || len(services) != 1 {
t.Fatalf("expected derived tasks/services, got %d/%d", len(tasks), len(services))
}
if swarmInfo == nil || swarmInfo.Scope != swarmScopeNode {
t.Fatalf("expected effective scope node, got %+v", swarmInfo)
}
})
t.Run("manager success uses manager data", func(t *testing.T) {
agent := &Agent{
supportsSwarm: true,
cfg: Config{
IncludeServices: true,
IncludeTasks: true,
SwarmScope: swarmScopeCluster,
},
docker: &fakeDockerClient{
serviceListFn: func(context.Context, swarmtypes.ServiceListOptions) ([]swarmtypes.Service, error) {
return []swarmtypes.Service{
{ID: "svc1", Spec: swarmtypes.ServiceSpec{Annotations: swarmtypes.Annotations{Name: "zeta"}}},
{ID: "svc2", Spec: swarmtypes.ServiceSpec{Annotations: swarmtypes.Annotations{Name: "alpha"}}},
}, nil
},
taskListFn: func(context.Context, swarmtypes.TaskListOptions) ([]swarmtypes.Task, error) {
return []swarmtypes.Task{
{ID: "task2", ServiceID: "svc2", Slot: 2},
{ID: "task1", ServiceID: "svc2", Slot: 1},
}, nil
},
},
}
info := systemtypes.Info{
Swarm: swarmtypes.Info{
NodeID: "node1",
ControlAvailable: true,
LocalNodeState: swarmtypes.LocalNodeStateActive,
},
}
services, tasks, swarmInfo := agent.collectSwarmData(context.Background(), info, nil)
if len(tasks) != 2 || len(services) != 2 {
t.Fatalf("expected manager tasks/services, got %d/%d", len(tasks), len(services))
}
if swarmInfo == nil || swarmInfo.Scope != swarmScopeCluster {
t.Fatalf("unexpected swarm info: %+v", swarmInfo)
}
})
t.Run("include flags prune outputs", func(t *testing.T) {
agent := &Agent{
supportsSwarm: true,
cfg: Config{
IncludeServices: false,
IncludeTasks: true,
SwarmScope: swarmScopeAuto,
},
}
containers := []agentsdocker.Container{
{
ID: "container1",
Name: "web.1",
Image: "nginx:latest",
State: "running",
Labels: map[string]string{
"com.docker.swarm.service.id": "svc1",
"com.docker.swarm.service.name": "web",
"com.docker.swarm.task.id": "task1",
},
},
}
info := systemtypes.Info{
Swarm: swarmtypes.Info{
NodeID: "node1",
ControlAvailable: false,
LocalNodeState: swarmtypes.LocalNodeStateActive,
},
}
services, tasks, swarmInfo := agent.collectSwarmData(context.Background(), info, containers)
if services != nil {
t.Fatal("expected services to be nil when disabled")
}
if len(tasks) != 1 {
t.Fatalf("expected tasks to be returned")
}
if swarmInfo == nil {
t.Fatalf("expected swarm info")
}
})
t.Run("cluster info populated", func(t *testing.T) {
agent := &Agent{
supportsSwarm: true,
cfg: Config{
IncludeServices: false,
IncludeTasks: false,
SwarmScope: swarmScopeAuto,
},
}
info := systemtypes.Info{
Swarm: swarmtypes.Info{
NodeID: "node1",
ControlAvailable: true,
LocalNodeState: swarmtypes.LocalNodeStateActive,
Cluster: &swarmtypes.ClusterInfo{
ID: "cluster1",
Spec: swarmtypes.Spec{
Annotations: swarmtypes.Annotations{Name: "prod"},
},
},
},
}
services, tasks, swarmInfo := agent.collectSwarmData(context.Background(), info, nil)
if services != nil || tasks != nil {
t.Fatal("expected nil services/tasks when disabled")
}
if swarmInfo == nil || swarmInfo.ClusterID != "cluster1" || swarmInfo.ClusterName != "prod" {
t.Fatalf("expected cluster info to be populated")
}
})
t.Run("sorts tasks and services", func(t *testing.T) {
agent := &Agent{
supportsSwarm: true,
cfg: Config{
IncludeServices: true,
IncludeTasks: true,
SwarmScope: swarmScopeCluster,
},
docker: &fakeDockerClient{
serviceListFn: func(context.Context, swarmtypes.ServiceListOptions) ([]swarmtypes.Service, error) {
return []swarmtypes.Service{
{ID: "b", Spec: swarmtypes.ServiceSpec{Annotations: swarmtypes.Annotations{Name: "web"}}},
{ID: "a", Spec: swarmtypes.ServiceSpec{Annotations: swarmtypes.Annotations{Name: "web"}}},
{ID: "c", Spec: swarmtypes.ServiceSpec{Annotations: swarmtypes.Annotations{Name: "api"}}},
}, nil
},
taskListFn: func(context.Context, swarmtypes.TaskListOptions) ([]swarmtypes.Task, error) {
return []swarmtypes.Task{
{ID: "b", ServiceID: "a", Slot: 1},
{ID: "a", ServiceID: "a", Slot: 1},
{ID: "c", ServiceID: "a", Slot: 2},
{ID: "d", ServiceID: "c", Slot: 1},
}, nil
},
},
}
info := systemtypes.Info{
Swarm: swarmtypes.Info{
NodeID: "node1",
ControlAvailable: true,
LocalNodeState: swarmtypes.LocalNodeStateActive,
},
}
services, tasks, swarmInfo := agent.collectSwarmData(context.Background(), info, nil)
if swarmInfo == nil {
t.Fatalf("expected swarm info")
}
if len(tasks) != 4 || len(services) != 3 {
t.Fatalf("expected tasks and services")
}
if tasks[0].ServiceName == "" || services[0].Name == "" {
t.Fatalf("expected sorted outputs to be populated")
}
})
}
func TestDeriveSwarmTasksFromContainers(t *testing.T) {
started := time.Date(2024, 1, 1, 1, 1, 1, 0, time.UTC)
finished := started.Add(time.Minute)
containers := []agentsdocker.Container{
{ID: "no-labels"},
{
ID: "no-service",
Labels: map[string]string{"com.docker.swarm.task.id": "task1"},
},
{
ID: "container1",
Name: "web.1",
State: "running",
CreatedAt: started,
Labels: map[string]string{
"com.docker.swarm.service.id": "svc1",
"com.docker.swarm.service.name": "web",
"com.docker.swarm.task.slot": "notint",
"com.docker.swarm.task.message": "ok",
"com.docker.swarm.task.error": "err",
"com.docker.swarm.task.desired-state": "running",
"com.docker.swarm.task.id": "",
"com.docker.swarm.node.name": "nodeA",
"com.docker.swarm.node.id": "",
},
StartedAt: &started,
FinishedAt: &finished,
},
{
ID: "container2",
Name: "web.2",
State: "running",
Labels: map[string]string{
"com.docker.swarm.service.id": "svc1",
"com.docker.swarm.service.name": "web",
"com.docker.swarm.task.id": "task2",
"com.docker.swarm.task.slot": "2",
},
},
}
info := systemtypes.Info{
Swarm: swarmtypes.Info{
NodeID: "node1",
},
}
tasks := deriveSwarmTasksFromContainers(containers, info)
if len(tasks) != 2 {
t.Fatalf("expected 2 tasks, got %d", len(tasks))
}
if tasks[0].NodeID == "" {
t.Fatalf("expected node id fallback to be set")
}
if tasks[0].Slot != 0 {
t.Fatalf("expected slot to remain 0 for invalid slot")
}
if tasks[0].StartedAt == nil || tasks[0].CompletedAt == nil {
t.Fatalf("expected timestamps to be set from container")
}
}
func TestDeriveSwarmTasksFromContainersEmpty(t *testing.T) {
if tasks := deriveSwarmTasksFromContainers(nil, systemtypes.Info{}); tasks != nil {
t.Fatalf("expected nil tasks for empty input")
}
}
func TestDeriveSwarmServicesFromData(t *testing.T) {
tasks := []agentsdocker.Task{
{
ID: "task1",
ServiceID: "svc1",
ServiceName: "web",
CurrentState: "running",
},
{
ID: "task2",
ServiceID: "svc1",
ServiceName: "web",
CurrentState: "complete",
},
{
ID: "task3",
ServiceID: "",
ServiceName: "",
CurrentState: "running",
},
}
containers := []agentsdocker.Container{
{
ID: "container1",
Image: "nginx:latest",
Labels: map[string]string{
"com.docker.swarm.service.id": "svc1",
"com.docker.stack.namespace": "stack",
"com.docker.swarm.service.name": "web",
},
},
}
services := deriveSwarmServicesFromData(tasks, containers)
if len(services) != 1 {
t.Fatalf("expected 1 service, got %d", len(services))
}
if services[0].Image != "nginx:latest" {
t.Fatalf("expected image to be populated")
}
if services[0].Stack != "stack" {
t.Fatalf("expected stack to be populated")
}
if got := deriveSwarmServicesFromData(nil, containers); got != nil {
t.Fatal("expected nil services for empty tasks")
}
}
func TestDeriveSwarmServicesFromDataNameFallback(t *testing.T) {
tasks := []agentsdocker.Task{
{
ID: "task1",
ServiceID: "",
ServiceName: "api",
CurrentState: "running",
},
}
containers := []agentsdocker.Container{
{
ID: "container1",
Image: "api:latest",
Labels: map[string]string{
"com.docker.swarm.service.name": "api",
},
},
}
services := deriveSwarmServicesFromData(tasks, containers)
if len(services) != 1 {
t.Fatalf("expected 1 service, got %d", len(services))
}
if services[0].ID != "api" {
t.Fatalf("expected service id to fall back to name")
}
if services[0].Labels != nil {
t.Fatalf("expected nil labels when none set")
}
}
func TestDeriveSwarmServicesFromDataEmptyAggregates(t *testing.T) {
tasks := []agentsdocker.Task{
{ID: "task1", ServiceID: "", ServiceName: ""},
}
containers := []agentsdocker.Container{
{ID: "container1"},
{
ID: "container2",
Labels: map[string]string{"unrelated": "true"},
},
{
ID: "container3",
Labels: map[string]string{"com.docker.swarm.service.id": "other"},
},
}
if services := deriveSwarmServicesFromData(tasks, containers); services != nil {
t.Fatalf("expected nil services for empty aggregates")
}
}
func TestDeriveSwarmServicesFromDataContainerSkips(t *testing.T) {
tasks := []agentsdocker.Task{
{ID: "task1", ServiceID: "svc1", ServiceName: "web", CurrentState: "running"},
}
containers := []agentsdocker.Container{
{ID: "container1"},
{ID: "container2", Labels: map[string]string{"foo": "bar"}},
{ID: "container3", Labels: map[string]string{"com.docker.swarm.service.id": "other"}},
}
services := deriveSwarmServicesFromData(tasks, containers)
if len(services) != 1 {
t.Fatalf("expected service aggregate to remain")
}
}