From cd69d082eac03c0b0debd3782c4ea752d73a7570 Mon Sep 17 00:00:00 2001 From: Ethan Haynes Date: Tue, 16 Jan 2018 10:52:26 -0600 Subject: [PATCH] added support for tmpfs-mode in compose file Signed-off-by: Ethan Haynes --- cli/compose/convert/volume.go | 102 +++++++++++++++++------- cli/compose/convert/volume_test.go | 115 +++++++++++++++++++++++++++- cli/compose/loader/full-example.yml | 8 +- cli/compose/loader/loader_test.go | 103 +++++++++++++++++++++++++ cli/compose/types/types.go | 7 +- 5 files changed, 304 insertions(+), 31 deletions(-) diff --git a/cli/compose/convert/volume.go b/cli/compose/convert/volume.go index 36dc54a13b..38806d2972 100644 --- a/cli/compose/convert/volume.go +++ b/cli/compose/convert/volume.go @@ -22,43 +22,37 @@ func Volumes(serviceVolumes []composetypes.ServiceVolumeConfig, stackVolumes vol return mounts, nil } -func convertVolumeToMount( +func createMountFromVolume(volume composetypes.ServiceVolumeConfig) mount.Mount { + return mount.Mount{ + Type: mount.Type(volume.Type), + Target: volume.Target, + ReadOnly: volume.ReadOnly, + Source: volume.Source, + Consistency: mount.Consistency(volume.Consistency), + } +} + +func handleVolumeToMount( volume composetypes.ServiceVolumeConfig, stackVolumes volumes, namespace Namespace, ) (mount.Mount, error) { - result := mount.Mount{ - Type: mount.Type(volume.Type), - Source: volume.Source, - Target: volume.Target, - ReadOnly: volume.ReadOnly, - Consistency: mount.Consistency(volume.Consistency), - } + result := createMountFromVolume(volume) + if volume.Tmpfs != nil { + return mount.Mount{}, errors.New("tmpfs options are incompatible with type volume") + } + if volume.Bind != nil { + return mount.Mount{}, errors.New("bind options are incompatible with type volume") + } // Anonymous volumes if volume.Source == "" { return result, nil } - if volume.Type == "volume" && volume.Bind != nil { - return result, errors.New("bind options are incompatible with type volume") - } - if volume.Type == "bind" && volume.Volume != nil { - return result, errors.New("volume options are incompatible with type bind") - } - - if volume.Bind != nil { - result.BindOptions = &mount.BindOptions{ - Propagation: mount.Propagation(volume.Bind.Propagation), - } - } - // Binds volumes - if volume.Type == "bind" { - return result, nil - } stackVolume, exists := stackVolumes[volume.Source] if !exists { - return result, errors.Errorf("undefined volume %q", volume.Source) + return mount.Mount{}, errors.Errorf("undefined volume %q", volume.Source) } result.Source = namespace.Scope(volume.Source) @@ -85,6 +79,62 @@ func convertVolumeToMount( } } - // Named volumes return result, nil } + +func handleBindToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) { + result := createMountFromVolume(volume) + + if volume.Source == "" { + return mount.Mount{}, errors.New("invalid bind source, source cannot be empty") + } + if volume.Volume != nil { + return mount.Mount{}, errors.New("volume options are incompatible with type bind") + } + if volume.Tmpfs != nil { + return mount.Mount{}, errors.New("tmpfs options are incompatible with type bind") + } + if volume.Bind != nil { + result.BindOptions = &mount.BindOptions{ + Propagation: mount.Propagation(volume.Bind.Propagation), + } + } + return result, nil +} + +func handleTmpfsToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) { + result := createMountFromVolume(volume) + + if volume.Source != "" { + return mount.Mount{}, errors.New("invalid tmpfs source, source must be empty") + } + if volume.Bind != nil { + return mount.Mount{}, errors.New("bind options are incompatible with type tmpfs") + } + if volume.Volume != nil { + return mount.Mount{}, errors.New("volume options are incompatible with type tmpfs") + } + if volume.Tmpfs != nil { + result.TmpfsOptions = &mount.TmpfsOptions{ + SizeBytes: volume.Tmpfs.Size, + } + } + return result, nil +} + +func convertVolumeToMount( + volume composetypes.ServiceVolumeConfig, + stackVolumes volumes, + namespace Namespace, +) (mount.Mount, error) { + + switch volume.Type { + case "volume", "": + return handleVolumeToMount(volume, stackVolumes, namespace) + case "bind": + return handleBindToMount(volume) + case "tmpfs": + return handleTmpfsToMount(volume) + } + return mount.Mount{}, errors.New("volume type must be volume, bind, or tmpfs") +} diff --git a/cli/compose/convert/volume_test.go b/cli/compose/convert/volume_test.go index 3df88c1728..1603e8453a 100644 --- a/cli/compose/convert/volume_test.go +++ b/cli/compose/convert/volume_test.go @@ -22,7 +22,28 @@ func TestConvertVolumeToMountAnonymousVolume(t *testing.T) { assert.Equal(t, expected, mount) } -func TestConvertVolumeToMountConflictingOptionsBind(t *testing.T) { +func TestConvertVolumeToMountAnonymousBind(t *testing.T) { + config := composetypes.ServiceVolumeConfig{ + Type: "bind", + Target: "/foo/bar", + Bind: &composetypes.ServiceVolumeBind{ + Propagation: "slave", + }, + } + _, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo")) + assert.EqualError(t, err, "invalid bind source, source cannot be empty") +} + +func TestConvertVolumeToMountUnapprovedType(t *testing.T) { + config := composetypes.ServiceVolumeConfig{ + Type: "foo", + Target: "/foo/bar", + } + _, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo")) + assert.EqualError(t, err, "volume type must be volume, bind, or tmpfs") +} + +func TestConvertVolumeToMountConflictingOptionsBindInVolume(t *testing.T) { namespace := NewNamespace("foo") config := composetypes.ServiceVolumeConfig{ @@ -37,7 +58,22 @@ func TestConvertVolumeToMountConflictingOptionsBind(t *testing.T) { assert.EqualError(t, err, "bind options are incompatible with type volume") } -func TestConvertVolumeToMountConflictingOptionsVolume(t *testing.T) { +func TestConvertVolumeToMountConflictingOptionsTmpfsInVolume(t *testing.T) { + namespace := NewNamespace("foo") + + config := composetypes.ServiceVolumeConfig{ + Type: "volume", + Source: "foo", + Target: "/target", + Tmpfs: &composetypes.ServiceVolumeTmpfs{ + Size: 1000, + }, + } + _, err := convertVolumeToMount(config, volumes{}, namespace) + assert.EqualError(t, err, "tmpfs options are incompatible with type volume") +} + +func TestConvertVolumeToMountConflictingOptionsVolumeInBind(t *testing.T) { namespace := NewNamespace("foo") config := composetypes.ServiceVolumeConfig{ @@ -52,6 +88,49 @@ func TestConvertVolumeToMountConflictingOptionsVolume(t *testing.T) { assert.EqualError(t, err, "volume options are incompatible with type bind") } +func TestConvertVolumeToMountConflictingOptionsTmpfsInBind(t *testing.T) { + namespace := NewNamespace("foo") + + config := composetypes.ServiceVolumeConfig{ + Type: "bind", + Source: "/foo", + Target: "/target", + Tmpfs: &composetypes.ServiceVolumeTmpfs{ + Size: 1000, + }, + } + _, err := convertVolumeToMount(config, volumes{}, namespace) + assert.EqualError(t, err, "tmpfs options are incompatible with type bind") +} + +func TestConvertVolumeToMountConflictingOptionsBindInTmpfs(t *testing.T) { + namespace := NewNamespace("foo") + + config := composetypes.ServiceVolumeConfig{ + Type: "tmpfs", + Target: "/target", + Bind: &composetypes.ServiceVolumeBind{ + Propagation: "slave", + }, + } + _, err := convertVolumeToMount(config, volumes{}, namespace) + assert.EqualError(t, err, "bind options are incompatible with type tmpfs") +} + +func TestConvertVolumeToMountConflictingOptionsVolumeInTmpfs(t *testing.T) { + namespace := NewNamespace("foo") + + config := composetypes.ServiceVolumeConfig{ + Type: "tmpfs", + Target: "/target", + Volume: &composetypes.ServiceVolumeVolume{ + NoCopy: true, + }, + } + _, err := convertVolumeToMount(config, volumes{}, namespace) + assert.EqualError(t, err, "volume options are incompatible with type tmpfs") +} + func TestConvertVolumeToMountNamedVolume(t *testing.T) { stackVolumes := volumes{ "normal": composetypes.VolumeConfig{ @@ -231,3 +310,35 @@ func TestConvertVolumeToMountVolumeDoesNotExist(t *testing.T) { _, err := convertVolumeToMount(config, volumes{}, namespace) assert.EqualError(t, err, "undefined volume \"unknown\"") } + +func TestConvertTmpfsToMountVolume(t *testing.T) { + config := composetypes.ServiceVolumeConfig{ + Type: "tmpfs", + Target: "/foo/bar", + Tmpfs: &composetypes.ServiceVolumeTmpfs{ + Size: 1000, + }, + } + expected := mount.Mount{ + Type: mount.TypeTmpfs, + Target: "/foo/bar", + TmpfsOptions: &mount.TmpfsOptions{SizeBytes: 1000}, + } + mount, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo")) + assert.NoError(t, err) + assert.Equal(t, expected, mount) +} + +func TestConvertTmpfsToMountVolumeWithSource(t *testing.T) { + config := composetypes.ServiceVolumeConfig{ + Type: "tmpfs", + Source: "/bar", + Target: "/foo/bar", + Tmpfs: &composetypes.ServiceVolumeTmpfs{ + Size: 1000, + }, + } + + _, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo")) + assert.EqualError(t, err, "invalid tmpfs source, source must be empty") +} diff --git a/cli/compose/loader/full-example.yml b/cli/compose/loader/full-example.yml index 0a52111c32..92a60f8e05 100644 --- a/cli/compose/loader/full-example.yml +++ b/cli/compose/loader/full-example.yml @@ -1,4 +1,4 @@ -version: "3.5" +version: "3.6" services: foo: @@ -10,7 +10,7 @@ services: foo: bar target: foo network: foo - cache_from: + cache_from: - foo - bar labels: [FOO=BAR] @@ -249,6 +249,10 @@ services: source: ./opt target: /opt consistency: cached + - type: tmpfs + target: /opt + tmpfs: + size: 10000 working_dir: /code diff --git a/cli/compose/loader/loader_test.go b/cli/compose/loader/loader_test.go index 2e607be17f..75fdce2c2a 100644 --- a/cli/compose/loader/loader_test.go +++ b/cli/compose/loader/loader_test.go @@ -1149,6 +1149,9 @@ func TestFullExample(t *testing.T) { {Source: homeDir + "/configs", Target: "/etc/configs/", Type: "bind", ReadOnly: true}, {Source: "datavolume", Target: "/var/lib/mysql", Type: "volume"}, {Source: workingDir + "/opt", Target: "/opt", Consistency: "cached", Type: "bind"}, + {Target: "/opt", Type: "tmpfs", Tmpfs: &types.ServiceVolumeTmpfs{ + Size: int64(10000), + }}, }, WorkingDir: "/code", } @@ -1220,6 +1223,106 @@ func TestFullExample(t *testing.T) { assert.Equal(t, expectedVolumeConfig, config.Volumes) } +func TestLoadTmpfsVolume(t *testing.T) { + config, err := loadYAML(` +version: "3.6" +services: + tmpfs: + image: nginx:latest + volumes: + - type: tmpfs + target: /app + tmpfs: + size: 10000 +`) + require.NoError(t, err) + + expected := types.ServiceVolumeConfig{ + Target: "/app", + Type: "tmpfs", + Tmpfs: &types.ServiceVolumeTmpfs{ + Size: int64(10000), + }, + } + + require.Len(t, config.Services, 1) + assert.Len(t, config.Services[0].Volumes, 1) + assert.Equal(t, expected, config.Services[0].Volumes[0]) +} + +func TestLoadTmpfsVolumeAdditionalPropertyNotAllowed(t *testing.T) { + _, err := loadYAML(` +version: "3.5" +services: + tmpfs: + image: nginx:latest + volumes: + - type: tmpfs + target: /app + tmpfs: + size: 10000 +`) + require.Error(t, err) + assert.Contains(t, err.Error(), "services.tmpfs.volumes.0 Additional property tmpfs is not allowed") +} + +func TestLoadTmpfsVolumeSizeCanBeZero(t *testing.T) { + config, err := loadYAML(` +version: "3.6" +services: + tmpfs: + image: nginx:latest + volumes: + - type: tmpfs + target: /app + tmpfs: + size: 0 +`) + require.NoError(t, err) + + expected := types.ServiceVolumeConfig{ + Target: "/app", + Type: "tmpfs", + Tmpfs: &types.ServiceVolumeTmpfs{}, + } + + require.Len(t, config.Services, 1) + assert.Len(t, config.Services[0].Volumes, 1) + assert.Equal(t, expected, config.Services[0].Volumes[0]) +} + +func TestLoadTmpfsVolumeSizeMustBeGTEQZero(t *testing.T) { + _, err := loadYAML(` +version: "3.6" +services: + tmpfs: + image: nginx:latest + volumes: + - type: tmpfs + target: /app + tmpfs: + size: -1 +`) + require.Error(t, err) + assert.Contains(t, err.Error(), "services.tmpfs.volumes.0.tmpfs.size Must be greater than or equal to 0") +} + +func TestLoadTmpfsVolumeSizeMustBeInteger(t *testing.T) { + _, err := loadYAML(` +version: "3.6" +services: + tmpfs: + image: nginx:latest + volumes: + - type: tmpfs + target: /app + tmpfs: + size: 0.0001 +`) + require.Error(t, err) + assert.Contains(t, err.Error(), "services.tmpfs.volumes.0.tmpfs.size must be a integer") +} + func serviceSort(services []types.ServiceConfig) []types.ServiceConfig { sort.Sort(servicesByName(services)) return services diff --git a/cli/compose/types/types.go b/cli/compose/types/types.go index 6a9b8c80c0..6bfc21b23e 100644 --- a/cli/compose/types/types.go +++ b/cli/compose/types/types.go @@ -23,7 +23,6 @@ var UnsupportedProperties = []string{ "security_opt", "shm_size", "sysctls", - "tmpfs", "ulimits", "userns_mode", } @@ -284,6 +283,7 @@ type ServiceVolumeConfig struct { Consistency string Bind *ServiceVolumeBind Volume *ServiceVolumeVolume + Tmpfs *ServiceVolumeTmpfs } // ServiceVolumeBind are options for a service volume of type bind @@ -296,6 +296,11 @@ type ServiceVolumeVolume struct { NoCopy bool `mapstructure:"nocopy"` } +// ServiceVolumeTmpfs are options for a service volume of type tmpfs +type ServiceVolumeTmpfs struct { + Size int64 +} + // FileReferenceConfig for a reference to a swarm file object type FileReferenceConfig struct { Source string