mirror of https://github.com/docker/cli.git
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
d2d48f3f69
commit
a442213b92
|
@ -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
|
||||||
|
}
|
|
@ -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
|
// ServiceVolumeBind are options for a service volume of type bind
|
||||||
type ServiceVolumeBind struct {
|
type ServiceVolumeBind struct {
|
||||||
Propogation string
|
Propagation string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceVolumeVolume are options for a service volume of type volume
|
// ServiceVolumeVolume are options for a service volume of type volume
|
||||||
|
|
Loading…
Reference in New Issue