feat(nodes): agent-reported container liveness at heartbeat

The agent now lists every containr-managed container's docker state at
each heartbeat; the server reconciles container_instances for rows that
node owns, so exits, OOMs, and crash loops surface in runtime state
between reconciles. Tombstoned rows are never resurrected.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-10-09 18:29:48 +02:00
1 parent c3a35081d0
commit 4028639239
6 files changed
+84 -6

No files matched your search

+21
View File
@@ -204,10 +204,31 @@ func sendHeartbeat(ctx context.Context, client *http.Client, cfg agentConfig, ag
"system_load": readSystemLoad(),
"uptime": readSystemUptime(),
"version": firstEnv("CONTAINR_AGENT_VERSION", version),
"containers": managedContainerStates(),
}
return postJSON(ctx, client, cfg, "/api/agents/heartbeat", payload, nil)
}
// managedContainerStates lists every containr-managed container's docker
// state so the server can reflect exits and crash loops between reconciles.
// One docker call per heartbeat; nil on error (states simply go stale).
func managedContainerStates() []map[string]string {
out, err := runDocker("ps", "-a",
"--filter", "label=containr.managed=true",
"--format", "{{.Names}}\t{{.State}}")
if err != nil {
return nil
}
var states []map[string]string
for _, line := range strings.Split(out, "\n") {
parts := strings.SplitN(line, "\t", 2)
if len(parts) == 2 && parts[0] != "" {
states = append(states, map[string]string{"name": parts[0], "state": parts[1]})
}
}
return states
}
func processCommands(ctx context.Context, client *http.Client, cfg agentConfig, agentID string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/api/agents/%s/commands", cfg.APIURL, agentID), nil)
if err != nil {