Merge pull request #31811 from vieux/bump_17.04.0

bump 17.04.0-rc1
diff --git a/api/common.go b/api/common.go
index cde250f..b44a2dc 100644
--- a/api/common.go
+++ b/api/common.go
@@ -21,7 +21,7 @@
 // Common constants for daemon and client.
 const (
 	// DefaultVersion of Current REST API
-	DefaultVersion string = "1.27"
+	DefaultVersion string = "1.28"
 
 	// NoBaseImageSpecifier is the symbol used by the FROM
 	// command to specify that no base image is to be used.
diff --git a/api/server/router/container/container_routes.go b/api/server/router/container/container_routes.go
index 078352f..55abf72 100644
--- a/api/server/router/container/container_routes.go
+++ b/api/server/router/container/container_routes.go
@@ -520,9 +520,9 @@
 		}()
 
 		conn := <-wsChan
-		// In case version is higher than 1.27, a binary frame will be sent.
+		// In case version 1.28 and above, a binary frame will be sent.
 		// See 28176 for details.
-		if versions.GreaterThanOrEqualTo(version, "1.27") {
+		if versions.GreaterThanOrEqualTo(version, "1.28") {
 			conn.PayloadType = websocket.BinaryFrame
 		}
 		return conn, conn, conn, nil
diff --git a/api/server/router/image/image_routes.go b/api/server/router/image/image_routes.go
index 6940365..48823b9 100644
--- a/api/server/router/image/image_routes.go
+++ b/api/server/router/image/image_routes.go
@@ -255,7 +255,7 @@
 
 	version := httputils.VersionFromContext(ctx)
 	filterParam := r.Form.Get("filter")
-	if versions.LessThan(version, "1.28") && filterParam != "" {
+	if versions.LessThanOrEqualTo(version, "1.28") && filterParam != "" {
 		imageFilters.Add("reference", filterParam)
 	}
 
diff --git a/api/server/router/network/network_routes.go b/api/server/router/network/network_routes.go
index 63a24c8..c331916 100644
--- a/api/server/router/network/network_routes.go
+++ b/api/server/router/network/network_routes.go
@@ -62,11 +62,11 @@
 		}
 
 		var nr *types.NetworkResource
-		// Versions < 1.27 fetches all the containers attached to a network
+		// Versions < 1.28 fetches all the containers attached to a network
 		// in a network list api call. It is a heavy weight operation when
-		// run across all the networks. Starting API version 1.27, this detailed
+		// run across all the networks. Starting API version 1.28, this detailed
 		// info is available for network specific GET API (equivalent to inspect)
-		if versions.LessThan(httputils.VersionFromContext(ctx), "1.27") {
+		if versions.LessThan(httputils.VersionFromContext(ctx), "1.28") {
 			nr = n.buildDetailedNetworkResources(nw, false)
 		} else {
 			nr = n.buildNetworkResource(nw)
diff --git a/api/swagger.yaml b/api/swagger.yaml
index 9acdaf7..a5f38d7 100644
--- a/api/swagger.yaml
+++ b/api/swagger.yaml
@@ -19,10 +19,10 @@
 consumes:
   - "application/json"
   - "text/plain"
-basePath: "/v1.27"
+basePath: "/v1.28"
 info:
   title: "Docker Engine API"
-  version: "1.27"
+  version: "1.28"
   x-logo:
     url: "https://docs.docker.com/images/logo-docker-main.png"
   description: |
@@ -44,7 +44,7 @@
 
     The API is usually changed in each release of Docker, so API calls are versioned to ensure that clients don't break.
 
-    For Docker Engine 17.04, the API version is 1.27. To lock to this version, you prefix the URL with `/v1.27`. For example, calling `/info` is the same as calling `/v1.27/info`.
+    For Docker Engine 17.04, the API version is 1.28. To lock to this version, you prefix the URL with `/v1.28`. For example, calling `/info` is the same as calling `/v1.28/info`.
 
     Engine releases in the near future should support this version of the API, so your client will continue to work even if it is talking to a newer Engine.
 
@@ -52,11 +52,12 @@
 
     The API uses an open schema model, which means server may add extra properties to responses. Likewise, the server will ignore any extra query parameters and request body properties. When you write clients, you need to ignore additional properties in responses to ensure they do not break when talking to newer Docker daemons.
 
-    This documentation is for version 1.27 of the API, which was introduced with Docker 17.04. Use this table to find documentation for previous versions of the API:
+    This documentation is for version 1.28 of the API, which was introduced with Docker 17.04. Use this table to find documentation for previous versions of the API:
 
     Docker version  | API version | Changes
     ----------------|-------------|---------
-    1.13.1 | [1.26](https://docs.docker.com/engine/api/v1.26/) | [API changes](https://docs.docker.com/engine/api/version-history/#v1-26-api-changes)
+    17.03.1 | [1.27](https://docs.docker.com/engine/api/v1.27/) | [API changes](https://docs.docker.com/engine/api/version-history/#v1-27-api-changes)
+    1.13.1 & 17.03.0 | [1.26](https://docs.docker.com/engine/api/v1.26/) | [API changes](https://docs.docker.com/engine/api/version-history/#v1-26-api-changes)
     1.13.0 | [1.25](https://docs.docker.com/engine/api/v1.25/) | [API changes](https://docs.docker.com/engine/api/version-history/#v1-25-api-changes)
     1.12.x | [1.24](https://docs.docker.com/engine/api/v1.24/) | [API changes](https://docs.docker.com/engine/api/version-history/#v1-24-api-changes)
     1.11.x | [1.23](https://docs.docker.com/engine/api/v1.23/) | [API changes](https://docs.docker.com/engine/api/version-history/#v1-23-api-changes)
@@ -398,6 +399,12 @@
         type: "array"
         items:
           $ref: "#/definitions/DeviceMapping"
+      DeviceCgroupRules:
+        description: "a list of cgroup rules to apply to the container"
+        type: "array"
+        items:
+          type: "string"
+          example: "c 13:* rwm"
       DiskQuota:
         description: "Disk limit (in bytes)."
         type: "integer"
diff --git a/cli/command/service/create.go b/cli/command/service/create.go
index c2eb817..fc1ecbd 100644
--- a/cli/command/service/create.go
+++ b/cli/command/service/create.go
@@ -38,7 +38,7 @@
 	flags.Var(&opts.mounts, flagMount, "Attach a filesystem mount to the service")
 	flags.Var(&opts.constraints, flagConstraint, "Placement constraints")
 	flags.Var(&opts.placementPrefs, flagPlacementPref, "Add a placement preference")
-	flags.SetAnnotation(flagPlacementPref, "version", []string{"1.27"})
+	flags.SetAnnotation(flagPlacementPref, "version", []string{"1.28"})
 	flags.Var(&opts.networks, flagNetwork, "Network attachments")
 	flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service")
 	flags.SetAnnotation(flagSecret, "version", []string{"1.25"})
diff --git a/cli/command/service/logs.go b/cli/command/service/logs.go
index 5e7cce3..5f50905 100644
--- a/cli/command/service/logs.go
+++ b/cli/command/service/logs.go
@@ -51,7 +51,7 @@
 	flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
 	flags.BoolVar(&opts.noTaskIDs, "no-task-ids", false, "Do not include task IDs")
 	flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output")
-	flags.StringVar(&opts.since, "since", "", "Show logs since timestamp")
+	flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)")
 	flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps")
 	flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs")
 	return cmd
diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go
index baaa58e..46fe919 100644
--- a/cli/command/service/opts.go
+++ b/cli/command/service/opts.go
@@ -498,15 +498,15 @@
 	flags.SetAnnotation(flagUpdateMaxFailureRatio, "version", []string{"1.25"})
 
 	flags.Uint64Var(&opts.rollback.parallelism, flagRollbackParallelism, 1, "Maximum number of tasks rolled back simultaneously (0 to roll back all at once)")
-	flags.SetAnnotation(flagRollbackParallelism, "version", []string{"1.27"})
+	flags.SetAnnotation(flagRollbackParallelism, "version", []string{"1.28"})
 	flags.DurationVar(&opts.rollback.delay, flagRollbackDelay, time.Duration(0), "Delay between task rollbacks (ns|us|ms|s|m|h) (default 0s)")
-	flags.SetAnnotation(flagRollbackDelay, "version", []string{"1.27"})
+	flags.SetAnnotation(flagRollbackDelay, "version", []string{"1.28"})
 	flags.DurationVar(&opts.rollback.monitor, flagRollbackMonitor, time.Duration(0), "Duration after each task rollback to monitor for failure (ns|us|ms|s|m|h) (default 0s)")
-	flags.SetAnnotation(flagRollbackMonitor, "version", []string{"1.27"})
+	flags.SetAnnotation(flagRollbackMonitor, "version", []string{"1.28"})
 	flags.StringVar(&opts.rollback.onFailure, flagRollbackFailureAction, "pause", `Action on rollback failure ("pause"|"continue")`)
-	flags.SetAnnotation(flagRollbackFailureAction, "version", []string{"1.27"})
+	flags.SetAnnotation(flagRollbackFailureAction, "version", []string{"1.28"})
 	flags.Var(&opts.rollback.maxFailureRatio, flagRollbackMaxFailureRatio, "Failure rate to tolerate during a rollback")
-	flags.SetAnnotation(flagRollbackMaxFailureRatio, "version", []string{"1.27"})
+	flags.SetAnnotation(flagRollbackMaxFailureRatio, "version", []string{"1.28"})
 
 	flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "vip", "Endpoint mode (vip or dnsrr)")
 
@@ -530,10 +530,10 @@
 	flags.SetAnnotation(flagTTY, "version", []string{"1.25"})
 
 	flags.BoolVar(&opts.readOnly, flagReadOnly, false, "Mount the container's root filesystem as read only")
-	flags.SetAnnotation(flagReadOnly, "version", []string{"1.27"})
+	flags.SetAnnotation(flagReadOnly, "version", []string{"1.28"})
 
 	flags.StringVar(&opts.stopSignal, flagStopSignal, "", "Signal to stop the container")
-	flags.SetAnnotation(flagStopSignal, "version", []string{"1.27"})
+	flags.SetAnnotation(flagStopSignal, "version", []string{"1.28"})
 }
 
 const (
diff --git a/cli/command/service/update.go b/cli/command/service/update.go
index ab8391e..fc6a229 100644
--- a/cli/command/service/update.go
+++ b/cli/command/service/update.go
@@ -72,9 +72,9 @@
 	flags.Var(&serviceOpts.mounts, flagMountAdd, "Add or update a mount on a service")
 	flags.Var(&serviceOpts.constraints, flagConstraintAdd, "Add or update a placement constraint")
 	flags.Var(&serviceOpts.placementPrefs, flagPlacementPrefAdd, "Add a placement preference")
-	flags.SetAnnotation(flagPlacementPrefAdd, "version", []string{"1.27"})
+	flags.SetAnnotation(flagPlacementPrefAdd, "version", []string{"1.28"})
 	flags.Var(&placementPrefOpts{}, flagPlacementPrefRemove, "Remove a placement preference")
-	flags.SetAnnotation(flagPlacementPrefRemove, "version", []string{"1.27"})
+	flags.SetAnnotation(flagPlacementPrefRemove, "version", []string{"1.28"})
 	flags.Var(&serviceOpts.endpoint.publishPorts, flagPublishAdd, "Add or update a published port")
 	flags.Var(&serviceOpts.groups, flagGroupAdd, "Add an additional supplementary user group to the container")
 	flags.SetAnnotation(flagGroupAdd, "version", []string{"1.25"})
@@ -132,7 +132,7 @@
 			return errors.New("other flags may not be combined with --rollback")
 		}
 
-		if versions.LessThan(dockerCli.Client().ClientVersion(), "1.27") {
+		if versions.LessThan(dockerCli.Client().ClientVersion(), "1.28") {
 			clientSideRollback = true
 			spec = service.PreviousSpec
 			if spec == nil {
diff --git a/cli/compose/convert/service.go b/cli/compose/convert/service.go
index 9af4a74..ab90d73 100644
--- a/cli/compose/convert/service.go
+++ b/cli/compose/convert/service.go
@@ -4,6 +4,7 @@
 	"fmt"
 	"os"
 	"sort"
+	"strings"
 	"time"
 
 	"github.com/docker/docker/api/types"
@@ -56,7 +57,7 @@
 ) (swarm.ServiceSpec, error) {
 	name := namespace.Scope(service.Name)
 
-	endpoint, err := convertEndpointSpec(service.Ports)
+	endpoint, err := convertEndpointSpec(service.Deploy.EndpointMode, service.Ports)
 	if err != nil {
 		return swarm.ServiceSpec{}, err
 	}
@@ -373,7 +374,7 @@
 func (a byPublishedPort) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
 func (a byPublishedPort) Less(i, j int) bool { return a[i].PublishedPort < a[j].PublishedPort }
 
-func convertEndpointSpec(source []composetypes.ServicePortConfig) (*swarm.EndpointSpec, error) {
+func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortConfig) (*swarm.EndpointSpec, error) {
 	portConfigs := []swarm.PortConfig{}
 	for _, port := range source {
 		portConfig := swarm.PortConfig{
@@ -386,7 +387,10 @@
 	}
 
 	sort.Sort(byPublishedPort(portConfigs))
-	return &swarm.EndpointSpec{Ports: portConfigs}, nil
+	return &swarm.EndpointSpec{
+		Mode:  swarm.ResolutionMode(strings.ToLower(endpointMode)),
+		Ports: portConfigs,
+	}, nil
 }
 
 func convertEnvironment(source map[string]string) []string {
diff --git a/cli/compose/convert/service_test.go b/cli/compose/convert/service_test.go
index 10bde35..56f495d 100644
--- a/cli/compose/convert/service_test.go
+++ b/cli/compose/convert/service_test.go
@@ -156,9 +156,10 @@
 			Published: 80,
 		},
 	}
-	endpoint, err := convertEndpointSpec(source)
+	endpoint, err := convertEndpointSpec("vip", source)
 
 	expected := swarm.EndpointSpec{
+		Mode: swarm.ResolutionMode(strings.ToLower("vip")),
 		Ports: []swarm.PortConfig{
 			{
 				TargetPort:    8080,
diff --git a/cli/compose/convert/volume.go b/cli/compose/convert/volume.go
index 53c5095..682b443 100644
--- a/cli/compose/convert/volume.go
+++ b/cli/compose/convert/volume.go
@@ -1,21 +1,19 @@
 package convert
 
 import (
-	"fmt"
-	"strings"
-
 	"github.com/docker/docker/api/types/mount"
 	composetypes "github.com/docker/docker/cli/compose/types"
+	"github.com/pkg/errors"
 )
 
 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) {
+func Volumes(serviceVolumes []composetypes.ServiceVolumeConfig, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
 	var mounts []mount.Mount
 
-	for _, volumeSpec := range serviceVolumes {
-		mount, err := convertVolumeToMount(volumeSpec, stackVolumes, namespace)
+	for _, volumeConfig := range serviceVolumes {
+		mount, err := convertVolumeToMount(volumeConfig, stackVolumes, namespace)
 		if err != nil {
 			return nil, err
 		}
@@ -24,108 +22,65 @@
 	return mounts, nil
 }
 
-func convertVolumeToMount(volumeSpec string, stackVolumes volumes, namespace Namespace) (mount.Mount, error) {
-	var source, target string
-	var mode []string
+func convertVolumeToMount(
+	volume composetypes.ServiceVolumeConfig,
+	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
-	parts := strings.SplitN(volumeSpec, ":", 3)
+	// Anonymous volumes
+	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 strings.TrimSpace(part) == "" {
-			return mount.Mount{}, fmt.Errorf("invalid volume: %s", volumeSpec)
+	if volume.Bind != nil {
+		result.BindOptions = &mount.BindOptions{
+			Propagation: mount.Propagation(volume.Bind.Propagation),
 		}
 	}
-
-	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]
+	// Binds volumes
+	if volume.Type == "bind" {
+		return result, nil
 	}
 
-	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]
+	stackVolume, exists := stackVolumes[volume.Source]
 	if !exists {
-		return mount.Mount{}, fmt.Errorf("undefined volume: %s", source)
+		return result, errors.Errorf("undefined volume: %s", volume.Source)
 	}
 
-	var volumeOptions *mount.VolumeOptions
-	if stackVolume.External.Name != "" {
-		volumeOptions = &mount.VolumeOptions{
-			NoCopy: isNoCopy(mode),
-		}
-		source = stackVolume.External.Name
-	} else {
-		volumeOptions = &mount.VolumeOptions{
-			Labels: AddStackLabel(namespace, stackVolume.Labels),
-			NoCopy: isNoCopy(mode),
-		}
+	result.Source = namespace.Scope(volume.Source)
+	result.VolumeOptions = &mount.VolumeOptions{}
 
-		if stackVolume.Driver != "" {
-			volumeOptions.DriverConfig = &mount.Driver{
-				Name:    stackVolume.Driver,
-				Options: stackVolume.DriverOpts,
-			}
-		}
-		source = namespace.Scope(source)
+	if volume.Volume != nil {
+		result.VolumeOptions.NoCopy = volume.Volume.NoCopy
 	}
-	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
+	// External named volumes
+	if stackVolume.External.External {
+		result.Source = stackVolume.External.Name
+		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 {
-	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
+	// Named volumes
+	return result, nil
 }
diff --git a/cli/compose/convert/volume_test.go b/cli/compose/convert/volume_test.go
index d218e7c..705f03f 100644
--- a/cli/compose/convert/volume_test.go
+++ b/cli/compose/convert/volume_test.go
@@ -8,51 +8,48 @@
 	"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) {
-	stackVolumes := volumes{}
-	namespace := NewNamespace("foo")
+	config := composetypes.ServiceVolumeConfig{
+		Type:   "volume",
+		Target: "/foo/bar",
+	}
 	expected := mount.Mount{
 		Type:   mount.TypeVolume,
 		Target: "/foo/bar",
 	}
-	mount, err := convertVolumeToMount("/foo/bar", stackVolumes, namespace)
+	mount, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
 	assert.NilError(t, err)
 	assert.DeepEqual(t, mount, expected)
 }
 
-func TestConvertVolumeToMountInvalidFormat(t *testing.T) {
+func TestConvertVolumeToMountConflictingOptionsBind(t *testing.T) {
 	namespace := NewNamespace("foo")
-	invalids := []string{"::", "::cc", ":bb:", "aa::", "aa::cc", "aa:bb:", " : : ", " : :cc", " :bb: ", "aa: : ", "aa: :cc", "aa:bb: "}
-	for _, vol := range invalids {
-		_, err := convertVolumeToMount(vol, volumes{}, namespace)
-		assert.Error(t, err, "invalid volume: "+vol)
+
+	config := composetypes.ServiceVolumeConfig{
+		Type:   "volume",
+		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) {
@@ -84,9 +81,19 @@
 					"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.DeepEqual(t, mount, expected)
 }
@@ -109,7 +116,12 @@
 			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.DeepEqual(t, mount, expected)
 }
@@ -132,7 +144,15 @@
 			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.DeepEqual(t, mount, expected)
 }
@@ -147,13 +167,26 @@
 		ReadOnly:    true,
 		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.DeepEqual(t, mount, expected)
 }
 
 func TestConvertVolumeToMountVolumeDoesNotExist(t *testing.T) {
 	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")
 }
diff --git a/cli/compose/interpolation/interpolation.go b/cli/compose/interpolation/interpolation.go
index 734f28e..29c2e0e 100644
--- a/cli/compose/interpolation/interpolation.go
+++ b/cli/compose/interpolation/interpolation.go
@@ -39,7 +39,7 @@
 		interpolatedValue, err := recursiveInterpolate(value, mapping)
 		if err != nil {
 			return nil, fmt.Errorf(
-				"Invalid interpolation format for %#v option in %s %#v: %#v",
+				"Invalid interpolation format for %#v option in %s %#v: %#v. You may need to escape any $ with another $.",
 				key, section, name, err.Template,
 			)
 		}
diff --git a/cli/compose/interpolation/interpolation_test.go b/cli/compose/interpolation/interpolation_test.go
index c392170..1852b9e 100644
--- a/cli/compose/interpolation/interpolation_test.go
+++ b/cli/compose/interpolation/interpolation_test.go
@@ -55,5 +55,5 @@
 		},
 	}
 	_, err := Interpolate(services, "service", defaultMapping)
-	assert.EqualError(t, err, `Invalid interpolation format for "image" option in service "servicea": "${"`)
+	assert.EqualError(t, err, `Invalid interpolation format for "image" option in service "servicea": "${". You may need to escape any $ with another $.`)
 }
diff --git a/cli/compose/loader/loader.go b/cli/compose/loader/loader.go
index 0b327dd..995047e 100644
--- a/cli/compose/loader/loader.go
+++ b/cli/compose/loader/loader.go
@@ -251,6 +251,8 @@
 		return transformMappingOrList(data, "="), nil
 	case reflect.TypeOf(types.MappingWithColon{}):
 		return transformMappingOrList(data, ":"), nil
+	case reflect.TypeOf(types.ServiceVolumeConfig{}):
+		return transformServiceVolumeConfig(data)
 	}
 	return data, nil
 }
@@ -333,10 +335,7 @@
 		return nil, err
 	}
 
-	if err := resolveVolumePaths(serviceConfig.Volumes, workingDir); err != nil {
-		return nil, err
-	}
-
+	resolveVolumePaths(serviceConfig.Volumes, workingDir)
 	return serviceConfig, nil
 }
 
@@ -369,22 +368,15 @@
 	return nil
 }
 
-func resolveVolumePaths(volumes []string, workingDir string) error {
-	for i, mapping := range volumes {
-		parts := strings.SplitN(mapping, ":", 2)
-		if len(parts) == 1 {
+func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string) {
+	for i, volume := range volumes {
+		if volume.Type != "bind" {
 			continue
 		}
 
-		if strings.HasPrefix(parts[0], ".") {
-			parts[0] = absPath(workingDir, parts[0])
-		}
-		parts[0] = expandUser(parts[0])
-
-		volumes[i] = strings.Join(parts, ":")
+		volume.Source = absPath(workingDir, expandUser(volume.Source))
+		volumes[i] = volume
 	}
-
-	return nil
 }
 
 // TODO: make this more robust
@@ -555,6 +547,20 @@
 	}
 }
 
+func transformServiceVolumeConfig(data interface{}) (interface{}, error) {
+	switch value := data.(type) {
+	case string:
+		return parseVolume(value)
+	case types.Dict:
+		return data, nil
+	case map[string]interface{}:
+		return data, nil
+	default:
+		return data, fmt.Errorf("invalid type %T for service volume", value)
+	}
+
+}
+
 func transformServiceNetworkMap(value interface{}) (interface{}, error) {
 	if list, ok := value.([]interface{}); ok {
 		mapValue := map[interface{}]interface{}{}
diff --git a/cli/compose/loader/loader_test.go b/cli/compose/loader/loader_test.go
index afa2882..b9fb10f 100644
--- a/cli/compose/loader/loader_test.go
+++ b/cli/compose/loader/loader_test.go
@@ -881,13 +881,13 @@
 			},
 		},
 		User: "someone",
-		Volumes: []string{
-			"/var/lib/mysql",
-			"/opt/data:/var/lib/mysql",
-			fmt.Sprintf("%s:/code", workingDir),
-			fmt.Sprintf("%s/static:/var/www/html", workingDir),
-			fmt.Sprintf("%s/configs:/etc/configs/:ro", homeDir),
-			"datavolume:/var/lib/mysql",
+		Volumes: []types.ServiceVolumeConfig{
+			{Target: "/var/lib/mysql", Type: "volume"},
+			{Source: "/opt/data", Target: "/var/lib/mysql", Type: "bind"},
+			{Source: workingDir, Target: "/code", Type: "bind"},
+			{Source: workingDir + "/static", Target: "/var/www/html", Type: "bind"},
+			{Source: homeDir + "/configs", Target: "/etc/configs/", Type: "bind", ReadOnly: true},
+			{Source: "datavolume", Target: "/var/lib/mysql", Type: "volume"},
 		},
 		WorkingDir: "/code",
 	}
@@ -977,7 +977,7 @@
 
 func TestLoadAttachableNetwork(t *testing.T) {
 	config, err := loadYAML(`
-version: "3.1"
+version: "3.2"
 networks:
   mynet1:
     driver: overlay
@@ -985,7 +985,9 @@
   mynet2:
     driver: bridge
 `)
-	assert.NoError(t, err)
+	if !assert.NoError(t, err) {
+		return
+	}
 
 	expected := map[string]types.NetworkConfig{
 		"mynet1": {
@@ -1003,7 +1005,7 @@
 
 func TestLoadExpandedPortFormat(t *testing.T) {
 	config, err := loadYAML(`
-version: "3.1"
+version: "3.2"
 services:
   web:
     image: busybox
@@ -1019,7 +1021,9 @@
         target: 22
         published: 10022
 `)
-	assert.NoError(t, err)
+	if !assert.NoError(t, err) {
+		return
+	}
 
 	expected := []types.ServicePortConfig{
 		{
@@ -1085,3 +1089,33 @@
 	assert.Equal(t, 1, len(config.Services))
 	assert.Equal(t, expected, config.Services[0].Ports)
 }
+
+func TestLoadExpandedMountFormat(t *testing.T) {
+	config, err := loadYAML(`
+version: "3.2"
+services:
+  web:
+    image: busybox
+    volumes:
+      - type: volume
+        source: foo
+        target: /target
+        read_only: true
+volumes:
+  foo: {}
+`)
+	if !assert.NoError(t, err) {
+		return
+	}
+
+	expected := types.ServiceVolumeConfig{
+		Type:     "volume",
+		Source:   "foo",
+		Target:   "/target",
+		ReadOnly: true,
+	}
+
+	assert.Equal(t, 1, len(config.Services))
+	assert.Equal(t, 1, len(config.Services[0].Volumes))
+	assert.Equal(t, expected, config.Services[0].Volumes[0])
+}
diff --git a/cli/compose/loader/volume.go b/cli/compose/loader/volume.go
new file mode 100644
index 0000000..3f33492
--- /dev/null
+++ b/cli/compose/loader/volume.go
@@ -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
+}
diff --git a/cli/compose/loader/volume_test.go b/cli/compose/loader/volume_test.go
new file mode 100644
index 0000000..0735d5a
--- /dev/null
+++ b/cli/compose/loader/volume_test.go
@@ -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)
+	}
+}
diff --git a/cli/compose/schema/bindata.go b/cli/compose/schema/bindata.go
index 0b5aa18..8857e36 100644
--- a/cli/compose/schema/bindata.go
+++ b/cli/compose/schema/bindata.go
@@ -2,6 +2,7 @@
 // sources:
 // data/config_schema_v3.0.json
 // data/config_schema_v3.1.json
+// data/config_schema_v3.2.json
 // DO NOT EDIT!
 
 package schema
@@ -89,7 +90,7 @@
 	return a, nil
 }
 
-var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5a\xcd\x8f\xdc\x28\x16\xbf\xd7\x5f\x61\x39\xb9\xa5\x3f\xb2\xda\x68\xa5\xcd\x6d\x8f\x7b\x9a\x39\x4f\xcb\xb1\x28\xfb\x95\x8b\x34\x06\x02\xb8\xd2\x95\xa8\xff\xf7\x11\xfe\x2a\x8c\xc1\xe0\x2e\xf7\x74\x34\x9a\x53\x77\x99\xdf\x03\xde\xf7\xe3\xc1\xcf\x5d\x92\xa4\xef\x65\x71\x84\x1a\xa5\x9f\x93\xf4\xa8\x14\xff\x7c\x7f\xff\x55\x32\x7a\xdb\x7d\xbd\x63\xa2\xba\x2f\x05\x3a\xa8\xdb\x8f\x9f\xee\xbb\x6f\xef\xd2\x1b\x4d\x87\x4b\x4d\x52\x30\x7a\xc0\x55\xde\x8d\xe4\xa7\x7f\xdf\xfd\xeb\x4e\x93\x77\x10\x75\xe6\xa0\x41\x6c\xff\x15\x0a\xd5\x7d\x13\xf0\xad\xc1\x02\x34\xf1\x43\x7a\x02\x21\x31\xa3\x69\x76\xb3\xd3\x63\x5c\x30\x0e\x42\x61\x90\xe9\xe7\x44\x6f\x2e\x49\x46\xc8\xf0\xc1\x98\x56\x2a\x81\x69\x95\xb6\x9f\x9f\xdb\x19\x92\x24\x95\x20\x4e\xb8\x30\x66\x18\xb7\xfa\xee\xfe\x32\xff\xfd\x08\xbb\xb1\x67\x35\x36\xdb\x7e\xe7\x48\x29\x10\xf4\xf7\xf9\xde\xda\xe1\x2f\x0f\xe8\xf6\xc7\xff\x6e\xff\xf8\x78\xfb\xdf\xbb\xfc\x36\xfb\xf0\x7e\x32\xac\xe5\x2b\xe0\xd0\x2d\x5f\xc2\x01\x53\xac\x30\xa3\xe3\xfa\xe9\x88\x7c\xee\xff\x7b\x1e\x17\x46\x65\xd9\x82\x11\x99\xac\x7d\x40\x44\xc2\x94\x67\x0a\xea\x3b\x13\x8f\x21\x9e\x47\xd8\x1b\xf1\xdc\xaf\xef\xe0\x79\xca\xce\x89\x91\xa6\x0e\x6a\x70\x40\xbd\x11\x33\xdd\xf2\xdb\xe8\x4f\x42\x21\x40\x85\x4d\xb6\x43\xbd\x99\xc5\xea\xe5\xaf\x63\x78\x37\x30\xbd\x88\xed\x10\xc6\xda\xed\x06\x27\xee\xed\x12\x95\xcb\xbd\xfc\xb2\x1a\x85\xe5\x91\x52\x09\x9c\xb0\xb3\xfe\xe6\x91\x47\x07\xa8\x81\xaa\x74\x14\x41\x92\xa4\xfb\x06\x93\xd2\x96\x28\xa3\xf0\x9b\x9e\xe2\xc1\xf8\x98\x24\x3f\xed\x48\x66\xcc\xd3\x8e\x4f\x7e\xf9\x15\x3e\x8e\x7b\x78\x19\xc7\x0b\x46\x15\x3c\xa9\x96\xa9\xe5\xa5\x3b\x11\xb0\xe2\x11\xc4\x01\x13\x88\xa5\x40\xa2\x92\x0b\x22\x23\x58\xaa\x9c\x89\xbc\xc4\x85\x4a\x9f\x2d\xf2\xd9\x7c\x61\x7b\x1a\x49\x8d\x5f\xd9\xce\x31\x61\x5a\x20\x9e\xa3\xb2\x9c\xf0\x81\x84\x40\xe7\xf4\x26\x49\xb1\x82\x5a\xba\x59\x4c\xd2\x86\xe2\x6f\x0d\xfc\xbf\x87\x28\xd1\x80\x3d\x6f\x29\x18\xdf\x7e\xe2\x4a\xb0\x86\xe7\x1c\x09\x6d\x60\xcb\xe2\x4f\x0b\x56\xd7\x88\x6e\x65\x75\x6b\xf8\x88\x90\x3c\xa3\x0a\x61\x0a\x22\xa7\xa8\x0e\x19\x92\xf6\x3a\xa0\xa5\xcc\xbb\x84\xbf\x68\x46\x87\xbc\xa3\x97\xd6\x04\x63\xf6\xdf\x54\x1f\x25\x5d\x32\xec\x6e\x1a\x6d\xda\x7a\x6f\xa9\x45\x98\x4b\x40\xa2\x38\xbe\x90\x9e\xd5\x08\xd3\x18\xd9\x01\x55\xe2\xcc\x19\xee\xec\xe5\x97\x33\x04\xa0\xa7\x7c\x8c\x25\xab\xc5\x00\xf4\x84\x05\xa3\xf5\xe0\x0d\x31\x01\x66\x0c\xf2\x9a\xfe\x89\x33\x09\xb6\x60\x2c\x06\xcd\xa1\x91\xd5\x89\x4c\x06\x8a\x87\x81\xf1\x9b\x24\xa5\x4d\xbd\x07\xa1\x6b\xd8\x09\xf2\xc0\x44\x8d\xf4\x66\x87\xb5\x8d\xe1\x89\xa4\x1d\x96\x67\x0a\xd0\xe4\x41\xa7\x75\x44\x72\x82\xe9\xe3\xf6\x26\x0e\x4f\x4a\xa0\xfc\xc8\xa4\x8a\x8f\xe1\x06\xf9\x11\x10\x51\xc7\xe2\x08\xc5\xe3\x02\xb9\x89\x9a\x50\x33\xa9\x62\x8c\x1c\xd7\xa8\x0a\x83\x78\x11\x82\x10\xb4\x07\xf2\x22\x3e\x37\x15\xbe\x31\x2d\xab\x2a\x0d\xf5\x59\xdc\xac\x72\xe9\x87\x43\x39\xbf\x14\xf8\x04\x22\x36\x81\x33\x7e\x29\xb8\xec\xc1\x70\x01\x92\x84\xab\xcf\x09\xf4\xcb\x5d\x57\x7c\x2e\x78\x55\xfb\x1f\x21\x69\x66\x97\x0b\x89\x95\xf7\x5d\x5f\x2c\x0e\xe3\x0a\x8a\x89\x56\x6a\x54\xe8\xba\x41\x80\xf4\xe8\xf5\x02\xed\x4f\x37\x79\xcd\x4a\x9f\x81\xce\xc0\xb6\x6c\xbc\x91\x7a\x75\x22\x4c\x5e\x54\x3f\x46\xa9\x2e\x78\x80\x08\x70\xe3\xdb\x5e\xec\x36\x2f\xdb\x0d\x9b\x58\x8b\x43\x04\x23\x09\x61\x67\xf7\x0a\x72\x32\x1b\xe6\xa7\x4f\x91\x36\xe1\xa2\xfd\xcf\x22\xad\x87\xd4\x3b\x67\x7c\x8d\x1c\x98\xea\xb2\x95\xd6\xdd\x5c\x1b\xc9\x02\xde\xf6\xca\x25\x3c\xc7\xa5\x3f\x56\xb4\x11\xc2\x74\x30\xce\x84\x9a\x79\xd7\xfa\x74\xef\xb3\x60\x53\x5c\x43\x9c\xba\x24\xfc\x6e\xf1\x99\x34\x66\xea\x8e\x22\x9a\xfb\x5f\xd0\x3f\xc2\x9e\x91\x2e\x44\x29\x07\x5a\x21\x51\xc1\xf4\x18\x82\xa9\x82\x0a\x84\x87\x80\x37\x7b\x82\xe5\x11\xca\x35\x34\x82\x29\x56\x30\x12\xe7\x18\xce\xe3\x67\xbc\x33\x4c\x27\xcc\xae\xae\xcd\xb8\xc0\x27\x4c\xa0\xb2\x38\xde\x33\x46\x00\xd1\x49\xa2\x10\x80\xca\x9c\x51\x72\x8e\x40\x4a\x85\x44\xf0\xf8\x27\xa1\x68\x04\x56\xe7\x9c\x71\xb5\x79\x55\x28\x8f\x75\x2e\xf1\x0f\x98\xfa\xde\xc5\xea\xfb\x89\x32\x6b\x43\x56\x3f\x2b\x79\x2d\xf7\xf3\x99\xed\x2b\xb9\x8d\x64\x8d\x28\xae\x73\x9c\x45\x7c\x33\x0d\x72\xcb\xe0\x6a\x0d\x78\xe6\xf0\xbd\x0a\x43\x35\xd4\xa2\xab\x38\x03\xb5\x3c\xcb\x42\xbd\xac\xb6\x96\xaa\xc4\x34\x67\x1c\x68\xd0\x37\xa4\x62\x3c\xaf\x04\x2a\x20\xe7\x20\x30\x73\x8a\x62\x12\x60\xcb\x46\x20\xbd\xfe\x7c\x1a\x89\x2b\x8a\xdc\x71\xc7\x80\xaa\x9a\x1f\x5e\xd8\x04\x50\x2a\xec\xec\x0d\xc1\x35\xf6\x3b\x8d\xc3\x6a\x23\xea\xb5\xae\x56\x73\x97\x68\x0b\xe5\x59\x54\xc8\x5e\x38\x21\x2c\x1f\x10\x22\x4e\x06\x47\x24\x56\xa4\x8e\xd6\x31\x0f\x9e\xfc\xe4\x3a\x37\x38\xf7\x35\xb9\x99\x6a\xe7\xbb\xe9\x37\x92\x39\xf1\xab\x4a\x2f\x7b\x1b\x99\xb7\xfa\x71\x3b\x55\x23\x83\x87\xb8\x16\x43\xe5\xd2\x01\x64\x84\x1a\x57\x2c\x9b\x66\x0b\x7d\xa8\xd1\x4e\x50\x62\xf7\x6e\x77\x16\x67\x2b\x2e\x49\xac\xfe\xc2\x30\x81\xab\xfb\x6f\x42\x83\xb7\x25\xcb\x37\x11\x3d\xc8\x7b\x4b\x80\x25\xda\x5b\xfd\x71\x97\x73\x6b\x6b\x14\xa7\x70\x8c\x11\xa0\x04\xb6\xf4\x32\x04\x6a\x33\x9e\x80\xfc\x35\x9b\x7c\x0a\xd7\xc0\x1a\x77\xc2\xdb\x99\xf6\xdd\x13\xa5\xc6\x2d\x4a\x40\xa9\x06\xd2\xd6\xe9\xc3\xa8\xd4\xe1\x2c\x10\x54\x5c\x8c\x93\x08\xe0\x04\x17\x48\x86\x02\xd1\x15\xcd\xa4\x86\x97\x48\x41\xde\xdd\xa2\xaf\x0a\xfd\x0b\x31\x9f\x23\x81\x08\x01\x82\x65\x1d\x13\x43\xd3\x12\x08\x3a\xbf\x28\x7d\xb6\xe4\x07\x84\x49\x23\x20\x47\x85\xea\x2f\xea\x03\x36\x97\xd6\x8c\x62\xc5\x9c\x11\x22\x6e\xc9\x1a\x3d\xe5\xc3\xb2\x2d\x24\x54\xd9\x4c\x8b\xfa\xd8\x3e\x90\x61\x09\x5d\xe1\xb7\x2e\x3b\x2f\xa8\xe8\x92\xeb\x3d\x16\x33\xac\x38\x63\x5d\x80\xd4\x91\x64\x6c\xd3\x05\xe9\x83\xa9\xa5\x3f\x65\xe4\x9c\x11\x5c\x9c\xb7\xe2\xb0\x60\xb4\x13\x72\x8c\x41\x5c\x69\x81\xda\x1c\x74\x29\x54\x73\x15\x74\xd6\x96\xe0\x3b\xa6\x25\xfb\xbe\x62\xc1\xed\x4c\x89\x13\x54\x80\x15\xef\xae\x15\xb4\x54\x02\x61\xaa\x56\xa7\xf3\x6b\xd9\xba\x22\x9b\x8f\xf6\x19\x88\xfa\x23\x2e\xfc\xea\xc1\x13\xe9\x0b\xde\x04\x7b\xb7\x35\xd4\x4c\x38\x0d\x70\x83\x67\x39\x21\x16\x07\xd8\x06\x59\x2d\xaa\xd9\xdf\xa3\x72\xc6\xb7\x3f\x6d\x84\x1b\xfa\x59\x38\x20\x61\x8e\xea\xad\xbc\x23\xfa\xfa\x23\x75\xe6\xe0\x64\xb9\x6f\x91\xf8\x7b\x17\xa1\x5d\x87\xf7\xde\x23\x64\xb3\xa7\x9e\x16\xc2\xfc\x94\xb1\x65\x53\x6c\xc3\xa0\x37\xdc\x5c\x7a\xb4\xfa\x30\xd6\xcc\x37\xa3\xac\xb2\x68\x15\x7b\xaf\x0d\xb7\xdb\x7f\x5b\xbe\xdb\x2d\x02\x57\x9d\x8f\x94\x42\xc5\x31\xea\x48\xb0\xb2\x68\xbc\x22\x0e\xf5\x4f\xd5\x02\x61\xa8\x47\xfd\x13\x85\xfe\x26\x36\xfb\xd7\xd9\x57\xff\x32\x30\xf8\x24\xaf\x45\xbd\x38\x8f\x47\xbc\x43\xfb\x05\x74\xf6\xd6\xaa\x98\xf6\x20\x0d\x95\xcc\xdb\x03\x4b\x92\x8c\xbe\x28\xed\x29\xb2\xe9\x36\x6c\x98\xe3\xf1\xf6\x34\x99\x2e\xf5\x9c\x06\x88\xe7\x2a\xc6\x5a\xb4\x17\xe2\x32\xe7\x1b\x06\x9b\xbb\x0f\x0b\x25\xc3\xd2\x83\x86\x57\xca\xb5\x1b\xf4\xf3\xdc\x3a\xb5\xce\x19\x83\x74\xe7\x0f\x72\x3d\xfe\x6f\xd0\xcf\x9e\xe7\x6a\x3e\xe9\x79\xd6\xbe\xfa\x39\xed\xc9\x76\x4f\x6b\xb3\x89\x7c\x2c\x48\xf7\x3c\xc8\x88\xee\x99\x79\xf4\xf2\xa9\xd1\xf9\x68\xd7\xee\x08\x0f\x8f\x67\x3d\x17\x20\x3b\xf3\x6f\xfb\xd0\x79\xf7\xbc\xfb\x33\x00\x00\xff\xff\xfa\xcc\x57\x15\x61\x31\x00\x00")
+var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x1a\xcb\x8e\xdb\x36\xf0\xee\xaf\x10\x94\xdc\xe2\xdd\x4d\xd1\xa0\x40\x73\xeb\xb1\xa7\xf6\xdc\x85\x23\xd0\xd2\x58\x66\x96\x22\x19\x92\x72\xd6\x09\xfc\xef\x05\xf5\x32\x45\x91\x22\x6d\x2b\xd9\x45\xd1\xd3\xae\xc5\x99\xe1\xbc\x67\x38\xe4\xf7\x55\x92\xa4\x6f\x65\xbe\x87\x0a\xa5\x1f\x93\x74\xaf\x14\xff\xf8\xf0\xf0\x59\x32\x7a\xd7\x7e\xbd\x67\xa2\x7c\x28\x04\xda\xa9\xbb\xf7\x1f\x1e\xda\x6f\x6f\xd2\xb5\xc6\xc3\x85\x46\xc9\x19\xdd\xe1\x32\x6b\x57\xb2\xc3\xaf\xf7\xbf\xdc\x6b\xf4\x16\x44\x1d\x39\x68\x20\xb6\xfd\x0c\xb9\x6a\xbf\x09\xf8\x52\x63\x01\x1a\xf9\x31\x3d\x80\x90\x98\xd1\x74\xb3\x5e\xe9\x35\x2e\x18\x07\xa1\x30\xc8\xf4\x63\xa2\x99\x4b\x92\x01\xa4\xff\x60\x90\x95\x4a\x60\x5a\xa6\xcd\xe7\x53\x43\x21\x49\x52\x09\xe2\x80\x73\x83\xc2\xc0\xea\x9b\x87\x33\xfd\x87\x01\x6c\x6d\x53\x35\x98\x6d\xbe\x73\xa4\x14\x08\xfa\xf7\x94\xb7\x66\xf9\xd3\x23\xba\xfb\xf6\xc7\xdd\x3f\xef\xef\x7e\xbf\xcf\xee\x36\xef\xde\x8e\x96\xb5\x7e\x05\xec\xda\xed\x0b\xd8\x61\x8a\x15\x66\x74\xd8\x3f\x1d\x20\x4f\xdd\x7f\xa7\x61\x63\x54\x14\x0d\x30\x22\xa3\xbd\x77\x88\x48\x18\xcb\x4c\x41\x7d\x65\xe2\x29\x24\xf3\x00\xf6\x42\x32\x77\xfb\x3b\x64\x1e\x8b\x73\x60\xa4\xae\x82\x16\xec\xa1\x5e\x48\x98\x76\xfb\x65\xec\x27\x21\x17\xa0\xc2\x2e\xdb\x42\xbd\x98\xc7\xea\xed\x6f\x13\x78\xd5\x0b\x3d\x0b\xdb\x42\x18\x7b\x37\x0c\x8e\xc2\xdb\xa5\x2a\x57\x78\xf9\x75\x35\x28\xcb\xa3\xa5\x02\x38\x61\x47\xfd\xcd\xa3\x8f\x16\xa0\x02\xaa\xd2\x41\x05\x49\x92\x6e\x6b\x4c\x0a\x5b\xa3\x8c\xc2\x5f\x9a\xc4\xa3\xf1\x31\x49\xbe\xdb\x99\xcc\xa0\xd3\xac\x8f\x7e\xf9\x0d\x3e\xac\x7b\x64\x19\xd6\x73\x46\x15\x3c\xab\x46\xa8\xf9\xad\x5b\x15\xb0\xfc\x09\xc4\x0e\x13\x88\xc5\x40\xa2\x94\x33\x2a\x23\x58\xaa\x8c\x89\xac\xc0\xb9\x4a\x4f\x16\xfa\x84\x5e\xd8\x9f\x06\x54\xe3\xd7\x66\xe5\x20\x98\xe6\x88\x67\xa8\x28\x46\x72\x20\x21\xd0\x31\x5d\x27\x29\x56\x50\x49\xb7\x88\x49\x5a\x53\xfc\xa5\x86\x3f\x3b\x10\x25\x6a\xb0\xe9\x16\x82\xf1\xe5\x09\x97\x82\xd5\x3c\xe3\x48\x68\x07\x9b\x57\x7f\x9a\xb3\xaa\x42\x74\x29\xaf\xbb\x44\x8e\x08\xcd\x33\xaa\x10\xa6\x20\x32\x8a\xaa\x90\x23\xe9\xa8\x03\x5a\xc8\xac\x2d\xf8\xb3\x6e\xb4\xcb\x5a\x7c\x69\x11\x18\xaa\xff\xa2\xf6\x28\xe8\x9c\x63\xb7\x64\xb4\x6b\x6b\xde\x52\x0b\x31\x93\x80\x44\xbe\xbf\x12\x9f\x55\x08\xd3\x18\xdd\x01\x55\xe2\xc8\x19\x6e\xfd\xe5\xd5\x39\x02\xd0\x43\x36\xe4\x92\x8b\xd5\x00\xf4\x80\x05\xa3\x55\x1f\x0d\x31\x09\x66\x48\xf2\x1a\xff\x99\x33\x09\xb6\x62\x2c\x01\xcd\xa5\x41\xd4\x91\x4e\x7a\x8c\xc7\x5e\xf0\x75\x92\xd2\xba\xda\x82\xd0\x3d\xec\x08\x72\xc7\x44\x85\x34\xb3\xfd\xde\xc6\xf2\x48\xd3\x0e\xcf\x33\x15\x68\xca\xa0\xcb\x3a\x22\x19\xc1\xf4\x69\x79\x17\x87\x67\x25\x50\xb6\x67\x52\xc5\xe7\x70\x03\x7d\x0f\x88\xa8\x7d\xbe\x87\xfc\x69\x06\xdd\x84\x1a\x61\x33\xa9\x62\x9c\x1c\x57\xa8\x0c\x03\xf1\x3c\x04\x42\xd0\x16\xc8\x55\x72\x2e\xaa\x7c\x83\x2c\x2b\x4b\x0d\xea\xf3\xb8\x49\xe7\xd2\x2d\x87\x6a\x7e\x21\xf0\x01\x44\x6c\x01\x67\xfc\xdc\x70\xd9\x8b\xe1\x06\x24\x09\x77\x9f\x23\xd0\x4f\xf7\x6d\xf3\x39\x13\x55\xcd\x7f\x84\xa4\x1b\xbb\x5d\x48\xac\xba\xef\xfa\x62\x49\x18\xd7\x50\x8c\xac\x52\xa1\x5c\xf7\x0d\x02\xa4\xc7\xae\x67\xd0\xee\x74\x93\x55\xac\xf0\x39\xe8\x04\xd8\xd6\x8d\x37\x53\x5f\x5c\x08\x93\xab\xfa\xc7\x28\xd3\x05\x0f\x10\x01\x69\x7c\xec\xc5\xb2\x79\x66\x37\xec\x62\x0d\x1c\x22\x18\x49\x08\x07\xbb\x57\x91\x23\x6a\x98\x1f\x3e\x44\xfa\x84\x0b\xf7\xb7\x59\x5c\x0f\xaa\x97\x66\x7c\x8f\x1c\x20\x75\x66\xa5\x09\x37\x17\x23\x9b\x40\xb4\xfd\xe0\x16\x9e\xe3\xc2\x9f\x2b\x9a\x0c\x61\x06\x18\x67\x42\x4d\xa2\xeb\xe7\x94\xfb\x76\xeb\x9b\xab\x3d\x17\xf8\x80\x09\x94\x30\x3e\xb5\x6c\x19\x23\x80\xe8\x28\xf5\x08\x40\x45\xc6\x28\x39\x46\x40\x4a\x85\x44\xf0\x40\x21\x21\xaf\x05\x56\xc7\x8c\x71\xb5\x78\x9f\x21\xf7\x55\x26\xf1\x37\x18\x5b\xf3\x9c\xef\x3b\x42\x1b\x8b\x21\x6b\x42\x72\xa5\x41\x7d\x29\x29\x1c\xc6\x8e\x44\x18\x4c\x54\xe1\x14\x95\x4a\x56\x8b\x3c\xf6\x80\xad\xf7\x44\xa2\x84\xd8\x23\xbc\x76\xb7\x71\xd8\xcc\x03\x97\x97\x00\x4f\x0a\x5d\x67\xc2\x50\x55\xb6\x7f\x9b\x79\xe5\xe4\x0c\x7d\x79\x94\xb9\xba\xae\x5b\x93\xaa\xc0\x34\x63\x1c\x68\x30\x36\xa4\x62\x3c\x2b\x05\xca\x21\xe3\x20\x30\x73\xaa\x62\x6d\x46\x7a\x51\x0b\xa4\xf7\x9f\x92\x91\xb8\xa4\x88\x84\xc2\x4c\x55\x7c\x77\xe5\xb1\x52\xa9\x70\xb0\xd7\x04\x57\xd8\x1f\x34\x0e\xaf\x8d\xe8\x00\xda\xea\xef\x2e\xfa\x33\x05\xff\xcc\x29\xa6\x0a\x4a\xed\x26\x53\xa7\x9a\xe9\x39\xe7\x5b\xce\x88\x5e\x73\x8f\xc4\xd8\xa0\x33\x7c\x24\x6d\x60\xee\x94\x1b\xc1\xd5\x89\x3a\xf9\x1a\xdd\x75\x34\xf4\xd6\x1d\x23\x1b\x27\xfc\x45\xc5\xdc\x66\x63\xe3\xad\xa7\xee\xa0\xaa\x65\xf0\x58\xd0\xc0\x50\x39\xd7\xd2\x0e\xa0\xc6\xd0\x7e\xd1\x6a\xa1\xdb\x64\x1d\x04\x05\x76\x73\xbb\xb2\x24\xbb\x60\xec\x6e\x9d\x58\x7b\x02\xae\x79\xb2\x09\x1a\x9c\xbf\xcf\xcf\xb6\x3b\x20\xef\xdc\x19\x4b\xb4\xb5\x26\xae\xae\xe0\xd6\xde\x28\x0e\xe1\x1c\x23\x40\x09\x6c\xd9\xa5\x4f\xd4\x66\x3e\x01\xf9\x3a\xc7\x46\x0a\x57\xc0\x6a\x77\xc1\x5b\x99\xfe\xdd\x21\xa5\xc6\x5c\x3e\x60\x54\x03\xd2\xb6\xe9\xe3\x60\xd4\xbe\xbb\x0c\x1a\x2e\x26\x48\x04\x70\x82\x73\x24\x43\x89\xe8\x86\xf1\x44\xcd\x0b\xa4\x20\x6b\xef\x65\x2f\x4a\xfd\x33\x39\x9f\x23\x81\x08\x01\x82\x65\x15\x93\x43\xd3\x02\x08\x3a\x5e\x55\x3e\x1b\xf4\x1d\xc2\xa4\x16\x90\xa1\x5c\x75\x57\xbf\x01\x9f\x4b\x2b\x46\xb1\x62\xce\x0c\x11\xb7\x65\x85\x9e\xb3\x7e\xdb\x06\x24\xd4\xd9\x8c\x9b\xfa\xd8\xc9\x82\xe1\x09\x6d\xe3\x77\x59\x75\x9e\x31\xd1\xb9\xd6\x7b\x3c\xa6\xdf\x71\x22\xba\x00\xa9\x33\xc9\x30\xf8\x09\xe2\x07\x4b\x4b\x77\xca\xc8\x38\x23\x38\x3f\x2e\x25\x61\xce\x68\xab\xe4\x18\x87\xb8\xd1\x03\xb5\x3b\xe8\x56\xa8\xe2\x2a\x18\xac\x0d\xc2\x57\x4c\x0b\xf6\xf5\x82\x0d\x97\x73\x25\x4e\x50\x0e\x56\xbe\xbb\x55\xd1\x52\x09\x84\xa9\xba\xb8\x9c\xdf\x2a\xd6\x0d\xd5\x7c\xf0\xcf\x40\xd6\x1f\xe0\xc2\xf7\xe8\x9e\x4c\x9f\xf3\x3a\x38\x0d\xac\xa0\x62\xc2\xe9\x80\x0b\x3c\xf4\x08\x89\xd8\x83\x2d\x50\xd5\xa2\xc6\xc7\x1d\x54\xc6\xf8\xf2\xa7\x8d\xf0\x88\x78\x13\x4e\x48\x98\xa3\x6a\xa9\xe8\x88\x1e\xa8\xa7\xce\x1a\x9c\xcc\xcf\x2d\x12\xff\xec\x22\xc4\x75\x98\xf7\x0e\x42\xd6\x5b\xea\x19\x21\x4c\x4f\x19\xae\x5b\xfe\xf8\x63\xca\xc9\x7f\x28\xb9\x2d\xe9\xf5\x77\x61\x1e\xab\x3e\x0e\x3d\xf3\x7a\xd0\xd5\x26\xda\xc4\xde\x8b\xa8\xe5\xf8\x6f\xda\x77\x7b\x44\xe0\xea\xf3\x2f\xec\x04\x6f\x48\x2e\xdd\x8b\xa6\x40\x6e\xe9\xa0\xfe\x4f\x2d\xff\x11\x47\xfc\x79\xfe\xd5\x3d\x20\x0b\xbe\xdc\x6a\xa0\xae\x2e\xce\x11\xcf\x95\x5e\x81\xcd\x5e\xda\x14\xe3\xc1\xa2\x61\x92\xe9\x99\x7f\x4e\x93\xd1\xf7\x69\x1d\xc6\x66\xcc\x86\x0d\xe6\x78\xe3\x3b\xae\x90\x73\x83\xa4\x1e\xc4\x73\xbf\x62\x6d\xda\x29\x71\x5e\xf2\x05\x93\xcd\xfd\xbb\x99\x3e\x60\xee\xde\xfb\x07\x15\xd0\x05\x86\x74\x6e\x9b\x5a\x87\x87\x5e\xbb\xd3\x77\x9b\x9e\xf8\x37\xf0\x27\xaf\x38\xb5\x9c\xf4\x38\x99\x49\x7d\x1f\x0f\x5a\xdb\x17\x98\x9b\x91\x7e\x2c\x90\xf6\x15\x89\x91\xdd\x37\xe6\x79\xca\x67\x46\xe7\xdb\x4e\x7b\xcc\xdb\xbf\xb1\xf4\xdc\x6a\xac\xcc\xbf\xcd\x7b\xd8\xd5\x69\xf5\x6f\x00\x00\x00\xff\xff\xfc\xf3\x11\x6a\x88\x2f\x00\x00")
 
 func dataConfig_schema_v31JsonBytes() ([]byte, error) {
 	return bindataRead(
@@ -109,6 +110,26 @@
 	return a, nil
 }
 
+var _dataConfig_schema_v32Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5b\xcd\x73\xdc\x28\x16\xbf\xf7\x5f\xa1\x52\x72\x8b\x3f\xb2\xb5\xa9\xad\xda\xdc\xf6\xb8\xa7\x99\xf3\xb8\x3a\x2a\x1a\xbd\x56\x13\x4b\x40\x00\xb5\xdd\x49\xf9\x7f\x9f\xd2\x67\x03\x02\x81\xba\xe5\x38\x33\x35\x27\xdb\xe2\xf7\x80\xf7\xfd\x1e\xe0\x1f\x9b\x24\x49\xdf\x4b\x7c\x80\x0a\xa5\x9f\x93\xf4\xa0\x14\xff\x7c\x7f\xff\x55\x32\x7a\xdb\x7d\xbd\x63\xa2\xb8\xcf\x05\xda\xab\xdb\x8f\x9f\xee\xbb\x6f\xef\xd2\x9b\x86\x8e\xe4\x0d\x09\x66\x74\x4f\x8a\xac\x1b\xc9\x8e\xff\xbe\xfb\xd7\x5d\x43\xde\x41\xd4\x89\x43\x03\x62\xbb\xaf\x80\x55\xf7\x4d\xc0\xb7\x9a\x08\x68\x88\x1f\xd2\x23\x08\x49\x18\x4d\xb7\x37\x9b\x66\x8c\x0b\xc6\x41\x28\x02\x32\xfd\x9c\x34\x9b\x4b\x92\x11\x32\x7c\xd0\xa6\x95\x4a\x10\x5a\xa4\xed\xe7\x97\x76\x86\x24\x49\x25\x88\x23\xc1\xda\x0c\xe3\x56\xdf\xdd\x9f\xe7\xbf\x1f\x61\x37\xf6\xac\xda\x66\xdb\xef\x1c\x29\x05\x82\xfe\x3e\xdd\x5b\x3b\xfc\xe5\x01\xdd\x7e\xff\xdf\xed\x1f\x1f\x6f\xff\x7b\x97\xdd\x6e\x3f\xbc\x37\x86\x1b\xf9\x0a\xd8\x77\xcb\xe7\xb0\x27\x94\x28\xc2\xe8\xb8\x7e\x3a\x22\x5f\xfa\xdf\x5e\xc6\x85\x51\x9e\xb7\x60\x54\x1a\x6b\xef\x51\x29\xc1\xe4\x99\x82\x7a\x62\xe2\x31\xc4\xf3\x08\x7b\x23\x9e\xfb\xf5\x1d\x3c\x9b\xec\x1c\x59\x59\x57\x41\x0d\x0e\xa8\x37\x62\xa6\x5b\x7e\x1d\xfd\x49\xc0\x02\x54\xd8\x64\x3b\xd4\x9b\x59\x6c\xb3\xfc\x75\x0c\x6f\x06\xa6\x67\xb1\x1d\x42\x5b\xbb\xdd\xa0\xe1\xde\x2e\x51\xb9\xdc\xcb\x2f\xab\x51\x58\x1e\x29\xe5\xc0\x4b\x76\x6a\xbe\x79\xe4\xd1\x01\x2a\xa0\x2a\x1d\x45\x90\x24\xe9\xae\x26\x65\x6e\x4b\x94\x51\xf8\xad\x99\xe2\x41\xfb\x98\x24\x3f\xec\x48\xa6\xcd\xd3\x8e\x1b\x7f\xf9\x15\x3e\x8e\x7b\x78\x19\xc7\x31\xa3\x0a\x9e\x55\xcb\xd4\xfc\xd2\x9d\x08\x18\x7e\x04\xb1\x27\x25\xc4\x52\x20\x51\xc8\x19\x91\x95\x44\xaa\x8c\x89\x2c\x27\x58\x39\xe9\x31\xc2\x07\xc8\xf6\x82\x55\xc1\x59\xf6\x59\xb7\x0f\x99\xbe\x58\xf3\x4c\x26\x0e\x1b\xe6\x48\xaa\xfd\xb5\xdd\x38\x26\x4c\x31\xe2\x19\xca\x73\x43\x20\x48\x08\x74\x4a\x6f\x92\x94\x28\xa8\xa4\x5b\x56\x49\x5a\x53\xf2\xad\x86\xff\xf7\x10\x25\x6a\xb0\xe7\xcd\x05\xe3\xeb\x4f\x5c\x08\x56\xf3\x8c\x23\xd1\x58\xea\xbc\x1e\x53\xcc\xaa\x0a\xd1\xb5\xcc\x77\x09\x1f\x11\x92\x67\x54\x21\x42\x41\x64\x14\x55\x21\x8b\x6c\xdc\x17\x68\x2e\xb3\xae\x72\x88\xb5\x24\x63\x82\xb1\x8c\x58\x55\x1f\x39\x9d\xf3\x90\x6e\x9a\xc6\x47\x9a\xbd\xa5\x16\x61\x26\x01\x09\x7c\xb8\x90\x9e\x55\x88\xd0\x18\xd9\x01\x55\xe2\xc4\x19\xe9\xec\xe5\x97\x33\x04\xa0\xc7\x6c\x0c\x4a\x8b\xc5\x00\xf4\x48\x04\xa3\xd5\xe0\x0d\x71\x91\x4a\xa3\x7f\xe6\x4c\x82\x2d\x18\x8b\x41\x7d\x68\x64\xd5\x90\xc9\x40\xf1\x30\x30\x7e\x93\xa4\xb4\xae\x76\x20\x9a\x62\xd8\x40\xee\x99\xa8\x50\xb3\xd9\x61\x6d\x6d\xd8\x90\xb4\xc3\xf2\x74\x01\xea\x3c\x34\xf5\x01\x2a\xb3\x92\xd0\xc7\xf5\x4d\x1c\x9e\x95\x40\xd9\x81\x49\x75\x49\x32\x48\x0f\x80\x4a\x75\xc0\x07\xc0\x8f\x33\xe4\x3a\xca\xa0\x66\x52\xc5\x18\x39\xa9\x50\x11\x06\x71\x1c\x82\x94\x68\x07\xe5\x45\x7c\xae\x2a\x7c\x6d\x5a\x56\x14\x0d\xd4\x67\x71\x93\x12\xa8\x1f\x0e\x15\x0f\xb9\x20\x47\x10\xb1\x95\x00\xe3\xe7\xca\xcd\x1e\x0c\x57\x32\x49\xb8\x8c\x35\xa0\x5f\xee\xba\x2a\x76\xc6\xab\xda\xdf\xca\x32\xdd\xda\xe5\x42\x62\xe5\x7d\xd7\x17\x8b\xc3\xb8\x82\xc2\xd0\x4a\x85\x70\x53\x37\x08\x90\x1e\xbd\x9e\xa1\x7d\x9b\x94\x55\x2c\xf7\x19\xe8\x04\x6c\xcb\xc6\x1b\xa9\x17\x27\xc2\xe4\xa2\x42\x34\x4a\x75\xc1\x4e\x24\xc0\x8d\x6f\x7b\xb1\xdb\x3c\x6f\x37\x6c\x62\x2d\x0e\x95\x04\x49\x08\x3b\xbb\x57\x90\xc6\x6c\x84\x1f\x3f\x45\xda\x84\x8b\xf6\x3f\xb3\xb4\x1e\x52\xef\x9c\xf1\x35\x72\x60\xaa\xf3\x56\x5a\x77\x73\x6d\x64\x1b\xf0\xb6\x57\x2e\xe1\x39\xc9\xfd\xb1\xa2\x8d\x10\xba\x83\x71\x26\xd4\xc4\xbb\x96\xa7\x7b\x9f\x05\xeb\xe2\x1a\xe2\xd4\x39\xe1\x77\x8b\x4f\xa4\x31\x51\x77\x14\xd1\xd4\xff\x82\xfe\x11\xf6\x8c\x74\x26\x4a\x39\xd0\x0a\x89\x02\xcc\x36\x84\x50\x05\x05\x08\x0f\x01\xaf\x77\x25\x91\x07\xc8\x97\xd0\x08\xa6\x18\x66\x65\x9c\x63\x38\xfb\xd8\x78\x67\x30\x27\xdc\x5e\x5d\x9b\x71\x41\x8e\xa4\x84\xc2\xe2\x78\xc7\x58\x09\x88\x1a\x89\x42\x00\xca\x33\x46\xcb\x53\x04\x52\x2a\x24\x82\xed\x9f\x04\x5c\x0b\xa2\x4e\x19\xe3\x6a\xf5\xaa\x50\x1e\xaa\x4c\x92\xef\x60\xfa\xde\xd9\xea\xfb\x89\xb6\xd6\x86\xac\x83\xb1\xe4\xb5\xdc\xcf\x67\xb6\xaf\xe4\x36\x92\xd5\x02\x5f\xe7\x38\xb3\xf8\xda\x0c\x72\xf3\xe0\x62\x09\x78\xe2\xf0\xbd\x0a\x43\x35\xd4\xac\xab\x38\x03\xb5\x3c\x49\xac\x2e\xab\xad\xa5\xca\x09\xcd\x18\x07\x1a\xf4\x0d\xa9\x18\xcf\x0a\x81\x30\x64\x1c\x04\x61\x4e\x51\x18\x01\x36\xaf\x05\x6a\xd6\x9f\x4e\x23\x49\x41\x91\x3b\xee\x68\x50\x55\xf1\xfd\x85\x87\x00\x4a\x85\x9d\xbd\x2e\x49\x45\xfc\x4e\xe3\xb0\xda\x88\x7a\xad\xab\xd5\xdc\x25\xda\x4c\x79\x16\x15\xb2\x67\x3a\x84\xf9\x06\x21\xa2\x33\x38\x20\xb1\x20\x75\xb4\x8e\xb9\xf7\xe4\x27\x57\xdf\xe0\xdc\x97\x71\xc5\xd5\xce\x77\xd3\x6f\x64\xeb\xc4\x2f\x2a\xbd\xec\x6d\x6c\xbd\xd5\x8f\xdb\xa9\x6a\x19\x6c\xe2\x5a\x0c\x95\x73\x0d\xc8\x08\x9d\xde\xd5\x24\x7f\x89\x08\x6d\xe8\xa8\x85\x3b\x74\x13\x11\xc7\xfb\x95\x22\x63\xe7\x6b\x47\xfd\xe8\x8a\x40\xa3\xd9\x91\xc9\x81\xef\x12\x49\xc6\xc9\x69\x44\xa1\xa2\x0b\x9d\xd1\x3d\x4b\xbc\xdb\xf5\x37\x72\x3f\x85\x15\xca\x30\xe3\x1e\x29\xc7\xb3\xb1\x34\x63\x5a\xa7\x10\x33\x25\xa5\xcf\xfb\x9f\x98\x78\x6c\x72\x4b\x4e\xdc\x41\x60\x63\x91\x2c\xb8\xc4\xb4\x8e\xed\x86\x09\x5c\xb7\x73\x3a\x34\x78\x9b\x39\x7f\x53\xd8\x83\xbc\xb7\x78\x44\xa2\x9d\x75\x7f\xe5\xca\x99\x4d\x90\x17\xc7\x70\xea\x16\xa0\x04\xb1\x6e\x05\x86\xfa\x47\x4f\xd3\x20\x7f\xcd\xb3\x73\x45\x2a\x60\xb5\x3b\xa2\x6c\x74\xc3\xe9\x89\x52\xed\x96\x33\xa0\x54\x0d\x69\xeb\xf4\x61\x54\xea\xd0\x62\x07\x15\x17\x93\x7b\x80\xe6\xed\x2d\x45\x54\xa2\x12\xc0\x4b\x82\x91\x0c\x15\x03\x57\x1c\xe8\xd6\x3c\x47\x0a\xb2\xee\x49\xcc\xa2\xf2\x6b\xa6\xee\xe2\x48\xa0\xb2\x84\x92\xc8\x2a\xa6\x8e\x49\x73\x28\xd1\xe9\xa2\x12\xb6\x25\xdf\x23\x52\xd6\x02\x32\x84\xbd\x61\xda\xa2\xa8\x18\x25\x8a\x39\xc3\x49\xdc\x92\x15\x7a\xce\x86\x65\x5b\x48\xa8\xbb\x30\x1b\xeb\xd8\xb3\x58\xcd\x12\xba\x34\xbc\xac\x42\x9e\x51\xd1\xb9\xde\xf6\x58\xcc\xb0\xe2\x84\x75\x01\xb2\x09\x3b\xe3\x51\x79\x90\x3e\x18\xe0\xfb\x4e\x3f\xe3\xac\x24\xf8\xb4\x16\x87\x98\xd1\x4e\xc8\x31\x06\x71\xa5\x05\x36\xe6\xd0\xb4\x23\x15\x57\x41\x67\x6d\x09\x9e\x08\xcd\xd9\xd3\x82\x05\xd7\x33\x25\x5e\x22\x0c\x56\x70\xbc\x56\xd0\x52\x09\x44\xa8\x5a\x7c\x33\x74\x2d\x5b\x57\xa4\xfe\xd1\x3e\x03\x29\x62\xc4\x05\x93\xbe\x2f\x2d\x60\x5e\x07\xef\x4f\x2a\xa8\x98\x70\x1a\xe0\x0a\x6f\xec\x42\x2c\x0e\xb0\x15\x52\x60\xd4\x85\x5b\x8f\xca\x18\x5f\xbf\xe3\x0f\x5f\xaa\x6d\xc3\x01\x89\x70\x54\xad\xe5\x1d\xd1\x57\x90\xa9\x33\x07\x27\xf3\x9d\x69\xe2\xef\x4e\x43\xbb\x0e\xef\xbd\x47\xc8\x7a\x47\x3d\x0d\xdd\xb4\x19\x58\xf3\x60\x7a\xc5\xa0\x37\xbc\x1e\xf0\x68\xf5\x61\x2c\xb0\x6f\x46\x59\x6d\xa3\x55\xec\xbd\xba\x5f\x6f\xff\x6d\xad\x6f\x1f\xd3\xb9\x9a\x02\xa4\x14\xc2\x87\xa8\xfe\x61\x61\xd1\x78\x45\x1c\x9a\x74\xb9\xce\x30\xd4\xa3\xfe\x89\x42\x7f\x13\x9b\xfd\x79\xf6\xd5\x3f\xf3\x0d\xbe\xaf\x6d\x51\x17\xe7\xf1\x88\x47\xa5\xbf\x80\xce\xde\x5a\x15\xe6\x3d\x80\xa6\x92\xe9\x59\xc2\x9c\x24\x97\x3e\xa4\xdd\x9a\xdb\xb0\x61\x8e\xff\xc4\x30\x93\xe9\xdc\x2d\xe1\x00\xf1\x9c\x5d\x59\x8b\xf6\x42\x9c\xe7\x7c\xc5\x60\x73\xf7\x61\xa6\x64\x98\x7b\x54\xf4\x4a\xb9\x76\x85\x1b\x58\xb7\x4e\xad\x3e\x63\x90\xee\xf4\x75\xbd\xc7\xff\x35\xfa\xc9\x5b\xfb\x86\x4f\x7a\x9a\x9c\x75\xfd\x30\xcf\xdc\xbb\x77\xf2\x5b\x43\x3e\x16\xa4\x7b\xa2\xa7\x45\xf7\xad\xde\x7a\xf9\xd4\xe8\x7c\x81\x6f\x9f\xf8\x0f\x2f\xe1\x3d\x97\x90\x1b\xfd\x67\xfb\x5f\x0b\x9b\x97\xcd\x9f\x01\x00\x00\xff\xff\x0b\x42\x15\x69\x2e\x35\x00\x00")
+
+func dataConfig_schema_v32JsonBytes() ([]byte, error) {
+	return bindataRead(
+		_dataConfig_schema_v32Json,
+		"data/config_schema_v3.2.json",
+	)
+}
+
+func dataConfig_schema_v32Json() (*asset, error) {
+	bytes, err := dataConfig_schema_v32JsonBytes()
+	if err != nil {
+		return nil, err
+	}
+
+	info := bindataFileInfo{name: "data/config_schema_v3.2.json", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)}
+	a := &asset{bytes: bytes, info: info}
+	return a, nil
+}
+
 // Asset loads and returns the asset for the given name.
 // It returns an error if the asset could not be found or
 // could not be loaded.
@@ -163,6 +184,7 @@
 var _bindata = map[string]func() (*asset, error){
 	"data/config_schema_v3.0.json": dataConfig_schema_v30Json,
 	"data/config_schema_v3.1.json": dataConfig_schema_v31Json,
+	"data/config_schema_v3.2.json": dataConfig_schema_v32Json,
 }
 
 // AssetDir returns the file names below a certain
@@ -208,6 +230,7 @@
 	"data": &bintree{nil, map[string]*bintree{
 		"config_schema_v3.0.json": &bintree{dataConfig_schema_v30Json, map[string]*bintree{}},
 		"config_schema_v3.1.json": &bintree{dataConfig_schema_v31Json, map[string]*bintree{}},
+		"config_schema_v3.2.json": &bintree{dataConfig_schema_v32Json, map[string]*bintree{}},
 	}},
 }}
 
diff --git a/cli/compose/schema/data/config_schema_v3.1.json b/cli/compose/schema/data/config_schema_v3.1.json
index b9d4221..b703748 100644
--- a/cli/compose/schema/data/config_schema_v3.1.json
+++ b/cli/compose/schema/data/config_schema_v3.1.json
@@ -167,20 +167,8 @@
         "ports": {
           "type": "array",
           "items": {
-            "oneOf": [
-              {"type": "number", "format": "ports"},
-              {"type": "string", "format": "ports"},
-              {
-                "type": "object",
-                "properties": {
-                  "mode": {"type": "string"},
-                  "target": {"type": "integer"},
-                  "published": {"type": "integer"},
-                  "protocol": {"type": "string"}
-                },
-                "additionalProperties": false
-              }
-            ]
+            "type": ["string", "number"],
+            "format": "ports"
           },
           "uniqueItems": true
         },
@@ -350,7 +338,6 @@
           "additionalProperties": false
         },
         "internal": {"type": "boolean"},
-        "attachable": {"type": "boolean"},
         "labels": {"$ref": "#/definitions/list_or_dict"}
       },
       "additionalProperties": false
diff --git a/cli/compose/schema/data/config_schema_v3.2.json b/cli/compose/schema/data/config_schema_v3.2.json
new file mode 100644
index 0000000..e47c879
--- /dev/null
+++ b/cli/compose/schema/data/config_schema_v3.2.json
@@ -0,0 +1,473 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "id": "config_schema_v3.1.json",
+  "type": "object",
+  "required": ["version"],
+
+  "properties": {
+    "version": {
+      "type": "string"
+    },
+
+    "services": {
+      "id": "#/properties/services",
+      "type": "object",
+      "patternProperties": {
+        "^[a-zA-Z0-9._-]+$": {
+          "$ref": "#/definitions/service"
+        }
+      },
+      "additionalProperties": false
+    },
+
+    "networks": {
+      "id": "#/properties/networks",
+      "type": "object",
+      "patternProperties": {
+        "^[a-zA-Z0-9._-]+$": {
+          "$ref": "#/definitions/network"
+        }
+      }
+    },
+
+    "volumes": {
+      "id": "#/properties/volumes",
+      "type": "object",
+      "patternProperties": {
+        "^[a-zA-Z0-9._-]+$": {
+          "$ref": "#/definitions/volume"
+        }
+      },
+      "additionalProperties": false
+    },
+
+    "secrets": {
+      "id": "#/properties/secrets",
+      "type": "object",
+      "patternProperties": {
+        "^[a-zA-Z0-9._-]+$": {
+          "$ref": "#/definitions/secret"
+        }
+      },
+      "additionalProperties": false
+    }
+  },
+
+  "additionalProperties": false,
+
+  "definitions": {
+
+    "service": {
+      "id": "#/definitions/service",
+      "type": "object",
+
+      "properties": {
+        "deploy": {"$ref": "#/definitions/deployment"},
+        "build": {
+          "oneOf": [
+            {"type": "string"},
+            {
+              "type": "object",
+              "properties": {
+                "context": {"type": "string"},
+                "dockerfile": {"type": "string"},
+                "args": {"$ref": "#/definitions/list_or_dict"},
+                "cache_from": {"$ref": "#/definitions/list_of_strings"}
+              },
+              "additionalProperties": false
+            }
+          ]
+        },
+        "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+        "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+        "cgroup_parent": {"type": "string"},
+        "command": {
+          "oneOf": [
+            {"type": "string"},
+            {"type": "array", "items": {"type": "string"}}
+          ]
+        },
+        "container_name": {"type": "string"},
+        "depends_on": {"$ref": "#/definitions/list_of_strings"},
+        "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+        "dns": {"$ref": "#/definitions/string_or_list"},
+        "dns_search": {"$ref": "#/definitions/string_or_list"},
+        "domainname": {"type": "string"},
+        "entrypoint": {
+          "oneOf": [
+            {"type": "string"},
+            {"type": "array", "items": {"type": "string"}}
+          ]
+        },
+        "env_file": {"$ref": "#/definitions/string_or_list"},
+        "environment": {"$ref": "#/definitions/list_or_dict"},
+
+        "expose": {
+          "type": "array",
+          "items": {
+            "type": ["string", "number"],
+            "format": "expose"
+          },
+          "uniqueItems": true
+        },
+
+        "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+        "extra_hosts": {"$ref": "#/definitions/list_or_dict"},
+        "healthcheck": {"$ref": "#/definitions/healthcheck"},
+        "hostname": {"type": "string"},
+        "image": {"type": "string"},
+        "ipc": {"type": "string"},
+        "labels": {"$ref": "#/definitions/list_or_dict"},
+        "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+
+        "logging": {
+            "type": "object",
+
+            "properties": {
+                "driver": {"type": "string"},
+                "options": {
+                  "type": "object",
+                  "patternProperties": {
+                    "^.+$": {"type": ["string", "number", "null"]}
+                  }
+                }
+            },
+            "additionalProperties": false
+        },
+
+        "mac_address": {"type": "string"},
+        "network_mode": {"type": "string"},
+
+        "networks": {
+          "oneOf": [
+            {"$ref": "#/definitions/list_of_strings"},
+            {
+              "type": "object",
+              "patternProperties": {
+                "^[a-zA-Z0-9._-]+$": {
+                  "oneOf": [
+                    {
+                      "type": "object",
+                      "properties": {
+                        "aliases": {"$ref": "#/definitions/list_of_strings"},
+                        "ipv4_address": {"type": "string"},
+                        "ipv6_address": {"type": "string"}
+                      },
+                      "additionalProperties": false
+                    },
+                    {"type": "null"}
+                  ]
+                }
+              },
+              "additionalProperties": false
+            }
+          ]
+        },
+        "pid": {"type": ["string", "null"]},
+
+        "ports": {
+          "type": "array",
+          "items": {
+            "oneOf": [
+              {"type": "number", "format": "ports"},
+              {"type": "string", "format": "ports"},
+              {
+                "type": "object",
+                "properties": {
+                  "mode": {"type": "string"},
+                  "target": {"type": "integer"},
+                  "published": {"type": "integer"},
+                  "protocol": {"type": "string"}
+                },
+                "additionalProperties": false
+              }
+            ]
+          },
+          "uniqueItems": true
+        },
+
+        "privileged": {"type": "boolean"},
+        "read_only": {"type": "boolean"},
+        "restart": {"type": "string"},
+        "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+        "shm_size": {"type": ["number", "string"]},
+        "secrets": {
+          "type": "array",
+          "items": {
+            "oneOf": [
+              {"type": "string"},
+              {
+                "type": "object",
+                "properties": {
+                  "source": {"type": "string"},
+                  "target": {"type": "string"},
+                  "uid": {"type": "string"},
+                  "gid": {"type": "string"},
+                  "mode": {"type": "number"}
+                }
+              }
+            ]
+          }
+        },
+        "sysctls": {"$ref": "#/definitions/list_or_dict"},
+        "stdin_open": {"type": "boolean"},
+        "stop_grace_period": {"type": "string", "format": "duration"},
+        "stop_signal": {"type": "string"},
+        "tmpfs": {"$ref": "#/definitions/string_or_list"},
+        "tty": {"type": "boolean"},
+        "ulimits": {
+          "type": "object",
+          "patternProperties": {
+            "^[a-z]+$": {
+              "oneOf": [
+                {"type": "integer"},
+                {
+                  "type":"object",
+                  "properties": {
+                    "hard": {"type": "integer"},
+                    "soft": {"type": "integer"}
+                  },
+                  "required": ["soft", "hard"],
+                  "additionalProperties": false
+                }
+              ]
+            }
+          }
+        },
+        "user": {"type": "string"},
+        "userns_mode": {"type": "string"},
+        "volumes": {
+          "type": "array",
+          "items": {
+            "oneOf": [
+              {"type": "string"},
+              {
+                "type": "object",
+                "required": ["type"],
+                "properties": {
+                  "type": {"type": "string"},
+                  "source": {"type": "string"},
+                  "target": {"type": "string"},
+                  "read_only": {"type": "boolean"},
+                  "bind": {
+                    "type": "object",
+                    "properties": {
+                      "propagation": {"type": "string"}
+                    }
+                  },
+                  "volume": {
+                    "type": "object",
+                    "properties": {
+                      "nocopy": {"type": "boolean"}
+                    }
+                  }
+                }
+              }
+            ],
+            "uniqueItems": true
+          }
+        },
+        "working_dir": {"type": "string"}
+      },
+      "additionalProperties": false
+    },
+
+    "healthcheck": {
+      "id": "#/definitions/healthcheck",
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "disable": {"type": "boolean"},
+        "interval": {"type": "string"},
+        "retries": {"type": "number"},
+        "test": {
+          "oneOf": [
+            {"type": "string"},
+            {"type": "array", "items": {"type": "string"}}
+          ]
+        },
+        "timeout": {"type": "string"}
+      }
+    },
+    "deployment": {
+      "id": "#/definitions/deployment",
+      "type": ["object", "null"],
+      "properties": {
+        "mode": {"type": "string"},
+        "endpoint_mode": {"type": "string"},
+        "replicas": {"type": "integer"},
+        "labels": {"$ref": "#/definitions/list_or_dict"},
+        "update_config": {
+          "type": "object",
+          "properties": {
+            "parallelism": {"type": "integer"},
+            "delay": {"type": "string", "format": "duration"},
+            "failure_action": {"type": "string"},
+            "monitor": {"type": "string", "format": "duration"},
+            "max_failure_ratio": {"type": "number"}
+          },
+          "additionalProperties": false
+        },
+        "resources": {
+          "type": "object",
+          "properties": {
+            "limits": {"$ref": "#/definitions/resource"},
+            "reservations": {"$ref": "#/definitions/resource"}
+          }
+        },
+        "restart_policy": {
+          "type": "object",
+          "properties": {
+            "condition": {"type": "string"},
+            "delay": {"type": "string", "format": "duration"},
+            "max_attempts": {"type": "integer"},
+            "window": {"type": "string", "format": "duration"}
+          },
+          "additionalProperties": false
+        },
+        "placement": {
+          "type": "object",
+          "properties": {
+            "constraints": {"type": "array", "items": {"type": "string"}}
+          },
+          "additionalProperties": false
+        }
+      },
+      "additionalProperties": false
+    },
+
+    "resource": {
+      "id": "#/definitions/resource",
+      "type": "object",
+      "properties": {
+        "cpus": {"type": "string"},
+        "memory": {"type": "string"}
+      },
+      "additionalProperties": false
+    },
+
+    "network": {
+      "id": "#/definitions/network",
+      "type": ["object", "null"],
+      "properties": {
+        "driver": {"type": "string"},
+        "driver_opts": {
+          "type": "object",
+          "patternProperties": {
+            "^.+$": {"type": ["string", "number"]}
+          }
+        },
+        "ipam": {
+          "type": "object",
+          "properties": {
+            "driver": {"type": "string"},
+            "config": {
+              "type": "array",
+              "items": {
+                "type": "object",
+                "properties": {
+                  "subnet": {"type": "string"}
+                },
+                "additionalProperties": false
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        "external": {
+          "type": ["boolean", "object"],
+          "properties": {
+            "name": {"type": "string"}
+          },
+          "additionalProperties": false
+        },
+        "internal": {"type": "boolean"},
+        "attachable": {"type": "boolean"},
+        "labels": {"$ref": "#/definitions/list_or_dict"}
+      },
+      "additionalProperties": false
+    },
+
+    "volume": {
+      "id": "#/definitions/volume",
+      "type": ["object", "null"],
+      "properties": {
+        "driver": {"type": "string"},
+        "driver_opts": {
+          "type": "object",
+          "patternProperties": {
+            "^.+$": {"type": ["string", "number"]}
+          }
+        },
+        "external": {
+          "type": ["boolean", "object"],
+          "properties": {
+            "name": {"type": "string"}
+          },
+          "additionalProperties": false
+        },
+        "labels": {"$ref": "#/definitions/list_or_dict"}
+      },
+      "additionalProperties": false
+    },
+
+    "secret": {
+      "id": "#/definitions/secret",
+      "type": "object",
+      "properties": {
+        "file": {"type": "string"},
+        "external": {
+          "type": ["boolean", "object"],
+          "properties": {
+            "name": {"type": "string"}
+          }
+        },
+        "labels": {"$ref": "#/definitions/list_or_dict"}
+      },
+      "additionalProperties": false
+    },
+
+    "string_or_list": {
+      "oneOf": [
+        {"type": "string"},
+        {"$ref": "#/definitions/list_of_strings"}
+      ]
+    },
+
+    "list_of_strings": {
+      "type": "array",
+      "items": {"type": "string"},
+      "uniqueItems": true
+    },
+
+    "list_or_dict": {
+      "oneOf": [
+        {
+          "type": "object",
+          "patternProperties": {
+            ".+": {
+              "type": ["string", "number", "null"]
+            }
+          },
+          "additionalProperties": false
+        },
+        {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
+      ]
+    },
+
+    "constraints": {
+      "service": {
+        "id": "#/definitions/constraints/service",
+        "anyOf": [
+          {"required": ["build"]},
+          {"required": ["image"]}
+        ],
+        "properties": {
+          "build": {
+            "required": ["context"]
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/cli/compose/schema/schema.go b/cli/compose/schema/schema.go
index ae33c77..9a70dc2 100644
--- a/cli/compose/schema/schema.go
+++ b/cli/compose/schema/schema.go
@@ -78,18 +78,27 @@
 
 func toError(result *gojsonschema.Result) error {
 	err := getMostSpecificError(result.Errors())
-	description := getDescription(err)
-	return fmt.Errorf("%s %s", err.Field(), description)
+	return err
 }
 
-func getDescription(err gojsonschema.ResultError) string {
-	if err.Type() == "invalid_type" {
-		if expectedType, ok := err.Details()["expected"].(string); ok {
+const (
+	jsonschemaOneOf = "number_one_of"
+	jsonschemaAnyOf = "number_any_of"
+)
+
+func getDescription(err validationError) string {
+	switch err.parent.Type() {
+	case "invalid_type":
+		if expectedType, ok := err.parent.Details()["expected"].(string); ok {
 			return fmt.Sprintf("must be a %s", humanReadableType(expectedType))
 		}
+	case jsonschemaOneOf, jsonschemaAnyOf:
+		if err.child == nil {
+			return err.parent.Description()
+		}
+		return err.child.Description()
 	}
-
-	return err.Description()
+	return err.parent.Description()
 }
 
 func humanReadableType(definition string) string {
@@ -113,23 +122,45 @@
 	return definition
 }
 
-func getMostSpecificError(errors []gojsonschema.ResultError) gojsonschema.ResultError {
-	var mostSpecificError gojsonschema.ResultError
+type validationError struct {
+	parent gojsonschema.ResultError
+	child  gojsonschema.ResultError
+}
 
-	for _, err := range errors {
-		if mostSpecificError == nil {
-			mostSpecificError = err
-		} else if specificity(err) > specificity(mostSpecificError) {
-			mostSpecificError = err
-		} else if specificity(err) == specificity(mostSpecificError) {
+func (err validationError) Error() string {
+	description := getDescription(err)
+	return fmt.Sprintf("%s %s", err.parent.Field(), description)
+}
+
+func getMostSpecificError(errors []gojsonschema.ResultError) validationError {
+	mostSpecificError := 0
+	for i, err := range errors {
+		if specificity(err) > specificity(errors[mostSpecificError]) {
+			mostSpecificError = i
+			continue
+		}
+
+		if specificity(err) == specificity(errors[mostSpecificError]) {
 			// Invalid type errors win in a tie-breaker for most specific field name
-			if err.Type() == "invalid_type" && mostSpecificError.Type() != "invalid_type" {
-				mostSpecificError = err
+			if err.Type() == "invalid_type" && errors[mostSpecificError].Type() != "invalid_type" {
+				mostSpecificError = i
 			}
 		}
 	}
 
-	return mostSpecificError
+	if mostSpecificError+1 == len(errors) {
+		return validationError{parent: errors[mostSpecificError]}
+	}
+
+	switch errors[mostSpecificError].Type() {
+	case "number_one_of", "number_any_of":
+		return validationError{
+			parent: errors[mostSpecificError],
+			child:  errors[mostSpecificError+1],
+		}
+	default:
+		return validationError{parent: errors[mostSpecificError]}
+	}
 }
 
 func specificity(err gojsonschema.ResultError) int {
diff --git a/cli/compose/types/types.go b/cli/compose/types/types.go
index ba11faa..e91b5a7 100644
--- a/cli/compose/types/types.go
+++ b/cli/compose/types/types.go
@@ -119,7 +119,7 @@
 	Tty             bool `mapstructure:"tty"`
 	Ulimits         map[string]*UlimitsConfig
 	User            string
-	Volumes         []string
+	Volumes         []ServiceVolumeConfig
 	WorkingDir      string `mapstructure:"working_dir"`
 }
 
@@ -156,6 +156,7 @@
 	Resources     Resources
 	RestartPolicy *RestartPolicy `mapstructure:"restart_policy"`
 	Placement     Placement
+	EndpointMode  string
 }
 
 // HealthCheckConfig the healthcheck configuration for a service
@@ -223,6 +224,26 @@
 	Protocol  string
 }
 
+// ServiceVolumeConfig are references to a volume used by a service
+type ServiceVolumeConfig struct {
+	Type     string
+	Source   string
+	Target   string
+	ReadOnly bool `mapstructure:"read_only"`
+	Bind     *ServiceVolumeBind
+	Volume   *ServiceVolumeVolume
+}
+
+// ServiceVolumeBind are options for a service volume of type bind
+type ServiceVolumeBind struct {
+	Propagation string
+}
+
+// ServiceVolumeVolume are options for a service volume of type volume
+type ServiceVolumeVolume struct {
+	NoCopy bool `mapstructure:"nocopy"`
+}
+
 // ServiceSecretConfig is the secret configuration for a service
 type ServiceSecretConfig struct {
 	Source string
diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker
index 642426c..78715d1 100644
--- a/contrib/completion/bash/docker
+++ b/contrib/completion/bash/docker
@@ -2846,7 +2846,7 @@
 
 	case "$cur" in
 		-*)
-			COMPREPLY=( $( compgen -W "--details --follow -f --help --no-resolve --since --tail --timestamps -t" -- "$cur" ) )
+			COMPREPLY=( $( compgen -W "--follow -f --help --no-resolve --no-task-ids --no-trunc --since --tail --timestamps -t" -- "$cur" ) )
 			;;
 		*)
 			local counter=$(__docker_pos_first_nonflag '--since|--tail')
@@ -3027,6 +3027,7 @@
 			--host
 			--mode
 			--name
+			--placement-pref
 			--publish -p
 			--secret
 		"
@@ -3052,6 +3053,11 @@
 				COMPREPLY=( $( compgen -W "global replicated" -- "$cur" ) )
 				return
 				;;
+			--placement-pref)
+				COMPREPLY=( $( compgen -W "spread" -S = -- "$cur" ) )
+				__docker_nospace
+				return
+				;;
 			--secret)
 				__docker_complete_secrets
 				return
@@ -3076,6 +3082,8 @@
 			--host-add
 			--host-rm
 			--image
+			--placement-pref-add
+			--placement-pref-rm
 			--publish-add
 			--publish-rm
 			--rollback
@@ -3100,6 +3108,11 @@
 				__docker_complete_image_repos_and_tags
 				return
 				;;
+			--placement-pref-add|--placement-pref-rm)
+				COMPREPLY=( $( compgen -W "spread" -S = -- "$cur" ) )
+				__docker_nospace
+				return
+				;;
 			--secret-add|--secret-rm)
 				__docker_complete_secrets
 				return
@@ -3107,6 +3120,15 @@
 		esac
 	fi
 
+	local strategy=$(__docker_map_key_of_current_option '--placement-pref|--placement-pref-add|--placement-pref-rm')
+	case "$strategy" in
+		spread)
+			COMPREPLY=( $( compgen -W "engine.labels node.labels" -S . -- "${cur##*=}" ) )
+			__docker_nospace
+			return
+			;;
+	esac
+
 	case "$prev" in
 		--endpoint-mode)
 			COMPREPLY=( $( compgen -W "dnsrr vip" -- "$cur" ) )
diff --git a/contrib/completion/fish/docker.fish b/contrib/completion/fish/docker.fish
index d3ebcf1..f3dddd9 100644
--- a/contrib/completion/fish/docker.fish
+++ b/contrib/completion/fish/docker.fish
@@ -26,20 +26,20 @@
 function __fish_print_docker_containers --description 'Print a list of docker containers' -a select
     switch $select
         case running
-            docker ps -a --no-trunc | command awk 'NR>1' | command awk 'BEGIN {FS="  +"}; $5 ~ "^Up" {print $1 "\n" $(NF)}' | tr ',' '\n'
+            docker ps -a --no-trunc --filter status=running --format "{{.ID}}\n{{.Names}}" | tr ',' '\n'
         case stopped
-            docker ps -a --no-trunc | command awk 'NR>1' | command awk 'BEGIN {FS="  +"}; $5 ~ "^Exit" {print $1 "\n" $(NF)}' | tr ',' '\n'
+            docker ps -a --no-trunc --filter status=exited --format "{{.ID}}\n{{.Names}}" | tr ',' '\n'
         case all
-            docker ps -a --no-trunc | command awk 'NR>1' | command awk 'BEGIN {FS="  +"}; {print $1 "\n" $(NF)}' | tr ',' '\n'
+            docker ps -a --no-trunc --format "{{.ID}}\n{{.Names}}" | tr ',' '\n'
     end
 end
 
 function __fish_print_docker_images --description 'Print a list of docker images'
-    docker images | command awk 'NR>1' | command grep -v '<none>' | command awk '{print $1":"$2}'
+    docker images --format "{{.Repository}}:{{.Tag}}" | command grep -v '<none>'
 end
 
 function __fish_print_docker_repositories --description 'Print a list of docker repositories'
-    docker images | command awk 'NR>1' | command grep -v '<none>' | command awk '{print $1}' | command sort | command uniq
+    docker images --format "{{.Repository}}" | command grep -v '<none>' | command sort | command uniq
 end
 
 # common options
diff --git a/daemon/cluster/executor/container/adapter.go b/daemon/cluster/executor/container/adapter.go
index 8847511..02ccf62 100644
--- a/daemon/cluster/executor/container/adapter.go
+++ b/daemon/cluster/executor/container/adapter.go
@@ -423,7 +423,10 @@
 		if err != nil {
 			return nil, err
 		}
-		apiOptions.Since = since.Format(time.RFC3339Nano)
+		// print since as this formatted string because the docker container
+		// logs interface expects it like this.
+		// see github.com/docker/docker/api/types/time.ParseTimestamps
+		apiOptions.Since = fmt.Sprintf("%d.%09d", since.Unix(), int64(since.Nanosecond()))
 	}
 
 	if options.Tail < 0 {
diff --git a/daemon/cluster/services.go b/daemon/cluster/services.go
index 24c718c..c9d0b93 100644
--- a/daemon/cluster/services.go
+++ b/daemon/cluster/services.go
@@ -6,7 +6,9 @@
 	"fmt"
 	"io"
 	"os"
+	"strconv"
 	"strings"
+	"time"
 
 	"github.com/Sirupsen/logrus"
 	"github.com/docker/distribution/reference"
@@ -14,6 +16,7 @@
 	apitypes "github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/backend"
 	types "github.com/docker/docker/api/types/swarm"
+	timetypes "github.com/docker/docker/api/types/time"
 	"github.com/docker/docker/daemon/cluster/convert"
 	"github.com/docker/docker/daemon/logger"
 	"github.com/docker/docker/pkg/ioutils"
@@ -119,6 +122,16 @@
 			} else {
 				logrus.Debugf("creating service using supplied digest reference %s", ctnr.Image)
 			}
+
+			// Replace the context with a fresh one.
+			// If we timed out while communicating with the
+			// registry, then "ctx" will already be expired, which
+			// would cause UpdateService below to fail. Reusing
+			// "ctx" could make it impossible to create a service
+			// if the registry is slow or unresponsive.
+			var cancel func()
+			ctx, cancel = c.getRequestContext()
+			defer cancel()
 		}
 
 		r, err := state.controlClient.CreateService(ctx, &swarmapi.CreateServiceRequest{Spec: &serviceSpec})
@@ -209,6 +222,16 @@
 			} else {
 				logrus.Debugf("updating service using supplied digest reference %s", newCtnr.Image)
 			}
+
+			// Replace the context with a fresh one.
+			// If we timed out while communicating with the
+			// registry, then "ctx" will already be expired, which
+			// would cause UpdateService below to fail. Reusing
+			// "ctx" could make it impossible to update a service
+			// if the registry is slow or unresponsive.
+			var cancel func()
+			ctx, cancel = c.getRequestContext()
+			defer cancel()
 		}
 
 		var rollback swarmapi.UpdateServiceRequest_Rollback
@@ -281,6 +304,44 @@
 		stdStreams = append(stdStreams, swarmapi.LogStreamStderr)
 	}
 
+	// Get tail value squared away - the number of previous log lines we look at
+	var tail int64
+	if config.Tail == "all" {
+		// tail of 0 means send all logs on the swarmkit side
+		tail = 0
+	} else {
+		t, err := strconv.Atoi(config.Tail)
+		if err != nil {
+			return errors.New("tail value must be a positive integer or \"all\"")
+		}
+		if t < 0 {
+			return errors.New("negative tail values not supported")
+		}
+		// we actually use negative tail in swarmkit to represent messages
+		// backwards starting from the beginning. also, -1 means no logs. so,
+		// basically, for api compat with docker container logs, add one and
+		// flip the sign. we error above if you try to negative tail, which
+		// isn't supported by docker (and would error deeper in the stack
+		// anyway)
+		//
+		// See the logs protobuf for more information
+		tail = int64(-(t + 1))
+	}
+
+	// get the since value - the time in the past we're looking at logs starting from
+	var sinceProto *gogotypes.Timestamp
+	if config.Since != "" {
+		s, n, err := timetypes.ParseTimestamps(config.Since, 0)
+		if err != nil {
+			return errors.Wrap(err, "could not parse since timestamp")
+		}
+		since := time.Unix(s, n)
+		sinceProto, err = gogotypes.TimestampProto(since)
+		if err != nil {
+			return errors.Wrap(err, "could not parse timestamp to proto")
+		}
+	}
+
 	stream, err := state.logsClient.SubscribeLogs(ctx, &swarmapi.SubscribeLogsRequest{
 		Selector: &swarmapi.LogSelector{
 			ServiceIDs: []string{service.ID},
@@ -288,6 +349,8 @@
 		Options: &swarmapi.LogSubscriptionOptions{
 			Follow:  config.Follow,
 			Streams: stdStreams,
+			Tail:    tail,
+			Since:   sinceProto,
 		},
 	})
 	if err != nil {
diff --git a/docs/api/version-history.md b/docs/api/version-history.md
index 104e94f..3a92bdb 100644
--- a/docs/api/version-history.md
+++ b/docs/api/version-history.md
@@ -13,18 +13,25 @@
      will be rejected.
 -->
 
-## v1.27 API changes
+## v1.28 API changes
 
-[Docker Engine API v1.27](https://docs.docker.com/engine/api/v1.27/) documentation
+[Docker Engine API v1.28](https://docs.docker.com/engine/api/v1.28/) documentation
 
+
+* `GET /containers/create` now takes a `DeviceCgroupRules` field in `HostConfig` allowing to set custom device cgroup rules for the created container.
 * Optional query parameter `verbose` for `GET /networks/(id or name)` will now list all services with all the tasks, including the non-local tasks on the given network.
-* `GET /containers/(id or name)/attach/ws` now returns WebSocket in binary frame format for API version >= v1.27, and returns WebSocket in text frame format for API version< v1.27, for the purpose of backward-compatibility.
+* `GET /containers/(id or name)/attach/ws` now returns WebSocket in binary frame format for API version >= v1.28, and returns WebSocket in text frame format for API version< v1.28, for the purpose of backward-compatibility.
 * `GET /networks` is optimised only to return list of all networks and network specific information. List of all containers attached to a specific network is removed from this API and is only available using the network specific `GET /networks/{network-id}.
 * `GET /containers/json` now supports `publish` and `expose` filters to filter containers that expose or publish certain ports.
 * `POST /services/create` and `POST /services/(id or name)/update` now accept the `ReadOnly` parameter, which mounts the container's root filesystem as read only.
 * `POST /build` now accepts `extrahosts` parameter to specify a host to ip mapping to use during the build.
 * `POST /services/create` and `POST /services/(id or name)/update` now accept a `rollback` value for `FailureAction`.
 * `POST /services/create` and `POST /services/(id or name)/update` now accept an optional `RollbackConfig` object which specifies rollback options.
+
+## v1.27 API changes
+
+[Docker Engine API v1.27](https://docs.docker.com/engine/api/v1.27/) documentation
+
 * `GET /containers/(id or name)/stats` now includes an `online_cpus` field in both `precpu_stats` and `cpu_stats`. If this field is `nil` then for compatibility with older daemons the length of the corresponding `cpu_usage.percpu_usage` array should be used.
 
 ## v1.26 API changes
diff --git a/docs/reference/commandline/dockerd.md b/docs/reference/commandline/dockerd.md
index 92b9349..3b40540 100644
--- a/docs/reference/commandline/dockerd.md
+++ b/docs/reference/commandline/dockerd.md
@@ -70,7 +70,6 @@
       --max-concurrent-uploads int            Set the max concurrent uploads for each push (default 5)
       --metrics-addr string                   Set address and port to serve the metrics api (default "")
       --mtu int                               Set the containers network MTU
-      --no-new-privileges                     Disable container processes from gaining new privileges
       --oom-score-adjust int                  Set the oom_score_adj for the daemon (default -500)
   -p, --pidfile string                        Path to use for daemon PID file (default "/var/run/docker.pid")
       --raw-logs                              Full timestamps without ANSI coloring
diff --git a/hack/integration-cli-on-swarm/host/host.go b/hack/integration-cli-on-swarm/host/host.go
index 254cf0d..4248e6a 100644
--- a/hack/integration-cli-on-swarm/host/host.go
+++ b/hack/integration-cli-on-swarm/host/host.go
@@ -25,13 +25,14 @@
 )
 
 func main() {
-	if err := xmain(); err != nil {
+	rc, err := xmain()
+	if err != nil {
 		logrus.Fatalf("fatal error: %v", err)
 	}
+	os.Exit(rc)
 }
 
-// xmain can call os.Exit()
-func xmain() error {
+func xmain() (int, error) {
 	// Should we use cobra maybe?
 	replicas := flag.Int("replicas", 1, "Number of worker service replica")
 	chunks := flag.Int("chunks", 0, "Number of test chunks executed in batch (0 == replicas)")
@@ -50,7 +51,7 @@
 	}
 	cli, err := client.NewEnvClient()
 	if err != nil {
-		return err
+		return 1, err
 	}
 	if hasStack(cli, defaultStackName) {
 		logrus.Infof("Removing stack %s", defaultStackName)
@@ -61,13 +62,13 @@
 		removeVolume(cli, defaultVolumeName)
 	}
 	if err = ensureImages(cli, []string{defaultWorkerImageName, defaultMasterImageName}); err != nil {
-		return err
+		return 1, err
 	}
 	workerImageForStack := defaultWorkerImageName
 	if *pushWorkerImage != "" {
 		logrus.Infof("Pushing %s to %s", defaultWorkerImageName, *pushWorkerImage)
 		if err = pushImage(cli, *pushWorkerImage, defaultWorkerImageName); err != nil {
-			return err
+			return 1, err
 		}
 		workerImageForStack = *pushWorkerImage
 	}
@@ -82,18 +83,18 @@
 		DryRun:      *dryRun,
 	})
 	if err != nil {
-		return err
+		return 1, err
 	}
 	filters, err := filtersBytes(*filtersFile)
 	if err != nil {
-		return err
+		return 1, err
 	}
 	logrus.Infof("Creating volume %s with input data", defaultVolumeName)
 	if err = createVolumeWithData(cli,
 		defaultVolumeName,
 		map[string][]byte{"/input": filters},
 		defaultMasterImageName); err != nil {
-		return err
+		return 1, err
 	}
 	logrus.Infof("Deploying stack %s from %s", defaultStackName, compose)
 	defer func() {
@@ -105,22 +106,21 @@
 		logrus.Infof(" - Worker image: %s", workerImageForStack)
 	}()
 	if err = deployStack(cli, defaultStackName, compose); err != nil {
-		return err
+		return 1, err
 	}
 	logrus.Infof("The log will be displayed here after some duration."+
 		"You can watch the live status via `docker service logs %s_worker`",
 		defaultStackName)
 	masterContainerID, err := waitForMasterUp(cli, defaultStackName)
 	if err != nil {
-		return err
+		return 1, err
 	}
 	rc, err := waitForContainerCompletion(cli, os.Stdout, os.Stderr, masterContainerID)
 	if err != nil {
-		return err
+		return 1, err
 	}
 	logrus.Infof("Exit status: %d", rc)
-	os.Exit(int(rc))
-	return nil
+	return int(rc), nil
 }
 
 func ensureImages(cli *client.Client, images []string) error {
diff --git a/integration-cli/docker_cli_service_logs_experimental_test.go b/integration-cli/docker_cli_service_logs_experimental_test.go
index 87f580e..55deabc 100644
--- a/integration-cli/docker_cli_service_logs_experimental_test.go
+++ b/integration-cli/docker_cli_service_logs_experimental_test.go
@@ -8,8 +8,10 @@
 	"io"
 	"os/exec"
 	"strings"
+	"time"
 
 	"github.com/docker/docker/integration-cli/checker"
+	"github.com/docker/docker/integration-cli/daemon"
 	"github.com/go-check/check"
 )
 
@@ -49,6 +51,122 @@
 	}
 }
 
+// countLogLines returns a closure that can be used with waitAndAssert to
+// verify that a minimum number of expected container log messages have been
+// output.
+func countLogLines(d *daemon.Swarm, name string) func(*check.C) (interface{}, check.CommentInterface) {
+	return func(c *check.C) (interface{}, check.CommentInterface) {
+		out, err := d.Cmd("service", "logs", "-t", name)
+		c.Assert(err, checker.IsNil)
+		lines := strings.Split(strings.TrimSpace(out), "\n")
+		return len(lines), check.Commentf("output, %q", string(out))
+	}
+}
+
+func (s *DockerSwarmSuite) TestServiceLogsCompleteness(c *check.C) {
+	testRequires(c, ExperimentalDaemon)
+	d := s.AddDaemon(c, true, true)
+
+	name := "TestServiceLogsCompleteness"
+
+	// make a service that prints 6 lines
+	out, err := d.Cmd("service", "create", "--name", name, "busybox", "sh", "-c", "for line in $(seq 1 6); do echo log test $line; done; sleep 100000")
+	c.Assert(err, checker.IsNil)
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
+
+	// make sure task has been deployed.
+	waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, 1)
+	// and make sure we have all the log lines
+	waitAndAssert(c, defaultReconciliationTimeout, countLogLines(d, name), checker.Equals, 6)
+
+	args := []string{"service", "logs", name}
+	cmd := exec.Command(dockerBinary, d.PrependHostArg(args)...)
+	r, w := io.Pipe()
+	cmd.Stdout = w
+	cmd.Stderr = w
+	c.Assert(cmd.Start(), checker.IsNil)
+
+	reader := bufio.NewReader(r)
+	// i have heard anecdotal reports that logs may come back from the engine
+	// mis-ordered. if this tests fails, consider the possibility that that
+	// might be occurring
+	for i := 1; i <= 6; i++ {
+		msg := &logMessage{}
+		msg.data, _, msg.err = reader.ReadLine()
+		c.Assert(msg.err, checker.IsNil)
+		c.Assert(string(msg.data), checker.Contains, fmt.Sprintf("log test %v", i))
+	}
+}
+
+func (s *DockerSwarmSuite) TestServiceLogsTail(c *check.C) {
+	testRequires(c, ExperimentalDaemon)
+	d := s.AddDaemon(c, true, true)
+
+	name := "TestServiceLogsTail"
+
+	// make a service that prints 6 lines
+	out, err := d.Cmd("service", "create", "--name", name, "busybox", "sh", "-c", "for line in $(seq 1 6); do echo log test $line; done; sleep 100000")
+	c.Assert(err, checker.IsNil)
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
+
+	// make sure task has been deployed.
+	waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, 1)
+	waitAndAssert(c, defaultReconciliationTimeout, countLogLines(d, name), checker.Equals, 6)
+
+	args := []string{"service", "logs", "--tail=2", name}
+	cmd := exec.Command(dockerBinary, d.PrependHostArg(args)...)
+	r, w := io.Pipe()
+	cmd.Stdout = w
+	cmd.Stderr = w
+	c.Assert(cmd.Start(), checker.IsNil)
+
+	reader := bufio.NewReader(r)
+	// see TestServiceLogsCompleteness for comments about logs being well-
+	// ordered, if this flakes
+	for i := 5; i <= 6; i++ {
+		msg := &logMessage{}
+		msg.data, _, msg.err = reader.ReadLine()
+		c.Assert(msg.err, checker.IsNil)
+		c.Assert(string(msg.data), checker.Contains, fmt.Sprintf("log test %v", i))
+	}
+}
+
+func (s *DockerSwarmSuite) TestServiceLogsSince(c *check.C) {
+	// See DockerSuite.TestLogsSince, which is where this comes from
+	testRequires(c, ExperimentalDaemon)
+	d := s.AddDaemon(c, true, true)
+
+	name := "TestServiceLogsSince"
+
+	out, err := d.Cmd("service", "create", "--name", name, "busybox", "sh", "-c", "for i in $(seq 1 3); do sleep .1; echo log$i; done; sleep 10000000")
+	c.Assert(err, checker.IsNil)
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "")
+	waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, 1)
+	// wait a sec for the logs to come in
+	waitAndAssert(c, defaultReconciliationTimeout, countLogLines(d, name), checker.Equals, 3)
+
+	out, err = d.Cmd("service", "logs", "-t", name)
+	c.Assert(err, checker.IsNil)
+
+	log2Line := strings.Split(strings.Split(out, "\n")[1], " ")
+	t, err := time.Parse(time.RFC3339Nano, log2Line[0]) // timestamp log2 is written
+	c.Assert(err, checker.IsNil)
+	u := t.Add(50 * time.Millisecond) // add .05s so log1 & log2 don't show up
+	since := u.Format(time.RFC3339Nano)
+
+	out, err = d.Cmd("service", "logs", "-t", fmt.Sprintf("--since=%v", since), name)
+	c.Assert(err, checker.IsNil)
+
+	unexpected := []string{"log1", "log2"}
+	expected := []string{"log3"}
+	for _, v := range unexpected {
+		c.Assert(out, checker.Not(checker.Contains), v, check.Commentf("unexpected log message returned, since=%v", u))
+	}
+	for _, v := range expected {
+		c.Assert(out, checker.Contains, v, check.Commentf("expected log message %v, was not present, since=%v", u))
+	}
+}
+
 func (s *DockerSwarmSuite) TestServiceLogsFollow(c *check.C) {
 	testRequires(c, ExperimentalDaemon)
 
diff --git a/integration-cli/environment/clean.go b/integration-cli/environment/clean.go
index 77a605f..b278383 100644
--- a/integration-cli/environment/clean.go
+++ b/integration-cli/environment/clean.go
@@ -25,7 +25,9 @@
 // and removing everything else. It's meant to run after any tests so that they don't
 // depend on each others.
 func (e *Execution) Clean(t testingT, dockerBinary string) {
-	unpauseAllContainers(t, dockerBinary)
+	if (e.DaemonPlatform() != "windows") || (e.DaemonPlatform() == "windows" && e.Isolation() == "hyperv") {
+		unpauseAllContainers(t, dockerBinary)
+	}
 	deleteAllContainers(t, dockerBinary)
 	deleteAllImages(t, dockerBinary, e.protectedElements.images)
 	deleteAllVolumes(t, dockerBinary)
diff --git a/opts/mount.go b/opts/mount.go
index 97895a7..d4ccf83 100644
--- a/opts/mount.go
+++ b/opts/mount.go
@@ -102,7 +102,7 @@
 		case "volume-nocopy":
 			volumeOptions().NoCopy, err = strconv.ParseBool(value)
 			if err != nil {
-				return fmt.Errorf("invalid value for populate: %s", value)
+				return fmt.Errorf("invalid value for volume-nocopy: %s", value)
 			}
 		case "volume-label":
 			setValueOnMap(volumeOptions().Labels, value)
diff --git a/pkg/listeners/group_unix.go b/pkg/listeners/group_unix.go
index 183a0eb..c487079 100644
--- a/pkg/listeners/group_unix.go
+++ b/pkg/listeners/group_unix.go
@@ -10,6 +10,8 @@
 	"github.com/pkg/errors"
 )
 
+const defaultSocketGroup = "docker"
+
 func lookupGID(name string) (int, error) {
 	groupFile, err := user.GetGroupPath()
 	if err != nil {
diff --git a/pkg/listeners/listeners_solaris.go b/pkg/listeners/listeners_solaris.go
index 58e43a0..0482f3d 100644
--- a/pkg/listeners/listeners_solaris.go
+++ b/pkg/listeners/listeners_solaris.go
@@ -4,7 +4,9 @@
 	"crypto/tls"
 	"fmt"
 	"net"
+	"os"
 
+	"github.com/Sirupsen/logrus"
 	"github.com/docker/go-connections/sockets"
 )
 
@@ -20,7 +22,11 @@
 	case "unix":
 		gid, err := lookupGID(socketGroup)
 		if err != nil {
-			return nil, err
+			if socketGroup != defaultSocketGroup {
+				return nil, err
+			}
+			logrus.Warnf("could not change group %s to %s: %v", addr, defaultSocketGroup, err)
+			gid = os.Getgid()
 		}
 		l, err := sockets.NewUnixSocket(addr, gid)
 		if err != nil {
diff --git a/pkg/listeners/listeners_unix.go b/pkg/listeners/listeners_unix.go
index 3e7bfcb..23ead61 100644
--- a/pkg/listeners/listeners_unix.go
+++ b/pkg/listeners/listeners_unix.go
@@ -6,8 +6,10 @@
 	"crypto/tls"
 	"fmt"
 	"net"
+	"os"
 	"strconv"
 
+	"github.com/Sirupsen/logrus"
 	"github.com/coreos/go-systemd/activation"
 	"github.com/docker/go-connections/sockets"
 )
@@ -33,7 +35,11 @@
 	case "unix":
 		gid, err := lookupGID(socketGroup)
 		if err != nil {
-			return nil, err
+			if socketGroup != defaultSocketGroup {
+				return nil, err
+			}
+			logrus.Warnf("could not change group %s to %s: %v", addr, defaultSocketGroup, err)
+			gid = os.Getgid()
 		}
 		l, err := sockets.NewUnixSocket(addr, gid)
 		if err != nil {