Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compose file v3.2 #31795

Merged
merged 7 commits into from Mar 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 7 additions & 3 deletions cli/compose/convert/service.go
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"sort"
"strings"
"time"

"github.com/docker/docker/api/types"
Expand Down Expand Up @@ -54,7 +55,7 @@ func convertService(
) (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
}
Expand Down Expand Up @@ -374,7 +375,7 @@ func (a byPublishedPort) Len() int { return len(a) }
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{
Expand All @@ -387,7 +388,10 @@ func convertEndpointSpec(source []composetypes.ServicePortConfig) (*swarm.Endpoi
}

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 {
Expand Down
3 changes: 2 additions & 1 deletion cli/compose/convert/service_test.go
Expand Up @@ -156,9 +156,10 @@ func TestConvertEndpointSpec(t *testing.T) {
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,
Expand Down
141 changes: 48 additions & 93 deletions 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
}
Expand All @@ -24,108 +22,65 @@ func Volumes(serviceVolumes []string, stackVolumes volumes, namespace Namespace)
return mounts, nil
}

func convertVolumeToMount(volumeSpec string, stackVolumes volumes, namespace Namespace) (mount.Mount, error) {
var source, target string
var mode []string

// TODO: split Windows path mappings properly
parts := strings.SplitN(volumeSpec, ":", 3)

for _, part := range parts {
if strings.TrimSpace(part) == "" {
return mount.Mount{}, fmt.Errorf("invalid volume: %s", volumeSpec)
}
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,
}

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]
// Anonymous volumes
if volume.Source == "" {
return result, nil
}

if source == "" {
// Anonymous volume
return mount.Mount{
Type: mount.TypeVolume,
Target: target,
}, 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")
}

// 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
if volume.Bind != nil {
result.BindOptions = &mount.BindOptions{
Propagation: mount.Propagation(volume.Bind.Propagation),
}
}
// Binds volumes
if volume.Type == "bind" {
return result, 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
}
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)}
}
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 nil

// Named volumes
return result, nil
}
111 changes: 72 additions & 39 deletions cli/compose/convert/volume_test.go
Expand Up @@ -8,51 +8,48 @@ import (
"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) {
Expand Down Expand Up @@ -84,9 +81,19 @@ func TestConvertVolumeToMountNamedVolume(t *testing.T) {
"opt": "value",
},
},
NoCopy: true,
},
}
config := composetypes.ServiceVolumeConfig{
Type: "volume",
Source: "normal",
Target: "/foo",
ReadOnly: true,
Volume: &composetypes.ServiceVolumeVolume{
NoCopy: true,
},
}
mount, err := convertVolumeToMount("normal:/foo:ro", stackVolumes, namespace)
mount, err := convertVolumeToMount(config, stackVolumes, namespace)
assert.NilError(t, err)
assert.DeepEqual(t, mount, expected)
}
Expand All @@ -109,7 +116,12 @@ func TestConvertVolumeToMountNamedVolumeExternal(t *testing.T) {
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)
}
Expand All @@ -132,7 +144,15 @@ func TestConvertVolumeToMountNamedVolumeExternalNoCopy(t *testing.T) {
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)
}
Expand All @@ -147,13 +167,26 @@ func TestConvertVolumeToMountBind(t *testing.T) {
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")
}