Bläddra i källkod

Parse a volume spec on the client, with support for windows drives

Signed-off-by: Daniel Nephin <dnephin@docker.com>
Daniel Nephin 8 år sedan
förälder
incheckning
32c955b8fe
3 ändrade filer med 254 tillägg och 1 borttagningar
  1. 119 0
      cli/compose/loader/volume.go
  2. 134 0
      cli/compose/loader/volume_test.go
  3. 1 1
      cli/compose/types/types.go

+ 119 - 0
cli/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
+}

+ 134 - 0
cli/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)
+	}
+}

+ 1 - 1
cli/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