fix(docker): dashboard api return 500 error [BE-12567] (#1784)
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
@@ -35,8 +36,10 @@ func CalculateContainerStats(ctx context.Context, cli DockerClient, isSwarm bool
|
||||
var aggErr error
|
||||
var aggMu sync.Mutex
|
||||
|
||||
var processedCount int
|
||||
for i := range containers {
|
||||
id := containers[i].ID
|
||||
|
||||
semaphore <- struct{}{}
|
||||
wg.Go(func() {
|
||||
defer func() { <-semaphore }()
|
||||
@@ -44,8 +47,17 @@ func CalculateContainerStats(ctx context.Context, cli DockerClient, isSwarm bool
|
||||
containerInspection, err := cli.ContainerInspect(ctx, id)
|
||||
stat := ContainerStats{}
|
||||
if err != nil {
|
||||
if errdefs.IsNotFound(err) {
|
||||
// An edge case is reported that Docker can list containers with no names,
|
||||
// but when inspecting a container with specific ID and it is not found.
|
||||
// In this case, we can safely ignore the error.
|
||||
// ref@https://linear.app/portainer/issue/BE-12567/500-error-when-loading-docker-dashboard-in-portainer
|
||||
return
|
||||
}
|
||||
|
||||
aggMu.Lock()
|
||||
aggErr = errors.Join(aggErr, err)
|
||||
processedCount++
|
||||
aggMu.Unlock()
|
||||
return
|
||||
}
|
||||
@@ -56,6 +68,7 @@ func CalculateContainerStats(ctx context.Context, cli DockerClient, isSwarm bool
|
||||
stopped += stat.Stopped
|
||||
healthy += stat.Healthy
|
||||
unhealthy += stat.Unhealthy
|
||||
processedCount++
|
||||
mu.Unlock()
|
||||
})
|
||||
}
|
||||
@@ -67,7 +80,7 @@ func CalculateContainerStats(ctx context.Context, cli DockerClient, isSwarm bool
|
||||
Stopped: stopped,
|
||||
Healthy: healthy,
|
||||
Unhealthy: unhealthy,
|
||||
Total: len(containers),
|
||||
Total: processedCount,
|
||||
}, aggErr
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,11 @@ package stats
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
@@ -37,6 +39,7 @@ func TestCalculateContainerStats(t *testing.T) {
|
||||
{ID: "container8"},
|
||||
{ID: "container9"},
|
||||
{ID: "container10"},
|
||||
{ID: "container11"},
|
||||
}
|
||||
|
||||
// Setup mock expectations with different container states to test various scenarios
|
||||
@@ -58,7 +61,6 @@ func TestCalculateContainerStats(t *testing.T) {
|
||||
{"container10", container.StateDead, nil, ContainerStats{Running: 0, Stopped: 1, Healthy: 0, Unhealthy: 0}},
|
||||
}
|
||||
|
||||
expected := ContainerStats{}
|
||||
// Setup mock expectations for all containers with artificial delays to simulate real Docker calls
|
||||
for _, state := range containerStates {
|
||||
mockClient.On("ContainerInspect", mock.Anything, state.id).Return(container.InspectResponse{
|
||||
@@ -68,15 +70,12 @@ func TestCalculateContainerStats(t *testing.T) {
|
||||
Health: state.health,
|
||||
},
|
||||
},
|
||||
}, nil).After(50 * time.Millisecond) // Simulate 50ms Docker API call
|
||||
|
||||
expected.Running += state.expected.Running
|
||||
expected.Stopped += state.expected.Stopped
|
||||
expected.Healthy += state.expected.Healthy
|
||||
expected.Unhealthy += state.expected.Unhealthy
|
||||
expected.Total++
|
||||
}, nil).After(30 * time.Millisecond) // Simulate 30ms Docker API call
|
||||
}
|
||||
|
||||
// Setup mock expectation for a container that returns NotFound error
|
||||
mockClient.On("ContainerInspect", mock.Anything, "container11").Return(container.InspectResponse{}, fmt.Errorf("No such container: %w", errdefs.ErrNotFound)).After(50 * time.Millisecond)
|
||||
|
||||
// Call the function and measure time
|
||||
startTime := time.Now()
|
||||
stats, err := CalculateContainerStats(context.Background(), mockClient, false, containers)
|
||||
@@ -84,11 +83,10 @@ func TestCalculateContainerStats(t *testing.T) {
|
||||
duration := time.Since(startTime)
|
||||
|
||||
// Assert results
|
||||
assert.Equal(t, expected, stats)
|
||||
assert.Equal(t, expected.Running, stats.Running)
|
||||
assert.Equal(t, expected.Stopped, stats.Stopped)
|
||||
assert.Equal(t, expected.Healthy, stats.Healthy)
|
||||
assert.Equal(t, expected.Unhealthy, stats.Unhealthy)
|
||||
assert.Equal(t, 6, stats.Running)
|
||||
assert.Equal(t, 4, stats.Stopped)
|
||||
assert.Equal(t, 2, stats.Healthy)
|
||||
assert.Equal(t, 2, stats.Unhealthy)
|
||||
assert.Equal(t, 10, stats.Total)
|
||||
|
||||
// Verify concurrent processing by checking that all mock calls were made
|
||||
|
||||
2
go.mod
2
go.mod
@@ -16,6 +16,7 @@ require (
|
||||
github.com/aws/smithy-go v1.20.3
|
||||
github.com/cbroglie/mustache v1.4.0
|
||||
github.com/compose-spec/compose-go/v2 v2.9.1
|
||||
github.com/containerd/containerd v1.7.29
|
||||
github.com/coreos/go-semver v0.3.1
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5
|
||||
github.com/docker/cli v28.5.1+incompatible
|
||||
@@ -113,7 +114,6 @@ require (
|
||||
github.com/cloudflare/cfssl v1.6.4 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/containerd/console v1.0.5 // indirect
|
||||
github.com/containerd/containerd v1.7.29 // indirect
|
||||
github.com/containerd/containerd/api v1.9.0 // indirect
|
||||
github.com/containerd/containerd/v2 v2.1.5 // indirect
|
||||
github.com/containerd/continuity v0.4.5 // indirect
|
||||
|
||||
Reference in New Issue
Block a user