From 666d51482edb0a819888c74121d7278a88b446e3 Mon Sep 17 00:00:00 2001 From: Oscar Zhou <100548325+oscarzhou-portainer@users.noreply.github.com> Date: Thu, 18 Sep 2025 16:29:35 +1200 Subject: [PATCH] fix(container): apply less accurate solution to calculate container status for swarm environment [BE-12256] (#1225) --- api/docker/stats/container_stats.go | 35 +++++++++++++++++++++++- api/docker/stats/container_stats_test.go | 23 ++++++++++++++-- api/http/handler/docker/dashboard.go | 2 +- pkg/snapshot/docker.go | 12 ++++---- 4 files changed, 62 insertions(+), 10 deletions(-) diff --git a/api/docker/stats/container_stats.go b/api/docker/stats/container_stats.go index 15e88f407..d06f903d6 100644 --- a/api/docker/stats/container_stats.go +++ b/api/docker/stats/container_stats.go @@ -3,6 +3,7 @@ package stats import ( "context" "errors" + "strings" "sync" "github.com/docker/docker/api/types/container" @@ -20,7 +21,11 @@ type DockerClient interface { ContainerInspect(ctx context.Context, containerID string) (container.InspectResponse, error) } -func CalculateContainerStats(ctx context.Context, cli DockerClient, containers []container.Summary) (ContainerStats, error) { +func CalculateContainerStats(ctx context.Context, cli DockerClient, isSwarm bool, containers []container.Summary) (ContainerStats, error) { + if isSwarm { + return CalculateContainerStatsForSwarm(containers), nil + } + var running, stopped, healthy, unhealthy int var mu sync.Mutex @@ -90,3 +95,31 @@ func getContainerStatus(state *container.State) ContainerStats { return stat } + +// This is a temporary workaround to calculate container stats for Swarm +// TODO: Remove this once we have a proper way to calculate container stats for Swarm +func CalculateContainerStatsForSwarm(containers []container.Summary) ContainerStats { + var running, stopped, healthy, unhealthy int + for _, container := range containers { + switch container.State { + case "running": + running++ + case "exited", "stopped": + stopped++ + } + + if strings.Contains(container.Status, "(healthy)") { + healthy++ + } else if strings.Contains(container.Status, "(unhealthy)") { + unhealthy++ + } + } + + return ContainerStats{ + Running: running, + Stopped: stopped, + Healthy: healthy, + Unhealthy: unhealthy, + Total: len(containers), + } +} diff --git a/api/docker/stats/container_stats_test.go b/api/docker/stats/container_stats_test.go index ae2fa6e2f..aa296cd22 100644 --- a/api/docker/stats/container_stats_test.go +++ b/api/docker/stats/container_stats_test.go @@ -79,7 +79,7 @@ func TestCalculateContainerStats(t *testing.T) { // Call the function and measure time startTime := time.Now() - stats, err := CalculateContainerStats(context.Background(), mockClient, containers) + stats, err := CalculateContainerStats(context.Background(), mockClient, false, containers) require.NoError(t, err, "failed to calculate container stats") duration := time.Since(startTime) @@ -120,7 +120,7 @@ func TestCalculateContainerStatsAllErrors(t *testing.T) { mockClient.On("ContainerInspect", mock.Anything, "container2").Return(container.InspectResponse{}, errors.New("permission denied")) // Call the function - stats, err := CalculateContainerStats(context.Background(), mockClient, containers) + stats, err := CalculateContainerStats(context.Background(), mockClient, false, containers) // Assert that an error was returned require.Error(t, err, "should return error when all containers fail to inspect") @@ -232,3 +232,22 @@ func TestGetContainerStatus(t *testing.T) { }) } } + +func TestCalculateContainerStatsForSwarm(t *testing.T) { + containers := []container.Summary{ + {State: "running"}, + {State: "running", Status: "Up 5 minutes (healthy)"}, + {State: "exited"}, + {State: "stopped"}, + {State: "running", Status: "Up 10 minutes"}, + {State: "running", Status: "Up about an hour (unhealthy)"}, + } + + stats := CalculateContainerStatsForSwarm(containers) + + assert.Equal(t, 4, stats.Running) + assert.Equal(t, 2, stats.Stopped) + assert.Equal(t, 1, stats.Healthy) + assert.Equal(t, 1, stats.Unhealthy) + assert.Equal(t, 6, stats.Total) +} diff --git a/api/http/handler/docker/dashboard.go b/api/http/handler/docker/dashboard.go index 7963c52d1..4120db02f 100644 --- a/api/http/handler/docker/dashboard.go +++ b/api/http/handler/docker/dashboard.go @@ -143,7 +143,7 @@ func (h *Handler) dashboard(w http.ResponseWriter, r *http.Request) *httperror.H stackCount = len(stacks) } - containersStats, err := stats.CalculateContainerStats(r.Context(), cli, containers) + containersStats, err := stats.CalculateContainerStats(r.Context(), cli, info.Swarm.ControlAvailable, containers) if err != nil { return httperror.InternalServerError("Unable to retrieve Docker containers stats", err) } diff --git a/pkg/snapshot/docker.go b/pkg/snapshot/docker.go index 255186063..875813b9e 100644 --- a/pkg/snapshot/docker.go +++ b/pkg/snapshot/docker.go @@ -209,16 +209,16 @@ func dockerSnapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Cl snapshot.GpuUseAll = gpuUseAll snapshot.GpuUseList = gpuUseList - stats, err := stats.CalculateContainerStats(ctx, cli, containers) + result, err := stats.CalculateContainerStats(ctx, cli, snapshot.Swarm, containers) if err != nil { return fmt.Errorf("failed to calculate container stats: %w", err) } - snapshot.ContainerCount = stats.Total - snapshot.RunningContainerCount = stats.Running - snapshot.StoppedContainerCount = stats.Stopped - snapshot.HealthyContainerCount = stats.Healthy - snapshot.UnhealthyContainerCount = stats.Unhealthy + snapshot.ContainerCount = result.Total + snapshot.RunningContainerCount = result.Running + snapshot.StoppedContainerCount = result.Stopped + snapshot.HealthyContainerCount = result.Healthy + snapshot.UnhealthyContainerCount = result.Unhealthy snapshot.StackCount += len(stacks) for _, container := range containers {