From a442213b9229fac39d096133c5e8a2da1102ea3c Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 24 Jan 2017 16:53:36 -0500 Subject: [PATCH] Parse a volume spec on the client, with support for windows drives Signed-off-by: Daniel Nephin --- compose/loader/volume.go | 119 ++++++++++++++++++++++++++++++ compose/loader/volume_test.go | 134 ++++++++++++++++++++++++++++++++++ compose/types/types.go | 2 +- 3 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 compose/loader/volume.go create mode 100644 compose/loader/volume_test.go diff --git a/compose/loader/volume.go b/compose/loader/volume.go new file mode 100644 index 0000000000..3f33492ea7 --- /dev/null +++ b/compose/loader/volume.go @@ -0,0 +1,119 @@ +package loader + +import ( + "strings" + "unicode" + "unicode/utf8" + + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/cli/compose/types" + "github.com/pkg/errors" +) + +func parseVolume(spec string) (types.ServiceVolumeConfig, error) { + volume := types.ServiceVolumeConfig{} + + switch len(spec) { + case 0: + return volume, errors.New("invalid empty volume spec") + case 1, 2: + volume.Target = spec + volume.Type = string(mount.TypeVolume) + return volume, nil + } + + buffer := []rune{} + for _, char := range spec { + switch { + case isWindowsDrive(char, buffer, volume): + buffer = append(buffer, char) + case char == ':': + if err := populateFieldFromBuffer(char, buffer, &volume); err != nil { + return volume, errors.Wrapf(err, "invalid spec: %s", spec) + } + buffer = []rune{} + default: + buffer = append(buffer, char) + } + } + + if err := populateFieldFromBuffer(rune(0), buffer, &volume); err != nil { + return volume, errors.Wrapf(err, "invalid spec: %s", spec) + } + populateType(&volume) + return volume, nil +} + +func isWindowsDrive(char rune, buffer []rune, volume types.ServiceVolumeConfig) bool { + return char == ':' && len(buffer) == 1 && unicode.IsLetter(buffer[0]) +} + +func populateFieldFromBuffer(char rune, buffer []rune, volume *types.ServiceVolumeConfig) error { + strBuffer := string(buffer) + switch { + case len(buffer) == 0: + return errors.New("empty section between colons") + // Anonymous volume + case volume.Source == "" && char == rune(0): + volume.Target = strBuffer + return nil + case volume.Source == "": + volume.Source = strBuffer + return nil + case volume.Target == "": + volume.Target = strBuffer + return nil + case char == ':': + return errors.New("too many colons") + } + for _, option := range strings.Split(strBuffer, ",") { + switch option { + case "ro": + volume.ReadOnly = true + case "nocopy": + volume.Volume = &types.ServiceVolumeVolume{NoCopy: true} + default: + if isBindOption(option) { + volume.Bind = &types.ServiceVolumeBind{Propagation: option} + } else { + return errors.Errorf("unknown option: %s", option) + } + } + } + return nil +} + +func isBindOption(option string) bool { + for _, propagation := range mount.Propagations { + if mount.Propagation(option) == propagation { + return true + } + } + return false +} + +func populateType(volume *types.ServiceVolumeConfig) { + switch { + // Anonymous volume + case volume.Source == "": + volume.Type = string(mount.TypeVolume) + case isFilePath(volume.Source): + volume.Type = string(mount.TypeBind) + default: + volume.Type = string(mount.TypeVolume) + } +} + +func isFilePath(source string) bool { + switch source[0] { + case '.', '/', '~': + return true + } + + // Windows absolute path + first, next := utf8.DecodeRuneInString(source) + if unicode.IsLetter(first) && source[next] == ':' { + return true + } + return false +} diff --git a/compose/loader/volume_test.go b/compose/loader/volume_test.go new file mode 100644 index 0000000000..0735d5a54a --- /dev/null +++ b/compose/loader/volume_test.go @@ -0,0 +1,134 @@ +package loader + +import ( + "testing" + + "github.com/docker/docker/cli/compose/types" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestParseVolumeAnonymousVolume(t *testing.T) { + for _, path := range []string{"/path", "/path/foo"} { + volume, err := parseVolume(path) + expected := types.ServiceVolumeConfig{Type: "volume", Target: path} + assert.NilError(t, err) + assert.DeepEqual(t, volume, expected) + } +} + +func TestParseVolumeAnonymousVolumeWindows(t *testing.T) { + for _, path := range []string{"C:\\path", "Z:\\path\\foo"} { + volume, err := parseVolume(path) + expected := types.ServiceVolumeConfig{Type: "volume", Target: path} + assert.NilError(t, err) + assert.DeepEqual(t, volume, expected) + } +} + +func TestParseVolumeTooManyColons(t *testing.T) { + _, err := parseVolume("/foo:/foo:ro:foo") + assert.Error(t, err, "too many colons") +} + +func TestParseVolumeShortVolumes(t *testing.T) { + for _, path := range []string{".", "/a"} { + volume, err := parseVolume(path) + expected := types.ServiceVolumeConfig{Type: "volume", Target: path} + assert.NilError(t, err) + assert.DeepEqual(t, volume, expected) + } +} + +func TestParseVolumeMissingSource(t *testing.T) { + for _, spec := range []string{":foo", "/foo::ro"} { + _, err := parseVolume(spec) + assert.Error(t, err, "empty section between colons") + } +} + +func TestParseVolumeBindMount(t *testing.T) { + for _, path := range []string{"./foo", "~/thing", "../other", "/foo", "/home/user"} { + volume, err := parseVolume(path + ":/target") + expected := types.ServiceVolumeConfig{ + Type: "bind", + Source: path, + Target: "/target", + } + assert.NilError(t, err) + assert.DeepEqual(t, volume, expected) + } +} + +func TestParseVolumeRelativeBindMountWindows(t *testing.T) { + for _, path := range []string{ + "./foo", + "~/thing", + "../other", + "D:\\path", "/home/user", + } { + volume, err := parseVolume(path + ":d:\\target") + expected := types.ServiceVolumeConfig{ + Type: "bind", + Source: path, + Target: "d:\\target", + } + assert.NilError(t, err) + assert.DeepEqual(t, volume, expected) + } +} + +func TestParseVolumeWithBindOptions(t *testing.T) { + volume, err := parseVolume("/source:/target:slave") + expected := types.ServiceVolumeConfig{ + Type: "bind", + Source: "/source", + Target: "/target", + Bind: &types.ServiceVolumeBind{Propagation: "slave"}, + } + assert.NilError(t, err) + assert.DeepEqual(t, volume, expected) +} + +func TestParseVolumeWithBindOptionsWindows(t *testing.T) { + volume, err := parseVolume("C:\\source\\foo:D:\\target:ro,rprivate") + expected := types.ServiceVolumeConfig{ + Type: "bind", + Source: "C:\\source\\foo", + Target: "D:\\target", + ReadOnly: true, + Bind: &types.ServiceVolumeBind{Propagation: "rprivate"}, + } + assert.NilError(t, err) + assert.DeepEqual(t, volume, expected) +} + +func TestParseVolumeWithInvalidVolumeOptions(t *testing.T) { + _, err := parseVolume("name:/target:bogus") + assert.Error(t, err, "invalid spec: name:/target:bogus: unknown option: bogus") +} + +func TestParseVolumeWithVolumeOptions(t *testing.T) { + volume, err := parseVolume("name:/target:nocopy") + expected := types.ServiceVolumeConfig{ + Type: "volume", + Source: "name", + Target: "/target", + Volume: &types.ServiceVolumeVolume{NoCopy: true}, + } + assert.NilError(t, err) + assert.DeepEqual(t, volume, expected) +} + +func TestParseVolumeWithReadOnly(t *testing.T) { + for _, path := range []string{"./foo", "/home/user"} { + volume, err := parseVolume(path + ":/target:ro") + expected := types.ServiceVolumeConfig{ + Type: "bind", + Source: path, + Target: "/target", + ReadOnly: true, + } + assert.NilError(t, err) + assert.DeepEqual(t, volume, expected) + } +} diff --git a/compose/types/types.go b/compose/types/types.go index d5454ec2f6..307b5fd907 100644 --- a/compose/types/types.go +++ b/compose/types/types.go @@ -235,7 +235,7 @@ type ServiceVolumeConfig struct { // ServiceVolumeBind are options for a service volume of type bind type ServiceVolumeBind struct { - Propogation string + Propagation string } // ServiceVolumeVolume are options for a service volume of type volume