浏览代码

Merge pull request #29003 from dnephin/pkg-compose-transform

Move composefile -> engine api type conversion to `cli/compose/convert`
Vincent Demeester 8 年之前
父节点
当前提交
914a72541f

+ 10 - 0
api/types/mount/mount.go

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

+ 13 - 19
cli/command/stack/common.go

@@ -6,24 +6,26 @@ import (
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/cli/compose/convert"
 	"github.com/docker/docker/client"
+	"github.com/docker/docker/opts"
 )
 
-const (
-	labelNamespace = "com.docker.stack.namespace"
-)
+func getStackFilter(namespace string) filters.Args {
+	filter := filters.NewArgs()
+	filter.Add("label", convert.LabelNamespace+"="+namespace)
+	return filter
+}
 
-func getStackLabels(namespace string, labels map[string]string) map[string]string {
-	if labels == nil {
-		labels = make(map[string]string)
-	}
-	labels[labelNamespace] = namespace
-	return labels
+func getStackFilterFromOpt(namespace string, opt opts.FilterOpt) filters.Args {
+	filter := opt.Value()
+	filter.Add("label", convert.LabelNamespace+"="+namespace)
+	return filter
 }
 
-func getStackFilter(namespace string) filters.Args {
+func getAllStacksFilter() filters.Args {
 	filter := filters.NewArgs()
-	filter.Add("label", labelNamespace+"="+namespace)
+	filter.Add("label", convert.LabelNamespace)
 	return filter
 }
 
@@ -46,11 +48,3 @@ func getStackNetworks(
 		ctx,
 		types.NetworkListOptions{Filters: getStackFilter(namespace)})
 }
-
-type namespace struct {
-	name string
-}
-
-func (n namespace) scope(name string) string {
-	return n.name + "_" + name
-}

+ 10 - 485
cli/command/stack/deploy.go

@@ -7,7 +7,6 @@ import (
 	"os"
 	"sort"
 	"strings"
-	"time"
 
 	"github.com/spf13/cobra"
 	"golang.org/x/net/context"
@@ -15,16 +14,11 @@ import (
 	"github.com/aanand/compose-file/loader"
 	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"
+	"github.com/docker/docker/cli/compose/convert"
 	dockerclient "github.com/docker/docker/client"
-	"github.com/docker/docker/opts"
-	runconfigopts "github.com/docker/docker/runconfig/opts"
-	"github.com/docker/go-connections/nat"
 )
 
 const (
@@ -121,16 +115,16 @@ func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deplo
 		return err
 	}
 
-	namespace := namespace{name: opts.namespace}
+	namespace := convert.NewNamespace(opts.namespace)
 
-	networks, externalNetworks := convertNetworks(namespace, config.Networks)
+	networks, externalNetworks := convert.Networks(namespace, config.Networks)
 	if err := validateExternalNetworks(ctx, dockerCli, externalNetworks); err != nil {
 		return err
 	}
 	if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil {
 		return err
 	}
-	services, err := convertServices(namespace, config)
+	services, err := convert.Services(namespace, config)
 	if err != nil {
 		return err
 	}
@@ -179,51 +173,6 @@ func getConfigFile(filename string) (*composetypes.ConfigFile, error) {
 	}, nil
 }
 
-func convertNetworks(
-	namespace namespace,
-	networks map[string]composetypes.NetworkConfig,
-) (map[string]types.NetworkCreate, []string) {
-	if networks == nil {
-		networks = make(map[string]composetypes.NetworkConfig)
-	}
-
-	// TODO: only add default network if it's used
-	networks["default"] = composetypes.NetworkConfig{}
-
-	externalNetworks := []string{}
-	result := make(map[string]types.NetworkCreate)
-
-	for internalName, network := range networks {
-		if network.External.External {
-			externalNetworks = append(externalNetworks, network.External.Name)
-			continue
-		}
-
-		createOpts := types.NetworkCreate{
-			Labels:  getStackLabels(namespace.name, network.Labels),
-			Driver:  network.Driver,
-			Options: network.DriverOpts,
-		}
-
-		if network.Ipam.Driver != "" || len(network.Ipam.Config) > 0 {
-			createOpts.IPAM = &networktypes.IPAM{}
-		}
-
-		if network.Ipam.Driver != "" {
-			createOpts.IPAM.Driver = network.Ipam.Driver
-		}
-		for _, ipamConfig := range network.Ipam.Config {
-			config := networktypes.IPAMConfig{
-				Subnet: ipamConfig.Subnet,
-			}
-			createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
-		}
-		result[internalName] = createOpts
-	}
-
-	return result, externalNetworks
-}
-
 func validateExternalNetworks(
 	ctx context.Context,
 	dockerCli *command.DockerCli,
@@ -249,12 +198,12 @@ func validateExternalNetworks(
 func createNetworks(
 	ctx context.Context,
 	dockerCli *command.DockerCli,
-	namespace namespace,
+	namespace convert.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
 	}
@@ -265,7 +214,7 @@ func createNetworks(
 	}
 
 	for internalName, createOpts := range networks {
-		name := namespace.scope(internalName)
+		name := namespace.Scope(internalName)
 		if _, exists := existingNetworkMap[name]; exists {
 			continue
 		}
@@ -283,164 +232,17 @@ func createNetworks(
 	return nil
 }
 
-func convertServiceNetworks(
-	networks map[string]*composetypes.ServiceNetworkConfig,
-	networkConfigs map[string]composetypes.NetworkConfig,
-	namespace namespace,
-	name string,
-) ([]swarm.NetworkAttachmentConfig, error) {
-	if len(networks) == 0 {
-		return []swarm.NetworkAttachmentConfig{
-			{
-				Target:  namespace.scope("default"),
-				Aliases: []string{name},
-			},
-		}, nil
-	}
-
-	nets := []swarm.NetworkAttachmentConfig{}
-	for networkName, network := range networks {
-		networkConfig, ok := networkConfigs[networkName]
-		if !ok {
-			return []swarm.NetworkAttachmentConfig{}, fmt.Errorf("invalid network: %s", networkName)
-		}
-		var aliases []string
-		if network != nil {
-			aliases = network.Aliases
-		}
-		target := namespace.scope(networkName)
-		if networkConfig.External.External {
-			target = networkName
-		}
-		nets = append(nets, swarm.NetworkAttachmentConfig{
-			Target:  target,
-			Aliases: append(aliases, name),
-		})
-	}
-	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)
-
-	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]
-	default:
-		return mount.Mount{}, fmt.Errorf("invald volume: %s", volumeSpec)
-	}
-
-	// 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,
 	services map[string]swarm.ServiceSpec,
-	namespace namespace,
+	namespace convert.Namespace,
 	sendAuth bool,
 ) error {
 	apiClient := dockerCli.Client()
 	out := dockerCli.Out()
 
-	existingServices, err := getServices(ctx, apiClient, namespace.name)
+	existingServices, err := getServices(ctx, apiClient, namespace.Name())
 	if err != nil {
 		return err
 	}
@@ -451,7 +253,7 @@ func deployServices(
 	}
 
 	for internalName, serviceSpec := range services {
-		name := namespace.scope(internalName)
+		name := namespace.Scope(internalName)
 
 		encodedAuth := ""
 		if sendAuth {
@@ -499,280 +301,3 @@ func deployServices(
 
 	return nil
 }
-
-func convertServices(
-	namespace namespace,
-	config *composetypes.Config,
-) (map[string]swarm.ServiceSpec, error) {
-	result := make(map[string]swarm.ServiceSpec)
-
-	services := config.Services
-	volumes := config.Volumes
-	networks := config.Networks
-
-	for _, service := range services {
-		serviceSpec, err := convertService(namespace, service, networks, volumes)
-		if err != nil {
-			return nil, err
-		}
-		result[service.Name] = serviceSpec
-	}
-
-	return result, nil
-}
-
-func convertService(
-	namespace namespace,
-	service composetypes.ServiceConfig,
-	networkConfigs map[string]composetypes.NetworkConfig,
-	volumes map[string]composetypes.VolumeConfig,
-) (swarm.ServiceSpec, error) {
-	name := namespace.scope(service.Name)
-
-	endpoint, err := convertEndpointSpec(service.Ports)
-	if err != nil {
-		return swarm.ServiceSpec{}, err
-	}
-
-	mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas)
-	if err != nil {
-		return swarm.ServiceSpec{}, err
-	}
-
-	mounts, err := convertVolumes(service.Volumes, volumes, namespace)
-	if err != nil {
-		// TODO: better error message (include service name)
-		return swarm.ServiceSpec{}, err
-	}
-
-	resources, err := convertResources(service.Deploy.Resources)
-	if err != nil {
-		return swarm.ServiceSpec{}, err
-	}
-
-	restartPolicy, err := convertRestartPolicy(
-		service.Restart, service.Deploy.RestartPolicy)
-	if err != nil {
-		return swarm.ServiceSpec{}, err
-	}
-
-	healthcheck, err := convertHealthcheck(service.HealthCheck)
-	if err != nil {
-		return swarm.ServiceSpec{}, err
-	}
-
-	networks, err := convertServiceNetworks(service.Networks, networkConfigs, namespace, service.Name)
-	if err != nil {
-		return swarm.ServiceSpec{}, err
-	}
-
-	var logDriver *swarm.Driver
-	if service.Logging != nil {
-		logDriver = &swarm.Driver{
-			Name:    service.Logging.Driver,
-			Options: service.Logging.Options,
-		}
-	}
-
-	serviceSpec := swarm.ServiceSpec{
-		Annotations: swarm.Annotations{
-			Name:   name,
-			Labels: getStackLabels(namespace.name, service.Deploy.Labels),
-		},
-		TaskTemplate: swarm.TaskSpec{
-			ContainerSpec: swarm.ContainerSpec{
-				Image:           service.Image,
-				Command:         service.Entrypoint,
-				Args:            service.Command,
-				Hostname:        service.Hostname,
-				Hosts:           convertExtraHosts(service.ExtraHosts),
-				Healthcheck:     healthcheck,
-				Env:             convertEnvironment(service.Environment),
-				Labels:          getStackLabels(namespace.name, service.Labels),
-				Dir:             service.WorkingDir,
-				User:            service.User,
-				Mounts:          mounts,
-				StopGracePeriod: service.StopGracePeriod,
-				TTY:             service.Tty,
-				OpenStdin:       service.StdinOpen,
-			},
-			LogDriver:     logDriver,
-			Resources:     resources,
-			RestartPolicy: restartPolicy,
-			Placement: &swarm.Placement{
-				Constraints: service.Deploy.Placement.Constraints,
-			},
-		},
-		EndpointSpec: endpoint,
-		Mode:         mode,
-		Networks:     networks,
-		UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig),
-	}
-
-	return serviceSpec, nil
-}
-
-func convertExtraHosts(extraHosts map[string]string) []string {
-	hosts := []string{}
-	for host, ip := range extraHosts {
-		hosts = append(hosts, fmt.Sprintf("%s %s", ip, host))
-	}
-	return hosts
-}
-
-func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) {
-	if healthcheck == nil {
-		return nil, nil
-	}
-	var (
-		err               error
-		timeout, interval time.Duration
-		retries           int
-	)
-	if healthcheck.Disable {
-		if len(healthcheck.Test) != 0 {
-			return nil, fmt.Errorf("command and disable key can't be set at the same time")
-		}
-		return &container.HealthConfig{
-			Test: []string{"NONE"},
-		}, nil
-
-	}
-	if healthcheck.Timeout != "" {
-		timeout, err = time.ParseDuration(healthcheck.Timeout)
-		if err != nil {
-			return nil, err
-		}
-	}
-	if healthcheck.Interval != "" {
-		interval, err = time.ParseDuration(healthcheck.Interval)
-		if err != nil {
-			return nil, err
-		}
-	}
-	if healthcheck.Retries != nil {
-		retries = int(*healthcheck.Retries)
-	}
-	return &container.HealthConfig{
-		Test:     healthcheck.Test,
-		Timeout:  timeout,
-		Interval: interval,
-		Retries:  retries,
-	}, nil
-}
-
-func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) {
-	// TODO: log if restart is being ignored
-	if source == nil {
-		policy, err := runconfigopts.ParseRestartPolicy(restart)
-		if err != nil {
-			return nil, err
-		}
-		// TODO: is this an accurate convertion?
-		switch {
-		case policy.IsNone():
-			return nil, nil
-		case policy.IsAlways(), policy.IsUnlessStopped():
-			return &swarm.RestartPolicy{
-				Condition: swarm.RestartPolicyConditionAny,
-			}, nil
-		case policy.IsOnFailure():
-			attempts := uint64(policy.MaximumRetryCount)
-			return &swarm.RestartPolicy{
-				Condition:   swarm.RestartPolicyConditionOnFailure,
-				MaxAttempts: &attempts,
-			}, nil
-		}
-	}
-	return &swarm.RestartPolicy{
-		Condition:   swarm.RestartPolicyCondition(source.Condition),
-		Delay:       source.Delay,
-		MaxAttempts: source.MaxAttempts,
-		Window:      source.Window,
-	}, nil
-}
-
-func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig {
-	if source == nil {
-		return nil
-	}
-	parallel := uint64(1)
-	if source.Parallelism != nil {
-		parallel = *source.Parallelism
-	}
-	return &swarm.UpdateConfig{
-		Parallelism:     parallel,
-		Delay:           source.Delay,
-		FailureAction:   source.FailureAction,
-		Monitor:         source.Monitor,
-		MaxFailureRatio: source.MaxFailureRatio,
-	}
-}
-
-func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) {
-	resources := &swarm.ResourceRequirements{}
-	if source.Limits != nil {
-		cpus, err := opts.ParseCPUs(source.Limits.NanoCPUs)
-		if err != nil {
-			return nil, err
-		}
-		resources.Limits = &swarm.Resources{
-			NanoCPUs:    cpus,
-			MemoryBytes: int64(source.Limits.MemoryBytes),
-		}
-	}
-	if source.Reservations != nil {
-		cpus, err := opts.ParseCPUs(source.Reservations.NanoCPUs)
-		if err != nil {
-			return nil, err
-		}
-		resources.Reservations = &swarm.Resources{
-			NanoCPUs:    cpus,
-			MemoryBytes: int64(source.Reservations.MemoryBytes),
-		}
-	}
-	return resources, nil
-}
-
-func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) {
-	portConfigs := []swarm.PortConfig{}
-	ports, portBindings, err := nat.ParsePortSpecs(source)
-	if err != nil {
-		return nil, err
-	}
-
-	for port := range ports {
-		portConfigs = append(
-			portConfigs,
-			opts.ConvertPortToPortConfig(port, portBindings)...)
-	}
-
-	return &swarm.EndpointSpec{Ports: portConfigs}, nil
-}
-
-func convertEnvironment(source map[string]string) []string {
-	var output []string
-
-	for name, value := range source {
-		output = append(output, fmt.Sprintf("%s=%s", name, value))
-	}
-
-	return output
-}
-
-func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) {
-	serviceMode := swarm.ServiceMode{}
-
-	switch mode {
-	case "global":
-		if replicas != nil {
-			return serviceMode, fmt.Errorf("replicas can only be used with replicated mode")
-		}
-		serviceMode.Global = &swarm.GlobalService{}
-	case "replicated", "":
-		serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
-	default:
-		return serviceMode, fmt.Errorf("Unknown mode: %s", mode)
-	}
-	return serviceMode, nil
-}

