From 63cc0e038be5c26840000a7e3964d5f9e70ca9ba Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 1 Apr 2026 12:24:28 +0100 Subject: [PATCH] Filter historical Docker swarm tasks --- internal/dockeragent/swarm.go | 30 +++++++++++++ internal/dockeragent/swarm_coverage_test.go | 50 ++++++++++++++++++--- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/internal/dockeragent/swarm.go b/internal/dockeragent/swarm.go index 2393f09c9..390bf2d77 100644 --- a/internal/dockeragent/swarm.go +++ b/internal/dockeragent/swarm.go @@ -172,6 +172,7 @@ func (a *Agent) collectSwarmDataFromManager(ctx context.Context, info systemtype var tasks []agentsdocker.Task if includeTasks { taskFilters := newDockerFilters() + taskFilters.Add("desired-state", string(swarmtypes.TaskStateRunning)) if scope != swarmScopeCluster && info.Swarm.NodeID != "" { taskFilters.Add("node", info.Swarm.NodeID) } @@ -183,6 +184,9 @@ func (a *Agent) collectSwarmDataFromManager(ctx context.Context, info systemtype tasks = make([]agentsdocker.Task, 0, len(taskList)) for i := range taskList { + if !isRuntimeSwarmTask(&taskList[i]) { + continue + } var svc *swarmtypes.Service if ptr, ok := servicePointers[taskList[i].ServiceID]; ok { svc = ptr @@ -212,6 +216,32 @@ func (a *Agent) collectSwarmDataFromManager(ctx context.Context, info systemtype return services, tasks, nil } +func isRuntimeSwarmTask(task *swarmtypes.Task) bool { + if task == nil { + return false + } + if task.DesiredState == swarmtypes.TaskStateRunning { + return true + } + + // Defensive fallback in case the daemon returns an empty desired state for an + // otherwise active task. Terminal tasks should never be retained in runtime state. + switch task.Status.State { + case swarmtypes.TaskStateNew, + swarmtypes.TaskStateAllocated, + swarmtypes.TaskStatePending, + swarmtypes.TaskStateAssigned, + swarmtypes.TaskStateAccepted, + swarmtypes.TaskStatePreparing, + swarmtypes.TaskStateReady, + swarmtypes.TaskStateStarting, + swarmtypes.TaskStateRunning: + return task.DesiredState == "" + default: + return false + } +} + func mapSwarmService(svc *swarmtypes.Service) agentsdocker.Service { service := agentsdocker.Service{ ID: svc.ID, diff --git a/internal/dockeragent/swarm_coverage_test.go b/internal/dockeragent/swarm_coverage_test.go index 2820c2770..5bc333f7e 100644 --- a/internal/dockeragent/swarm_coverage_test.go +++ b/internal/dockeragent/swarm_coverage_test.go @@ -217,8 +217,12 @@ func TestCollectSwarmDataFromManager(t *testing.T) { 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 }, }, @@ -237,6 +241,9 @@ func TestCollectSwarmDataFromManager(t *testing.T) { 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)) } @@ -271,6 +278,37 @@ func TestCollectSwarmDataFromManager(t *testing.T) { }) } +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 TestCollectSwarmData(t *testing.T) { t.Run("unsupported swarm returns nils", func(t *testing.T) { agent := &Agent{supportsSwarm: false} @@ -376,8 +414,8 @@ func TestCollectSwarmData(t *testing.T) { }, taskListFn: func(context.Context, taskListOptions) ([]swarmtypes.Task, error) { return []swarmtypes.Task{ - {ID: "task2", ServiceID: "svc2", Slot: 2}, - {ID: "task1", ServiceID: "svc2", Slot: 1}, + {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 }, }, @@ -495,10 +533,10 @@ func TestCollectSwarmData(t *testing.T) { }, taskListFn: func(context.Context, 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}, + {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 }, },