Filter historical Docker swarm tasks

This commit is contained in:
rcourtman
2026-04-01 13:57:56 +01:00
parent bf7ca9fa0b
commit 8a44d27675
6 changed files with 163 additions and 32 deletions
@@ -39,6 +39,7 @@ truth for live infrastructure data.
15. `docker-entrypoint.sh`
16. `internal/monitoring/truenas_poller.go`
17. `internal/monitoring/vmware_poller.go`
18. `internal/dockeragent/swarm.go`
## Shared Boundaries
@@ -51,6 +52,7 @@ truth for live infrastructure data.
3. Add typed read access through `internal/unifiedresources/views.go`
4. Add unified supplemental ingest through `internal/monitoring/poll_providers.go`
5. Add or change container startup ownership/bootstrap behavior for hosted or managed Pulse runtime mounts through `docker-entrypoint.sh`
6. Add or change Docker Swarm manager task/service runtime collection through `internal/dockeragent/swarm.go`
## Forbidden Paths
@@ -62,7 +64,7 @@ truth for live infrastructure data.
1. Update this contract when monitoring truth ownership changes
2. Tighten guardrails when `GetState()`-centric paths are removed
3. Keep discovery-provider, metrics-history, and container bootstrap proof routes explicit in `registry.json`
3. Keep discovery-provider, metrics-history, Docker Swarm collection, and container bootstrap proof routes explicit in `registry.json`
4. Update related read-state or monitor tests when new collector paths land
5. Keep platform ingestion semantics aligned with
`docs/release-control/v6/internal/PLATFORM_SUPPORT_MODEL.md`: hybrid is a
@@ -238,6 +240,10 @@ boundary. Hosted or managed tenant bootstrap changes must preserve safe startup
when immutable read-only mounts are layered into `/etc/pulse`; the entrypoint
may not reintroduce ownership mutation against those read-only files during
container boot.
That same monitoring boundary now also owns Docker Swarm runtime truth at the
collection seam. `internal/dockeragent/swarm.go` is the canonical manager-side
filter for live Swarm services and tasks, so monitoring consumers do not ingest
historical shutdown tasks as if they were still part of the active runtime.
Storage export is now derived from canonical `ReadState.StoragePools()`
instead of `GetState().Storage`; `models.Storage` is treated as a boundary
@@ -2929,7 +2929,8 @@
"internal/truenas/"
],
"owned_files": [
"docker-entrypoint.sh"
"docker-entrypoint.sh",
"internal/dockeragent/swarm.go"
],
"verification": {
"allow_same_subsystem_tests": true,
@@ -3036,6 +3037,20 @@
"internal/monitoring/node_memory_sources_test.go"
]
},
{
"id": "docker-swarm-runtime",
"label": "docker swarm runtime proof",
"match_prefixes": [],
"match_files": [
"internal/dockeragent/swarm.go"
],
"allow_same_subsystem_tests": false,
"test_prefixes": [],
"exact_files": [
"internal/dockeragent/swarm_coverage_test.go",
"internal/dockeragent/swarm_test.go"
]
},
{
"id": "container-entrypoint-runtime",
"label": "container entrypoint runtime proof",
+30
View File
@@ -175,6 +175,7 @@ func (a *Agent) collectSwarmDataFromManager(ctx context.Context, info systemtype
var tasks []agentsdocker.Task
if includeTasks {
taskFilters := filters.NewArgs()
taskFilters.Add("desired-state", string(swarmtypes.TaskStateRunning))
if scope != swarmScopeCluster && info.Swarm.NodeID != "" {
taskFilters.Add("node", info.Swarm.NodeID)
}
@@ -188,6 +189,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
@@ -217,6 +221,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,
+44 -6
View File
@@ -216,8 +216,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
},
},
@@ -236,6 +240,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))
}
@@ -375,8 +382,8 @@ func TestCollectSwarmData(t *testing.T) {
},
taskListFn: func(context.Context, swarmtypes.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
},
},
@@ -494,10 +501,10 @@ func TestCollectSwarmData(t *testing.T) {
},
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},
{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
},
},
@@ -524,6 +531,37 @@ func TestCollectSwarmData(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 TestDeriveSwarmTasksFromContainers(t *testing.T) {
started := time.Date(2024, 1, 1, 1, 1, 1, 0, time.UTC)
finished := started.Add(time.Minute)
@@ -34,6 +34,30 @@ PLATFORM_CONNECTIONS_WORKSPACE_EXACT_FILES = [
]
def _contract_reference(contract_path: str, needle: str, runtime_path: str) -> dict:
lines = (REPO_ROOT / contract_path).read_text(encoding="utf-8").splitlines()
current_heading = None
current_heading_line = None
for line_number, line in enumerate(lines, start=1):
if line.startswith("## "):
current_heading = line
current_heading_line = line_number
if needle in line:
if current_heading is None or current_heading_line is None:
raise AssertionError(
f"reference {needle!r} in {contract_path} has no enclosing heading"
)
return {
"heading": current_heading,
"path": runtime_path,
"line": line_number,
"heading_line": current_heading_line,
}
raise AssertionError(f"reference {needle!r} not found in {contract_path}")
def split_workspace_path(path: str) -> tuple[Path, str]:
if ":" not in path:
return REPO_ROOT, path
@@ -184,6 +208,7 @@ class CanonicalCompletionGuardTest(unittest.TestCase):
"metrics-hot-path",
"metrics-history-runtime",
"memory-source-runtime",
"docker-swarm-runtime",
"container-entrypoint-runtime",
"monitoring-runtime",
],
@@ -2823,18 +2848,16 @@ class CanonicalCompletionGuardTest(unittest.TestCase):
self.assertEqual(
required["docs/release-control/v6/internal/subsystems/monitoring.md"]["matched_reference_details"],
[
{
"heading": "## Canonical Files",
"path": "internal/unifiedresources/views.go",
"line": 36,
"heading_line": 23,
},
{
"heading": "## Extension Points",
"path": "internal/unifiedresources/views.go",
"line": 51,
"heading_line": 47,
},
_contract_reference(
"docs/release-control/v6/internal/subsystems/monitoring.md",
"12. `internal/unifiedresources/views.go`",
"internal/unifiedresources/views.go",
),
_contract_reference(
"docs/release-control/v6/internal/subsystems/monitoring.md",
"3. Add typed read access through `internal/unifiedresources/views.go`",
"internal/unifiedresources/views.go",
),
],
)
@@ -4461,6 +4461,27 @@ class SubsystemLookupTest(unittest.TestCase):
],
)
def test_lookup_paths_assigns_docker_swarm_runtime_to_monitoring(self) -> None:
result = lookup_paths(["internal/dockeragent/swarm.go"])
self.assertEqual(result["unowned_runtime_files"], [])
file_entry = result["files"][0]
self.assertEqual(file_entry["classification"], "runtime")
self.assertEqual(len(file_entry["matches"]), 1)
match = file_entry["matches"][0]
self.assertEqual(match["subsystem"], "monitoring")
self.assertEqual(match["contract"], "docs/release-control/v6/internal/subsystems/monitoring.md")
self.assertEqual(match["lane_context"]["lane_id"], "L13")
self.assertEqual(match["verification_requirement"]["id"], "docker-swarm-runtime")
self.assertEqual(
match["verification_requirement"]["exact_files"],
[
"internal/dockeragent/swarm_coverage_test.go",
"internal/dockeragent/swarm_test.go",
],
)
def test_lookup_paths_assigns_system_logs_runtime_owner_to_frontend_primitives(self) -> None:
result = lookup_paths(
["frontend-modern/src/components/Settings/useSystemLogsPanelState.ts"]
@@ -4631,18 +4652,16 @@ class SubsystemLookupTest(unittest.TestCase):
self.assertEqual(
monitoring_contract["matched_reference_details"],
[
{
"heading": "## Canonical Files",
"path": "internal/unifiedresources/views.go",
"line": 36,
"heading_line": 23,
},
{
"heading": "## Extension Points",
"path": "internal/unifiedresources/views.go",
"line": 51,
"heading_line": 47,
},
_contract_reference(
"docs/release-control/v6/internal/subsystems/monitoring.md",
"12. `internal/unifiedresources/views.go`",
"internal/unifiedresources/views.go",
),
_contract_reference(
"docs/release-control/v6/internal/subsystems/monitoring.md",
"3. Add typed read access through `internal/unifiedresources/views.go`",
"internal/unifiedresources/views.go",
),
],
)
self.assertEqual(