+ 7 - 6
cli/command/stack/deploy_bundlefile.go

@@ -6,6 +6,7 @@ import (
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/swarm"
 	"github.com/docker/docker/cli/command"
+	"github.com/docker/docker/cli/compose/convert"
 )
 
 func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deployOptions) error {
@@ -18,20 +19,20 @@ func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deploy
 		return err
 	}
 
-	namespace := namespace{name: opts.namespace}
+	namespace := convert.NewNamespace(opts.namespace)
 
 	networks := make(map[string]types.NetworkCreate)
 	for _, service := range bundle.Services {
 		for _, networkName := range service.Networks {
 			networks[networkName] = types.NetworkCreate{
-				Labels: getStackLabels(namespace.name, nil),
+				Labels: convert.AddStackLabel(namespace, nil),
 			}
 		}
 	}
 
 	services := make(map[string]swarm.ServiceSpec)
 	for internalName, service := range bundle.Services {
-		name := namespace.scope(internalName)
+		name := namespace.Scope(internalName)
 
 		var ports []swarm.PortConfig
 		for _, portSpec := range service.Ports {
@@ -44,7 +45,7 @@ func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deploy
 		nets := []swarm.NetworkAttachmentConfig{}
 		for _, networkName := range service.Networks {
 			nets = append(nets, swarm.NetworkAttachmentConfig{
-				Target:  namespace.scope(networkName),
+				Target:  namespace.Scope(networkName),
 				Aliases: []string{networkName},
 			})
 		}
@@ -52,7 +53,7 @@ func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deploy
 		serviceSpec := swarm.ServiceSpec{
 			Annotations: swarm.Annotations{
 				Name:   name,
-				Labels: getStackLabels(namespace.name, service.Labels),
+				Labels: convert.AddStackLabel(namespace, service.Labels),
 			},
 			TaskTemplate: swarm.TaskSpec{
 				ContainerSpec: swarm.ContainerSpec{
@@ -63,7 +64,7 @@ func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deploy
 					// Service Labels will not be copied to Containers
 					// automatically during the deployment so we apply
 					// it here.
-					Labels: getStackLabels(namespace.name, nil),
+					Labels: convert.AddStackLabel(namespace, nil),
 				},
 			},
 			EndpointSpec: &swarm.EndpointSpec{

+ 4 - 8
cli/command/stack/list.go

@@ -9,9 +9,9 @@ import (
 	"golang.org/x/net/context"
 
 	"github.com/docker/docker/api/types"
-	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/cli/command"
+	"github.com/docker/docker/cli/compose/convert"
 	"github.com/docker/docker/client"
 	"github.com/spf13/cobra"
 )
@@ -81,23 +81,19 @@ func getStacks(
 	ctx context.Context,
 	apiclient client.APIClient,
 ) ([]*stack, error) {
-
-	filter := filters.NewArgs()
-	filter.Add("label", labelNamespace)
-
 	services, err := apiclient.ServiceList(
 		ctx,
-		types.ServiceListOptions{Filters: filter})
+		types.ServiceListOptions{Filters: getAllStacksFilter()})
 	if err != nil {
 		return nil, err
 	}
 	m := make(map[string]*stack, 0)
 	for _, service := range services {
 		labels := service.Spec.Labels
-		name, ok := labels[labelNamespace]
+		name, ok := labels[convert.LabelNamespace]
 		if !ok {
 			return nil, fmt.Errorf("cannot get label %s for service %s",
-				labelNamespace, service.ID)
+				convert.LabelNamespace, service.ID)
 		}
 		ztack, ok := m[name]
 		if !ok {

+ 1 - 2
cli/command/stack/ps.go

@@ -49,8 +49,7 @@ func runPS(dockerCli *command.DockerCli, opts psOptions) error {
 	client := dockerCli.Client()
 	ctx := context.Background()
 
-	filter := opts.filter.Value()
-	filter.Add("label", labelNamespace+"="+opts.namespace)
+	filter := getStackFilterFromOpt(opts.namespace, opts.filter)
 	if !opts.all && !filter.Include("desired-state") {
 		filter.Add("desired-state", string(swarm.TaskStateRunning))
 		filter.Add("desired-state", string(swarm.TaskStateAccepted))

+ 1 - 3
cli/command/stack/services.go

@@ -43,9 +43,7 @@ func runServices(dockerCli *command.DockerCli, opts servicesOptions) error {
 	ctx := context.Background()
 	client := dockerCli.Client()
 
-	filter := opts.filter.Value()
-	filter.Add("label", labelNamespace+"="+opts.namespace)
-
+	filter := getStackFilterFromOpt(opts.namespace, opts.filter)
 	services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: filter})
 	if err != nil {
 		return err

+ 88 - 0
cli/compose/convert/compose.go

@@ -0,0 +1,88 @@
+package convert
+
+import (
+	composetypes "github.com/aanand/compose-file/types"
+	"github.com/docker/docker/api/types"
+	networktypes "github.com/docker/docker/api/types/network"
+)
+
+const (
+	// LabelNamespace is the label used to track stack resources
+	LabelNamespace = "com.docker.stack.namespace"
+)
+
+// Namespace mangles names by prepending the name
+type Namespace struct {
+	name string
+}
+
+// Scope prepends the namespace to a name
+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
+	return labels
+}
+
+type networkMap map[string]composetypes.NetworkConfig
+
+// Networks from the compose-file type to the engine API type
+func Networks(namespace Namespace, networks networkMap) (map[string]types.NetworkCreate, []string) {
+	if networks == nil {
+		networks = make(map[string]composetypes.NetworkConfig)
+	}
+
+	// TODO: only add default network if it's used
+	if _, ok := networks["default"]; !ok {
+		networks["default"] = composetypes.NetworkConfig{}
+	}
+
+	externalNetworks := []string{}
+	result := make(map[string]types.NetworkCreate)
+
+	for internalName, network := range networks {
+		if network.External.External {
+			externalNetworks = append(externalNetworks, network.External.Name)
+			continue
+		}
+
+		createOpts := types.NetworkCreate{
+			Labels:  AddStackLabel(namespace, network.Labels),
+			Driver:  network.Driver,
+			Options: network.DriverOpts,
+		}
+
+		if network.Ipam.Driver != "" || len(network.Ipam.Config) > 0 {
+			createOpts.IPAM = &networktypes.IPAM{}
+		}
+
+		if network.Ipam.Driver != "" {
+			createOpts.IPAM.Driver = network.Ipam.Driver
+		}
+		for _, ipamConfig := range network.Ipam.Config {
+			config := networktypes.IPAMConfig{
+				Subnet: ipamConfig.Subnet,
+			}
+			createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
+		}
+		result[internalName] = createOpts
+	}
+
+	return result, externalNetworks
+}

+ 85 - 0
cli/compose/convert/compose_test.go

@@ -0,0 +1,85 @@
+package convert
+
+import (
+	"testing"
+
+	composetypes "github.com/aanand/compose-file/types"
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/network"
+	"github.com/docker/docker/pkg/testutil/assert"
+)
+
+func TestNamespaceScope(t *testing.T) {
+	scoped := Namespace{name: "foo"}.Scope("bar")
+	assert.Equal(t, scoped, "foo_bar")
+}
+
+func TestAddStackLabel(t *testing.T) {
+	labels := map[string]string{
+		"something": "labeled",
+	}
+	actual := AddStackLabel(Namespace{name: "foo"}, labels)
+	expected := map[string]string{
+		"something":    "labeled",
+		LabelNamespace: "foo",
+	}
+	assert.DeepEqual(t, actual, expected)
+}
+
+func TestNetworks(t *testing.T) {
+	namespace := Namespace{name: "foo"}
+	source := networkMap{
+		"normal": composetypes.NetworkConfig{
+			Driver: "overlay",
+			DriverOpts: map[string]string{
+				"opt": "value",
+			},
+			Ipam: composetypes.IPAMConfig{
+				Driver: "driver",
+				Config: []*composetypes.IPAMPool{
+					{
+						Subnet: "10.0.0.0",
+					},
+				},
+			},
+			Labels: map[string]string{
+				"something": "labeled",
+			},
+		},
+		"outside": composetypes.NetworkConfig{
+			External: composetypes.External{
+				External: true,
+				Name:     "special",
+			},
+		},
+	}
+	expected := map[string]types.NetworkCreate{
+		"default": {
+			Labels: map[string]string{
+				LabelNamespace: "foo",
+			},
+		},
+		"normal": {
+			Driver: "overlay",
+			IPAM: &network.IPAM{
+				Driver: "driver",
+				Config: []network.IPAMConfig{
+					{
+						Subnet: "10.0.0.0",
+					},
+				},
+			},
+			Options: map[string]string{
+				"opt": "value",
+			},
+			Labels: map[string]string{
+				LabelNamespace: "foo",
+				"something":    "labeled",
+			},
+		},
+	}
+
+	networks, externals := Networks(namespace, source)
+	assert.DeepEqual(t, networks, expected)
+	assert.DeepEqual(t, externals, []string{"special"})
+}

+ 330 - 0
cli/compose/convert/service.go

@@ -0,0 +1,330 @@
+package convert
+
+import (
+	"fmt"
+	"time"
+
+	composetypes "github.com/aanand/compose-file/types"
+	"github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/opts"
+	runconfigopts "github.com/docker/docker/runconfig/opts"
+	"github.com/docker/go-connections/nat"
+)
+
+// Services from compose-file types to engine API types
+func Services(
+	namespace Namespace,
+	config *composetypes.Config,
+) (map[string]swarm.ServiceSpec, error) {
+	result := make(map[string]swarm.ServiceSpec)
+
+	services := config.Services
+	volumes := config.Volumes
+	networks := config.Networks
+
+	for _, service := range services {
+		serviceSpec, err := convertService(namespace, service, networks, volumes)
+		if err != nil {
+			return nil, err
+		}
+		result[service.Name] = serviceSpec
+	}
+
+	return result, nil
+}
+
+func convertService(
+	namespace Namespace,
+	service composetypes.ServiceConfig,
+	networkConfigs map[string]composetypes.NetworkConfig,
+	volumes map[string]composetypes.VolumeConfig,
+) (swarm.ServiceSpec, error) {
+	name := namespace.Scope(service.Name)
+
+	endpoint, err := convertEndpointSpec(service.Ports)
+	if err != nil {
+		return swarm.ServiceSpec{}, err
+	}
+
+	mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas)
+	if err != nil {
+		return swarm.ServiceSpec{}, err
+	}
+
+	mounts, err := Volumes(service.Volumes, volumes, namespace)
+	if err != nil {
+		// TODO: better error message (include service name)
+		return swarm.ServiceSpec{}, err
+	}
+
+	resources, err := convertResources(service.Deploy.Resources)
+	if err != nil {
+		return swarm.ServiceSpec{}, err
+	}
+
+	restartPolicy, err := convertRestartPolicy(
+		service.Restart, service.Deploy.RestartPolicy)
+	if err != nil {
+		return swarm.ServiceSpec{}, err
+	}
+
+	healthcheck, err := convertHealthcheck(service.HealthCheck)
+	if err != nil {
+		return swarm.ServiceSpec{}, err
+	}
+
+	networks, err := convertServiceNetworks(service.Networks, networkConfigs, namespace, service.Name)
+	if err != nil {
+		return swarm.ServiceSpec{}, err
+	}
+
+	var logDriver *swarm.Driver
+	if service.Logging != nil {
+		logDriver = &swarm.Driver{
+			Name:    service.Logging.Driver,
+			Options: service.Logging.Options,
+		}
+	}
+
+	serviceSpec := swarm.ServiceSpec{
+		Annotations: swarm.Annotations{
+			Name:   name,
+			Labels: AddStackLabel(namespace, service.Deploy.Labels),
+		},
+		TaskTemplate: swarm.TaskSpec{
+			ContainerSpec: swarm.ContainerSpec{
+				Image:           service.Image,
+				Command:         service.Entrypoint,
+				Args:            service.Command,
+				Hostname:        service.Hostname,
+				Hosts:           convertExtraHosts(service.ExtraHosts),
+				Healthcheck:     healthcheck,
+				Env:             convertEnvironment(service.Environment),
+				Labels:          AddStackLabel(namespace, service.Labels),
+				Dir:             service.WorkingDir,
+				User:            service.User,
+				Mounts:          mounts,
+				StopGracePeriod: service.StopGracePeriod,
+				TTY:             service.Tty,
+				OpenStdin:       service.StdinOpen,
+			},
+			LogDriver:     logDriver,
+			Resources:     resources,
+			RestartPolicy: restartPolicy,
+			Placement: &swarm.Placement{
+				Constraints: service.Deploy.Placement.Constraints,
+			},
+		},
+		EndpointSpec: endpoint,
+		Mode:         mode,
+		Networks:     networks,
+		UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig),
+	}
+
+	return serviceSpec, nil
+}
+
+func convertServiceNetworks(
+	networks map[string]*composetypes.ServiceNetworkConfig,
+	networkConfigs networkMap,
+	namespace Namespace,
+	name string,
+) ([]swarm.NetworkAttachmentConfig, error) {
+	if len(networks) == 0 {
+		return []swarm.NetworkAttachmentConfig{
+			{
+				Target:  namespace.Scope("default"),
+				Aliases: []string{name},
+			},
+		}, nil
+	}
+
+	nets := []swarm.NetworkAttachmentConfig{}
+	for networkName, network := range networks {
+		networkConfig, ok := networkConfigs[networkName]
+		if !ok {
+			return []swarm.NetworkAttachmentConfig{}, fmt.Errorf(
+				"service %q references network %q, which is not declared", name, networkName)
+		}
+		var aliases []string
+		if network != nil {
+			aliases = network.Aliases
+		}
+		target := namespace.Scope(networkName)
+		if networkConfig.External.External {
+			target = networkConfig.External.Name
+		}
+		nets = append(nets, swarm.NetworkAttachmentConfig{
+			Target:  target,
+			Aliases: append(aliases, name),
+		})
+	}
+	return nets, nil
+}
+
+func convertExtraHosts(extraHosts map[string]string) []string {
+	hosts := []string{}
+	for host, ip := range extraHosts {
+		hosts = append(hosts, fmt.Sprintf("%s %s", ip, host))
+	}
+	return hosts
+}
+
+func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) {
+	if healthcheck == nil {
+		return nil, nil
+	}
+	var (
+		err               error
+		timeout, interval time.Duration
+		retries           int
+	)
+	if healthcheck.Disable {
+		if len(healthcheck.Test) != 0 {
+			return nil, fmt.Errorf("test and disable can't be set at the same time")
+		}
+		return &container.HealthConfig{
+			Test: []string{"NONE"},
+		}, nil
+
+	}
+	if healthcheck.Timeout != "" {
+		timeout, err = time.ParseDuration(healthcheck.Timeout)
+		if err != nil {
+			return nil, err
+		}
+	}
+	if healthcheck.Interval != "" {
+		interval, err = time.ParseDuration(healthcheck.Interval)
+		if err != nil {
+			return nil, err
+		}
+	}
+	if healthcheck.Retries != nil {
+		retries = int(*healthcheck.Retries)
+	}
+	return &container.HealthConfig{
+		Test:     healthcheck.Test,
+		Timeout:  timeout,
+		Interval: interval,
+		Retries:  retries,
+	}, nil
+}
+
+func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) {
+	// TODO: log if restart is being ignored
+	if source == nil {
+		policy, err := runconfigopts.ParseRestartPolicy(restart)
+		if err != nil {
+			return nil, err
+		}
+		switch {
+		case policy.IsNone():
+			return nil, nil
+		case policy.IsAlways(), policy.IsUnlessStopped():
+			return &swarm.RestartPolicy{
+				Condition: swarm.RestartPolicyConditionAny,
+			}, nil
+		case policy.IsOnFailure():
+			attempts := uint64(policy.MaximumRetryCount)
+			return &swarm.RestartPolicy{
+				Condition:   swarm.RestartPolicyConditionOnFailure,
+				MaxAttempts: &attempts,
+			}, nil
+		default:
+			return nil, fmt.Errorf("unknown restart policy: %s", restart)
+		}
+	}
+	return &swarm.RestartPolicy{
+		Condition:   swarm.RestartPolicyCondition(source.Condition),
+		Delay:       source.Delay,
+		MaxAttempts: source.MaxAttempts,
+		Window:      source.Window,
+	}, nil
+}
+
+func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig {
+	if source == nil {
+		return nil
+	}
+	parallel := uint64(1)
+	if source.Parallelism != nil {
+		parallel = *source.Parallelism
+	}
+	return &swarm.UpdateConfig{
+		Parallelism:     parallel,
+		Delay:           source.Delay,
+		FailureAction:   source.FailureAction,
+		Monitor:         source.Monitor,
+		MaxFailureRatio: source.MaxFailureRatio,
+	}
+}
+
+func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) {
+	resources := &swarm.ResourceRequirements{}
+	if source.Limits != nil {
+		cpus, err := opts.ParseCPUs(source.Limits.NanoCPUs)
+		if err != nil {
+			return nil, err
+		}
+		resources.Limits = &swarm.Resources{
+			NanoCPUs:    cpus,
+			MemoryBytes: int64(source.Limits.MemoryBytes),
+		}
+	}
+	if source.Reservations != nil {
+		cpus, err := opts.ParseCPUs(source.Reservations.NanoCPUs)
+		if err != nil {
+			return nil, err
+		}
+		resources.Reservations = &swarm.Resources{
+			NanoCPUs:    cpus,
+			MemoryBytes: int64(source.Reservations.MemoryBytes),
+		}
+	}
+	return resources, nil
+}
+
+func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) {
+	portConfigs := []swarm.PortConfig{}
+	ports, portBindings, err := nat.ParsePortSpecs(source)
+	if err != nil {
+		return nil, err
+	}
+
+	for port := range ports {
+		portConfigs = append(
+			portConfigs,
+			opts.ConvertPortToPortConfig(port, portBindings)...)
+	}
+
+	return &swarm.EndpointSpec{Ports: portConfigs}, nil
+}
+
+func convertEnvironment(source map[string]string) []string {
+	var output []string
+
+	for name, value := range source {
+		output = append(output, fmt.Sprintf("%s=%s", name, value))
+	}
+
+	return output
+}
+
+func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) {
+	serviceMode := swarm.ServiceMode{}
+
+	switch mode {
+	case "global":
+		if replicas != nil {
+			return serviceMode, fmt.Errorf("replicas can only be used with replicated mode")
+		}
+		serviceMode.Global = &swarm.GlobalService{}
+	case "replicated", "":
+		serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
+	default:
+		return serviceMode, fmt.Errorf("Unknown mode: %s", mode)
+	}
+	return serviceMode, nil
+}

