From 4105b65160fa924c06596c03b356e84719a47e74 Mon Sep 17 00:00:00 2001 From: Giulio Pilotto Date: Thu, 27 Nov 2025 14:17:06 +0100 Subject: [PATCH 1/6] add status stopped --- internal/orchestrator/helpers.go | 14 +++++++++----- internal/orchestrator/helpers_test.go | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/internal/orchestrator/helpers.go b/internal/orchestrator/helpers.go index 3b94b654..675ffdf4 100644 --- a/internal/orchestrator/helpers.go +++ b/internal/orchestrator/helpers.go @@ -44,11 +44,11 @@ type AppStatusInfo struct { // For app that have at least 1 dependency, we calculate the overall state // as follow: // -// running: all running -// stopped: all stopped -// failed: at least one failed -// stopping: at least one stopping -// starting: at least one starting +// running: all running +// stopped: all stopped +// failed: at least one failed +// stopped: at least one stopped +// starting: at least one starting func parseAppStatus(containers []container.Summary) []AppStatusInfo { apps := make([]AppStatusInfo, 0, len(containers)) appsStatusMap := make(map[string][]Status) @@ -93,6 +93,10 @@ func parseAppStatus(containers []container.Summary) []AppStatusInfo { appendResult(appPath, StatusStopping) continue } + if slices.ContainsFunc(s, func(v Status) bool { return v == StatusStopped }) { + appendResult(appPath, StatusFailed) + continue + } if slices.ContainsFunc(s, func(v Status) bool { return v == StatusStarting }) { appendResult(appPath, StatusStarting) continue diff --git a/internal/orchestrator/helpers_test.go b/internal/orchestrator/helpers_test.go index d89dc732..f3108632 100644 --- a/internal/orchestrator/helpers_test.go +++ b/internal/orchestrator/helpers_test.go @@ -46,7 +46,7 @@ func TestParseAppStatus(t *testing.T) { }, { name: "failed container takes precedence over stopping and starting", - containerState: []container.ContainerState{container.StateRunning, container.StateDead, container.StateRemoving, container.StateRestarting}, + containerState: []container.ContainerState{container.StateRunning, container.StateDead, container.StateRemoving, container.StateRestarting, container.StateExited}, want: StatusFailed, }, { @@ -61,7 +61,7 @@ func TestParseAppStatus(t *testing.T) { }, { name: "starting", - containerState: []container.ContainerState{container.StateRestarting, container.StateExited}, + containerState: []container.ContainerState{container.StateRestarting}, want: StatusStarting, }, } From 99354fdf0df9d6df75cceef23127dcc40bb242ba Mon Sep 17 00:00:00 2001 From: Giulio Date: Fri, 28 Nov 2025 10:00:45 +0100 Subject: [PATCH 2/6] Apply suggestion from @lucarin91 Co-authored-by: Luca Rinaldi --- internal/orchestrator/helpers.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/orchestrator/helpers.go b/internal/orchestrator/helpers.go index 675ffdf4..3c90e057 100644 --- a/internal/orchestrator/helpers.go +++ b/internal/orchestrator/helpers.go @@ -44,11 +44,12 @@ type AppStatusInfo struct { // For app that have at least 1 dependency, we calculate the overall state // as follow: // -// running: all running -// stopped: all stopped -// failed: at least one failed -// stopped: at least one stopped -// starting: at least one starting +// running: all running +// stopped: all stopped +// failed: at least one failed +// stopping: at least one stopping +// stopped: at least one stopped +// starting: at least one starting func parseAppStatus(containers []container.Summary) []AppStatusInfo { apps := make([]AppStatusInfo, 0, len(containers)) appsStatusMap := make(map[string][]Status) From a09d652b99ddd59c25071f77a6f5646baa3370b0 Mon Sep 17 00:00:00 2001 From: Giulio Pilotto Date: Fri, 5 Dec 2025 12:35:12 +0100 Subject: [PATCH 3/6] Add state failed --- cmd/arduino-app-cli/app/list.go | 2 +- cmd/arduino-app-cli/app/start.go | 4 +- cmd/arduino-app-cli/app/stop.go | 4 +- cmd/gendoc/docs.go | 10 ++--- internal/api/handlers/app_list.go | 6 +-- internal/api/handlers/app_status.go | 2 +- internal/orchestrator/app_status.go | 2 +- internal/orchestrator/helpers.go | 46 +++++++++++++++------- internal/orchestrator/helpers_test.go | 4 +- internal/orchestrator/orchestrator.go | 37 ++++++++++------- internal/orchestrator/orchestrator_test.go | 18 ++++----- internal/orchestrator/status.go | 24 +++++------ 12 files changed, 93 insertions(+), 66 deletions(-) diff --git a/cmd/arduino-app-cli/app/list.go b/cmd/arduino-app-cli/app/list.go index 18a21d97..09300f55 100644 --- a/cmd/arduino-app-cli/app/list.go +++ b/cmd/arduino-app-cli/app/list.go @@ -83,7 +83,7 @@ func (r appListResult) String() string { cmdutil.IDToAlias(app.ID), app.Name, app.Icon, - app.Status, + app.State, app.Example, }) } diff --git a/cmd/arduino-app-cli/app/start.go b/cmd/arduino-app-cli/app/start.go index 6899f338..459e6138 100644 --- a/cmd/arduino-app-cli/app/start.go +++ b/cmd/arduino-app-cli/app/start.go @@ -47,8 +47,8 @@ func newStartCmd(cfg config.Configuration) *cobra.Command { return startHandler(cmd.Context(), cfg, app) }, ValidArgsFunction: completion.ApplicationNamesWithFilterFunc(cfg, func(apps orchestrator.AppInfo) bool { - return apps.Status != orchestrator.StatusStarting && - apps.Status != orchestrator.StatusRunning + return apps.State != orchestrator.StatusStarting && + apps.State != orchestrator.StatusRunning }), } } diff --git a/cmd/arduino-app-cli/app/stop.go b/cmd/arduino-app-cli/app/stop.go index eff55dbd..02d08751 100644 --- a/cmd/arduino-app-cli/app/stop.go +++ b/cmd/arduino-app-cli/app/stop.go @@ -45,8 +45,8 @@ func newStopCmd(cfg config.Configuration) *cobra.Command { return stopHandler(cmd.Context(), app) }, ValidArgsFunction: completion.ApplicationNamesWithFilterFunc(cfg, func(apps orchestrator.AppInfo) bool { - return apps.Status == orchestrator.StatusStarting || - apps.Status == orchestrator.StatusRunning + return apps.State == orchestrator.StatusStarting || + apps.State == orchestrator.StatusRunning }), } } diff --git a/cmd/gendoc/docs.go b/cmd/gendoc/docs.go index 0b36ffe8..d5ea1b65 100644 --- a/cmd/gendoc/docs.go +++ b/cmd/gendoc/docs.go @@ -69,10 +69,10 @@ func NewOpenApiGenerator(version string) *Generator { openapi3.SchemaOrRef{ Schema: &openapi3.Schema{ UniqueItems: f.Ptr(true), - Enum: f.Map(orchestrator.Status("").AllowedStatuses(), func(v orchestrator.Status) interface{} { return v }), + Enum: f.Map(orchestrator.State("").AllowedStatuses(), func(v orchestrator.State) interface{} { return v }), Type: f.Ptr(openapi3.SchemaTypeString), Description: f.Ptr("Application status"), - ReflectType: reflect.TypeOf(orchestrator.Status("")), + ReflectType: reflect.TypeOf(orchestrator.State("")), }, }, ) @@ -205,7 +205,7 @@ func NewOpenApiGenerator(version string) *Generator { reflector.DefaultOptions = append(reflector.DefaultOptions, jsonschema.InterceptSchema(func(params jsonschema.InterceptSchemaParams) (stop bool, err error) { - if params.Value.Type() == reflect.TypeOf(orchestrator.Status("")) { + if params.Value.Type() == reflect.TypeOf(orchestrator.State("")) { params.Schema.WithRef("#/components/schemas/Status") return true, nil } @@ -632,8 +632,8 @@ Contains a JSON object with the details of an error. Path: "/v1/apps", Request: (*orchestrator.ListAppRequest)(nil), Parameters: (*struct { - Filter string `query:"filter" description:"Filters apps by apps,examples,default"` - Status orchestrator.Status `query:"status" description:"Filters applications by status"` + Filter string `query:"filter" description:"Filters apps by apps,examples,default"` + Status orchestrator.State `query:"status" description:"Filters applications by status"` })(nil), CustomSuccessResponse: &CustomResponseDef{ ContentType: "application/json", diff --git a/internal/api/handlers/app_list.go b/internal/api/handlers/app_list.go index a41efea3..2f23a758 100644 --- a/internal/api/handlers/app_list.go +++ b/internal/api/handlers/app_list.go @@ -51,21 +51,21 @@ func HandleAppList( showApps = slices.Contains(filters, "apps") } - var statusFilter orchestrator.Status + var stateFilter orchestrator.State if status := queryParams.Get("status"); status != "" { status, err := orchestrator.ParseStatus(status) if err != nil { render.EncodeResponse(w, http.StatusBadRequest, models.ErrorResponse{Details: "invalid status filter"}) return } - statusFilter = status + stateFilter = status } res, err := orchestrator.ListApps(r.Context(), dockerCli, orchestrator.ListAppRequest{ ShowApps: showApps, ShowExamples: showExamples, ShowOnlyDefault: showOnlyDefault, - StatusFilter: statusFilter, + StateFilter: stateFilter, }, idProvider, cfg) if err != nil { slog.Error("Unable to parse the app.yaml", slog.String("error", err.Error())) diff --git a/internal/api/handlers/app_status.go b/internal/api/handlers/app_status.go index 89a82e41..d38aab05 100644 --- a/internal/api/handlers/app_status.go +++ b/internal/api/handlers/app_status.go @@ -47,7 +47,7 @@ func HandlerAppStatus( sseStream.SendError(render.SSEErrorData{Code: render.InternalServiceErr, Message: err.Error()}) } for _, app := range result.Apps { - if app.Status != "" { + if app.State != "" { sseStream.Send(render.SSEEvent{Type: "app", Data: app}) } } diff --git a/internal/orchestrator/app_status.go b/internal/orchestrator/app_status.go index 7e9c32fc..c5fd6409 100644 --- a/internal/orchestrator/app_status.go +++ b/internal/orchestrator/app_status.go @@ -115,7 +115,7 @@ func parseDockerStatusEvent(ctx context.Context, cfg config.Configuration, docke Name: app.Descriptor.Name, Description: app.Descriptor.Description, Icon: app.Descriptor.Icon, - Status: appStatus.Status, + State: appStatus.State, Example: id.IsExample(), Default: isDefault, }, nil diff --git a/internal/orchestrator/helpers.go b/internal/orchestrator/helpers.go index 3c90e057..6787f2e9 100644 --- a/internal/orchestrator/helpers.go +++ b/internal/orchestrator/helpers.go @@ -19,6 +19,7 @@ import ( "context" "errors" "fmt" + "log/slog" "slices" "strings" @@ -36,7 +37,12 @@ import ( type AppStatusInfo struct { AppPath *paths.Path - Status Status + State State +} + +type containerStateInfo struct { + State State + StatusMessage string } // parseAppStatus takes all the containers that matches the DockerAppLabel, @@ -52,19 +58,29 @@ 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][]containerStateInfo) 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], containerStateInfo{ + State: StatusFromDockerState(c.State), + StatusMessage: c.Status, + }) + + slog.Debug("Container status", + slog.String("appPath", appPath), + slog.String("containerID", c.ID), + slog.String("state", string(c.State)), + slog.String("statusMessage", c.Status), + ) } - appendResult := func(appPath *paths.Path, status Status) { + appendResult := func(appPath *paths.Path, status State) { apps = append(apps, AppStatusInfo{ AppPath: appPath, - Status: status, + State: status, }) } @@ -74,31 +90,33 @@ 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 containerStateInfo) bool { return v.State != 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 containerStateInfo) bool { return v.State != 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 containerStateInfo) bool { return v.State == StatusFailed }) { appendResult(appPath, StatusFailed) continue } - if slices.ContainsFunc(s, func(v Status) bool { return v == StatusStopping }) { - appendResult(appPath, StatusStopping) + if slices.ContainsFunc(s, func(v containerStateInfo) bool { + return v.State == StatusStopped && strings.Contains(v.StatusMessage, "Exited (0)") + }) { + appendResult(appPath, StatusFailed) continue } - if slices.ContainsFunc(s, func(v Status) bool { return v == StatusStopped }) { - appendResult(appPath, StatusFailed) + if slices.ContainsFunc(s, func(v containerStateInfo) bool { return v.State == StatusStopping }) { + appendResult(appPath, StatusStopping) continue } - if slices.ContainsFunc(s, func(v Status) bool { return v == StatusStarting }) { + if slices.ContainsFunc(s, func(v containerStateInfo) bool { return v.State == StatusStarting }) { appendResult(appPath, StatusStarting) continue } @@ -193,7 +211,7 @@ func getRunningApp( return nil, fmt.Errorf("failed to get running apps: %w", err) } idx := slices.IndexFunc(apps, func(a AppStatusInfo) bool { - return a.Status == StatusRunning || a.Status == StatusStarting + return a.State == StatusRunning || a.State == StatusStarting }) if idx == -1 { return nil, nil diff --git a/internal/orchestrator/helpers_test.go b/internal/orchestrator/helpers_test.go index f3108632..1d45dd94 100644 --- a/internal/orchestrator/helpers_test.go +++ b/internal/orchestrator/helpers_test.go @@ -27,7 +27,7 @@ func TestParseAppStatus(t *testing.T) { tests := []struct { name string containerState []container.ContainerState - want Status + want State }{ { name: "everything running", @@ -76,7 +76,7 @@ func TestParseAppStatus(t *testing.T) { }) res := parseAppStatus(input) require.Len(t, res, 1) - require.Equal(t, tc.want, res[0].Status) + require.Equal(t, tc.want, res[0].State) require.Equal(t, "path1", res[0].AppPath.String()) }) } diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 51a808cd..49cfd072 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -389,6 +389,16 @@ func stopAppWithCmd(ctx context.Context, docker command.Cli, app app.ArduinoApp, ctx, cancel := context.WithCancel(ctx) defer cancel() + appStatus, err := getAppStatus(ctx, docker, app) + if err != nil { + yield(StreamMessage{error: err}) + return + } + if appStatus.State != StatusStarting && appStatus.State != StatusRunning { + yield(StreamMessage{data: fmt.Sprintf("app %q is not running", app.Name)}) + return + } + if !yield(StreamMessage{data: fmt.Sprintf("Stopping app %q", app.Name)}) { return } @@ -410,7 +420,7 @@ func stopAppWithCmd(ctx context.Context, docker command.Cli, app app.ArduinoApp, yield(StreamMessage{error: err}) return } - if appStatus.Status != StatusStarting && appStatus.Status != StatusRunning { + if appStatus.State != StatusStarting && appStatus.State != StatusRunning { yield(StreamMessage{data: fmt.Sprintf("app %q is not running", app.Name)}) return } @@ -513,7 +523,7 @@ func StartDefaultApp( if err != nil { return fmt.Errorf("failed to get app details: %w", err) } - if status.Status == "running" { + if status.State == "running" { return nil } @@ -537,7 +547,7 @@ type AppInfo struct { Name string `json:"name"` Description string `json:"description"` Icon string `json:"icon"` - Status Status `json:"status,omitempty"` + State State `json:"state,omitempty"` Example bool `json:"example"` Default bool `json:"default"` } @@ -551,8 +561,7 @@ type ListAppRequest struct { ShowExamples bool ShowOnlyDefault bool ShowApps bool - StatusFilter Status - + StateFilter State // IncludeNonStandardLocationApps will include apps that are not in the standard apps directory. // We will search by looking for docker container metadata, and add the app not present in the // standard apps directory in the result list. @@ -628,14 +637,14 @@ func ListApps( continue } - var status Status + var state State if idx := slices.IndexFunc(apps, func(a AppStatusInfo) bool { return a.AppPath.EqualsTo(app.FullPath) }); idx != -1 { - status = apps[idx].Status + state = apps[idx].State } - if req.StatusFilter != "" && req.StatusFilter != status { + if req.StateFilter != "" && req.StateFilter != state { continue } @@ -650,7 +659,7 @@ func ListApps( Name: app.Name, Description: app.Descriptor.Description, Icon: app.Descriptor.Icon, - Status: status, + State: state, Example: id.IsExample(), Default: isDefault, }, @@ -666,7 +675,7 @@ type AppDetailedInfo struct { Path string `json:"path"` Description string `json:"description"` Icon string `json:"icon"` - Status Status `json:"status" required:"true"` + State State `json:"state" required:"true"` Example bool `json:"example"` Default bool `json:"default"` Bricks []AppDetailedBrick `json:"bricks,omitempty"` @@ -690,15 +699,15 @@ func AppDetails( var wg sync.WaitGroup wg.Add(2) var defaultAppPath string - var status Status + var state State go func() { defer wg.Done() app, err := getAppStatus(ctx, docker, userApp) if err != nil { slog.Warn("unable to get app status", slog.String("error", err.Error()), slog.String("path", userApp.FullPath.String())) - status = StatusStopped + state = StatusStopped } else { - status = app.Status + state = app.State } }() go func() { @@ -727,7 +736,7 @@ func AppDetails( Path: userApp.FullPath.String(), Description: userApp.Descriptor.Description, Icon: userApp.Descriptor.Icon, - Status: status, + State: state, Example: id.IsExample(), Default: defaultAppPath == userApp.FullPath.String(), Bricks: f.Map(userApp.Descriptor.Bricks, func(b app.Brick) AppDetailedBrick { diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index ab42d287..92a47afe 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -266,7 +266,7 @@ func TestListApp(t *testing.T) { res, err := ListApps(t.Context(), dockerCli, ListAppRequest{ ShowApps: true, ShowExamples: true, - StatusFilter: "", + StateFilter: "", }, idProvider, cfg) require.NoError(t, err) assert.Empty(t, res.BrokenApps) @@ -276,7 +276,7 @@ func TestListApp(t *testing.T) { Name: "example1", Description: "", Icon: "😃", - Status: "", + State: "", Example: true, Default: false, }, @@ -285,7 +285,7 @@ func TestListApp(t *testing.T) { Name: "app1", Description: "", Icon: "😃", - Status: "", + State: "", Example: false, Default: false, }, @@ -294,7 +294,7 @@ func TestListApp(t *testing.T) { Name: "app2", Description: "", Icon: "😃", - Status: "", + State: "", Example: false, Default: false, }, @@ -305,7 +305,7 @@ func TestListApp(t *testing.T) { res, err := ListApps(t.Context(), dockerCli, ListAppRequest{ ShowApps: true, ShowExamples: false, - StatusFilter: "", + StateFilter: "", }, idProvider, cfg) require.NoError(t, err) assert.Empty(t, res.BrokenApps) @@ -315,7 +315,7 @@ func TestListApp(t *testing.T) { Name: "app1", Description: "", Icon: "😃", - Status: "", + State: "", Example: false, Default: false, }, @@ -324,7 +324,7 @@ func TestListApp(t *testing.T) { Name: "app2", Description: "", Icon: "😃", - Status: "", + State: "", Example: false, Default: false, }, @@ -335,7 +335,7 @@ func TestListApp(t *testing.T) { res, err := ListApps(t.Context(), dockerCli, ListAppRequest{ ShowApps: false, ShowExamples: true, - StatusFilter: "", + StateFilter: "", }, idProvider, cfg) require.NoError(t, err) assert.Empty(t, res.BrokenApps) @@ -345,7 +345,7 @@ func TestListApp(t *testing.T) { Name: "example1", Description: "", Icon: "😃", - Status: "", + State: "", Example: true, Default: false, }, diff --git a/internal/orchestrator/status.go b/internal/orchestrator/status.go index 71bc3228..eae4fdca 100644 --- a/internal/orchestrator/status.go +++ b/internal/orchestrator/status.go @@ -21,17 +21,17 @@ import ( "github.com/docker/docker/api/types/container" ) -type Status string +type State string const ( - StatusStarting Status = "starting" - StatusRunning Status = "running" - StatusStopping Status = "stopping" - StatusStopped Status = "stopped" - StatusFailed Status = "failed" + StatusStarting State = "starting" + StatusRunning State = "running" + StatusStopping State = "stopping" + StatusStopped State = "stopped" + StatusFailed State = "failed" ) -func StatusFromDockerState(s container.ContainerState) Status { +func StatusFromDockerState(s container.ContainerState) State { switch s { case container.StateRunning: return StatusRunning @@ -48,12 +48,12 @@ func StatusFromDockerState(s container.ContainerState) Status { } } -func ParseStatus(s string) (Status, error) { - s1 := Status(s) +func ParseStatus(s string) (State, error) { + s1 := State(s) return s1, s1.Validate() } -func (s Status) Validate() error { +func (s State) Validate() error { switch s { case StatusStarting, StatusRunning, StatusStopping, StatusStopped, StatusFailed: return nil @@ -62,6 +62,6 @@ func (s Status) Validate() error { } } -func (s Status) AllowedStatuses() []Status { - return []Status{StatusStarting, StatusRunning, StatusStopping, StatusStopped, StatusFailed} +func (s State) AllowedStatuses() []State { + return []State{StatusStarting, StatusRunning, StatusStopping, StatusStopped, StatusFailed} } From c9deb5fb41dbe66f18c8c237398b5d97879b706a Mon Sep 17 00:00:00 2001 From: Giulio Pilotto Date: Fri, 5 Dec 2025 16:02:46 +0100 Subject: [PATCH 4/6] States --- cmd/gendoc/docs.go | 2 +- internal/api/handlers/app_list.go | 2 +- internal/orchestrator/status.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/gendoc/docs.go b/cmd/gendoc/docs.go index d5ea1b65..d65ebb44 100644 --- a/cmd/gendoc/docs.go +++ b/cmd/gendoc/docs.go @@ -69,7 +69,7 @@ func NewOpenApiGenerator(version string) *Generator { openapi3.SchemaOrRef{ Schema: &openapi3.Schema{ UniqueItems: f.Ptr(true), - Enum: f.Map(orchestrator.State("").AllowedStatuses(), func(v orchestrator.State) interface{} { return v }), + Enum: f.Map(orchestrator.State("").AllowedStates(), func(v orchestrator.State) interface{} { return v }), Type: f.Ptr(openapi3.SchemaTypeString), Description: f.Ptr("Application status"), ReflectType: reflect.TypeOf(orchestrator.State("")), diff --git a/internal/api/handlers/app_list.go b/internal/api/handlers/app_list.go index 2f23a758..886e2925 100644 --- a/internal/api/handlers/app_list.go +++ b/internal/api/handlers/app_list.go @@ -53,7 +53,7 @@ func HandleAppList( var stateFilter orchestrator.State if status := queryParams.Get("status"); status != "" { - status, err := orchestrator.ParseStatus(status) + status, err := orchestrator.ParseStates(status) if err != nil { render.EncodeResponse(w, http.StatusBadRequest, models.ErrorResponse{Details: "invalid status filter"}) return diff --git a/internal/orchestrator/status.go b/internal/orchestrator/status.go index eae4fdca..6e0cef2b 100644 --- a/internal/orchestrator/status.go +++ b/internal/orchestrator/status.go @@ -48,7 +48,7 @@ func StatusFromDockerState(s container.ContainerState) State { } } -func ParseStatus(s string) (State, error) { +func ParseStates(s string) (State, error) { s1 := State(s) return s1, s1.Validate() } @@ -58,10 +58,10 @@ func (s State) Validate() error { case StatusStarting, StatusRunning, StatusStopping, StatusStopped, StatusFailed: return nil default: - return fmt.Errorf("status should be one of %v", s.AllowedStatuses()) + return fmt.Errorf("status should be one of %v", s.AllowedStates()) } } -func (s State) AllowedStatuses() []State { +func (s State) AllowedStates() []State { return []State{StatusStarting, StatusRunning, StatusStopping, StatusStopped, StatusFailed} } From 25d90c6ab22a84a371be2d177a394cc842e918a1 Mon Sep 17 00:00:00 2001 From: Giulio Pilotto Date: Fri, 5 Dec 2025 16:56:39 +0100 Subject: [PATCH 5/6] revert --- cmd/arduino-app-cli/app/list.go | 2 +- cmd/arduino-app-cli/app/start.go | 4 +-- cmd/arduino-app-cli/app/stop.go | 4 +-- cmd/gendoc/docs.go | 10 +++--- internal/api/handlers/app_list.go | 8 ++--- internal/api/handlers/app_status.go | 2 +- internal/orchestrator/app_status.go | 2 +- internal/orchestrator/helpers_test.go | 8 ++--- internal/orchestrator/orchestrator.go | 37 ++++++++-------------- internal/orchestrator/orchestrator_test.go | 18 +++++------ internal/orchestrator/status.go | 26 +++++++-------- 11 files changed, 56 insertions(+), 65 deletions(-) diff --git a/cmd/arduino-app-cli/app/list.go b/cmd/arduino-app-cli/app/list.go index 09300f55..18a21d97 100644 --- a/cmd/arduino-app-cli/app/list.go +++ b/cmd/arduino-app-cli/app/list.go @@ -83,7 +83,7 @@ func (r appListResult) String() string { cmdutil.IDToAlias(app.ID), app.Name, app.Icon, - app.State, + app.Status, app.Example, }) } diff --git a/cmd/arduino-app-cli/app/start.go b/cmd/arduino-app-cli/app/start.go index 459e6138..6899f338 100644 --- a/cmd/arduino-app-cli/app/start.go +++ b/cmd/arduino-app-cli/app/start.go @@ -47,8 +47,8 @@ func newStartCmd(cfg config.Configuration) *cobra.Command { return startHandler(cmd.Context(), cfg, app) }, ValidArgsFunction: completion.ApplicationNamesWithFilterFunc(cfg, func(apps orchestrator.AppInfo) bool { - return apps.State != orchestrator.StatusStarting && - apps.State != orchestrator.StatusRunning + return apps.Status != orchestrator.StatusStarting && + apps.Status != orchestrator.StatusRunning }), } } diff --git a/cmd/arduino-app-cli/app/stop.go b/cmd/arduino-app-cli/app/stop.go index 02d08751..eff55dbd 100644 --- a/cmd/arduino-app-cli/app/stop.go +++ b/cmd/arduino-app-cli/app/stop.go @@ -45,8 +45,8 @@ func newStopCmd(cfg config.Configuration) *cobra.Command { return stopHandler(cmd.Context(), app) }, ValidArgsFunction: completion.ApplicationNamesWithFilterFunc(cfg, func(apps orchestrator.AppInfo) bool { - return apps.State == orchestrator.StatusStarting || - apps.State == orchestrator.StatusRunning + return apps.Status == orchestrator.StatusStarting || + apps.Status == orchestrator.StatusRunning }), } } diff --git a/cmd/gendoc/docs.go b/cmd/gendoc/docs.go index d65ebb44..0b36ffe8 100644 --- a/cmd/gendoc/docs.go +++ b/cmd/gendoc/docs.go @@ -69,10 +69,10 @@ func NewOpenApiGenerator(version string) *Generator { openapi3.SchemaOrRef{ Schema: &openapi3.Schema{ UniqueItems: f.Ptr(true), - Enum: f.Map(orchestrator.State("").AllowedStates(), func(v orchestrator.State) interface{} { return v }), + Enum: f.Map(orchestrator.Status("").AllowedStatuses(), func(v orchestrator.Status) interface{} { return v }), Type: f.Ptr(openapi3.SchemaTypeString), Description: f.Ptr("Application status"), - ReflectType: reflect.TypeOf(orchestrator.State("")), + ReflectType: reflect.TypeOf(orchestrator.Status("")), }, }, ) @@ -205,7 +205,7 @@ func NewOpenApiGenerator(version string) *Generator { reflector.DefaultOptions = append(reflector.DefaultOptions, jsonschema.InterceptSchema(func(params jsonschema.InterceptSchemaParams) (stop bool, err error) { - if params.Value.Type() == reflect.TypeOf(orchestrator.State("")) { + if params.Value.Type() == reflect.TypeOf(orchestrator.Status("")) { params.Schema.WithRef("#/components/schemas/Status") return true, nil } @@ -632,8 +632,8 @@ Contains a JSON object with the details of an error. Path: "/v1/apps", Request: (*orchestrator.ListAppRequest)(nil), Parameters: (*struct { - Filter string `query:"filter" description:"Filters apps by apps,examples,default"` - Status orchestrator.State `query:"status" description:"Filters applications by status"` + Filter string `query:"filter" description:"Filters apps by apps,examples,default"` + Status orchestrator.Status `query:"status" description:"Filters applications by status"` })(nil), CustomSuccessResponse: &CustomResponseDef{ ContentType: "application/json", diff --git a/internal/api/handlers/app_list.go b/internal/api/handlers/app_list.go index 886e2925..a41efea3 100644 --- a/internal/api/handlers/app_list.go +++ b/internal/api/handlers/app_list.go @@ -51,21 +51,21 @@ func HandleAppList( showApps = slices.Contains(filters, "apps") } - var stateFilter orchestrator.State + var statusFilter orchestrator.Status if status := queryParams.Get("status"); status != "" { - status, err := orchestrator.ParseStates(status) + status, err := orchestrator.ParseStatus(status) if err != nil { render.EncodeResponse(w, http.StatusBadRequest, models.ErrorResponse{Details: "invalid status filter"}) return } - stateFilter = status + statusFilter = status } res, err := orchestrator.ListApps(r.Context(), dockerCli, orchestrator.ListAppRequest{ ShowApps: showApps, ShowExamples: showExamples, ShowOnlyDefault: showOnlyDefault, - StateFilter: stateFilter, + StatusFilter: statusFilter, }, idProvider, cfg) if err != nil { slog.Error("Unable to parse the app.yaml", slog.String("error", err.Error())) diff --git a/internal/api/handlers/app_status.go b/internal/api/handlers/app_status.go index d38aab05..89a82e41 100644 --- a/internal/api/handlers/app_status.go +++ b/internal/api/handlers/app_status.go @@ -47,7 +47,7 @@ func HandlerAppStatus( sseStream.SendError(render.SSEErrorData{Code: render.InternalServiceErr, Message: err.Error()}) } for _, app := range result.Apps { - if app.State != "" { + if app.Status != "" { sseStream.Send(render.SSEEvent{Type: "app", Data: app}) } } diff --git a/internal/orchestrator/app_status.go b/internal/orchestrator/app_status.go index c5fd6409..7e9c32fc 100644 --- a/internal/orchestrator/app_status.go +++ b/internal/orchestrator/app_status.go @@ -115,7 +115,7 @@ func parseDockerStatusEvent(ctx context.Context, cfg config.Configuration, docke Name: app.Descriptor.Name, Description: app.Descriptor.Description, Icon: app.Descriptor.Icon, - State: appStatus.State, + Status: appStatus.Status, Example: id.IsExample(), Default: isDefault, }, nil diff --git a/internal/orchestrator/helpers_test.go b/internal/orchestrator/helpers_test.go index 1d45dd94..d89dc732 100644 --- a/internal/orchestrator/helpers_test.go +++ b/internal/orchestrator/helpers_test.go @@ -27,7 +27,7 @@ func TestParseAppStatus(t *testing.T) { tests := []struct { name string containerState []container.ContainerState - want State + want Status }{ { name: "everything running", @@ -46,7 +46,7 @@ func TestParseAppStatus(t *testing.T) { }, { name: "failed container takes precedence over stopping and starting", - containerState: []container.ContainerState{container.StateRunning, container.StateDead, container.StateRemoving, container.StateRestarting, container.StateExited}, + containerState: []container.ContainerState{container.StateRunning, container.StateDead, container.StateRemoving, container.StateRestarting}, want: StatusFailed, }, { @@ -61,7 +61,7 @@ func TestParseAppStatus(t *testing.T) { }, { name: "starting", - containerState: []container.ContainerState{container.StateRestarting}, + containerState: []container.ContainerState{container.StateRestarting, container.StateExited}, want: StatusStarting, }, } @@ -76,7 +76,7 @@ func TestParseAppStatus(t *testing.T) { }) res := parseAppStatus(input) require.Len(t, res, 1) - require.Equal(t, tc.want, res[0].State) + require.Equal(t, tc.want, res[0].Status) require.Equal(t, "path1", res[0].AppPath.String()) }) } diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 49cfd072..51a808cd 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -389,16 +389,6 @@ func stopAppWithCmd(ctx context.Context, docker command.Cli, app app.ArduinoApp, ctx, cancel := context.WithCancel(ctx) defer cancel() - appStatus, err := getAppStatus(ctx, docker, app) - if err != nil { - yield(StreamMessage{error: err}) - return - } - if appStatus.State != StatusStarting && appStatus.State != StatusRunning { - yield(StreamMessage{data: fmt.Sprintf("app %q is not running", app.Name)}) - return - } - if !yield(StreamMessage{data: fmt.Sprintf("Stopping app %q", app.Name)}) { return } @@ -420,7 +410,7 @@ func stopAppWithCmd(ctx context.Context, docker command.Cli, app app.ArduinoApp, yield(StreamMessage{error: err}) return } - if appStatus.State != StatusStarting && appStatus.State != StatusRunning { + if appStatus.Status != StatusStarting && appStatus.Status != StatusRunning { yield(StreamMessage{data: fmt.Sprintf("app %q is not running", app.Name)}) return } @@ -523,7 +513,7 @@ func StartDefaultApp( if err != nil { return fmt.Errorf("failed to get app details: %w", err) } - if status.State == "running" { + if status.Status == "running" { return nil } @@ -547,7 +537,7 @@ type AppInfo struct { Name string `json:"name"` Description string `json:"description"` Icon string `json:"icon"` - State State `json:"state,omitempty"` + Status Status `json:"status,omitempty"` Example bool `json:"example"` Default bool `json:"default"` } @@ -561,7 +551,8 @@ type ListAppRequest struct { ShowExamples bool ShowOnlyDefault bool ShowApps bool - StateFilter State + StatusFilter Status + // IncludeNonStandardLocationApps will include apps that are not in the standard apps directory. // We will search by looking for docker container metadata, and add the app not present in the // standard apps directory in the result list. @@ -637,14 +628,14 @@ func ListApps( continue } - var state State + var status Status if idx := slices.IndexFunc(apps, func(a AppStatusInfo) bool { return a.AppPath.EqualsTo(app.FullPath) }); idx != -1 { - state = apps[idx].State + status = apps[idx].Status } - if req.StateFilter != "" && req.StateFilter != state { + if req.StatusFilter != "" && req.StatusFilter != status { continue } @@ -659,7 +650,7 @@ func ListApps( Name: app.Name, Description: app.Descriptor.Description, Icon: app.Descriptor.Icon, - State: state, + Status: status, Example: id.IsExample(), Default: isDefault, }, @@ -675,7 +666,7 @@ type AppDetailedInfo struct { Path string `json:"path"` Description string `json:"description"` Icon string `json:"icon"` - State State `json:"state" required:"true"` + Status Status `json:"status" required:"true"` Example bool `json:"example"` Default bool `json:"default"` Bricks []AppDetailedBrick `json:"bricks,omitempty"` @@ -699,15 +690,15 @@ func AppDetails( var wg sync.WaitGroup wg.Add(2) var defaultAppPath string - var state State + var status Status go func() { defer wg.Done() app, err := getAppStatus(ctx, docker, userApp) if err != nil { slog.Warn("unable to get app status", slog.String("error", err.Error()), slog.String("path", userApp.FullPath.String())) - state = StatusStopped + status = StatusStopped } else { - state = app.State + status = app.Status } }() go func() { @@ -736,7 +727,7 @@ func AppDetails( Path: userApp.FullPath.String(), Description: userApp.Descriptor.Description, Icon: userApp.Descriptor.Icon, - State: state, + Status: status, Example: id.IsExample(), Default: defaultAppPath == userApp.FullPath.String(), Bricks: f.Map(userApp.Descriptor.Bricks, func(b app.Brick) AppDetailedBrick { diff --git a/internal/orchestrator/orchestrator_test.go b/internal/orchestrator/orchestrator_test.go index 92a47afe..ab42d287 100644 --- a/internal/orchestrator/orchestrator_test.go +++ b/internal/orchestrator/orchestrator_test.go @@ -266,7 +266,7 @@ func TestListApp(t *testing.T) { res, err := ListApps(t.Context(), dockerCli, ListAppRequest{ ShowApps: true, ShowExamples: true, - StateFilter: "", + StatusFilter: "", }, idProvider, cfg) require.NoError(t, err) assert.Empty(t, res.BrokenApps) @@ -276,7 +276,7 @@ func TestListApp(t *testing.T) { Name: "example1", Description: "", Icon: "😃", - State: "", + Status: "", Example: true, Default: false, }, @@ -285,7 +285,7 @@ func TestListApp(t *testing.T) { Name: "app1", Description: "", Icon: "😃", - State: "", + Status: "", Example: false, Default: false, }, @@ -294,7 +294,7 @@ func TestListApp(t *testing.T) { Name: "app2", Description: "", Icon: "😃", - State: "", + Status: "", Example: false, Default: false, }, @@ -305,7 +305,7 @@ func TestListApp(t *testing.T) { res, err := ListApps(t.Context(), dockerCli, ListAppRequest{ ShowApps: true, ShowExamples: false, - StateFilter: "", + StatusFilter: "", }, idProvider, cfg) require.NoError(t, err) assert.Empty(t, res.BrokenApps) @@ -315,7 +315,7 @@ func TestListApp(t *testing.T) { Name: "app1", Description: "", Icon: "😃", - State: "", + Status: "", Example: false, Default: false, }, @@ -324,7 +324,7 @@ func TestListApp(t *testing.T) { Name: "app2", Description: "", Icon: "😃", - State: "", + Status: "", Example: false, Default: false, }, @@ -335,7 +335,7 @@ func TestListApp(t *testing.T) { res, err := ListApps(t.Context(), dockerCli, ListAppRequest{ ShowApps: false, ShowExamples: true, - StateFilter: "", + StatusFilter: "", }, idProvider, cfg) require.NoError(t, err) assert.Empty(t, res.BrokenApps) @@ -345,7 +345,7 @@ func TestListApp(t *testing.T) { Name: "example1", Description: "", Icon: "😃", - State: "", + Status: "", Example: true, Default: false, }, diff --git a/internal/orchestrator/status.go b/internal/orchestrator/status.go index 6e0cef2b..71bc3228 100644 --- a/internal/orchestrator/status.go +++ b/internal/orchestrator/status.go @@ -21,17 +21,17 @@ import ( "github.com/docker/docker/api/types/container" ) -type State string +type Status string const ( - StatusStarting State = "starting" - StatusRunning State = "running" - StatusStopping State = "stopping" - StatusStopped State = "stopped" - StatusFailed State = "failed" + StatusStarting Status = "starting" + StatusRunning Status = "running" + StatusStopping Status = "stopping" + StatusStopped Status = "stopped" + StatusFailed Status = "failed" ) -func StatusFromDockerState(s container.ContainerState) State { +func StatusFromDockerState(s container.ContainerState) Status { switch s { case container.StateRunning: return StatusRunning @@ -48,20 +48,20 @@ func StatusFromDockerState(s container.ContainerState) State { } } -func ParseStates(s string) (State, error) { - s1 := State(s) +func ParseStatus(s string) (Status, error) { + s1 := Status(s) return s1, s1.Validate() } -func (s State) Validate() error { +func (s Status) Validate() error { switch s { case StatusStarting, StatusRunning, StatusStopping, StatusStopped, StatusFailed: return nil default: - return fmt.Errorf("status should be one of %v", s.AllowedStates()) + return fmt.Errorf("status should be one of %v", s.AllowedStatuses()) } } -func (s State) AllowedStates() []State { - return []State{StatusStarting, StatusRunning, StatusStopping, StatusStopped, StatusFailed} +func (s Status) AllowedStatuses() []Status { + return []Status{StatusStarting, StatusRunning, StatusStopping, StatusStopped, StatusFailed} } From 21e3fdef85806a616a76d952e99af416579cb44c Mon Sep 17 00:00:00 2001 From: Giulio Pilotto Date: Fri, 5 Dec 2025 17:01:36 +0100 Subject: [PATCH 6/6] containerState struct with Status and StatusMessage --- internal/orchestrator/helpers.go | 33 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/internal/orchestrator/helpers.go b/internal/orchestrator/helpers.go index 6787f2e9..19360356 100644 --- a/internal/orchestrator/helpers.go +++ b/internal/orchestrator/helpers.go @@ -37,11 +37,11 @@ import ( type AppStatusInfo struct { AppPath *paths.Path - State State + Status Status } -type containerStateInfo struct { - State State +type containerState struct { + Status Status StatusMessage string } @@ -58,17 +58,16 @@ type containerStateInfo struct { // starting: at least one starting func parseAppStatus(containers []container.Summary) []AppStatusInfo { apps := make([]AppStatusInfo, 0, len(containers)) - appsStatusMap := make(map[string][]containerStateInfo) + appsStatusMap := make(map[string][]containerState) for _, c := range containers { appPath, ok := c.Labels[DockerAppPathLabel] if !ok { continue } - appsStatusMap[appPath] = append(appsStatusMap[appPath], containerStateInfo{ - State: StatusFromDockerState(c.State), + appsStatusMap[appPath] = append(appsStatusMap[appPath], containerState{ + Status: StatusFromDockerState(c.State), StatusMessage: c.Status, }) - slog.Debug("Container status", slog.String("appPath", appPath), slog.String("containerID", c.ID), @@ -77,10 +76,10 @@ func parseAppStatus(containers []container.Summary) []AppStatusInfo { ) } - appendResult := func(appPath *paths.Path, status State) { + appendResult := func(appPath *paths.Path, status Status) { apps = append(apps, AppStatusInfo{ AppPath: appPath, - State: status, + Status: status, }) } @@ -90,33 +89,33 @@ func parseAppStatus(containers []container.Summary) []AppStatusInfo { appPath := paths.New(appPath) // running: all running - if !slices.ContainsFunc(s, func(v containerStateInfo) bool { return v.State != 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 containerStateInfo) bool { return v.State != 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 containerStateInfo) bool { return v.State == StatusFailed }) { + if slices.ContainsFunc(s, func(v containerState) bool { return v.Status == StatusFailed }) { appendResult(appPath, StatusFailed) continue } - if slices.ContainsFunc(s, func(v containerStateInfo) bool { - return v.State == StatusStopped && strings.Contains(v.StatusMessage, "Exited (0)") + if slices.ContainsFunc(s, func(v containerState) bool { + return v.Status == StatusStopped && strings.Contains(v.StatusMessage, "Exited (0)") }) { appendResult(appPath, StatusFailed) continue } - if slices.ContainsFunc(s, func(v containerStateInfo) bool { return v.State == StatusStopping }) { + if slices.ContainsFunc(s, func(v containerState) bool { return v.Status == StatusStopping }) { appendResult(appPath, StatusStopping) continue } - if slices.ContainsFunc(s, func(v containerStateInfo) bool { return v.State == StatusStarting }) { + if slices.ContainsFunc(s, func(v containerState) bool { return v.Status == StatusStarting }) { appendResult(appPath, StatusStarting) continue } @@ -211,7 +210,7 @@ func getRunningApp( return nil, fmt.Errorf("failed to get running apps: %w", err) } idx := slices.IndexFunc(apps, func(a AppStatusInfo) bool { - return a.State == StatusRunning || a.State == StatusStarting + return a.Status == StatusRunning || a.Status == StatusStarting }) if idx == -1 { return nil, nil