Filter historical Docker swarm tasks

This commit is contained in:
rcourtman 2026-04-01 12:24:28 +01:00
parent 2a99e03831
commit 63cc0e038b
2 changed files with 74 additions and 6 deletions

View file

@ -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,

View file

@ -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
},
},