+ 193 - 0
cli/compose/convert/service_test.go

@@ -0,0 +1,193 @@
+package convert
+
+import (
+	"sort"
+	"strings"
+	"testing"
+	"time"
+
+	composetypes "github.com/aanand/compose-file/types"
+	"github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/pkg/testutil/assert"
+)
+
+func TestConvertRestartPolicyFromNone(t *testing.T) {
+	policy, err := convertRestartPolicy("no", nil)
+	assert.NilError(t, err)
+	assert.Equal(t, policy, (*swarm.RestartPolicy)(nil))
+}
+
+func TestConvertRestartPolicyFromUnknown(t *testing.T) {
+	_, err := convertRestartPolicy("unknown", nil)
+	assert.Error(t, err, "unknown restart policy: unknown")
+}
+
+func TestConvertRestartPolicyFromAlways(t *testing.T) {
+	policy, err := convertRestartPolicy("always", nil)
+	expected := &swarm.RestartPolicy{
+		Condition: swarm.RestartPolicyConditionAny,
+	}
+	assert.NilError(t, err)
+	assert.DeepEqual(t, policy, expected)
+}
+
+func TestConvertRestartPolicyFromFailure(t *testing.T) {
+	policy, err := convertRestartPolicy("on-failure:4", nil)
+	attempts := uint64(4)
+	expected := &swarm.RestartPolicy{
+		Condition:   swarm.RestartPolicyConditionOnFailure,
+		MaxAttempts: &attempts,
+	}
+	assert.NilError(t, err)
+	assert.DeepEqual(t, policy, expected)
+}
+
+func TestConvertEnvironment(t *testing.T) {
+	source := map[string]string{
+		"foo": "bar",
+		"key": "value",
+	}
+	env := convertEnvironment(source)
+	sort.Strings(env)
+	assert.DeepEqual(t, env, []string{"foo=bar", "key=value"})
+}
+
+func TestConvertResourcesFull(t *testing.T) {
+	source := composetypes.Resources{
+		Limits: &composetypes.Resource{
+			NanoCPUs:    "0.003",
+			MemoryBytes: composetypes.UnitBytes(300000000),
+		},
+		Reservations: &composetypes.Resource{
+			NanoCPUs:    "0.002",
+			MemoryBytes: composetypes.UnitBytes(200000000),
+		},
+	}
+	resources, err := convertResources(source)
+	assert.NilError(t, err)
+
+	expected := &swarm.ResourceRequirements{
+		Limits: &swarm.Resources{
+			NanoCPUs:    3000000,
+			MemoryBytes: 300000000,
+		},
+		Reservations: &swarm.Resources{
+			NanoCPUs:    2000000,
+			MemoryBytes: 200000000,
+		},
+	}
+	assert.DeepEqual(t, resources, expected)
+}
+
+func TestConvertHealthcheck(t *testing.T) {
+	retries := uint64(10)
+	source := &composetypes.HealthCheckConfig{
+		Test:     []string{"EXEC", "touch", "/foo"},
+		Timeout:  "30s",
+		Interval: "2ms",
+		Retries:  &retries,
+	}
+	expected := &container.HealthConfig{
+		Test:     source.Test,
+		Timeout:  30 * time.Second,
+		Interval: 2 * time.Millisecond,
+		Retries:  10,
+	}
+
+	healthcheck, err := convertHealthcheck(source)
+	assert.NilError(t, err)
+	assert.DeepEqual(t, healthcheck, expected)
+}
+
+func TestConvertHealthcheckDisable(t *testing.T) {
+	source := &composetypes.HealthCheckConfig{Disable: true}
+	expected := &container.HealthConfig{
+		Test: []string{"NONE"},
+	}
+
+	healthcheck, err := convertHealthcheck(source)
+	assert.NilError(t, err)
+	assert.DeepEqual(t, healthcheck, expected)
+}
+
+func TestConvertHealthcheckDisableWithTest(t *testing.T) {
+	source := &composetypes.HealthCheckConfig{
+		Disable: true,
+		Test:    []string{"EXEC", "touch"},
+	}
+	_, err := convertHealthcheck(source)
+	assert.Error(t, err, "test and disable can't be set")
+}
+
+func TestConvertServiceNetworksOnlyDefault(t *testing.T) {
+	networkConfigs := networkMap{}
+	networks := map[string]*composetypes.ServiceNetworkConfig{}
+
+	configs, err := convertServiceNetworks(
+		networks, networkConfigs, NewNamespace("foo"), "service")
+
+	expected := []swarm.NetworkAttachmentConfig{
+		{
+			Target:  "foo_default",
+			Aliases: []string{"service"},
+		},
+	}
+
+	assert.NilError(t, err)
+	assert.DeepEqual(t, configs, expected)
+}
+
+func TestConvertServiceNetworks(t *testing.T) {
+	networkConfigs := networkMap{
+		"front": composetypes.NetworkConfig{
+			External: composetypes.External{
+				External: true,
+				Name:     "fronttier",
+			},
+		},
+		"back": composetypes.NetworkConfig{},
+	}
+	networks := map[string]*composetypes.ServiceNetworkConfig{
+		"front": {
+			Aliases: []string{"something"},
+		},
+		"back": {
+			Aliases: []string{"other"},
+		},
+	}
+
+	configs, err := convertServiceNetworks(
+		networks, networkConfigs, NewNamespace("foo"), "service")
+
+	expected := []swarm.NetworkAttachmentConfig{
+		{
+			Target:  "foo_back",
+			Aliases: []string{"other", "service"},
+		},
+		{
+			Target:  "fronttier",
+			Aliases: []string{"something", "service"},
+		},
+	}
+
+	sortedConfigs := byTargetSort(configs)
+	sort.Sort(&sortedConfigs)
+
+	assert.NilError(t, err)
+	assert.DeepEqual(t, []swarm.NetworkAttachmentConfig(sortedConfigs), expected)
+}
+
+type byTargetSort []swarm.NetworkAttachmentConfig
+
+func (s byTargetSort) Len() int {
+	return len(s)
+}
+
+func (s byTargetSort) Less(i, j int) bool {
+	return strings.Compare(s[i].Target, s[j].Target) < 0
+}
+
+func (s byTargetSort) Swap(i, j int) {
+	s[i], s[j] = s[j], s[i]
+}

