diff --git a/internal/orchestrator/helpers.go b/internal/orchestrator/helpers.go index 3b94b654..460b19b3 100644 --- a/internal/orchestrator/helpers.go +++ b/internal/orchestrator/helpers.go @@ -57,7 +57,8 @@ func parseAppStatus(containers []container.Summary) []AppStatusInfo { if !ok { continue } - appsStatusMap[appPath] = append(appsStatusMap[appPath], StatusFromDockerState(c.State)) + appsStatusMap[appPath] = append(appsStatusMap[appPath], StatusFromDockerState(c.State, c.Status)) + } appendResult := func(appPath *paths.Path, status Status) { diff --git a/internal/orchestrator/helpers_test.go b/internal/orchestrator/helpers_test.go index d89dc732..e4f89355 100644 --- a/internal/orchestrator/helpers_test.go +++ b/internal/orchestrator/helpers_test.go @@ -20,60 +20,75 @@ import ( "github.com/docker/docker/api/types/container" "github.com/stretchr/testify/require" - "go.bug.st/f" ) func TestParseAppStatus(t *testing.T) { tests := []struct { name string containerState []container.ContainerState + statusMessage []string want Status }{ { name: "everything running", containerState: []container.ContainerState{container.StateRunning, container.StateRunning}, + statusMessage: []string{"Up 5 minutes", "Up 10 minutes"}, want: StatusRunning, }, { name: "everything stopped", containerState: []container.ContainerState{container.StateCreated, container.StatePaused, container.StateExited}, + statusMessage: []string{"Created", "Paused", "Exited (137)"}, want: StatusStopped, }, { name: "failed container", containerState: []container.ContainerState{container.StateRunning, container.StateDead}, + statusMessage: []string{"Up 5 minutes", "Dead"}, want: StatusFailed, }, { name: "failed container takes precedence over stopping and starting", containerState: []container.ContainerState{container.StateRunning, container.StateDead, container.StateRemoving, container.StateRestarting}, + statusMessage: []string{"Up 5 minutes", "Dead", "Removing", "Restarting"}, want: StatusFailed, }, { name: "stopping", containerState: []container.ContainerState{container.StateRunning, container.StateRemoving}, + statusMessage: []string{"Up 5 minutes", "Removing"}, want: StatusStopping, }, { name: "stopping takes precedence over starting", containerState: []container.ContainerState{container.StateRunning, container.StateRestarting, container.StateRemoving}, + statusMessage: []string{"Up 5 minutes", "Restarting", "Removing"}, want: StatusStopping, }, { name: "starting", containerState: []container.ContainerState{container.StateRestarting, container.StateExited}, + statusMessage: []string{"Restarting", "Exited (129)"}, want: StatusStarting, }, + { + name: "failed", + containerState: []container.ContainerState{container.StateRestarting, container.StateExited}, + statusMessage: []string{"Restarting", "Exited (1)"}, + want: StatusFailed, + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - input := f.Map(tc.containerState, func(c container.ContainerState) container.Summary { - return container.Summary{ + var input []container.Summary + for i, c := range tc.containerState { + input = append(input, container.Summary{ Labels: map[string]string{DockerAppPathLabel: "path1"}, State: c, - } - }) + Status: tc.statusMessage[i], + }) + } res := parseAppStatus(input) require.Len(t, res, 1) require.Equal(t, tc.want, res[0].Status) diff --git a/internal/orchestrator/status.go b/internal/orchestrator/status.go index 71bc3228..9a549720 100644 --- a/internal/orchestrator/status.go +++ b/internal/orchestrator/status.go @@ -17,8 +17,11 @@ package orchestrator import ( "fmt" + "regexp" + "strconv" "github.com/docker/docker/api/types/container" + "go.bug.st/f" ) type Status string @@ -31,7 +34,7 @@ const ( StatusFailed Status = "failed" ) -func StatusFromDockerState(s container.ContainerState) Status { +func StatusFromDockerState(s container.ContainerState, statusMessage string) Status { switch s { case container.StateRunning: return StatusRunning @@ -39,10 +42,16 @@ func StatusFromDockerState(s container.ContainerState) Status { return StatusStarting case container.StateRemoving: return StatusStopping - case container.StateCreated, container.StateExited, container.StatePaused: + case container.StateCreated, container.StatePaused: + return StatusStopped + case container.StateExited: + if !isExitBySignal(statusMessage) { + return StatusFailed + } return StatusStopped case container.StateDead: return StatusFailed + default: panic("unreachable") } @@ -65,3 +74,17 @@ func (s Status) Validate() error { func (s Status) AllowedStatuses() []Status { return []Status{StatusStarting, StatusRunning, StatusStopping, StatusStopped, StatusFailed} } + +func isExitBySignal(statusMessage string) bool { + var exitCodeRegex = regexp.MustCompile(`Exited \((\d+)\)`) + matches := exitCodeRegex.FindStringSubmatch(statusMessage) + if len(matches) < 2 { + // not matching an exit code + return false + } + exitCode := f.Must(strconv.Atoi(matches[1])) + + // posix exit code greater than 128+n means terminated by signal https://tldp.org/LDP/abs/html/exitcodes.html + return exitCode > 128 + +}