Skip to content

Commit b04a9b6

Browse files
committed
fix: explicitly set docker api version based on daemon api version
1 parent 170cb15 commit b04a9b6

25 files changed

+211
-115
lines changed

backend/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ require (
3232
github.com/lmittmann/tint v1.1.3
3333
github.com/moby/buildkit v0.27.1
3434
github.com/moby/go-archive v0.2.0
35-
github.com/moby/moby/api v1.53.0
36-
github.com/moby/moby/client v0.2.2
35+
github.com/moby/moby/api v1.54.0
36+
github.com/moby/moby/client v0.3.0
3737
github.com/nicholas-fedor/shoutrrr v0.13.2
3838
github.com/opencontainers/image-spec v1.1.1
3939
github.com/orandin/slog-gorm v1.4.0

backend/go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -357,10 +357,10 @@ github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8
357357
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
358358
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
359359
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
360-
github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=
361-
github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
362-
github.com/moby/moby/client v0.2.2 h1:Pt4hRMCAIlyjL3cr8M5TrXCwKzguebPAc2do2ur7dEM=
363-
github.com/moby/moby/client v0.2.2/go.mod h1:2EkIPVNCqR05CMIzL1mfA07t0HvVUUOl85pasRz/GmQ=
360+
github.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0=
361+
github.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
362+
github.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs=
363+
github.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ=
364364
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
365365
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
366366
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=

backend/internal/bootstrap/bootstrap.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/getarcaneapp/arcane/backend/internal/utils"
2222
"github.com/getarcaneapp/arcane/backend/internal/utils/crypto"
2323
httputils "github.com/getarcaneapp/arcane/backend/internal/utils/http"
24+
"github.com/getarcaneapp/arcane/backend/pkg/libarcane"
2425
"github.com/getarcaneapp/arcane/backend/pkg/libarcane/edge"
2526
tunnelpb "github.com/getarcaneapp/arcane/backend/pkg/libarcane/edge/proto/tunnel/v1"
2627
"github.com/getarcaneapp/arcane/backend/pkg/scheduler"
@@ -139,7 +140,7 @@ func initializeStartupState(appCtx context.Context, cfg *config.Config, appServi
139140
}
140141

141142
utils.TestDockerConnection(appCtx, func(ctx context.Context) error {
142-
dockerClient, err := dockerClientService.GetClient()
143+
dockerClient, err := dockerClientService.GetClient(ctx)
143144
if err != nil {
144145
return err
145146
}
@@ -149,7 +150,8 @@ func initializeStartupState(appCtx context.Context, cfg *config.Config, appServi
149150
return err
150151
}
151152

152-
slog.InfoContext(ctx, "Docker API versions detected", "client_api_version", dockerClient.ClientVersion(), "server_api_version", version.APIVersion)
153+
effectiveAPIVersion := libarcane.DetectDockerAPIVersion(ctx, dockerClient)
154+
slog.InfoContext(ctx, "Docker API versions detected", "client_api_version", dockerClient.ClientVersion(), "server_api_version", version.APIVersion, "effective_api_version", effectiveAPIVersion)
153155
return nil
154156
})
155157

backend/internal/huma/handlers/system.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ func (h *SystemHandler) Health(ctx context.Context, input *SystemHealthInput) (*
249249
return nil, huma.Error503ServiceUnavailable("docker service not available")
250250
}
251251

252-
dockerClient, err := h.dockerService.GetClient()
252+
dockerClient, err := h.dockerService.GetClient(ctx)
253253
if err != nil {
254254
return nil, huma.Error503ServiceUnavailable((&common.DockerConnectionError{Err: err}).Error())
255255
}
@@ -268,7 +268,7 @@ func (h *SystemHandler) GetDockerInfo(ctx context.Context, input *GetDockerInfoI
268268
return nil, huma.Error500InternalServerError("service not available")
269269
}
270270

271-
dockerClient, err := h.dockerService.GetClient()
271+
dockerClient, err := h.dockerService.GetClient(ctx)
272272
if err != nil {
273273
return nil, huma.Error500InternalServerError((&common.DockerConnectionError{Err: err}).Error())
274274
}

backend/internal/services/container_service.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func NewContainerService(db *database.DB, eventService *EventService, dockerServ
4646
}
4747

4848
func (s *ContainerService) StartContainer(ctx context.Context, containerID string, user models.User) error {
49-
dockerClient, err := s.dockerService.GetClient()
49+
dockerClient, err := s.dockerService.GetClient(ctx)
5050
if err != nil {
5151
s.eventService.LogErrorEvent(ctx, models.EventTypeContainerError, "container", containerID, "", user.ID, user.Username, "0", err, models.JSON{"action": "start"})
5252
return fmt.Errorf("failed to connect to Docker: %w", err)
@@ -70,7 +70,7 @@ func (s *ContainerService) StartContainer(ctx context.Context, containerID strin
7070
}
7171

7272
func (s *ContainerService) StopContainer(ctx context.Context, containerID string, user models.User) error {
73-
dockerClient, err := s.dockerService.GetClient()
73+
dockerClient, err := s.dockerService.GetClient(ctx)
7474
if err != nil {
7575
s.eventService.LogErrorEvent(ctx, models.EventTypeContainerError, "container", containerID, "", user.ID, user.Username, "0", err, models.JSON{"action": "stop"})
7676
return fmt.Errorf("failed to connect to Docker: %w", err)
@@ -95,7 +95,7 @@ func (s *ContainerService) StopContainer(ctx context.Context, containerID string
9595
}
9696

9797
func (s *ContainerService) RestartContainer(ctx context.Context, containerID string, user models.User) error {
98-
dockerClient, err := s.dockerService.GetClient()
98+
dockerClient, err := s.dockerService.GetClient(ctx)
9999
if err != nil {
100100
s.eventService.LogErrorEvent(ctx, models.EventTypeContainerError, "container", containerID, "", user.ID, user.Username, "0", err, models.JSON{"action": "restart"})
101101
return fmt.Errorf("failed to connect to Docker: %w", err)
@@ -119,7 +119,7 @@ func (s *ContainerService) RestartContainer(ctx context.Context, containerID str
119119
}
120120

121121
func (s *ContainerService) GetContainerByID(ctx context.Context, id string) (*container.InspectResponse, error) {
122-
dockerClient, err := s.dockerService.GetClient()
122+
dockerClient, err := s.dockerService.GetClient(ctx)
123123
if err != nil {
124124
return nil, fmt.Errorf("failed to connect to Docker: %w", err)
125125
}
@@ -134,7 +134,7 @@ func (s *ContainerService) GetContainerByID(ctx context.Context, id string) (*co
134134
}
135135

136136
func (s *ContainerService) DeleteContainer(ctx context.Context, containerID string, force bool, removeVolumes bool, user models.User) error {
137-
dockerClient, err := s.dockerService.GetClient()
137+
dockerClient, err := s.dockerService.GetClient(ctx)
138138
if err != nil {
139139
s.eventService.LogErrorEvent(ctx, models.EventTypeContainerError, "container", containerID, "", user.ID, user.Username, "0", err, models.JSON{"action": "delete", "force": force, "removeVolumes": removeVolumes})
140140
return fmt.Errorf("failed to connect to Docker: %w", err)
@@ -188,7 +188,7 @@ func (s *ContainerService) DeleteContainer(ctx context.Context, containerID stri
188188
}
189189

190190
func (s *ContainerService) CreateContainer(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string, user models.User, credentials []containerregistry.Credential) (*container.InspectResponse, error) {
191-
dockerClient, err := s.dockerService.GetClient()
191+
dockerClient, err := s.dockerService.GetClient(ctx)
192192
if err != nil {
193193
s.eventService.LogErrorEvent(ctx, models.EventTypeContainerError, "container", "", containerName, user.ID, user.Username, "0", err, models.JSON{"action": "create", "image": config.Image})
194194
return nil, fmt.Errorf("failed to connect to Docker: %w", err)
@@ -264,7 +264,7 @@ func (s *ContainerService) CreateContainer(ctx context.Context, config *containe
264264
}
265265

266266
func (s *ContainerService) StreamStats(ctx context.Context, containerID string, statsChan chan<- any) error {
267-
dockerClient, err := s.dockerService.GetClient()
267+
dockerClient, err := s.dockerService.GetClient(ctx)
268268
if err != nil {
269269
return fmt.Errorf("failed to connect to Docker: %w", err)
270270
}
@@ -302,7 +302,7 @@ func (s *ContainerService) StreamStats(ctx context.Context, containerID string,
302302
}
303303

304304
func (s *ContainerService) StreamLogs(ctx context.Context, containerID string, logsChan chan<- string, follow bool, tail, since string, timestamps bool) error {
305-
dockerClient, err := s.dockerService.GetClient()
305+
dockerClient, err := s.dockerService.GetClient(ctx)
306306
if err != nil {
307307
return fmt.Errorf("failed to connect to Docker: %w", err)
308308
}
@@ -437,7 +437,7 @@ func (s *ContainerService) readAllLogs(logs io.ReadCloser, logsChan chan<- strin
437437
}
438438

439439
func (s *ContainerService) ListContainersPaginated(ctx context.Context, params pagination.QueryParams, includeAll bool, includeInternal bool) ([]containertypes.Summary, pagination.Response, containertypes.StatusCounts, error) {
440-
dockerClient, err := s.dockerService.GetClient()
440+
dockerClient, err := s.dockerService.GetClient(ctx)
441441
if err != nil {
442442
return nil, pagination.Response{}, containertypes.StatusCounts{}, fmt.Errorf("failed to connect to Docker: %w", err)
443443
}
@@ -622,7 +622,7 @@ func (s *ContainerService) calculateContainerStatusCounts(items []containertypes
622622

623623
// CreateExec creates an exec instance in the container
624624
func (s *ContainerService) CreateExec(ctx context.Context, containerID string, cmd []string) (string, error) {
625-
dockerClient, err := s.dockerService.GetClient()
625+
dockerClient, err := s.dockerService.GetClient(ctx)
626626
if err != nil {
627627
return "", fmt.Errorf("failed to connect to Docker: %w", err)
628628
}
@@ -675,7 +675,7 @@ func (e *ExecSession) Close(ctx context.Context) error {
675675

676676
// AttachExec attaches to an exec instance and returns an ExecSession for lifecycle management.
677677
func (s *ContainerService) AttachExec(ctx context.Context, containerID, execID string) (*ExecSession, error) {
678-
dockerClient, err := s.dockerService.GetClient()
678+
dockerClient, err := s.dockerService.GetClient(ctx)
679679
if err != nil {
680680
return nil, fmt.Errorf("failed to connect to Docker: %w", err)
681681
}

backend/internal/services/docker_client_service.go

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package services
33
import (
44
"context"
55
"fmt"
6+
"strings"
67
"sync"
8+
"time"
79

810
"github.com/getarcaneapp/arcane/backend/internal/config"
911
"github.com/getarcaneapp/arcane/backend/internal/database"
@@ -17,6 +19,8 @@ import (
1719
"github.com/moby/moby/client"
1820
)
1921

22+
const dockerClientNegotiationTimeout = 5 * time.Second
23+
2024
type DockerClientService struct {
2125
db *database.DB
2226
config *config.Config
@@ -33,9 +37,44 @@ func NewDockerClientService(db *database.DB, cfg *config.Config, settingsService
3337
}
3438
}
3539

40+
func newDockerClientInternal(ctx context.Context, host string) (*client.Client, error) {
41+
probeClient, err := client.New(
42+
client.WithHost(host),
43+
)
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
ctx, cancel := context.WithTimeout(ctx, dockerClientNegotiationTimeout)
49+
defer cancel()
50+
51+
pingResult, err := probeClient.Ping(ctx, client.PingOptions{})
52+
if err != nil {
53+
_ = probeClient.Close()
54+
return nil, fmt.Errorf("failed to negotiate Docker API version: %w", err)
55+
}
56+
57+
apiVersion := strings.TrimSpace(pingResult.APIVersion)
58+
if apiVersion == "" {
59+
return probeClient, nil
60+
}
61+
62+
_ = probeClient.Close()
63+
64+
configuredClient, err := client.New(
65+
client.WithHost(host),
66+
client.WithAPIVersion(apiVersion),
67+
)
68+
if err != nil {
69+
return nil, fmt.Errorf("failed to configure Docker client API version %s: %w", apiVersion, err)
70+
}
71+
72+
return configuredClient, nil
73+
}
74+
3675
// GetClient returns a singleton Docker client instance.
3776
// It initializes the client on the first call.
38-
func (s *DockerClientService) GetClient() (*client.Client, error) {
77+
func (s *DockerClientService) GetClient(ctx context.Context) (*client.Client, error) {
3978
if s.client != nil {
4079
return s.client, nil
4180
}
@@ -48,10 +87,7 @@ func (s *DockerClientService) GetClient() (*client.Client, error) {
4887
return s.client, nil
4988
}
5089

51-
cli, err := client.New(
52-
client.WithHost(s.config.DockerHost),
53-
client.WithAPIVersionFromEnv(),
54-
)
90+
cli, err := newDockerClientInternal(ctx, s.config.DockerHost)
5591
if err != nil {
5692
return nil, fmt.Errorf("failed to create Docker client: %w", err)
5793
}
@@ -61,7 +97,7 @@ func (s *DockerClientService) GetClient() (*client.Client, error) {
6197
}
6298

6399
func (s *DockerClientService) GetAllContainers(ctx context.Context) ([]container.Summary, int, int, int, error) {
64-
dockerClient, err := s.GetClient()
100+
dockerClient, err := s.GetClient(ctx)
65101
if err != nil {
66102
return nil, 0, 0, 0, fmt.Errorf("failed to connect to Docker: %w", err)
67103
}
@@ -90,7 +126,7 @@ func (s *DockerClientService) GetAllContainers(ctx context.Context) ([]container
90126
}
91127

92128
func (s *DockerClientService) GetAllImages(ctx context.Context) ([]image.Summary, int, int, int, error) {
93-
dockerClient, err := s.GetClient()
129+
dockerClient, err := s.GetClient(ctx)
94130
if err != nil {
95131
return nil, 0, 0, 0, fmt.Errorf("failed to connect to Docker: %w", err)
96132
}
@@ -138,7 +174,7 @@ func countImageUsageInternal(images []image.Summary, containers []container.Summ
138174
}
139175

140176
func (s *DockerClientService) GetAllNetworks(ctx context.Context) (_ []network.Summary, inuseNetworks int, unusedNetworks int, totalNetworks int, error error) {
141-
dockerClient, err := s.GetClient()
177+
dockerClient, err := s.GetClient(ctx)
142178
if err != nil {
143179
return nil, 0, 0, 0, fmt.Errorf("failed to connect to Docker: %w", err)
144180
}
@@ -192,7 +228,7 @@ func (s *DockerClientService) GetAllNetworks(ctx context.Context) (_ []network.S
192228
}
193229

194230
func (s *DockerClientService) GetAllVolumes(ctx context.Context) ([]*volume.Volume, int, int, int, error) {
195-
dockerClient, err := s.GetClient()
231+
dockerClient, err := s.GetClient(ctx)
196232
if err != nil {
197233
return nil, 0, 0, 0, fmt.Errorf("failed to connect to Docker: %w", err)
198234
}

backend/internal/services/docker_client_service_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,42 @@
11
package services
22

33
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
47
"testing"
58

69
"github.com/moby/moby/api/types/container"
710
"github.com/moby/moby/api/types/image"
811
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
913
)
1014

15+
func TestNewDockerClient_UsesConfiguredHostAndPinsServerAPIVersion(t *testing.T) {
16+
t.Setenv("DOCKER_API_VERSION", "1.54")
17+
t.Setenv("DOCKER_HOST", "tcp://docker-from-env:2375")
18+
19+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20+
if r.URL.Path != "/_ping" {
21+
http.NotFound(w, r)
22+
return
23+
}
24+
25+
w.Header().Set("Api-Version", "1.41")
26+
w.WriteHeader(http.StatusOK)
27+
}))
28+
t.Cleanup(server.Close)
29+
30+
cli, err := newDockerClientInternal(context.Background(), server.URL)
31+
require.NoError(t, err)
32+
t.Cleanup(func() {
33+
_ = cli.Close()
34+
})
35+
36+
assert.Equal(t, server.URL, cli.DaemonHost())
37+
assert.Equal(t, "1.41", cli.ClientVersion())
38+
}
39+
1140
func TestCountImageUsage_UsesContainerImageIDs(t *testing.T) {
1241
images := []image.Summary{
1342
{ID: "sha256:image-a", Containers: -1},

backend/internal/services/environment_service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ func (s *EnvironmentService) testLocalDockerConnection(ctx context.Context, id s
325325
reqCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
326326
defer cancel()
327327

328-
dockerClient, err := s.dockerService.GetClient()
328+
dockerClient, err := s.dockerService.GetClient(ctx)
329329
if err != nil {
330330
_ = s.updateEnvironmentStatusInternal(ctx, id, string(models.EnvironmentStatusOffline))
331331
return "offline", fmt.Errorf("failed to connect to Docker: %w", err)

0 commit comments

Comments
 (0)