+ 116 - 0
cli/compose/convert/volume.go

@@ -0,0 +1,116 @@
+package convert
+
+import (
+	"fmt"
+	"strings"
+
+	composetypes "github.com/aanand/compose-file/types"
+	"github.com/docker/docker/api/types/mount"
+)
+
+type volumes map[string]composetypes.VolumeConfig
+
+// Volumes from compose-file types to engine api types
+func Volumes(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)
+
+	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]
+	default:
+		return mount.Mount{}, fmt.Errorf("invald volume: %s", volumeSpec)
+	}
+
+	// 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
+}

+ 112 - 0
cli/compose/convert/volume_test.go

@@ -0,0 +1,112 @@
+package convert
+
+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, (*mount.BindOptions)(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")
+}

+ 4 - 1
pkg/testutil/assert/assert.go

@@ -7,6 +7,8 @@ import (
 	"reflect"
 	"runtime"
 	"strings"
+
+	"github.com/davecgh/go-spew/spew"
 )
 
 // TestingT is an interface which defines the methods of testing.T that are
@@ -49,7 +51,8 @@ func NilError(t TestingT, err error) {
 // they are not "deeply equal".
 func DeepEqual(t TestingT, actual, expected interface{}) {
 	if !reflect.DeepEqual(actual, expected) {
-		fatal(t, "Expected '%v' (%T) got '%v' (%T)", expected, expected, actual, actual)
+		fatal(t, "Expected (%T):\n%v\n\ngot (%T):\n%s\n",
+			expected, spew.Sdump(expected), actual, spew.Sdump(actual))
 	}
 }