Move ConvertVolumes to composetransform package.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
Daniel Nephin 2016-11-30 16:34:29 -05:00
parent c9ffd3586c
commit 732e19892d
7 changed files with 268 additions and 137 deletions

View file

@ -50,6 +50,16 @@ const (
PropagationSlave Propagation = "slave"
)
// Propagations is the list of all valid mount propagations
var Propagations = []Propagation{
PropagationRPrivate,
PropagationPrivate,
PropagationRShared,
PropagationShared,
PropagationRSlave,
PropagationSlave,
}
// BindOptions defines options specific to mounts of type "bind".
type BindOptions struct {
Propagation Propagation `json:",omitempty"`

View file

@ -7,11 +7,12 @@ import (
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/composetransform"
)
func getStackFilter(namespace string) filters.Args {
filter := filters.NewArgs()
filter.Add("label", labelNamespace+"="+namespace)
filter.Add("label", composetransform.LabelNamespace+"="+namespace)
return filter
}

View file

@ -13,13 +13,12 @@ import (
composetypes "github.com/aanand/compose-file/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
dockerclient "github.com/docker/docker/client"
"github.com/docker/docker/opts"
"github.com/docker/docker/pkg/composetransform"
runconfigopts "github.com/docker/docker/runconfig/opts"
"github.com/docker/go-connections/nat"
"github.com/spf13/cobra"
@ -120,10 +119,10 @@ func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deplo
return err
}
namespace := namespace{name: opts.namespace}
namespace := composetransform.NewNamespace(opts.namespace)
serviceNetworks := getServicesDeclaredNetworks(config.Services)
networks, externalNetworks := convertNetworks(namespace, config.Networks, serviceNetworks)
networks, externalNetworks := composetransform.ConvertNetworks(namespace, config.Networks, serviceNetworks)
if err := validateExternalNetworks(ctx, dockerCli, externalNetworks); err != nil {
return err
}
@ -217,12 +216,12 @@ func validateExternalNetworks(
func createNetworks(
ctx context.Context,
dockerCli *command.DockerCli,
namespace namespace,
namespace composetransform.Namespace,
networks map[string]types.NetworkCreate,
) error {
client := dockerCli.Client()
existingNetworks, err := getStackNetworks(ctx, client, namespace.name)
existingNetworks, err := getStackNetworks(ctx, client, namespace.Name())
if err != nil {
return err
}
@ -233,7 +232,7 @@ func createNetworks(
}
for internalName, createOpts := range networks {
name := namespace.scope(internalName)
name := namespace.Scope(internalName)
if _, exists := existingNetworkMap[name]; exists {
continue
}
@ -254,7 +253,7 @@ func createNetworks(
func convertServiceNetworks(
networks map[string]*composetypes.ServiceNetworkConfig,
networkConfigs map[string]composetypes.NetworkConfig,
namespace namespace,
namespace composetransform.Namespace,
name string,
) ([]swarm.NetworkAttachmentConfig, error) {
if len(networks) == 0 {
@ -288,128 +287,6 @@ func convertServiceNetworks(
return nets, nil
}
func convertVolumes(
serviceVolumes []string,
stackVolumes map[string]composetypes.VolumeConfig,
namespace namespace,
) ([]mount.Mount, error) {
var mounts []mount.Mount
for _, volumeSpec := range serviceVolumes {
mount, err := convertVolumeToMount(volumeSpec, stackVolumes, namespace)
if err != nil {
return nil, err
}
mounts = append(mounts, mount)
}
return mounts, nil
}
func convertVolumeToMount(
volumeSpec string,
stackVolumes map[string]composetypes.VolumeConfig,
namespace namespace,
) (mount.Mount, error) {
var source, target string
var mode []string
// TODO: split Windows path mappings properly
parts := strings.SplitN(volumeSpec, ":", 3)
for _, part := range parts {
if strings.TrimSpace(part) == "" {
return mount.Mount{}, fmt.Errorf("invalid volume: %s", volumeSpec)
}
}
switch len(parts) {
case 3:
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 == "" {
// 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 {
return mount.Mount{}, fmt.Errorf("undefined volume: %s", source)
}
var volumeOptions *mount.VolumeOptions
if stackVolume.External.Name != "" {
source = stackVolume.External.Name
} else {
volumeOptions = &mount.VolumeOptions{
Labels: getStackLabels(namespace.name, stackVolume.Labels),
NoCopy: isNoCopy(mode),
}
if stackVolume.Driver != "" {
volumeOptions.DriverConfig = &mount.Driver{
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 {
for _, item := range mode {
if item == field {
return true
}
}
return false
}
func isReadOnly(mode []string) bool {
return modeHas(mode, "ro")
}
func isNoCopy(mode []string) bool {
return modeHas(mode, "nocopy")
}
func getBindOptions(mode []string) *mount.BindOptions {
for _, item := range mode {
if strings.Contains(item, "private") || strings.Contains(item, "shared") || strings.Contains(item, "slave") {
return &mount.BindOptions{Propagation: mount.Propagation(item)}
}
}
return nil
}
func deployServices(
ctx context.Context,
dockerCli *command.DockerCli,
@ -519,7 +396,7 @@ func convertService(
return swarm.ServiceSpec{}, err
}
mounts, err := convertVolumes(service.Volumes, volumes, namespace)
mounts, err := composetransform.ConvertVolumes(service.Volumes, volumes, namespace)
if err != nil {
// TODO: better error message (include service name)
return swarm.ServiceSpec{}, err

View file

@ -7,7 +7,8 @@ import (
)
const (
labelNamespace = "com.docker.stack.namespace"
// LabelNamespace is the label used to track stack resources
LabelNamespace = "com.docker.stack.namespace"
)
// Namespace mangles names by prepending the name
@ -20,12 +21,22 @@ func (n Namespace) Scope(name string) string {
return n.name + "_" + name
}
// Name returns the name of the namespace
func (n Namespace) Name() string {
return n.name
}
// NewNamespace returns a new Namespace for scoping of names
func NewNamespace(name string) Namespace {
return Namespace{name: name}
}
// AddStackLabel returns labels with the namespace label added
func AddStackLabel(namespace Namespace, labels map[string]string) map[string]string {
if labels == nil {
labels = make(map[string]string)
}
labels[labelNamespace] = namespace.name
labels[LabelNamespace] = namespace.name
return labels
}

View file

@ -21,7 +21,7 @@ func TestAddStackLabel(t *testing.T) {
actual := AddStackLabel(Namespace{name: "foo"}, labels)
expected := map[string]string{
"something": "labeled",
labelNamespace: "foo",
LabelNamespace: "foo",
}
assert.DeepEqual(t, actual, expected)
}
@ -56,7 +56,7 @@ func TestConvertNetworks(t *testing.T) {
expected := map[string]types.NetworkCreate{
"default": {
Labels: map[string]string{
labelNamespace: "foo",
LabelNamespace: "foo",
},
},
"normal": {
@ -73,7 +73,7 @@ func TestConvertNetworks(t *testing.T) {
"opt": "value",
},
Labels: map[string]string{
labelNamespace: "foo",
LabelNamespace: "foo",
"something": "labeled",
},
},

View file

@ -0,0 +1,120 @@
package composetransform
import (
"fmt"
"strings"
composetypes "github.com/aanand/compose-file/types"
"github.com/docker/docker/api/types/mount"
)
type volumes map[string]composetypes.VolumeConfig
// ConvertVolumes from compose-file types to engine api types
func ConvertVolumes(serviceVolumes []string, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
var mounts []mount.Mount
for _, volumeSpec := range serviceVolumes {
mount, err := convertVolumeToMount(volumeSpec, stackVolumes, namespace)
if err != nil {
return nil, err
}
mounts = append(mounts, mount)
}
return mounts, nil
}
func convertVolumeToMount(volumeSpec string, stackVolumes volumes, namespace Namespace) (mount.Mount, error) {
var source, target string
var mode []string
// TODO: split Windows path mappings properly
parts := strings.SplitN(volumeSpec, ":", 3)
for _, part := range parts {
if strings.TrimSpace(part) == "" {
return mount.Mount{}, fmt.Errorf("invalid volume: %s", volumeSpec)
}
}
switch len(parts) {
case 3:
source = parts[0]
target = parts[1]
mode = strings.Split(parts[2], ",")
case 2:
source = parts[0]
target = parts[1]
case 1:
target = parts[0]
}
// 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 {
return mount.Mount{}, fmt.Errorf("undefined volume: %s", source)
}
var volumeOptions *mount.VolumeOptions
if stackVolume.External.Name != "" {
source = stackVolume.External.Name
} else {
volumeOptions = &mount.VolumeOptions{
Labels: AddStackLabel(namespace, stackVolume.Labels),
NoCopy: isNoCopy(mode),
}
if stackVolume.Driver != "" {
volumeOptions.DriverConfig = &mount.Driver{
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 {
for _, item := range mode {
if item == field {
return true
}
}
return false
}
func isReadOnly(mode []string) bool {
return modeHas(mode, "ro")
}
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
}

View file

@ -0,0 +1,112 @@
package composetransform
import (
"testing"
composetypes "github.com/aanand/compose-file/types"
"github.com/docker/docker/api/types/mount"
"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, nil)
}
func TestConvertVolumeToMountNamedVolume(t *testing.T) {
stackVolumes := volumes{
"normal": composetypes.VolumeConfig{
Driver: "glusterfs",
DriverOpts: map[string]string{
"opt": "value",
},
Labels: map[string]string{
"something": "labeled",
},
},
}
namespace := NewNamespace("foo")
expected := mount.Mount{
Type: mount.TypeVolume,
Source: "foo_normal",
Target: "/foo",
ReadOnly: true,
VolumeOptions: &mount.VolumeOptions{
Labels: map[string]string{
LabelNamespace: "foo",
"something": "labeled",
},
DriverConfig: &mount.Driver{
Name: "glusterfs",
Options: map[string]string{
"opt": "value",
},
},
},
}
mount, err := convertVolumeToMount("normal:/foo:ro", stackVolumes, namespace)
assert.NilError(t, err)
assert.DeepEqual(t, mount, expected)
}
func TestConvertVolumeToMountNamedVolumeExternal(t *testing.T) {
stackVolumes := volumes{
"outside": composetypes.VolumeConfig{
External: composetypes.External{
External: true,
Name: "special",
},
},
}
namespace := NewNamespace("foo")
expected := mount.Mount{
Type: mount.TypeVolume,
Source: "special",
Target: "/foo",
}
mount, err := convertVolumeToMount("outside:/foo", stackVolumes, namespace)
assert.NilError(t, err)
assert.DeepEqual(t, mount, expected)
}
func TestConvertVolumeToMountBind(t *testing.T) {
stackVolumes := volumes{}
namespace := NewNamespace("foo")
expected := mount.Mount{
Type: mount.TypeBind,
Source: "/bar",
Target: "/foo",
ReadOnly: true,
BindOptions: &mount.BindOptions{Propagation: mount.PropagationShared},
}
mount, err := convertVolumeToMount("/bar:/foo:ro,shared", stackVolumes, namespace)
assert.NilError(t, err)
assert.DeepEqual(t, mount, expected)
}
func TestConvertVolumeToMountVolumeDoesNotExist(t *testing.T) {
namespace := NewNamespace("foo")
_, err := convertVolumeToMount("unknown:/foo:ro", volumes{}, namespace)
assert.Error(t, err, "undefined volume: unknown")
}