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:
parent
65c899bee5
commit
32c955b8fe
3 changed files with 254 additions and 1 deletions
119
cli/compose/loader/volume.go
Normal file
119
cli/compose/loader/volume.go
Normal 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
|
||||
}
|
134
cli/compose/loader/volume_test.go
Normal file
134
cli/compose/loader/volume_test.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue