mirror of https://github.com/docker/cli.git
Convert new compose volume type to swarm mount type
Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
parent
29f39ea244
commit
63c3221dd3
|
@ -1,21 +1,19 @@
|
||||||
package convert
|
package convert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/mount"
|
"github.com/docker/docker/api/types/mount"
|
||||||
composetypes "github.com/docker/docker/cli/compose/types"
|
composetypes "github.com/docker/docker/cli/compose/types"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type volumes map[string]composetypes.VolumeConfig
|
type volumes map[string]composetypes.VolumeConfig
|
||||||
|
|
||||||
// Volumes from compose-file types to engine api types
|
// Volumes from compose-file types to engine api types
|
||||||
func Volumes(serviceVolumes []string, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
|
func Volumes(serviceVolumes []composetypes.ServiceVolumeConfig, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
|
||||||
var mounts []mount.Mount
|
var mounts []mount.Mount
|
||||||
|
|
||||||
for _, volumeSpec := range serviceVolumes {
|
for _, volumeConfig := range serviceVolumes {
|
||||||
mount, err := convertVolumeToMount(volumeSpec, stackVolumes, namespace)
|
mount, err := convertVolumeToMount(volumeConfig, stackVolumes, namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -24,108 +22,65 @@ func Volumes(serviceVolumes []string, stackVolumes volumes, namespace Namespace)
|
||||||
return mounts, nil
|
return mounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertVolumeToMount(volumeSpec string, stackVolumes volumes, namespace Namespace) (mount.Mount, error) {
|
func convertVolumeToMount(
|
||||||
var source, target string
|
volume composetypes.ServiceVolumeConfig,
|
||||||
var mode []string
|
stackVolumes volumes,
|
||||||
|
namespace Namespace,
|
||||||
|
) (mount.Mount, error) {
|
||||||
|
result := mount.Mount{
|
||||||
|
Type: mount.Type(volume.Type),
|
||||||
|
Source: volume.Source,
|
||||||
|
Target: volume.Target,
|
||||||
|
ReadOnly: volume.ReadOnly,
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: split Windows path mappings properly
|
// Anonymous volumes
|
||||||
parts := strings.SplitN(volumeSpec, ":", 3)
|
if volume.Source == "" {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
if volume.Type == "volume" && volume.Bind != nil {
|
||||||
|
return result, errors.New("bind options are incompatible with type volume")
|
||||||
|
}
|
||||||
|
if volume.Type == "bind" && volume.Volume != nil {
|
||||||
|
return result, errors.New("volume options are incompatible with type bind")
|
||||||
|
}
|
||||||
|
|
||||||
for _, part := range parts {
|
if volume.Bind != nil {
|
||||||
if strings.TrimSpace(part) == "" {
|
result.BindOptions = &mount.BindOptions{
|
||||||
return mount.Mount{}, fmt.Errorf("invalid volume: %s", volumeSpec)
|
Propagation: mount.Propagation(volume.Bind.Propagation),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Binds volumes
|
||||||
switch len(parts) {
|
if volume.Type == "bind" {
|
||||||
case 3:
|
return result, nil
|
||||||
source = parts[0]
|
|
||||||
target = parts[1]
|
|
||||||
mode = strings.Split(parts[2], ",")
|
|
||||||
case 2:
|
|
||||||
source = parts[0]
|
|
||||||
target = parts[1]
|
|
||||||
case 1:
|
|
||||||
target = parts[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if source == "" {
|
stackVolume, exists := stackVolumes[volume.Source]
|
||||||
// Anonymous volume
|
|
||||||
return mount.Mount{
|
|
||||||
Type: mount.TypeVolume,
|
|
||||||
Target: target,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: catch Windows paths here
|
|
||||||
if strings.HasPrefix(source, "/") {
|
|
||||||
return mount.Mount{
|
|
||||||
Type: mount.TypeBind,
|
|
||||||
Source: source,
|
|
||||||
Target: target,
|
|
||||||
ReadOnly: isReadOnly(mode),
|
|
||||||
BindOptions: getBindOptions(mode),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
stackVolume, exists := stackVolumes[source]
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return mount.Mount{}, fmt.Errorf("undefined volume: %s", source)
|
return result, errors.Errorf("undefined volume: %s", volume.Source)
|
||||||
}
|
}
|
||||||
|
|
||||||
var volumeOptions *mount.VolumeOptions
|
result.Source = namespace.Scope(volume.Source)
|
||||||
if stackVolume.External.Name != "" {
|
result.VolumeOptions = &mount.VolumeOptions{}
|
||||||
volumeOptions = &mount.VolumeOptions{
|
|
||||||
NoCopy: isNoCopy(mode),
|
|
||||||
}
|
|
||||||
source = stackVolume.External.Name
|
|
||||||
} else {
|
|
||||||
volumeOptions = &mount.VolumeOptions{
|
|
||||||
Labels: AddStackLabel(namespace, stackVolume.Labels),
|
|
||||||
NoCopy: isNoCopy(mode),
|
|
||||||
}
|
|
||||||
|
|
||||||
if stackVolume.Driver != "" {
|
if volume.Volume != nil {
|
||||||
volumeOptions.DriverConfig = &mount.Driver{
|
result.VolumeOptions.NoCopy = volume.Volume.NoCopy
|
||||||
Name: stackVolume.Driver,
|
|
||||||
Options: stackVolume.DriverOpts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
source = namespace.Scope(source)
|
|
||||||
}
|
}
|
||||||
return mount.Mount{
|
|
||||||
Type: mount.TypeVolume,
|
|
||||||
Source: source,
|
|
||||||
Target: target,
|
|
||||||
ReadOnly: isReadOnly(mode),
|
|
||||||
VolumeOptions: volumeOptions,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func modeHas(mode []string, field string) bool {
|
// External named volumes
|
||||||
for _, item := range mode {
|
if stackVolume.External.External {
|
||||||
if item == field {
|
result.Source = stackVolume.External.Name
|
||||||
return true
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result.VolumeOptions.Labels = AddStackLabel(namespace, stackVolume.Labels)
|
||||||
|
if stackVolume.Driver != "" || stackVolume.DriverOpts != nil {
|
||||||
|
result.VolumeOptions.DriverConfig = &mount.Driver{
|
||||||
|
Name: stackVolume.Driver,
|
||||||
|
Options: stackVolume.DriverOpts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isReadOnly(mode []string) bool {
|
// Named volumes
|
||||||
return modeHas(mode, "ro")
|
return result, nil
|
||||||
}
|
|
||||||
|
|
||||||
func isNoCopy(mode []string) bool {
|
|
||||||
return modeHas(mode, "nocopy")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBindOptions(mode []string) *mount.BindOptions {
|
|
||||||
for _, item := range mode {
|
|
||||||
for _, propagation := range mount.Propagations {
|
|
||||||
if mount.Propagation(item) == propagation {
|
|
||||||
return &mount.BindOptions{Propagation: mount.Propagation(item)}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,51 +8,48 @@ import (
|
||||||
"github.com/docker/docker/pkg/testutil/assert"
|
"github.com/docker/docker/pkg/testutil/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIsReadOnly(t *testing.T) {
|
|
||||||
assert.Equal(t, isReadOnly([]string{"foo", "bar", "ro"}), true)
|
|
||||||
assert.Equal(t, isReadOnly([]string{"ro"}), true)
|
|
||||||
assert.Equal(t, isReadOnly([]string{}), false)
|
|
||||||
assert.Equal(t, isReadOnly([]string{"foo", "rw"}), false)
|
|
||||||
assert.Equal(t, isReadOnly([]string{"foo"}), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsNoCopy(t *testing.T) {
|
|
||||||
assert.Equal(t, isNoCopy([]string{"foo", "bar", "nocopy"}), true)
|
|
||||||
assert.Equal(t, isNoCopy([]string{"nocopy"}), true)
|
|
||||||
assert.Equal(t, isNoCopy([]string{}), false)
|
|
||||||
assert.Equal(t, isNoCopy([]string{"foo", "rw"}), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetBindOptions(t *testing.T) {
|
|
||||||
opts := getBindOptions([]string{"slave"})
|
|
||||||
expected := mount.BindOptions{Propagation: mount.PropagationSlave}
|
|
||||||
assert.Equal(t, *opts, expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetBindOptionsNone(t *testing.T) {
|
|
||||||
opts := getBindOptions([]string{"ro"})
|
|
||||||
assert.Equal(t, opts, (*mount.BindOptions)(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConvertVolumeToMountAnonymousVolume(t *testing.T) {
|
func TestConvertVolumeToMountAnonymousVolume(t *testing.T) {
|
||||||
stackVolumes := volumes{}
|
config := composetypes.ServiceVolumeConfig{
|
||||||
namespace := NewNamespace("foo")
|
Type: "volume",
|
||||||
|
Target: "/foo/bar",
|
||||||
|
}
|
||||||
expected := mount.Mount{
|
expected := mount.Mount{
|
||||||
Type: mount.TypeVolume,
|
Type: mount.TypeVolume,
|
||||||
Target: "/foo/bar",
|
Target: "/foo/bar",
|
||||||
}
|
}
|
||||||
mount, err := convertVolumeToMount("/foo/bar", stackVolumes, namespace)
|
mount, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.DeepEqual(t, mount, expected)
|
assert.DeepEqual(t, mount, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConvertVolumeToMountInvalidFormat(t *testing.T) {
|
func TestConvertVolumeToMountConflictingOptionsBind(t *testing.T) {
|
||||||
namespace := NewNamespace("foo")
|
namespace := NewNamespace("foo")
|
||||||
invalids := []string{"::", "::cc", ":bb:", "aa::", "aa::cc", "aa:bb:", " : : ", " : :cc", " :bb: ", "aa: : ", "aa: :cc", "aa:bb: "}
|
|
||||||
for _, vol := range invalids {
|
config := composetypes.ServiceVolumeConfig{
|
||||||
_, err := convertVolumeToMount(vol, volumes{}, namespace)
|
Type: "volume",
|
||||||
assert.Error(t, err, "invalid volume: "+vol)
|
Source: "foo",
|
||||||
|
Target: "/target",
|
||||||
|
Bind: &composetypes.ServiceVolumeBind{
|
||||||
|
Propagation: "slave",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||||
|
assert.Error(t, err, "bind options are incompatible")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertVolumeToMountConflictingOptionsVolume(t *testing.T) {
|
||||||
|
namespace := NewNamespace("foo")
|
||||||
|
|
||||||
|
config := composetypes.ServiceVolumeConfig{
|
||||||
|
Type: "bind",
|
||||||
|
Source: "/foo",
|
||||||
|
Target: "/target",
|
||||||
|
Volume: &composetypes.ServiceVolumeVolume{
|
||||||
|
NoCopy: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||||
|
assert.Error(t, err, "volume options are incompatible")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConvertVolumeToMountNamedVolume(t *testing.T) {
|
func TestConvertVolumeToMountNamedVolume(t *testing.T) {
|
||||||
|
@ -84,9 +81,19 @@ func TestConvertVolumeToMountNamedVolume(t *testing.T) {
|
||||||
"opt": "value",
|
"opt": "value",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
NoCopy: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
mount, err := convertVolumeToMount("normal:/foo:ro", stackVolumes, namespace)
|
config := composetypes.ServiceVolumeConfig{
|
||||||
|
Type: "volume",
|
||||||
|
Source: "normal",
|
||||||
|
Target: "/foo",
|
||||||
|
ReadOnly: true,
|
||||||
|
Volume: &composetypes.ServiceVolumeVolume{
|
||||||
|
NoCopy: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mount, err := convertVolumeToMount(config, stackVolumes, namespace)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.DeepEqual(t, mount, expected)
|
assert.DeepEqual(t, mount, expected)
|
||||||
}
|
}
|
||||||
|
@ -109,7 +116,12 @@ func TestConvertVolumeToMountNamedVolumeExternal(t *testing.T) {
|
||||||
NoCopy: false,
|
NoCopy: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
mount, err := convertVolumeToMount("outside:/foo", stackVolumes, namespace)
|
config := composetypes.ServiceVolumeConfig{
|
||||||
|
Type: "volume",
|
||||||
|
Source: "outside",
|
||||||
|
Target: "/foo",
|
||||||
|
}
|
||||||
|
mount, err := convertVolumeToMount(config, stackVolumes, namespace)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.DeepEqual(t, mount, expected)
|
assert.DeepEqual(t, mount, expected)
|
||||||
}
|
}
|
||||||
|
@ -132,7 +144,15 @@ func TestConvertVolumeToMountNamedVolumeExternalNoCopy(t *testing.T) {
|
||||||
NoCopy: true,
|
NoCopy: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
mount, err := convertVolumeToMount("outside:/foo:nocopy", stackVolumes, namespace)
|
config := composetypes.ServiceVolumeConfig{
|
||||||
|
Type: "volume",
|
||||||
|
Source: "outside",
|
||||||
|
Target: "/foo",
|
||||||
|
Volume: &composetypes.ServiceVolumeVolume{
|
||||||
|
NoCopy: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mount, err := convertVolumeToMount(config, stackVolumes, namespace)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.DeepEqual(t, mount, expected)
|
assert.DeepEqual(t, mount, expected)
|
||||||
}
|
}
|
||||||
|
@ -147,13 +167,26 @@ func TestConvertVolumeToMountBind(t *testing.T) {
|
||||||
ReadOnly: true,
|
ReadOnly: true,
|
||||||
BindOptions: &mount.BindOptions{Propagation: mount.PropagationShared},
|
BindOptions: &mount.BindOptions{Propagation: mount.PropagationShared},
|
||||||
}
|
}
|
||||||
mount, err := convertVolumeToMount("/bar:/foo:ro,shared", stackVolumes, namespace)
|
config := composetypes.ServiceVolumeConfig{
|
||||||
|
Type: "bind",
|
||||||
|
Source: "/bar",
|
||||||
|
Target: "/foo",
|
||||||
|
ReadOnly: true,
|
||||||
|
Bind: &composetypes.ServiceVolumeBind{Propagation: "shared"},
|
||||||
|
}
|
||||||
|
mount, err := convertVolumeToMount(config, stackVolumes, namespace)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.DeepEqual(t, mount, expected)
|
assert.DeepEqual(t, mount, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConvertVolumeToMountVolumeDoesNotExist(t *testing.T) {
|
func TestConvertVolumeToMountVolumeDoesNotExist(t *testing.T) {
|
||||||
namespace := NewNamespace("foo")
|
namespace := NewNamespace("foo")
|
||||||
_, err := convertVolumeToMount("unknown:/foo:ro", volumes{}, namespace)
|
config := composetypes.ServiceVolumeConfig{
|
||||||
|
Type: "volume",
|
||||||
|
Source: "unknown",
|
||||||
|
Target: "/foo",
|
||||||
|
ReadOnly: true,
|
||||||
|
}
|
||||||
|
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||||
assert.Error(t, err, "undefined volume: unknown")
|
assert.Error(t, err, "undefined volume: unknown")
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,13 +81,18 @@ func toError(result *gojsonschema.Result) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
jsonschemaOneOf = "number_one_of"
|
||||||
|
jsonschemaAnyOf = "number_any_of"
|
||||||
|
)
|
||||||
|
|
||||||
func getDescription(err validationError) string {
|
func getDescription(err validationError) string {
|
||||||
switch err.parent.Type() {
|
switch err.parent.Type() {
|
||||||
case "invalid_type":
|
case "invalid_type":
|
||||||
if expectedType, ok := err.parent.Details()["expected"].(string); ok {
|
if expectedType, ok := err.parent.Details()["expected"].(string); ok {
|
||||||
return fmt.Sprintf("must be a %s", humanReadableType(expectedType))
|
return fmt.Sprintf("must be a %s", humanReadableType(expectedType))
|
||||||
}
|
}
|
||||||
case "number_one_of", "number_any_of":
|
case jsonschemaOneOf, jsonschemaAnyOf:
|
||||||
if err.child == nil {
|
if err.child == nil {
|
||||||
return err.parent.Description()
|
return err.parent.Description()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue