This commit is contained in:
2026-02-13 21:15:00 +01:00
parent bcc7877c41
commit fd29e602e8
4 changed files with 134 additions and 124 deletions
+79 -19
View File
@@ -18,12 +18,14 @@ import (
type ContainerState string
const (
StateRunning ContainerState = "running"
StateStopped ContainerState = "stopped"
StateRestarting ContainerState = "restarting"
StateExited ContainerState = "exited"
StatePaused ContainerState = "paused"
StateUnknown ContainerState = "unknown"
StateRunning ContainerState = "running"
StateStarting ContainerState = "starting" // running but health: starting
StateUnhealthy ContainerState = "unhealthy" // running but health: unhealthy
StateStopped ContainerState = "stopped"
StateRestarting ContainerState = "restarting"
StateExited ContainerState = "exited"
StatePaused ContainerState = "paused"
StateUnknown ContainerState = "unknown"
StateNotDeployed ContainerState = "not_deployed"
)
@@ -32,7 +34,7 @@ type ContainerInfo struct {
Name string `json:"name"`
Image string `json:"image"`
State ContainerState `json:"state"`
Status string `json:"status"` // e.g. "Up 3 hours"
Status string `json:"status"` // e.g. "Up 3 hours (healthy)"
}
// Stack represents a docker compose stack on disk.
@@ -193,9 +195,9 @@ func (m *Manager) refreshStatusLocked() error {
}
ci := ContainerInfo{
Name: parts[0],
Image: parts[1],
State: parseContainerState(parts[2]),
Name: parts[0],
Image: parts[1],
State: resolveContainerState(parts[2], parts[3]),
Status: parts[3],
}
projectContainers[parts[4]] = append(projectContainers[parts[4]], ci)
@@ -220,10 +222,27 @@ func (m *Manager) refreshStatusLocked() error {
return nil
}
func parseContainerState(s string) ContainerState {
switch strings.ToLower(strings.TrimSpace(s)) {
// resolveContainerState determines the effective state by combining Docker's
// State field (running/exited/etc.) with the Status field that contains health info.
//
// Docker State: "running", "exited", "restarting", "paused", "created", "dead", "removing"
// Docker Status: "Up 3 hours (healthy)", "Up 9 seconds (health: starting)", "Up 2 min (unhealthy)"
func resolveContainerState(dockerState, dockerStatus string) ContainerState {
state := strings.ToLower(strings.TrimSpace(dockerState))
status := strings.ToLower(dockerStatus)
switch state {
case "running":
// Check health sub-status for containers with healthchecks
if strings.Contains(status, "(health: starting)") {
return StateStarting
}
if strings.Contains(status, "(unhealthy)") {
return StateUnhealthy
}
// "(healthy)" or no healthcheck = running
return StateRunning
case "exited":
return StateExited
case "restarting":
@@ -237,20 +256,61 @@ func parseContainerState(s string) ContainerState {
}
}
// aggregateState determines the overall stack state from its containers.
// Priority: unhealthy/starting > restarting > all-running > stopped
func aggregateState(containers []ContainerInfo) ContainerState {
if len(containers) == 0 {
return StateNotDeployed
}
running := 0
starting := 0
unhealthy := 0
restarting := 0
stopped := 0
for _, c := range containers {
if c.State == StateRunning {
return StateRunning
switch c.State {
case StateRunning:
running++
case StateStarting:
starting++
case StateUnhealthy:
unhealthy++
case StateRestarting:
restarting++
case StateStopped, StateExited:
stopped++
}
}
for _, c := range containers {
if c.State == StateRestarting {
return StateRestarting
}
total := len(containers)
// Any unhealthy → whole stack is unhealthy
if unhealthy > 0 {
return StateUnhealthy
}
// Any still starting → stack is starting
if starting > 0 {
return StateStarting
}
// Any restarting → stack is restarting
if restarting > 0 {
return StateRestarting
}
// All running (and healthy) → stack is running
if running == total {
return StateRunning
}
// All stopped → stack is stopped
if stopped == total {
return StateStopped
}
// Mix (some running, some stopped) — report as running (partial)
if running > 0 {
return StateRunning
}
return StateStopped
}
@@ -449,4 +509,4 @@ func (m *Manager) execCommand(name string, args ...string) (string, error) {
}
return stdout.String(), nil
}
}