diff --git a/api/http/handler/endpoints/filter.go b/api/http/handler/endpoints/filter.go index 2bfcae43c..6702ef2ae 100644 --- a/api/http/handler/endpoints/filter.go +++ b/api/http/handler/endpoints/filter.go @@ -627,6 +627,7 @@ func getEdgeStackStatusParam(r *http.Request) (*portainer.EdgeStackStatusType, e portainer.EdgeStackStatusRunning, portainer.EdgeStackStatusDeploying, portainer.EdgeStackStatusRemoving, + portainer.EdgeStackStatusCompleted, }, edgeStackStatus) { return nil, errors.New("invalid edgeStackStatus parameter") } diff --git a/api/portainer.go b/api/portainer.go index 0c660524e..7598901aa 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1728,6 +1728,8 @@ const ( EdgeStackStatusRollingBack // EdgeStackStatusRolledBack represents an Edge stack which has rolled back EdgeStackStatusRolledBack + // EdgeStackStatusCompleted represents a completed Edge stack + EdgeStackStatusCompleted ) const ( diff --git a/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/DeploymentCounter.tsx b/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/DeploymentCounter.tsx index 91c0ed84f..4a24c9c8d 100644 --- a/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/DeploymentCounter.tsx +++ b/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/DeploymentCounter.tsx @@ -68,6 +68,8 @@ function getLabel(type: StatusType): ReactNode { switch (type) { case StatusType.Running: return 'deployments running'; + case StatusType.Completed: + return 'deployments completed'; case StatusType.DeploymentReceived: return 'deployments received'; case StatusType.Error: diff --git a/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/EdgeStacksStatus.tsx b/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/EdgeStacksStatus.tsx index edeb18fc6..ef4661b6d 100644 --- a/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/EdgeStacksStatus.tsx +++ b/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/EdgeStacksStatus.tsx @@ -84,6 +84,16 @@ function getStatus( }; } + const allCompleted = envStatus.every((s) => s.Type === StatusType.Completed); + + if (allCompleted) { + return { + label: 'Completed', + icon: CheckCircle, + mode: 'success', + }; + } + const allRunning = envStatus.every( (s) => s.Type === StatusType.Running || diff --git a/app/react/edge/edge-stacks/types.ts b/app/react/edge/edge-stacks/types.ts index d03666b09..8f1b4a820 100644 --- a/app/react/edge/edge-stacks/types.ts +++ b/app/react/edge/edge-stacks/types.ts @@ -44,6 +44,8 @@ export enum StatusType { RollingBack, /** PausedRemoving represents an Edge stack which has been rolled back */ RolledBack, + /** Completed represents a completed Edge stack */ + Completed, } export interface DeploymentStatus { diff --git a/pkg/libstack/compose/internal/composeplugin/status.go b/pkg/libstack/compose/internal/composeplugin/status.go index 47b903def..7930a3865 100644 --- a/pkg/libstack/compose/internal/composeplugin/status.go +++ b/pkg/libstack/compose/internal/composeplugin/status.go @@ -51,7 +51,12 @@ func getServiceStatus(service service) (libstack.Status, string) { return libstack.StatusRunning, "" case "removing": return libstack.StatusRemoving, "" - case "exited", "dead": + case "exited": + if service.ExitCode != 0 { + return libstack.StatusError, fmt.Sprintf("service %s exited with code %d", service.Name, service.ExitCode) + } + return libstack.StatusCompleted, "" + case "dead": if service.ExitCode != 0 { return libstack.StatusError, fmt.Sprintf("service %s exited with code %d", service.Name, service.ExitCode) } @@ -94,7 +99,9 @@ func aggregateStatuses(services []service) (libstack.Status, string) { return libstack.StatusStarting, "" case statusCounts[libstack.StatusRemoving] > 0: return libstack.StatusRemoving, "" - case statusCounts[libstack.StatusRunning] == servicesCount: + case statusCounts[libstack.StatusCompleted] == servicesCount: + return libstack.StatusCompleted, "" + case statusCounts[libstack.StatusRunning]+statusCounts[libstack.StatusCompleted] == servicesCount: return libstack.StatusRunning, "" case statusCounts[libstack.StatusStopped] == servicesCount: return libstack.StatusStopped, "" @@ -106,15 +113,19 @@ func aggregateStatuses(services []service) (libstack.Status, string) { } -func (wrapper *PluginWrapper) WaitForStatus(ctx context.Context, name string, status libstack.Status) <-chan string { - errorMessageCh := make(chan string) +func (wrapper *PluginWrapper) WaitForStatus(ctx context.Context, name string, status libstack.Status) <-chan libstack.WaitResult { + waitResultCh := make(chan libstack.WaitResult) + waitResult := libstack.WaitResult{ + Status: status, + } go func() { OUTER: for { select { case <-ctx.Done(): - errorMessageCh <- fmt.Sprintf("failed to wait for status: %s", ctx.Err().Error()) + waitResult.ErrorMsg = fmt.Sprintf("failed to wait for status: %s", ctx.Err().Error()) + waitResultCh <- waitResult default: } @@ -129,7 +140,7 @@ func (wrapper *PluginWrapper) WaitForStatus(ctx context.Context, name string, st Msg("no output from docker compose ps") if status == libstack.StatusRemoved { - errorMessageCh <- "" + waitResultCh <- waitResult return } @@ -165,18 +176,25 @@ func (wrapper *PluginWrapper) WaitForStatus(ctx context.Context, name string, st } if len(services) == 0 && status == libstack.StatusRemoved { - errorMessageCh <- "" + waitResultCh <- waitResult return } aggregateStatus, errorMessage := aggregateStatuses(services) if aggregateStatus == status { - errorMessageCh <- "" + waitResultCh <- waitResult + return + } + + if status == libstack.StatusRunning && aggregateStatus == libstack.StatusCompleted { + waitResult.Status = libstack.StatusCompleted + waitResultCh <- waitResult return } if errorMessage != "" { - errorMessageCh <- errorMessage + waitResult.ErrorMsg = errorMessage + waitResultCh <- waitResult return } @@ -188,5 +206,5 @@ func (wrapper *PluginWrapper) WaitForStatus(ctx context.Context, name string, st } }() - return errorMessageCh + return waitResultCh } diff --git a/pkg/libstack/compose/internal/composeplugin/status_integration_test.go b/pkg/libstack/compose/internal/composeplugin/status_integration_test.go index 29cce2eec..a5051b2cc 100644 --- a/pkg/libstack/compose/internal/composeplugin/status_integration_test.go +++ b/pkg/libstack/compose/internal/composeplugin/status_integration_test.go @@ -108,9 +108,9 @@ func waitForStatus(deployer libstack.Deployer, ctx context.Context, stackName st statusCh := deployer.WaitForStatus(ctx, stackName, requiredStatus) result := <-statusCh - if result == "" { - return requiredStatus, "", nil + if result.ErrorMsg == "" { + return result.Status, "", nil } - return libstack.StatusError, result, nil + return libstack.StatusError, result.ErrorMsg, nil } diff --git a/pkg/libstack/libstack.go b/pkg/libstack/libstack.go index 294eb1c94..9ff08012a 100644 --- a/pkg/libstack/libstack.go +++ b/pkg/libstack/libstack.go @@ -13,21 +13,27 @@ type Deployer interface { Remove(ctx context.Context, projectName string, filePaths []string, options Options) error Pull(ctx context.Context, filePaths []string, options Options) error Validate(ctx context.Context, filePaths []string, options Options) error - WaitForStatus(ctx context.Context, name string, status Status) <-chan string + WaitForStatus(ctx context.Context, name string, status Status) <-chan WaitResult } type Status string const ( - StatusUnknown Status = "unknown" - StatusStarting Status = "starting" - StatusRunning Status = "running" - StatusStopped Status = "stopped" - StatusError Status = "error" - StatusRemoving Status = "removing" - StatusRemoved Status = "removed" + StatusUnknown Status = "unknown" + StatusStarting Status = "starting" + StatusRunning Status = "running" + StatusStopped Status = "stopped" + StatusError Status = "error" + StatusRemoving Status = "removing" + StatusRemoved Status = "removed" + StatusCompleted Status = "completed" ) +type WaitResult struct { + Status Status + ErrorMsg string +} + type Options struct { WorkingDir string Host string