Skip to content

Commit 3a75f29

Browse files
committed
Add compose support for cluster volumes
Signed-off-by: Drew Erny <derny@mirantis.com>
1 parent 247f568 commit 3a75f29

7 files changed

Lines changed: 411 additions & 16 deletions

File tree

cli/compose/convert/volume.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package convert
22

33
import (
4+
"strings"
5+
46
composetypes "github.com/docker/cli/cli/compose/types"
57
"github.com/docker/docker/api/types/mount"
68
"github.com/pkg/errors"
@@ -45,6 +47,9 @@ func handleVolumeToMount(
4547
if volume.Bind != nil {
4648
return mount.Mount{}, errors.New("bind options are incompatible with type volume")
4749
}
50+
if volume.Cluster != nil {
51+
return mount.Mount{}, errors.New("cluster options are incompatible with type volume")
52+
}
4853
// Anonymous volumes
4954
if volume.Source == "" {
5055
return result, nil
@@ -94,6 +99,9 @@ func handleBindToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, er
9499
if volume.Tmpfs != nil {
95100
return mount.Mount{}, errors.New("tmpfs options are incompatible with type bind")
96101
}
102+
if volume.Cluster != nil {
103+
return mount.Mount{}, errors.New("cluster options are incompatible with type bind")
104+
}
97105
if volume.Bind != nil {
98106
result.BindOptions = &mount.BindOptions{
99107
Propagation: mount.Propagation(volume.Bind.Propagation),
@@ -114,6 +122,9 @@ func handleTmpfsToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, e
114122
if volume.Volume != nil {
115123
return mount.Mount{}, errors.New("volume options are incompatible with type tmpfs")
116124
}
125+
if volume.Cluster != nil {
126+
return mount.Mount{}, errors.New("cluster options are incompatible with type tmpfs")
127+
}
117128
if volume.Tmpfs != nil {
118129
result.TmpfsOptions = &mount.TmpfsOptions{
119130
SizeBytes: volume.Tmpfs.Size,
@@ -142,6 +153,49 @@ func handleNpipeToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, e
142153
return result, nil
143154
}
144155

156+
func handleClusterToMount(
157+
volume composetypes.ServiceVolumeConfig,
158+
stackVolumes volumes,
159+
namespace Namespace,
160+
) (mount.Mount, error) {
161+
if volume.Source == "" {
162+
return mount.Mount{}, errors.New("invalid cluster source, source cannot be empty")
163+
}
164+
if volume.Tmpfs != nil {
165+
return mount.Mount{}, errors.New("tmpfs options are incompatible with type cluster")
166+
}
167+
if volume.Bind != nil {
168+
return mount.Mount{}, errors.New("bind options are incompatible with type cluster")
169+
}
170+
if volume.Volume != nil {
171+
return mount.Mount{}, errors.New("volume options are incompatible with type cluster")
172+
}
173+
174+
result := createMountFromVolume(volume)
175+
result.ClusterOptions = &mount.ClusterOptions{}
176+
177+
if !strings.HasPrefix(volume.Source, "group:") {
178+
// if the volume is a cluster volume and the source is a volumegroup, we
179+
// will ignore checking to see if such a volume is defined. the volume
180+
// group isn't namespaced, and there's no simple way to indicate that
181+
// external volumes with a given group exist.
182+
stackVolume, exists := stackVolumes[volume.Source]
183+
if !exists {
184+
return mount.Mount{}, errors.Errorf("undefined volume %q", volume.Source)
185+
}
186+
187+
// if the volume is not specified with a group source, we may namespace
188+
// the name, if one is not otherwise specified.
189+
if stackVolume.Name != "" {
190+
result.Source = stackVolume.Name
191+
} else {
192+
result.Source = namespace.Scope(volume.Source)
193+
}
194+
}
195+
196+
return result, nil
197+
}
198+
145199
func convertVolumeToMount(
146200
volume composetypes.ServiceVolumeConfig,
147201
stackVolumes volumes,
@@ -156,6 +210,8 @@ func convertVolumeToMount(
156210
return handleTmpfsToMount(volume)
157211
case "npipe":
158212
return handleNpipeToMount(volume)
213+
case "cluster":
214+
return handleClusterToMount(volume, stackVolumes, namespace)
159215
}
160-
return mount.Mount{}, errors.New("volume type must be volume, bind, tmpfs or npipe")
216+
return mount.Mount{}, errors.New("volume type must be volume, bind, tmpfs, npipe, or cluster")
161217
}

cli/compose/convert/volume_test.go

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func TestConvertVolumeToMountUnapprovedType(t *testing.T) {
4141
Target: "/foo/bar",
4242
}
4343
_, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
44-
assert.Error(t, err, "volume type must be volume, bind, tmpfs or npipe")
44+
assert.Error(t, err, "volume type must be volume, bind, tmpfs, npipe, or cluster")
4545
}
4646

4747
func TestConvertVolumeToMountConflictingOptionsBindInVolume(t *testing.T) {
@@ -359,3 +359,71 @@ func TestConvertVolumeToMountAnonymousNpipe(t *testing.T) {
359359
assert.NilError(t, err)
360360
assert.Check(t, is.DeepEqual(expected, mount))
361361
}
362+
363+
func TestConvertVolumeMountClusterName(t *testing.T) {
364+
stackVolumes := volumes{
365+
"my-csi": composetypes.VolumeConfig{
366+
Driver: "mycsidriver",
367+
Spec: &composetypes.ClusterVolumeSpec{
368+
Group: "mygroup",
369+
AccessMode: &composetypes.AccessMode{
370+
Scope: "single",
371+
Sharing: "none",
372+
BlockVolume: &composetypes.BlockVolume{},
373+
},
374+
Availability: "active",
375+
},
376+
},
377+
}
378+
379+
config := composetypes.ServiceVolumeConfig{
380+
Type: "cluster",
381+
Source: "my-csi",
382+
Target: "/srv",
383+
}
384+
385+
expected := mount.Mount{
386+
Type: mount.TypeCluster,
387+
Source: "foo_my-csi",
388+
Target: "/srv",
389+
ClusterOptions: &mount.ClusterOptions{},
390+
}
391+
392+
mount, err := convertVolumeToMount(config, stackVolumes, NewNamespace("foo"))
393+
assert.NilError(t, err)
394+
assert.Check(t, is.DeepEqual(expected, mount))
395+
}
396+
397+
func TestConvertVolumeMountClusterGroup(t *testing.T) {
398+
stackVolumes := volumes{
399+
"my-csi": composetypes.VolumeConfig{
400+
Driver: "mycsidriver",
401+
Spec: &composetypes.ClusterVolumeSpec{
402+
Group: "mygroup",
403+
AccessMode: &composetypes.AccessMode{
404+
Scope: "single",
405+
Sharing: "none",
406+
BlockVolume: &composetypes.BlockVolume{},
407+
},
408+
Availability: "active",
409+
},
410+
},
411+
}
412+
413+
config := composetypes.ServiceVolumeConfig{
414+
Type: "cluster",
415+
Source: "group:mygroup",
416+
Target: "/srv",
417+
}
418+
419+
expected := mount.Mount{
420+
Type: mount.TypeCluster,
421+
Source: "group:mygroup",
422+
Target: "/srv",
423+
ClusterOptions: &mount.ClusterOptions{},
424+
}
425+
426+
mount, err := convertVolumeToMount(config, stackVolumes, NewNamespace("foo"))
427+
assert.NilError(t, err)
428+
assert.Check(t, is.DeepEqual(expected, mount))
429+
}

cli/compose/loader/full-example.yml

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version: "3.9"
1+
version: "3.10"
22

33
services:
44
foo:
@@ -287,6 +287,9 @@ services:
287287
target: /opt
288288
tmpfs:
289289
size: 10000
290+
- type: cluster
291+
source: group:mygroup
292+
target: /srv
290293

291294
working_dir: /code
292295
x-bar: baz
@@ -379,6 +382,36 @@ volumes:
379382
x-bar: baz
380383
x-foo: bar
381384

385+
cluster-volume:
386+
driver: my-csi-driver
387+
spec:
388+
group: mygroup
389+
access_mode:
390+
scope: single
391+
sharing: none
392+
block_volume: {}
393+
accessibility_requirements:
394+
requisite:
395+
- segments:
396+
- region=R1
397+
- zone=Z1
398+
- segments:
399+
region: R1
400+
zone: Z2
401+
preferred:
402+
- segments:
403+
region: R1
404+
zone: Z1
405+
capacity_range:
406+
required_bytes: 1G
407+
limit_bytes: 8G
408+
secrets:
409+
- key: mycsisecret
410+
secret: secret1
411+
- key: mycsisecret2
412+
secret: secret4
413+
availability: active
414+
382415
configs:
383416
config1:
384417
file: ./config_data

0 commit comments

Comments
 (0)