Skip to content

Commit d2077df

Browse files
committed
fix: check Docker Engine version for create_host_path: false support
The CreateMountpoint option for bind mounts was introduced in Docker Engine API v1.42 (Engine v23.0). Previously, when using create_host_path: false on an older Docker Engine, the setting was silently ignored and the host path was created anyway. This change adds an API version check that fails early with a clear error message when a user tries to use create_host_path: false on Docker Engine earlier than v23.0. Fixes: #13602
1 parent f9828df commit d2077df

File tree

3 files changed

+76
-15
lines changed

3 files changed

+76
-15
lines changed

pkg/compose/api_versions.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ package compose
1919
// Docker Engine API version constants.
2020
// These versions correspond to specific Docker Engine releases and their features.
2121
const (
22+
// apiVersion142 represents Docker Engine API version 1.42 (Engine v23.0).
23+
//
24+
// New features in this version:
25+
// - CreateMountpoint option for bind mounts (allows create_host_path: false)
26+
//
27+
// Before this version:
28+
// - Bind mounts always created host path if missing, regardless of CreateMountpoint setting
29+
apiVersion142 = "1.42"
30+
2231
// apiVersion148 represents Docker Engine API version 1.48 (Engine v28.0).
2332
//
2433
// New features in this version:
@@ -43,6 +52,9 @@ const (
4352
// Docker Engine version strings for user-facing error messages.
4453
// These should be used in error messages to provide clear version requirements.
4554
const (
55+
// dockerEngineV23 is the major version string for Docker Engine 23.x
56+
dockerEngineV23 = "v23"
57+
4658
// dockerEngineV28 is the major version string for Docker Engine 28.x
4759
dockerEngineV28 = "v28"
4860

pkg/compose/create.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,13 @@ func (s *composeService) buildContainerVolumes(
866866
return nil, nil, err
867867
}
868868

869+
// Check Docker Engine API version for CreateMountpoint support
870+
version, err := s.RuntimeVersion(ctx)
871+
if err != nil {
872+
return nil, nil, err
873+
}
874+
supportsCreateMountpoint := versions.GreaterThanOrEqualTo(version, apiVersion142)
875+
869876
for _, m := range mountOptions {
870877
switch m.Type {
871878
case mount.TypeBind:
@@ -885,6 +892,10 @@ func (s *composeService) buildContainerVolumes(
885892
binds = append(binds, toBindString(source, v))
886893
continue
887894
}
895+
// Check if create_host_path: false is used on an engine that doesn't support it
896+
if v.Bind != nil && !bool(v.Bind.CreateHostPath) && !supportsCreateMountpoint {
897+
return nil, nil, fmt.Errorf("bind mount create_host_path: false requires Docker Engine %s or later", dockerEngineV23)
898+
}
888899
}
889900
case mount.TypeVolume:
890901
v := findVolumeByTarget(service.Volumes, m.Target)
@@ -897,10 +908,6 @@ func (s *composeService) buildContainerVolumes(
897908
}
898909
}
899910
case mount.TypeImage:
900-
version, err := s.RuntimeVersion(ctx)
901-
if err != nil {
902-
return nil, nil, err
903-
}
904911
if versions.LessThan(version, apiVersion148) {
905912
return nil, nil, fmt.Errorf("volume with type=image require Docker Engine %s or later", dockerEngineV28)
906913
}

pkg/compose/create_test.go

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626

2727
composeloader "github.com/compose-spec/compose-go/v2/loader"
2828
composetypes "github.com/compose-spec/compose-go/v2/types"
29+
"github.com/docker/cli/cli/config/configfile"
2930
"github.com/google/go-cmp/cmp/cmpopts"
3031
"github.com/moby/moby/api/types/container"
3132
mountTypes "github.com/moby/moby/api/types/mount"
@@ -36,6 +37,7 @@ import (
3637
"gotest.tools/v3/assert/cmp"
3738

3839
"github.com/docker/compose/v5/pkg/api"
40+
"github.com/docker/compose/v5/pkg/mocks"
3941
)
4042

4143
func TestBuildBindMount(t *testing.T) {
@@ -346,13 +348,16 @@ func Test_buildContainerVolumes(t *testing.T) {
346348
assert.NilError(t, err)
347349

348350
tests := []struct {
349-
name string
350-
yaml string
351-
binds []string
352-
mounts []mountTypes.Mount
351+
name string
352+
yaml string
353+
binds []string
354+
mounts []mountTypes.Mount
355+
apiVersion string
356+
expectError string
353357
}{
354358
{
355-
name: "bind mount local path",
359+
name: "bind mount local path",
360+
apiVersion: "1.44",
356361
yaml: `
357362
services:
358363
test:
@@ -363,7 +368,8 @@ services:
363368
mounts: nil,
364369
},
365370
{
366-
name: "bind mount, not create host path",
371+
name: "bind mount, not create host path",
372+
apiVersion: "1.44",
367373
yaml: `
368374
services:
369375
test:
@@ -385,7 +391,23 @@ services:
385391
},
386392
},
387393
{
388-
name: "mount volume",
394+
name: "bind mount, not create host path with old engine",
395+
apiVersion: "1.41",
396+
expectError: "bind mount create_host_path: false requires Docker Engine v23 or later",
397+
yaml: `
398+
services:
399+
test:
400+
volumes:
401+
- type: bind
402+
source: ./data
403+
target: /data
404+
bind:
405+
create_host_path: false
406+
`,
407+
},
408+
{
409+
name: "mount volume",
410+
apiVersion: "1.44",
389411
yaml: `
390412
services:
391413
test:
@@ -399,7 +421,8 @@ volumes:
399421
mounts: nil,
400422
},
401423
{
402-
name: "mount volume, readonly",
424+
name: "mount volume, readonly",
425+
apiVersion: "1.44",
403426
yaml: `
404427
services:
405428
test:
@@ -413,7 +436,8 @@ volumes:
413436
mounts: nil,
414437
},
415438
{
416-
name: "mount volume subpath",
439+
name: "mount volume subpath",
440+
apiVersion: "1.44",
417441
yaml: `
418442
services:
419443
test:
@@ -440,6 +464,21 @@ volumes:
440464
}
441465
for _, tt := range tests {
442466
t.Run(tt.name, func(t *testing.T) {
467+
mockCtrl := gomock.NewController(t)
468+
defer mockCtrl.Finish()
469+
apiClient := mocks.NewMockAPIClient(mockCtrl)
470+
cli := mocks.NewMockCli(mockCtrl)
471+
tested, err := NewComposeService(cli)
472+
assert.NilError(t, err)
473+
cli.EXPECT().Client().Return(apiClient).AnyTimes()
474+
cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes()
475+
476+
// force `RuntimeVersion` to fetch fresh version
477+
runtimeVersion = runtimeVersionCache{}
478+
apiClient.EXPECT().ServerVersion(gomock.Any(), gomock.Any()).Return(client.ServerVersionResult{
479+
APIVersion: tt.apiVersion,
480+
}, nil).AnyTimes()
481+
443482
p, err := composeloader.LoadWithContext(t.Context(), composetypes.ConfigDetails{
444483
ConfigFiles: []composetypes.ConfigFile{
445484
{
@@ -452,8 +491,11 @@ volumes:
452491
options.SkipConsistencyCheck = true
453492
})
454493
assert.NilError(t, err)
455-
s := &composeService{}
456-
binds, mounts, err := s.buildContainerVolumes(t.Context(), *p, p.Services["test"], nil)
494+
binds, mounts, err := tested.(*composeService).buildContainerVolumes(t.Context(), *p, p.Services["test"], nil)
495+
if tt.expectError != "" {
496+
assert.ErrorContains(t, err, tt.expectError)
497+
return
498+
}
457499
assert.NilError(t, err)
458500
assert.DeepEqual(t, tt.binds, binds)
459501
assert.DeepEqual(t, tt.mounts, mounts)

0 commit comments

Comments
 (0)