Files
pulse/internal/dockeragent/swarm_coverage_test.go

1049 lines
33 KiB
Go

package dockeragent
import (
"context"
"errors"
"testing"
"time"
networktypes "github.com/moby/moby/api/types/network"
swarmtypes "github.com/moby/moby/api/types/swarm"
systemtypes "github.com/moby/moby/api/types/system"
agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker"
)
func TestDockerClientContractCarriesTypedContainerRestart(t *testing.T) {
called := false
client := &fakeDockerClient{containerRestartFn: func(_ context.Context, id string, opts dockerContainerRestartOptions) error {
called = true
if id != "container-1" || opts.Signal != "SIGTERM" || opts.Timeout == nil || *opts.Timeout != 30 {
t.Fatalf("unexpected restart request: id=%q opts=%+v", id, opts)
}
return nil
}}
timeout := 30
var contract dockerClient = client
if err := contract.ContainerRestart(context.Background(), "container-1", dockerContainerRestartOptions{Signal: "SIGTERM", Timeout: &timeout}); err != nil {
t.Fatalf("ContainerRestart: %v", err)
}
if !called {
t.Fatal("typed restart request was not forwarded")
}
}
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: networktypes.TCP, 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, _ dockerServiceListOptions) ([]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 dockerTaskListOptions) ([]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)
}
if got := opts.Filters.Get("desired-state"); len(got) != 1 || got[0] != string(swarmtypes.TaskStateRunning) {
t.Fatalf("expected desired-state filter to include running, got %v", got)
}
return []swarmtypes.Task{
{ID: "task1", ServiceID: "svc1", DesiredState: swarmtypes.TaskStateRunning, Status: swarmtypes.TaskStatus{State: swarmtypes.TaskStateRunning}},
{ID: "task-old", ServiceID: "svc2", DesiredState: swarmtypes.TaskStateShutdown, Status: swarmtypes.TaskStatus{State: swarmtypes.TaskStateComplete}},
}, 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 tasks[0].ID != "task1" {
t.Fatalf("expected running task only, got %#v", 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, _ dockerServiceListOptions) ([]swarmtypes.Service, error) {
return []swarmtypes.Service{
{ID: "svc1", Spec: swarmtypes.ServiceSpec{Annotations: swarmtypes.Annotations{Name: "alpha"}}},
}, nil
},
taskListFn: func(_ context.Context, _ dockerTaskListOptions) ([]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")
}
})
t.Run("node scope uses local task counts for services", func(t *testing.T) {
agent := &Agent{
docker: &fakeDockerClient{
serviceListFn: func(_ context.Context, _ dockerServiceListOptions) ([]swarmtypes.Service, error) {
return []swarmtypes.Service{
{
ID: "svc1",
Spec: swarmtypes.ServiceSpec{Annotations: swarmtypes.Annotations{Name: "alpha"}},
ServiceStatus: &swarmtypes.ServiceStatus{
DesiredTasks: 4,
RunningTasks: 3,
CompletedTasks: 1,
},
},
{
ID: "svc2",
Spec: swarmtypes.ServiceSpec{Annotations: swarmtypes.Annotations{Name: "beta"}},
ServiceStatus: &swarmtypes.ServiceStatus{
DesiredTasks: 2,
RunningTasks: 2,
},
},
}, nil
},
taskListFn: func(_ context.Context, _ dockerTaskListOptions) ([]swarmtypes.Task, error) {
return []swarmtypes.Task{
{
ID: "task1",
ServiceID: "svc1",
DesiredState: swarmtypes.TaskStateRunning,
Status: swarmtypes.TaskStatus{State: swarmtypes.TaskStateRunning},
},
{
ID: "task2",
ServiceID: "svc1",
DesiredState: swarmtypes.TaskStateRunning,
Status: swarmtypes.TaskStatus{State: swarmtypes.TaskStatePreparing},
},
}, 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) != 2 {
t.Fatalf("expected 2 local tasks, got %d", len(tasks))
}
if len(services) != 1 {
t.Fatalf("expected only local services, got %d", len(services))
}
if services[0].ID != "svc1" {
t.Fatalf("expected local service svc1, got %+v", services[0])
}
if services[0].DesiredTasks != 2 || services[0].RunningTasks != 1 || services[0].CompletedTasks != 0 {
t.Fatalf("expected node-local task counts, got %+v", services[0])
}
})
t.Run("node scope drops services when no local runtime tasks remain", func(t *testing.T) {
agent := &Agent{
docker: &fakeDockerClient{
serviceListFn: func(_ context.Context, _ dockerServiceListOptions) ([]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, _ dockerTaskListOptions) ([]swarmtypes.Task, error) {
return []swarmtypes.Task{}, 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) != 0 {
t.Fatalf("expected no local tasks, got %d", len(tasks))
}
if len(services) != 0 {
t.Fatalf("expected no node-scoped services without local tasks, got %d", len(services))
}
})
}
func TestMapSwarmNode(t *testing.T) {
createdAt := time.Date(2024, 3, 4, 5, 6, 7, 0, time.UTC)
updatedAt := createdAt.Add(5 * time.Minute)
node := &swarmtypes.Node{
ID: "node-1",
Spec: swarmtypes.NodeSpec{
Annotations: swarmtypes.Annotations{
Labels: map[string]string{"zone": "rack-a"},
},
Role: swarmtypes.NodeRoleManager,
Availability: swarmtypes.NodeAvailabilityActive,
},
Description: swarmtypes.NodeDescription{
Hostname: "manager-1",
Platform: swarmtypes.Platform{
OS: "linux",
Architecture: "amd64",
},
Resources: swarmtypes.Resources{
NanoCPUs: 4_000_000_000,
MemoryBytes: 16 * 1024 * 1024 * 1024,
},
Engine: swarmtypes.EngineDescription{
EngineVersion: "27.5.1",
Labels: map[string]string{"engine": "primary"},
},
},
Status: swarmtypes.NodeStatus{
State: swarmtypes.NodeStateReady,
Message: "ready",
Addr: "192.0.2.10",
},
ManagerStatus: &swarmtypes.ManagerStatus{
Leader: true,
Reachability: swarmtypes.ReachabilityReachable,
Addr: "192.0.2.10:2377",
},
Meta: swarmtypes.Meta{
CreatedAt: createdAt,
UpdatedAt: updatedAt,
},
}
got := mapSwarmNode(node)
if got.ID != "node-1" || got.Hostname != "manager-1" || got.Role != string(swarmtypes.NodeRoleManager) {
t.Fatalf("unexpected node identity: %+v", got)
}
if got.Availability != string(swarmtypes.NodeAvailabilityActive) || got.State != string(swarmtypes.NodeStateReady) {
t.Fatalf("unexpected node state: %+v", got)
}
if got.ManagerReachability != string(swarmtypes.ReachabilityReachable) || !got.Leader {
t.Fatalf("expected manager reachability and leader flag, got %+v", got)
}
if got.EngineVersion != "27.5.1" || got.NanoCPUs != 4_000_000_000 || got.MemoryBytes == 0 {
t.Fatalf("expected engine resources, got %+v", got)
}
if got.Labels["zone"] != "rack-a" || got.EngineLabels["engine"] != "primary" {
t.Fatalf("expected node labels to be mapped, got labels=%+v engine=%+v", got.Labels, got.EngineLabels)
}
if got.CreatedAt.IsZero() || got.UpdatedAt == nil || got.UpdatedAt.IsZero() {
t.Fatalf("expected node timestamps, got %+v", got)
}
}
func TestMapSwarmSecretAndConfigOmitPayloadData(t *testing.T) {
createdAt := time.Date(2024, 3, 4, 5, 6, 7, 0, time.UTC)
updatedAt := createdAt.Add(5 * time.Minute)
secret := mapSwarmSecret(&swarmtypes.Secret{
ID: "secret-1",
Meta: swarmtypes.Meta{
CreatedAt: createdAt,
UpdatedAt: updatedAt,
},
Spec: swarmtypes.SecretSpec{
Annotations: swarmtypes.Annotations{
Name: "db-password",
Labels: map[string]string{"stack": "backend"},
},
Data: []byte("must-not-be-reported"),
Driver: &swarmtypes.Driver{Name: "vault", Options: map[string]string{"path": "secret/db"}},
Templating: &swarmtypes.Driver{Name: "golang"},
},
})
if secret.ID != "secret-1" || secret.Name != "db-password" || secret.DriverName != "vault" || secret.TemplatingDriver != "golang" {
t.Fatalf("unexpected mapped secret metadata: %+v", secret)
}
if secret.Labels["stack"] != "backend" || secret.CreatedAt.IsZero() || secret.UpdatedAt == nil {
t.Fatalf("expected secret labels and timestamps, got %+v", secret)
}
config := mapSwarmConfig(&swarmtypes.Config{
ID: "config-1",
Meta: swarmtypes.Meta{
CreatedAt: createdAt,
UpdatedAt: updatedAt,
},
Spec: swarmtypes.ConfigSpec{
Annotations: swarmtypes.Annotations{
Name: "nginx-conf",
Labels: map[string]string{"stack": "frontend"},
},
Data: []byte("must-not-be-reported"),
Templating: &swarmtypes.Driver{Name: "golang"},
},
})
if config.ID != "config-1" || config.Name != "nginx-conf" || config.TemplatingDriver != "golang" {
t.Fatalf("unexpected mapped config metadata: %+v", config)
}
if config.Labels["stack"] != "frontend" || config.CreatedAt.IsZero() || config.UpdatedAt == nil {
t.Fatalf("expected config labels and timestamps, got %+v", config)
}
}
func TestCollectSwarmData(t *testing.T) {
t.Run("unsupported swarm returns nils", func(t *testing.T) {
agent := &Agent{supportsSwarm: false}
services, tasks, nodes, secrets, configs, info := agent.collectSwarmData(context.Background(), systemtypes.Info{}, nil)
if services != nil || tasks != nil || nodes != nil || secrets != nil || configs != 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, nodes, secrets, configs, info := agent.collectSwarmData(context.Background(), systemtypes.Info{}, nil)
if services != nil || tasks != nil || nodes != nil || secrets != nil || configs != nil || info != nil {
t.Fatal("expected nil outputs for empty swarm info")
}
})
t.Run("standalone inactive swarm returns nil", func(t *testing.T) {
agent := &Agent{supportsSwarm: true, cfg: Config{SwarmScope: swarmScopeNode}}
info := systemtypes.Info{
Swarm: swarmtypes.Info{
LocalNodeState: swarmtypes.LocalNodeStateInactive,
},
}
services, tasks, nodes, secrets, configs, swarmInfo := agent.collectSwarmData(context.Background(), info, nil)
if services != nil || tasks != nil || nodes != nil || secrets != nil || configs != nil || swarmInfo != nil {
t.Fatal("expected standalone inactive swarm state to be ignored")
}
})
t.Run("pending 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, nodes, secrets, configs, swarmInfo := agent.collectSwarmData(context.Background(), info, nil)
if services != nil || tasks != nil || nodes != nil || secrets != nil || configs != nil {
t.Fatal("expected nil services/tasks for pending 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, dockerServiceListOptions) ([]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, nodes, secrets, configs, 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 len(secrets) != 0 || len(configs) != 0 {
t.Fatalf("expected no secrets/configs after manager collection error, got %+v/%+v", secrets, configs)
}
if len(nodes) != 1 || nodes[0].ID != "node1" {
t.Fatalf("expected derived local node, got %+v", nodes)
}
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{
nodeListFn: func(context.Context, dockerNodeListOptions) ([]swarmtypes.Node, error) {
return []swarmtypes.Node{{
ID: "node-manager",
Spec: swarmtypes.NodeSpec{
Role: swarmtypes.NodeRoleManager,
Availability: swarmtypes.NodeAvailabilityActive,
},
Description: swarmtypes.NodeDescription{
Hostname: "manager-1",
Engine: swarmtypes.EngineDescription{
EngineVersion: "27.5.1",
},
},
Status: swarmtypes.NodeStatus{State: swarmtypes.NodeStateReady},
ManagerStatus: &swarmtypes.ManagerStatus{
Leader: true,
Reachability: swarmtypes.ReachabilityReachable,
},
}}, nil
},
serviceListFn: func(context.Context, dockerServiceListOptions) ([]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, dockerTaskListOptions) ([]swarmtypes.Task, error) {
return []swarmtypes.Task{
{ID: "task2", ServiceID: "svc2", Slot: 2, DesiredState: swarmtypes.TaskStateRunning, Status: swarmtypes.TaskStatus{State: swarmtypes.TaskStateRunning}},
{ID: "task1", ServiceID: "svc2", Slot: 1, DesiredState: swarmtypes.TaskStateRunning, Status: swarmtypes.TaskStatus{State: swarmtypes.TaskStateRunning}},
}, nil
},
secretListFn: func(context.Context, dockerSecretListOptions) ([]swarmtypes.Secret, error) {
return []swarmtypes.Secret{
{ID: "secret-z", Spec: swarmtypes.SecretSpec{Annotations: swarmtypes.Annotations{Name: "zeta-secret"}}},
{ID: "secret-a", Spec: swarmtypes.SecretSpec{Annotations: swarmtypes.Annotations{Name: "alpha-secret"}}},
}, nil
},
configListFn: func(context.Context, dockerConfigListOptions) ([]swarmtypes.Config, error) {
return []swarmtypes.Config{
{ID: "config-z", Spec: swarmtypes.ConfigSpec{Annotations: swarmtypes.Annotations{Name: "zeta-config"}}},
{ID: "config-a", Spec: swarmtypes.ConfigSpec{Annotations: swarmtypes.Annotations{Name: "alpha-config"}}},
}, nil
},
},
}
info := systemtypes.Info{
Swarm: swarmtypes.Info{
NodeID: "node1",
ControlAvailable: true,
LocalNodeState: swarmtypes.LocalNodeStateActive,
},
}
services, tasks, nodes, secrets, configs, 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 len(secrets) != 2 || secrets[0].Name != "alpha-secret" {
t.Fatalf("expected sorted manager secret inventory, got %+v", secrets)
}
if len(configs) != 2 || configs[0].Name != "alpha-config" {
t.Fatalf("expected sorted manager config inventory, got %+v", configs)
}
if len(nodes) != 1 || nodes[0].ID != "node-manager" || nodes[0].Hostname != "manager-1" {
t.Fatalf("expected manager node inventory, got %+v", nodes)
}
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, nodes, _, _, 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 len(nodes) != 1 {
t.Fatalf("expected local swarm node 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, nodes, _, _, swarmInfo := agent.collectSwarmData(context.Background(), info, nil)
if services != nil || tasks != nil {
t.Fatal("expected nil services/tasks when disabled")
}
if len(nodes) != 1 {
t.Fatalf("expected local swarm node to be returned")
}
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, dockerServiceListOptions) ([]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, dockerTaskListOptions) ([]swarmtypes.Task, error) {
return []swarmtypes.Task{
{ID: "b", ServiceID: "a", Slot: 1, DesiredState: swarmtypes.TaskStateRunning, Status: swarmtypes.TaskStatus{State: swarmtypes.TaskStateRunning}},
{ID: "a", ServiceID: "a", Slot: 1, DesiredState: swarmtypes.TaskStateRunning, Status: swarmtypes.TaskStatus{State: swarmtypes.TaskStateRunning}},
{ID: "c", ServiceID: "a", Slot: 2, DesiredState: swarmtypes.TaskStateRunning, Status: swarmtypes.TaskStatus{State: swarmtypes.TaskStateRunning}},
{ID: "d", ServiceID: "c", Slot: 1, DesiredState: swarmtypes.TaskStateRunning, Status: swarmtypes.TaskStatus{State: swarmtypes.TaskStateRunning}},
}, nil
},
},
}
info := systemtypes.Info{
Swarm: swarmtypes.Info{
NodeID: "node1",
ControlAvailable: true,
LocalNodeState: swarmtypes.LocalNodeStateActive,
},
}
services, tasks, nodes, _, _, swarmInfo := agent.collectSwarmData(context.Background(), info, nil)
if swarmInfo == nil {
t.Fatalf("expected swarm info")
}
if len(nodes) != 1 {
t.Fatalf("expected local swarm node to be returned")
}
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 TestIsRuntimeSwarmTask(t *testing.T) {
t.Run("accepts desired running task", func(t *testing.T) {
task := &swarmtypes.Task{
DesiredState: swarmtypes.TaskStateRunning,
Status: swarmtypes.TaskStatus{State: swarmtypes.TaskStateRunning},
}
if !isRuntimeSwarmTask(task) {
t.Fatal("expected running task to be retained")
}
})
t.Run("accepts empty desired state active task as fallback", func(t *testing.T) {
task := &swarmtypes.Task{
Status: swarmtypes.TaskStatus{State: swarmtypes.TaskStatePreparing},
}
if !isRuntimeSwarmTask(task) {
t.Fatal("expected active task with empty desired state to be retained")
}
})
t.Run("rejects shutdown historical task", func(t *testing.T) {
task := &swarmtypes.Task{
DesiredState: swarmtypes.TaskStateShutdown,
Status: swarmtypes.TaskStatus{State: swarmtypes.TaskStateComplete},
}
if isRuntimeSwarmTask(task) {
t.Fatal("expected historical task to be excluded")
}
})
}
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")
}
}