Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions pkg/compose/api_versions.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ package compose
// Docker Engine API version constants.
// These versions correspond to specific Docker Engine releases and their features.
const (
// apiVersion142 represents Docker Engine API version 1.42 (Engine v23.0).
//
// New features in this version:
// - CreateMountpoint option for bind mounts (allows create_host_path: false)
//
// Before this version:
// - Bind mounts always created host path if missing, regardless of CreateMountpoint setting
apiVersion142 = "1.42"

// apiVersion148 represents Docker Engine API version 1.48 (Engine v28.0).
//
// New features in this version:
Expand All @@ -43,6 +52,9 @@ const (
// Docker Engine version strings for user-facing error messages.
// These should be used in error messages to provide clear version requirements.
const (
// dockerEngineV23 is the major version string for Docker Engine 23.x
dockerEngineV23 = "v23"

// dockerEngineV28 is the major version string for Docker Engine 28.x
dockerEngineV28 = "v28"

Expand Down
15 changes: 11 additions & 4 deletions pkg/compose/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,13 @@ func (s *composeService) buildContainerVolumes(
return nil, nil, err
}

// Check Docker Engine API version for CreateMountpoint support
version, err := s.RuntimeVersion(ctx)
if err != nil {
return nil, nil, err
}
supportsCreateMountpoint := versions.GreaterThanOrEqualTo(version, apiVersion142)

for _, m := range mountOptions {
switch m.Type {
case mount.TypeBind:
Expand All @@ -885,6 +892,10 @@ func (s *composeService) buildContainerVolumes(
binds = append(binds, toBindString(source, v))
continue
}
// Check if create_host_path: false is used on an engine that doesn't support it
if v.Bind != nil && !bool(v.Bind.CreateHostPath) && !supportsCreateMountpoint {
return nil, nil, fmt.Errorf("bind mount create_host_path: false requires Docker Engine %s or later", dockerEngineV23)
}
}
case mount.TypeVolume:
v := findVolumeByTarget(service.Volumes, m.Target)
Expand All @@ -897,10 +908,6 @@ func (s *composeService) buildContainerVolumes(
}
}
case mount.TypeImage:
version, err := s.RuntimeVersion(ctx)
if err != nil {
return nil, nil, err
}
if versions.LessThan(version, apiVersion148) {
return nil, nil, fmt.Errorf("volume with type=image require Docker Engine %s or later", dockerEngineV28)
}
Expand Down
64 changes: 53 additions & 11 deletions pkg/compose/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (

composeloader "github.com/compose-spec/compose-go/v2/loader"
composetypes "github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/config/configfile"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/container"
mountTypes "github.com/moby/moby/api/types/mount"
Expand All @@ -36,6 +37,7 @@ import (
"gotest.tools/v3/assert/cmp"

"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/mocks"
)

func TestBuildBindMount(t *testing.T) {
Expand Down Expand Up @@ -346,13 +348,16 @@ func Test_buildContainerVolumes(t *testing.T) {
assert.NilError(t, err)

tests := []struct {
name string
yaml string
binds []string
mounts []mountTypes.Mount
name string
yaml string
binds []string
mounts []mountTypes.Mount
apiVersion string
expectError string
}{
{
name: "bind mount local path",
name: "bind mount local path",
apiVersion: "1.44",
yaml: `
services:
test:
Expand All @@ -363,7 +368,8 @@ services:
mounts: nil,
},
{
name: "bind mount, not create host path",
name: "bind mount, not create host path",
apiVersion: "1.44",
yaml: `
services:
test:
Expand All @@ -385,7 +391,23 @@ services:
},
},
{
name: "mount volume",
name: "bind mount, not create host path with old engine",
apiVersion: "1.41",
expectError: "bind mount create_host_path: false requires Docker Engine v23 or later",
yaml: `
services:
test:
volumes:
- type: bind
source: ./data
target: /data
bind:
create_host_path: false
`,
},
{
name: "mount volume",
apiVersion: "1.44",
yaml: `
services:
test:
Expand All @@ -399,7 +421,8 @@ volumes:
mounts: nil,
},
{
name: "mount volume, readonly",
name: "mount volume, readonly",
apiVersion: "1.44",
yaml: `
services:
test:
Expand All @@ -413,7 +436,8 @@ volumes:
mounts: nil,
},
{
name: "mount volume subpath",
name: "mount volume subpath",
apiVersion: "1.44",
yaml: `
services:
test:
Expand All @@ -440,6 +464,21 @@ volumes:
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
apiClient := mocks.NewMockAPIClient(mockCtrl)
cli := mocks.NewMockCli(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
cli.EXPECT().Client().Return(apiClient).AnyTimes()
cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes()

// force `RuntimeVersion` to fetch fresh version
runtimeVersion = runtimeVersionCache{}
apiClient.EXPECT().ServerVersion(gomock.Any(), gomock.Any()).Return(client.ServerVersionResult{
APIVersion: tt.apiVersion,
}, nil).AnyTimes()

p, err := composeloader.LoadWithContext(t.Context(), composetypes.ConfigDetails{
ConfigFiles: []composetypes.ConfigFile{
{
Expand All @@ -452,8 +491,11 @@ volumes:
options.SkipConsistencyCheck = true
})
assert.NilError(t, err)
s := &composeService{}
binds, mounts, err := s.buildContainerVolumes(t.Context(), *p, p.Services["test"], nil)
binds, mounts, err := tested.(*composeService).buildContainerVolumes(t.Context(), *p, p.Services["test"], nil)
if tt.expectError != "" {
assert.ErrorContains(t, err, tt.expectError)
return
}
assert.NilError(t, err)
assert.DeepEqual(t, tt.binds, binds)
assert.DeepEqual(t, tt.mounts, mounts)
Expand Down