diff --git a/internal/orchestrator/helpers.go b/internal/orchestrator/helpers.go index 3b94b654..bb95e968 100644 --- a/internal/orchestrator/helpers.go +++ b/internal/orchestrator/helpers.go @@ -19,7 +19,10 @@ import ( "context" "errors" "fmt" + "log/slog" + "regexp" "slices" + "strconv" "strings" "github.com/arduino/go-paths-helper" @@ -39,6 +42,11 @@ type AppStatusInfo struct { Status Status } +type containerState struct { + Status Status + StatusMessage string +} + // parseAppStatus takes all the containers that matches the DockerAppLabel, // and construct a map of the state of an app and all its dependencies state. // For app that have at least 1 dependency, we calculate the overall state @@ -51,13 +59,16 @@ type AppStatusInfo struct { // starting: at least one starting func parseAppStatus(containers []container.Summary) []AppStatusInfo { apps := make([]AppStatusInfo, 0, len(containers)) - appsStatusMap := make(map[string][]Status) + appsStatusMap := make(map[string][]containerState) for _, c := range containers { appPath, ok := c.Labels[DockerAppPathLabel] if !ok { continue } - appsStatusMap[appPath] = append(appsStatusMap[appPath], StatusFromDockerState(c.State)) + appsStatusMap[appPath] = append(appsStatusMap[appPath], containerState{ + Status: StatusFromDockerState(c.State), + StatusMessage: c.Status, + }) } appendResult := func(appPath *paths.Path, status Status) { @@ -73,27 +84,31 @@ func parseAppStatus(containers []container.Summary) []AppStatusInfo { appPath := paths.New(appPath) // running: all running - if !slices.ContainsFunc(s, func(v Status) bool { return v != StatusRunning }) { + if !slices.ContainsFunc(s, func(v containerState) bool { return v.Status != StatusRunning }) { appendResult(appPath, StatusRunning) continue } // stopped: all stopped - if !slices.ContainsFunc(s, func(v Status) bool { return v != StatusStopped }) { + if !slices.ContainsFunc(s, func(v containerState) bool { return v.Status != StatusStopped }) { appendResult(appPath, StatusStopped) continue } // ...else we have multiple different status we calculate the status // among the possible left: {failed, stopping, starting} - if slices.ContainsFunc(s, func(v Status) bool { return v == StatusFailed }) { + if slices.ContainsFunc(s, func(v containerState) bool { return v.Status == StatusFailed }) { appendResult(appPath, StatusFailed) continue } - if slices.ContainsFunc(s, func(v Status) bool { return v == StatusStopping }) { + if slices.ContainsFunc(s, func(v containerState) bool { return v.Status == StatusStopped && checkExitCode(v) }) { + appendResult(appPath, StatusFailed) + continue + } + if slices.ContainsFunc(s, func(v containerState) bool { return v.Status == StatusStopping }) { appendResult(appPath, StatusStopping) continue } - if slices.ContainsFunc(s, func(v Status) bool { return v == StatusStarting }) { + if slices.ContainsFunc(s, func(v containerState) bool { return v.Status == StatusStarting }) { appendResult(appPath, StatusStarting) continue } @@ -250,3 +265,20 @@ func setStatusLeds(trigger LedTrigger) error { } return nil } + +func checkExitCode(state containerState) bool { + var exitCodeRegex = regexp.MustCompile(`Exited \((\d+)\)`) + result := false + matches := exitCodeRegex.FindStringSubmatch(state.StatusMessage) + + exitCode, err := strconv.Atoi(matches[1]) + if err != nil { + slog.Error("Failed to parse exit code from status message", slog.String("statusMessage", state.StatusMessage), slog.String("error", err.Error())) + return false + } + if exitCode >= 0 && exitCode < 128 { + result = true + } + + return result +} diff --git a/internal/orchestrator/helpers_test.go b/internal/orchestrator/helpers_test.go index d89dc732..e21ec22d 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 (0)"}, + 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)