From 5b9f0a86bdf68528bdbd82255c79cfbd2a1c0ed9 Mon Sep 17 00:00:00 2001 From: Laurent Goderre Date: Wed, 15 Jan 2025 16:28:30 -0500 Subject: [PATCH] Add image mount options --- cli/compose/convert/volume.go | 40 +++++++++++++++++++++++++++++++++++ cli/compose/types/types.go | 6 ++++++ opts/mount.go | 12 +++++++++++ opts/mount_test.go | 21 ++++++++++++++++++ 4 files changed, 79 insertions(+) diff --git a/cli/compose/convert/volume.go b/cli/compose/convert/volume.go index 387362203feb..a60f4bd32e3a 100644 --- a/cli/compose/convert/volume.go +++ b/cli/compose/convert/volume.go @@ -40,6 +40,9 @@ func handleVolumeToMount( ) (mount.Mount, error) { result := createMountFromVolume(volume) + if volume.Image != nil { + return mount.Mount{}, errors.New("images options are incompatible with type volume") + } if volume.Tmpfs != nil { return mount.Mount{}, errors.New("tmpfs options are incompatible with type volume") } @@ -86,6 +89,32 @@ func handleVolumeToMount( return result, nil } +func handleImageToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) { + result := createMountFromVolume(volume) + + if volume.Source == "" { + return mount.Mount{}, errors.New("invalid image source, source cannot be empty") + } + if volume.Volume != nil { + return mount.Mount{}, errors.New("volume options are incompatible with type image") + } + if volume.Bind != nil { + return mount.Mount{}, errors.New("bind options are incompatible with type image") + } + if volume.Tmpfs != nil { + return mount.Mount{}, errors.New("tmpfs options are incompatible with type image") + } + if volume.Cluster != nil { + return mount.Mount{}, errors.New("cluster options are incompatible with type image") + } + if volume.Bind != nil { + result.BindOptions = &mount.BindOptions{ + Propagation: mount.Propagation(volume.Bind.Propagation), + } + } + return result, nil +} + func handleBindToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) { result := createMountFromVolume(volume) @@ -95,6 +124,9 @@ func handleBindToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, er if volume.Volume != nil { return mount.Mount{}, errors.New("volume options are incompatible with type bind") } + if volume.Image != nil { + return mount.Mount{}, errors.New("image options are incompatible with type bind") + } if volume.Tmpfs != nil { return mount.Mount{}, errors.New("tmpfs options are incompatible with type bind") } @@ -121,6 +153,9 @@ func handleTmpfsToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, e if volume.Volume != nil { return mount.Mount{}, errors.New("volume options are incompatible with type tmpfs") } + if volume.Image != nil { + return mount.Mount{}, errors.New("image options are incompatible with type tmpfs") + } if volume.Cluster != nil { return mount.Mount{}, errors.New("cluster options are incompatible with type tmpfs") } @@ -141,6 +176,9 @@ func handleNpipeToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, e if volume.Volume != nil { return mount.Mount{}, errors.New("volume options are incompatible with type npipe") } + if volume.Image != nil { + return mount.Mount{}, errors.New("image options are incompatible with type npipe") + } if volume.Tmpfs != nil { return mount.Mount{}, errors.New("tmpfs options are incompatible with type npipe") } @@ -203,6 +241,8 @@ func convertVolumeToMount( switch volume.Type { case "volume", "": return handleVolumeToMount(volume, stackVolumes, namespace) + case "image": + return handleImageToMount(volume) case "bind": return handleBindToMount(volume) case "tmpfs": diff --git a/cli/compose/types/types.go b/cli/compose/types/types.go index 55b80365feca..7f425df74ab5 100644 --- a/cli/compose/types/types.go +++ b/cli/compose/types/types.go @@ -398,6 +398,7 @@ type ServiceVolumeConfig struct { Consistency string `yaml:",omitempty" json:"consistency,omitempty"` Bind *ServiceVolumeBind `yaml:",omitempty" json:"bind,omitempty"` Volume *ServiceVolumeVolume `yaml:",omitempty" json:"volume,omitempty"` + Image *ServiceVolumeImage `yaml:",omitempty" json:"volume,omitempty"` Tmpfs *ServiceVolumeTmpfs `yaml:",omitempty" json:"tmpfs,omitempty"` Cluster *ServiceVolumeCluster `yaml:",omitempty" json:"cluster,omitempty"` } @@ -412,6 +413,11 @@ type ServiceVolumeVolume struct { NoCopy bool `mapstructure:"nocopy" yaml:"nocopy,omitempty" json:"nocopy,omitempty"` } +// ServiceVolumeImage are options for a service volume of type image +type ServiceVolumeImage struct { + Subpath bool `mapstructure:"subpath" yaml:"subpath,omitempty" json:"subpath,omitempty"` +} + // ServiceVolumeTmpfs are options for a service volume of type tmpfs type ServiceVolumeTmpfs struct { Size int64 `yaml:",omitempty" json:"size,omitempty"` diff --git a/opts/mount.go b/opts/mount.go index 3a4ee31a27c7..5e23a513945a 100644 --- a/opts/mount.go +++ b/opts/mount.go @@ -43,6 +43,13 @@ func (m *MountOpt) Set(value string) error { return mount.VolumeOptions } + imageOptions := func() *mounttypes.ImageOptions { + if mount.ImageOptions == nil { + mount.ImageOptions = new(mounttypes.ImageOptions) + } + return mount.ImageOptions + } + bindOptions := func() *mounttypes.BindOptions { if mount.BindOptions == nil { mount.BindOptions = new(mounttypes.BindOptions) @@ -147,6 +154,8 @@ func (m *MountOpt) Set(value string) error { volumeOptions().DriverConfig.Options = make(map[string]string) } setValueOnMap(volumeOptions().DriverConfig.Options, val) + case "image-subpath": + imageOptions().Subpath = val case "tmpfs-size": sizeBytes, err := units.RAMInBytes(val) if err != nil { @@ -175,6 +184,9 @@ func (m *MountOpt) Set(value string) error { if mount.VolumeOptions != nil && mount.Type != mounttypes.TypeVolume { return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", mount.Type) } + if mount.ImageOptions != nil && mount.Type != mounttypes.TypeImage { + return fmt.Errorf("cannot mix 'image-*' options with mount type '%s'", mount.Type) + } if mount.BindOptions != nil && mount.Type != mounttypes.TypeBind { return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", mount.Type) } diff --git a/opts/mount_test.go b/opts/mount_test.go index fb3cd0329a92..d21556e144fc 100644 --- a/opts/mount_test.go +++ b/opts/mount_test.go @@ -188,6 +188,27 @@ func TestMountOptTypeConflict(t *testing.T) { assert.ErrorContains(t, m.Set("type=volume,target=/foo,source=/foo,bind-propagation=rprivate"), "cannot mix") } +func TestMountOptSetImageNoError(t *testing.T) { + for _, testcase := range []string{ + "type=image,source=foo,target=/target,image-subpath=/bar", + } { + var mount MountOpt + + assert.NilError(t, mount.Set(testcase)) + + mounts := mount.Value() + assert.Assert(t, is.Len(mounts, 1)) + assert.Check(t, is.DeepEqual(mounttypes.Mount{ + Type: mounttypes.TypeImage, + Source: "foo", + Target: "/target", + ImageOptions: &mounttypes.ImageOptions{ + Subpath: "/bar", + }, + }, mounts[0])) + } +} + func TestMountOptSetTmpfsNoError(t *testing.T) { for _, testcase := range []string{ // tests several aliases that should have same result.