mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Filter historical Docker swarm tasks
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user