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

Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
Daniel Nephin 2017-01-24 16:53:36 -05:00
parent 65c899bee5
commit 32c955b8fe
3 changed files with 254 additions and 1 deletions

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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