mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Use container Mounts API for Swarm containers.
Instead of converting nicely typed service mounts into untyped `Binds` when creating containers, use the new `Mounts` API which is a 1-1 mapping between service mounts and container mounts. Signed-off-by: Brian Goff <cpuguy83@gmail.com>
This commit is contained in:
parent
e350d44dbe
commit
821aeb6a6f
3 changed files with 132 additions and 113 deletions
|
@ -14,6 +14,7 @@ import (
|
||||||
enginecontainer "github.com/docker/docker/api/types/container"
|
enginecontainer "github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/events"
|
"github.com/docker/docker/api/types/events"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
enginemount "github.com/docker/docker/api/types/mount"
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
volumetypes "github.com/docker/docker/api/types/volume"
|
volumetypes "github.com/docker/docker/api/types/volume"
|
||||||
clustertypes "github.com/docker/docker/daemon/cluster/provider"
|
clustertypes "github.com/docker/docker/daemon/cluster/provider"
|
||||||
|
@ -191,7 +192,6 @@ func (c *containerConfig) config() *enginecontainer.Config {
|
||||||
Hostname: c.spec().Hostname,
|
Hostname: c.spec().Hostname,
|
||||||
WorkingDir: c.spec().Dir,
|
WorkingDir: c.spec().Dir,
|
||||||
Image: c.image(),
|
Image: c.image(),
|
||||||
Volumes: c.volumes(),
|
|
||||||
ExposedPorts: c.exposedPorts(),
|
ExposedPorts: c.exposedPorts(),
|
||||||
Healthcheck: c.healthcheck(),
|
Healthcheck: c.healthcheck(),
|
||||||
}
|
}
|
||||||
|
@ -243,49 +243,79 @@ func (c *containerConfig) labels() map[string]string {
|
||||||
return labels
|
return labels
|
||||||
}
|
}
|
||||||
|
|
||||||
// volumes gets placed into the Volumes field on the containerConfig.
|
func (c *containerConfig) mounts() []enginemount.Mount {
|
||||||
func (c *containerConfig) volumes() map[string]struct{} {
|
var r []enginemount.Mount
|
||||||
r := make(map[string]struct{})
|
|
||||||
// Volumes *only* creates anonymous volumes. The rest is mixed in with
|
|
||||||
// binds, which aren't actually binds. Basically, any volume that
|
|
||||||
// results in a single component must be added here.
|
|
||||||
//
|
|
||||||
// This is reversed engineered from the behavior of the engine API.
|
|
||||||
for _, mount := range c.spec().Mounts {
|
for _, mount := range c.spec().Mounts {
|
||||||
if mount.Type == api.MountTypeVolume && mount.Source == "" {
|
r = append(r, convertMount(mount))
|
||||||
r[mount.Target] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerConfig) tmpfs() map[string]string {
|
func convertMount(m api.Mount) enginemount.Mount {
|
||||||
r := make(map[string]string)
|
mount := enginemount.Mount{
|
||||||
|
Source: m.Source,
|
||||||
for _, spec := range c.spec().Mounts {
|
Target: m.Target,
|
||||||
if spec.Type != api.MountTypeTmpfs {
|
ReadOnly: m.ReadOnly,
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
r[spec.Target] = getMountMask(&spec)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return r
|
switch m.Type {
|
||||||
}
|
case api.MountTypeBind:
|
||||||
|
mount.Type = enginemount.TypeBind
|
||||||
|
case api.MountTypeVolume:
|
||||||
|
mount.Type = enginemount.TypeVolume
|
||||||
|
case api.MountTypeTmpfs:
|
||||||
|
mount.Type = enginemount.TypeTmpfs
|
||||||
|
}
|
||||||
|
|
||||||
func (c *containerConfig) binds() []string {
|
if m.BindOptions != nil {
|
||||||
var r []string
|
mount.BindOptions = &enginemount.BindOptions{}
|
||||||
for _, mount := range c.spec().Mounts {
|
switch m.BindOptions.Propagation {
|
||||||
if mount.Type == api.MountTypeBind || (mount.Type == api.MountTypeVolume && mount.Source != "") {
|
case api.MountPropagationRPrivate:
|
||||||
spec := fmt.Sprintf("%s:%s", mount.Source, mount.Target)
|
mount.BindOptions.Propagation = enginemount.PropagationRPrivate
|
||||||
mask := getMountMask(&mount)
|
case api.MountPropagationPrivate:
|
||||||
if mask != "" {
|
mount.BindOptions.Propagation = enginemount.PropagationPrivate
|
||||||
spec = fmt.Sprintf("%s:%s", spec, mask)
|
case api.MountPropagationRSlave:
|
||||||
|
mount.BindOptions.Propagation = enginemount.PropagationRSlave
|
||||||
|
case api.MountPropagationSlave:
|
||||||
|
mount.BindOptions.Propagation = enginemount.PropagationSlave
|
||||||
|
case api.MountPropagationRShared:
|
||||||
|
mount.BindOptions.Propagation = enginemount.PropagationRShared
|
||||||
|
case api.MountPropagationShared:
|
||||||
|
mount.BindOptions.Propagation = enginemount.PropagationShared
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.VolumeOptions != nil {
|
||||||
|
mount.VolumeOptions = &enginemount.VolumeOptions{
|
||||||
|
NoCopy: m.VolumeOptions.NoCopy,
|
||||||
|
}
|
||||||
|
if m.VolumeOptions.Labels != nil {
|
||||||
|
mount.VolumeOptions.Labels = make(map[string]string, len(m.VolumeOptions.Labels))
|
||||||
|
for k, v := range m.VolumeOptions.Labels {
|
||||||
|
mount.VolumeOptions.Labels[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.VolumeOptions.DriverConfig != nil {
|
||||||
|
mount.VolumeOptions.DriverConfig = &enginemount.Driver{
|
||||||
|
Name: m.VolumeOptions.DriverConfig.Name,
|
||||||
|
}
|
||||||
|
if m.VolumeOptions.DriverConfig.Options != nil {
|
||||||
|
mount.VolumeOptions.DriverConfig.Options = make(map[string]string, len(m.VolumeOptions.DriverConfig.Options))
|
||||||
|
for k, v := range m.VolumeOptions.DriverConfig.Options {
|
||||||
|
mount.VolumeOptions.DriverConfig.Options[k] = v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
r = append(r, spec)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return r
|
|
||||||
|
if m.TmpfsOptions != nil {
|
||||||
|
mount.TmpfsOptions = &enginemount.TmpfsOptions{
|
||||||
|
SizeBytes: m.TmpfsOptions.SizeBytes,
|
||||||
|
Mode: m.TmpfsOptions.Mode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mount
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *containerConfig) healthcheck() *enginecontainer.HealthConfig {
|
func (c *containerConfig) healthcheck() *enginecontainer.HealthConfig {
|
||||||
|
@ -303,88 +333,12 @@ func (c *containerConfig) healthcheck() *enginecontainer.HealthConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMountMask(m *api.Mount) string {
|
|
||||||
var maskOpts []string
|
|
||||||
if m.ReadOnly {
|
|
||||||
maskOpts = append(maskOpts, "ro")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch m.Type {
|
|
||||||
case api.MountTypeVolume:
|
|
||||||
if m.VolumeOptions != nil && m.VolumeOptions.NoCopy {
|
|
||||||
maskOpts = append(maskOpts, "nocopy")
|
|
||||||
}
|
|
||||||
case api.MountTypeBind:
|
|
||||||
if m.BindOptions == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
switch m.BindOptions.Propagation {
|
|
||||||
case api.MountPropagationPrivate:
|
|
||||||
maskOpts = append(maskOpts, "private")
|
|
||||||
case api.MountPropagationRPrivate:
|
|
||||||
maskOpts = append(maskOpts, "rprivate")
|
|
||||||
case api.MountPropagationShared:
|
|
||||||
maskOpts = append(maskOpts, "shared")
|
|
||||||
case api.MountPropagationRShared:
|
|
||||||
maskOpts = append(maskOpts, "rshared")
|
|
||||||
case api.MountPropagationSlave:
|
|
||||||
maskOpts = append(maskOpts, "slave")
|
|
||||||
case api.MountPropagationRSlave:
|
|
||||||
maskOpts = append(maskOpts, "rslave")
|
|
||||||
}
|
|
||||||
case api.MountTypeTmpfs:
|
|
||||||
if m.TmpfsOptions == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.TmpfsOptions.Mode != 0 {
|
|
||||||
maskOpts = append(maskOpts, fmt.Sprintf("mode=%o", m.TmpfsOptions.Mode))
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.TmpfsOptions.SizeBytes != 0 {
|
|
||||||
// calculate suffix here, making this linux specific, but that is
|
|
||||||
// okay, since API is that way anyways.
|
|
||||||
|
|
||||||
// we do this by finding the suffix that divides evenly into the
|
|
||||||
// value, returing the value itself, with no suffix, if it fails.
|
|
||||||
//
|
|
||||||
// For the most part, we don't enforce any semantic to this values.
|
|
||||||
// The operating system will usually align this and enforce minimum
|
|
||||||
// and maximums.
|
|
||||||
var (
|
|
||||||
size = m.TmpfsOptions.SizeBytes
|
|
||||||
suffix string
|
|
||||||
)
|
|
||||||
for _, r := range []struct {
|
|
||||||
suffix string
|
|
||||||
divisor int64
|
|
||||||
}{
|
|
||||||
{"g", 1 << 30},
|
|
||||||
{"m", 1 << 20},
|
|
||||||
{"k", 1 << 10},
|
|
||||||
} {
|
|
||||||
if size%r.divisor == 0 {
|
|
||||||
size = size / r.divisor
|
|
||||||
suffix = r.suffix
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
maskOpts = append(maskOpts, fmt.Sprintf("size=%d%s", size, suffix))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(maskOpts, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *containerConfig) hostConfig() *enginecontainer.HostConfig {
|
func (c *containerConfig) hostConfig() *enginecontainer.HostConfig {
|
||||||
hc := &enginecontainer.HostConfig{
|
hc := &enginecontainer.HostConfig{
|
||||||
Resources: c.resources(),
|
Resources: c.resources(),
|
||||||
Binds: c.binds(),
|
|
||||||
Tmpfs: c.tmpfs(),
|
|
||||||
GroupAdd: c.spec().Groups,
|
GroupAdd: c.spec().Groups,
|
||||||
PortBindings: c.portBindings(),
|
PortBindings: c.portBindings(),
|
||||||
|
Mounts: c.mounts(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.spec().DNSConfig != nil {
|
if c.spec().DNSConfig != nil {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/mount"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
"github.com/docker/docker/pkg/integration/checker"
|
"github.com/docker/docker/pkg/integration/checker"
|
||||||
"github.com/go-check/check"
|
"github.com/go-check/check"
|
||||||
|
@ -15,7 +16,7 @@ import (
|
||||||
|
|
||||||
func (s *DockerSwarmSuite) TestServiceCreateMountVolume(c *check.C) {
|
func (s *DockerSwarmSuite) TestServiceCreateMountVolume(c *check.C) {
|
||||||
d := s.AddDaemon(c, true, true)
|
d := s.AddDaemon(c, true, true)
|
||||||
out, err := d.Cmd("service", "create", "--mount", "type=volume,source=foo,target=/foo", "busybox", "top")
|
out, err := d.Cmd("service", "create", "--mount", "type=volume,source=foo,target=/foo,volume-nocopy", "busybox", "top")
|
||||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||||
id := strings.TrimSpace(out)
|
id := strings.TrimSpace(out)
|
||||||
|
|
||||||
|
@ -33,6 +34,21 @@ func (s *DockerSwarmSuite) TestServiceCreateMountVolume(c *check.C) {
|
||||||
return task.NodeID != "" && task.Status.ContainerStatus.ContainerID != "", nil
|
return task.NodeID != "" && task.Status.ContainerStatus.ContainerID != "", nil
|
||||||
}, checker.Equals, true)
|
}, checker.Equals, true)
|
||||||
|
|
||||||
|
// check container mount config
|
||||||
|
out, err = s.nodeCmd(c, task.NodeID, "inspect", "--format", "{{json .HostConfig.Mounts}}", task.Status.ContainerStatus.ContainerID)
|
||||||
|
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||||
|
|
||||||
|
var mountConfig []mount.Mount
|
||||||
|
c.Assert(json.Unmarshal([]byte(out), &mountConfig), checker.IsNil)
|
||||||
|
c.Assert(mountConfig, checker.HasLen, 1)
|
||||||
|
|
||||||
|
c.Assert(mountConfig[0].Source, checker.Equals, "foo")
|
||||||
|
c.Assert(mountConfig[0].Target, checker.Equals, "/foo")
|
||||||
|
c.Assert(mountConfig[0].Type, checker.Equals, mount.TypeVolume)
|
||||||
|
c.Assert(mountConfig[0].VolumeOptions, checker.NotNil)
|
||||||
|
c.Assert(mountConfig[0].VolumeOptions.NoCopy, checker.True)
|
||||||
|
|
||||||
|
// check container mounts actual
|
||||||
out, err = s.nodeCmd(c, task.NodeID, "inspect", "--format", "{{json .Mounts}}", task.Status.ContainerStatus.ContainerID)
|
out, err = s.nodeCmd(c, task.NodeID, "inspect", "--format", "{{json .Mounts}}", task.Status.ContainerStatus.ContainerID)
|
||||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||||
|
|
||||||
|
@ -40,6 +56,7 @@ func (s *DockerSwarmSuite) TestServiceCreateMountVolume(c *check.C) {
|
||||||
c.Assert(json.Unmarshal([]byte(out), &mounts), checker.IsNil)
|
c.Assert(json.Unmarshal([]byte(out), &mounts), checker.IsNil)
|
||||||
c.Assert(mounts, checker.HasLen, 1)
|
c.Assert(mounts, checker.HasLen, 1)
|
||||||
|
|
||||||
|
c.Assert(mounts[0].Type, checker.Equals, mount.TypeVolume)
|
||||||
c.Assert(mounts[0].Name, checker.Equals, "foo")
|
c.Assert(mounts[0].Name, checker.Equals, "foo")
|
||||||
c.Assert(mounts[0].Destination, checker.Equals, "/foo")
|
c.Assert(mounts[0].Destination, checker.Equals, "/foo")
|
||||||
c.Assert(mounts[0].RW, checker.Equals, true)
|
c.Assert(mounts[0].RW, checker.Equals, true)
|
||||||
|
@ -103,3 +120,53 @@ func (s *DockerSwarmSuite) TestServiceCreateWithSecretSourceTarget(c *check.C) {
|
||||||
c.Assert(refs[0].File, checker.Not(checker.IsNil))
|
c.Assert(refs[0].File, checker.Not(checker.IsNil))
|
||||||
c.Assert(refs[0].File.Name, checker.Equals, testTarget)
|
c.Assert(refs[0].File.Name, checker.Equals, testTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DockerSwarmSuite) TestServiceCreateMountTmpfs(c *check.C) {
|
||||||
|
d := s.AddDaemon(c, true, true)
|
||||||
|
out, err := d.Cmd("service", "create", "--mount", "type=tmpfs,target=/foo", "busybox", "sh", "-c", "mount | grep foo; tail -f /dev/null")
|
||||||
|
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||||
|
id := strings.TrimSpace(out)
|
||||||
|
|
||||||
|
var tasks []swarm.Task
|
||||||
|
waitAndAssert(c, defaultReconciliationTimeout, func(c *check.C) (interface{}, check.CommentInterface) {
|
||||||
|
tasks = d.getServiceTasks(c, id)
|
||||||
|
return len(tasks) > 0, nil
|
||||||
|
}, checker.Equals, true)
|
||||||
|
|
||||||
|
task := tasks[0]
|
||||||
|
waitAndAssert(c, defaultReconciliationTimeout, func(c *check.C) (interface{}, check.CommentInterface) {
|
||||||
|
if task.NodeID == "" || task.Status.ContainerStatus.ContainerID == "" {
|
||||||
|
task = d.getTask(c, task.ID)
|
||||||
|
}
|
||||||
|
return task.NodeID != "" && task.Status.ContainerStatus.ContainerID != "", nil
|
||||||
|
}, checker.Equals, true)
|
||||||
|
|
||||||
|
// check container mount config
|
||||||
|
out, err = s.nodeCmd(c, task.NodeID, "inspect", "--format", "{{json .HostConfig.Mounts}}", task.Status.ContainerStatus.ContainerID)
|
||||||
|
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||||
|
|
||||||
|
var mountConfig []mount.Mount
|
||||||
|
c.Assert(json.Unmarshal([]byte(out), &mountConfig), checker.IsNil)
|
||||||
|
c.Assert(mountConfig, checker.HasLen, 1)
|
||||||
|
|
||||||
|
c.Assert(mountConfig[0].Source, checker.Equals, "")
|
||||||
|
c.Assert(mountConfig[0].Target, checker.Equals, "/foo")
|
||||||
|
c.Assert(mountConfig[0].Type, checker.Equals, mount.TypeTmpfs)
|
||||||
|
|
||||||
|
// check container mounts actual
|
||||||
|
out, err = s.nodeCmd(c, task.NodeID, "inspect", "--format", "{{json .Mounts}}", task.Status.ContainerStatus.ContainerID)
|
||||||
|
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||||
|
|
||||||
|
var mounts []types.MountPoint
|
||||||
|
c.Assert(json.Unmarshal([]byte(out), &mounts), checker.IsNil)
|
||||||
|
c.Assert(mounts, checker.HasLen, 1)
|
||||||
|
|
||||||
|
c.Assert(mounts[0].Type, checker.Equals, mount.TypeTmpfs)
|
||||||
|
c.Assert(mounts[0].Name, checker.Equals, "")
|
||||||
|
c.Assert(mounts[0].Destination, checker.Equals, "/foo")
|
||||||
|
c.Assert(mounts[0].RW, checker.Equals, true)
|
||||||
|
|
||||||
|
out, err = s.nodeCmd(c, task.NodeID, "logs", task.Status.ContainerStatus.ContainerID)
|
||||||
|
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||||
|
c.Assert(strings.TrimSpace(out), checker.HasPrefix, "tmpfs on /foo type tmpfs")
|
||||||
|
}
|
||||||
|
|
|
@ -11,8 +11,6 @@ import (
|
||||||
|
|
||||||
// ConvertTmpfsOptions converts *mounttypes.TmpfsOptions to the raw option string
|
// ConvertTmpfsOptions converts *mounttypes.TmpfsOptions to the raw option string
|
||||||
// for mount(2).
|
// for mount(2).
|
||||||
// The logic is copy-pasted from daemon/cluster/executer/container.getMountMask.
|
|
||||||
// It will be deduplicated when we migrated the cluster to the new mount scheme.
|
|
||||||
func ConvertTmpfsOptions(opt *mounttypes.TmpfsOptions, readOnly bool) (string, error) {
|
func ConvertTmpfsOptions(opt *mounttypes.TmpfsOptions, readOnly bool) (string, error) {
|
||||||
var rawOpts []string
|
var rawOpts []string
|
||||||
if readOnly {
|
if readOnly {
|
||||||
|
|
Loading…
Reference in a new issue