From d84951a6295d9b2bf127898e1a7d10ded4e45885 Mon Sep 17 00:00:00 2001 From: Rudy Zhang Date: Mon, 16 Apr 2018 14:07:47 +0800 Subject: [PATCH] feature: add volumes-from for container Add volumes-from for container, container can use the volumes of another containers. The format is "[:mode]" Signed-off-by: Rudy Zhang --- cli/common_flags.go | 1 + cli/container.go | 30 +++-- daemon/mgr/container.go | 125 +++++++----------- pkg/opts/mountpoint.go | 104 +++++++++++++++ .../opts/mountpoint_test.go | 37 +++++- test/cli_run_test.go | 47 +++++++ 6 files changed, 249 insertions(+), 95 deletions(-) create mode 100644 pkg/opts/mountpoint.go rename daemon/mgr/container_test.go => pkg/opts/mountpoint_test.go (68%) diff --git a/cli/common_flags.go b/cli/common_flags.go index 5bd2aa849..952eb8c2c 100644 --- a/cli/common_flags.go +++ b/cli/common_flags.go @@ -80,6 +80,7 @@ func addCommonFlags(flagSet *pflag.FlagSet) *container { flagSet.StringVar(&c.utsMode, "uts", "", "UTS namespace to use") flagSet.StringSliceVarP(&c.volume, "volume", "v", nil, "Bind mount volumes to container, format is: [source:][:mode], [source] can be volume or host's path, is container's path, [mode] can be \"ro/rw/dr/rr/z/Z/nocopy/private/rprivate/slave/rslave/shared/rshared\"") + flagSet.StringSliceVar(&c.volumesFrom, "volumes-from", nil, "set volumes from other containers, format is [:mode]") flagSet.StringVarP(&c.workdir, "workdir", "w", "", "Set the working directory in a container") diff --git a/cli/container.go b/cli/container.go index 0cf7aac38..e84805cde 100644 --- a/cli/container.go +++ b/cli/container.go @@ -10,18 +10,19 @@ import ( ) type container struct { - labels []string - name string - tty bool - volume []string - runtime string - env []string - entrypoint string - workdir string - user string - groupAdd []string - hostname string - rm bool + labels []string + name string + tty bool + volume []string + volumesFrom []string + runtime string + env []string + entrypoint string + workdir string + user string + groupAdd []string + hostname string + rm bool blkioWeight uint16 blkioWeightDevice WeightDevice @@ -187,8 +188,9 @@ func (c *container) config() (*types.ContainerCreateConfig, error) { }, HostConfig: &types.HostConfig{ - Binds: c.volume, - Runtime: c.runtime, + Binds: c.volume, + VolumesFrom: c.volumesFrom, + Runtime: c.runtime, Resources: types.Resources{ // cpu CPUShares: c.cpushare, diff --git a/daemon/mgr/container.go b/daemon/mgr/container.go index da80df14d..d6316df19 100644 --- a/daemon/mgr/container.go +++ b/daemon/mgr/container.go @@ -22,6 +22,7 @@ import ( "github.com/alibaba/pouch/pkg/collect" "github.com/alibaba/pouch/pkg/errtypes" "github.com/alibaba/pouch/pkg/meta" + "github.com/alibaba/pouch/pkg/opts" "github.com/alibaba/pouch/pkg/quota" "github.com/alibaba/pouch/pkg/randomid" "github.com/alibaba/pouch/pkg/reference" @@ -1449,12 +1450,54 @@ func (mgr *ContainerManager) parseBinds(ctx context.Context, meta *ContainerMeta } }() - // TODO: parse c.HostConfig.VolumesFrom + for _, v := range meta.HostConfig.VolumesFrom { + var containerID, mode string + containerID, mode, err = opts.ParseVolumesFrom(v) + if err != nil { + return err + } + + var oldMeta *ContainerMeta + oldMeta, err = mgr.Get(ctx, containerID) + if err != nil { + return err + } + + for _, oldMountPoint := range oldMeta.Mounts { + mp := &types.MountPoint{ + Name: oldMountPoint.Name, + Source: oldMountPoint.Source, + Destination: oldMountPoint.Destination, + Driver: oldMountPoint.Driver, + Named: oldMountPoint.Named, + RW: oldMountPoint.RW, + Propagation: oldMountPoint.Propagation, + } + + if _, exist := meta.Config.Volumes[oldMountPoint.Name]; !exist { + mp.Name = oldMountPoint.Name + mp.Source, mp.Driver, err = mgr.bindVolume(ctx, oldMountPoint.Name, meta) + if err != nil { + logrus.Errorf("failed to bind volume: %s, err: %v", oldMountPoint.Name, err) + return errors.Wrap(err, "failed to bind volume") + } + meta.Config.Volumes[mp.Name] = oldMountPoint.Destination + } + + err = opts.ParseBindMode(mp, mode) + if err != nil { + logrus.Errorf("failed to parse volumes-from mode: %s, err: %v", mode, err) + return err + } + + meta.Mounts = append(meta.Mounts, mp) + } + + } for _, b := range meta.HostConfig.Binds { var parts []string - // TODO: when caused error, how to rollback. - parts, err = checkBind(b) + parts, err = opts.CheckBind(b) if err != nil { return err } @@ -1481,7 +1524,7 @@ func (mgr *ContainerManager) parseBinds(ctx context.Context, meta *ContainerMeta mp.Source = randomid.Generate() } - err = parseBindMode(mp, mode) + err = opts.ParseBindMode(mp, mode) if err != nil { logrus.Errorf("failed to parse bind mode: %s, err: %v", mode, err) return err @@ -1699,77 +1742,3 @@ func (mgr *ContainerManager) setBaseFS(ctx context.Context, meta *ContainerMeta, // io.containerd.runtime.v1.linux as a const used by runc meta.BaseFS = filepath.Join(mgr.Config.HomeDir, "containerd/state", "io.containerd.runtime.v1.linux", namespaces.Default, info.Name, "rootfs") } - -func checkBind(b string) ([]string, error) { - if strings.Count(b, ":") > 2 { - return nil, fmt.Errorf("unknown volume bind: %s", b) - } - - arr := strings.SplitN(b, ":", 3) - switch len(arr) { - case 1: - if arr[0] == "" { - return nil, fmt.Errorf("unknown volume bind: %s", b) - } - if arr[0][:1] != "/" { - return nil, fmt.Errorf("invalid bind path: %s", arr[0]) - } - case 2, 3: - if arr[1] == "" { - return nil, fmt.Errorf("unknown volume bind: %s", b) - } - if arr[1][:1] != "/" { - return nil, fmt.Errorf("invalid bind path: %s", arr[1]) - } - default: - return nil, fmt.Errorf("unknown volume bind: %s", b) - } - - return arr, nil -} - -func parseBindMode(mp *types.MountPoint, mode string) error { - mp.RW = true - mp.CopyData = true - - defaultMode := 0 - rwMode := 0 - labelMode := 0 - replaceMode := 0 - copyMode := 0 - propagationMode := 0 - - for _, m := range strings.Split(mode, ",") { - switch m { - case "": - defaultMode++ - case "ro": - mp.RW = false - rwMode++ - case "rw": - mp.RW = true - rwMode++ - case "dr", "rr": - // direct replace mode, random replace mode - mp.Replace = m - replaceMode++ - case "z", "Z": - labelMode++ - case "nocopy": - mp.CopyData = false - copyMode++ - case "private", "rprivate", "slave", "rslave", "shared", "rshared": - mp.Propagation = m - propagationMode++ - default: - return fmt.Errorf("unknown bind mode: %s", mode) - } - } - - if defaultMode > 1 || rwMode > 1 || replaceMode > 1 || copyMode > 1 || propagationMode > 1 { - return fmt.Errorf("invalid bind mode: %s", mode) - } - - mp.Mode = mode - return nil -} diff --git a/pkg/opts/mountpoint.go b/pkg/opts/mountpoint.go new file mode 100644 index 000000000..89c9f1342 --- /dev/null +++ b/pkg/opts/mountpoint.go @@ -0,0 +1,104 @@ +package opts + +import ( + "fmt" + "strings" + + "github.com/alibaba/pouch/apis/types" +) + +// CheckBind is used to check the volume bind information. +func CheckBind(b string) ([]string, error) { + if strings.Count(b, ":") > 2 { + return nil, fmt.Errorf("unknown volume bind: %s", b) + } + + arr := strings.SplitN(b, ":", 3) + switch len(arr) { + case 1: + if arr[0] == "" { + return nil, fmt.Errorf("unknown volume bind: %s", b) + } + if arr[0][:1] != "/" { + return nil, fmt.Errorf("invalid bind path: %s", arr[0]) + } + case 2, 3: + if arr[1] == "" { + return nil, fmt.Errorf("unknown volume bind: %s", b) + } + if arr[1][:1] != "/" { + return nil, fmt.Errorf("invalid bind path: %s", arr[1]) + } + default: + return nil, fmt.Errorf("unknown volume bind: %s", b) + } + + return arr, nil +} + +// ParseVolumesFrom is used to parse the parameter of VolumesFrom. +func ParseVolumesFrom(volume string) (string, string, error) { + if len(volume) == 0 { + return "", "", fmt.Errorf("invalid argument volumes-from") + } + + parts := strings.Split(volume, ":") + containerID := parts[0] + mode := "" + if len(parts) > 1 { + mode = parts[1] + } + + if containerID == "" { + return "", "", fmt.Errorf("failed to parse container's id") + } + + return containerID, mode, nil +} + +// ParseBindMode is used to parse the bind's mode. +func ParseBindMode(mp *types.MountPoint, mode string) error { + mp.RW = true + mp.CopyData = true + + defaultMode := 0 + rwMode := 0 + labelMode := 0 + replaceMode := 0 + copyMode := 0 + propagationMode := 0 + + for _, m := range strings.Split(mode, ",") { + switch m { + case "": + defaultMode++ + case "ro": + mp.RW = false + rwMode++ + case "rw": + mp.RW = true + rwMode++ + case "dr", "rr": + // direct replace mode, random replace mode + mp.Replace = m + replaceMode++ + case "z", "Z": + labelMode++ + case "nocopy": + mp.CopyData = false + copyMode++ + case "private", "rprivate", "slave", "rslave", "shared", "rshared": + mp.Propagation = m + propagationMode++ + default: + return fmt.Errorf("unknown bind mode: %s", mode) + } + } + + if defaultMode > 1 || rwMode > 1 || replaceMode > 1 || copyMode > 1 || propagationMode > 1 { + return fmt.Errorf("invalid bind mode: %s", mode) + } + + mp.Mode = mode + return nil +} diff --git a/daemon/mgr/container_test.go b/pkg/opts/mountpoint_test.go similarity index 68% rename from daemon/mgr/container_test.go rename to pkg/opts/mountpoint_test.go index 3d59ef2b6..312338fbf 100644 --- a/daemon/mgr/container_test.go +++ b/pkg/opts/mountpoint_test.go @@ -1,4 +1,4 @@ -package mgr +package opts import ( "fmt" @@ -32,7 +32,7 @@ func TestCheckBind(t *testing.T) { } for _, p := range parseds { - arr, err := checkBind(p.bind) + arr, err := CheckBind(p.bind) if p.err { assert.Equal(err, p.expectErr) } else { @@ -63,7 +63,7 @@ func TestParseBindMode(t *testing.T) { for _, p := range parseds { mp := &types.MountPoint{} - err := parseBindMode(mp, p.mode) + err := ParseBindMode(mp, p.mode) if p.err { assert.Equal(err, p.expectErr) } else { @@ -74,3 +74,34 @@ func TestParseBindMode(t *testing.T) { } } } + +func TestParseVolumesFrom(t *testing.T) { + assert := assert.New(t) + + type parsed struct { + volumesFrom string + expectID string + expectMode string + err bool + expectErr error + } + + parseds := []parsed{ + {volumesFrom: "123456789", expectID: "123456789", expectMode: "", err: false, expectErr: nil}, + {volumesFrom: "123456789:nocopy", expectID: "123456789", expectMode: "nocopy", err: false, expectErr: nil}, + {volumesFrom: "123456789:", expectID: "123456789", expectMode: "", err: false, expectErr: nil}, + {volumesFrom: "", expectID: "", expectMode: "", err: true, expectErr: fmt.Errorf("invalid argument volumes-from")}, + {volumesFrom: ":", expectID: "", expectMode: "", err: true, expectErr: fmt.Errorf("failed to parse container's id")}, + } + + for _, p := range parseds { + containerID, mode, err := ParseVolumesFrom(p.volumesFrom) + if p.err { + assert.Equal(err, p.expectErr) + } else { + assert.NoError(err, p.expectErr) + assert.Equal(p.expectID, containerID) + assert.Equal(p.expectMode, mode) + } + } +} diff --git a/test/cli_run_test.go b/test/cli_run_test.go index f43da9ee3..ed611a773 100644 --- a/test/cli_run_test.go +++ b/test/cli_run_test.go @@ -925,3 +925,50 @@ func (suite *PouchRunSuite) TestRunWithRM(c *check.C) { output := command.PouchRun("inspect", cname).Stderr() c.Assert(util.PartialEqual(output, cname+": not found"), check.IsNil) } + +// TestRunWithVolumesFrom tests running container with --volumes-from. +func (suite *PouchRunSuite) TestRunWithVolumesFrom(c *check.C) { + volumeName := "volumesfrom-test-volume" + containerName1 := "volumesfrom-test-1" + containerName2 := "volumesfrom-test-2" + + // create volume + command.PouchRun("volume", "create", "-n", volumeName).Assert(c, icmd.Success) + defer func() { + command.PouchRun("volume", "rm", volumeName).Assert(c, icmd.Success) + }() + + // run container1 + command.PouchRun("run", "-d", + "-v", volumeName+":/mnt", + "--name", containerName1, busyboxImage, "top").Assert(c, icmd.Success) + defer func() { + command.PouchRun("rm", "-f", containerName1).Assert(c, icmd.Success) + }() + + // stop container1 + command.PouchRun("stop", containerName1).Assert(c, icmd.Success) + + // run container2 + command.PouchRun("run", "-d", + "--volumes-from", containerName1, + "--name", containerName2, busyboxImage, "top").Assert(c, icmd.Success) + defer func() { + command.PouchRun("rm", "-f", containerName2).Assert(c, icmd.Success) + }() + + // inspect container2 + ret := command.PouchRun("inspect", containerName2) + ret.Assert(c, icmd.Success) + out := ret.Stdout() + + volumeFound := false + for _, line := range strings.Split(out, "\n") { + if strings.Contains(line, "\"volumesfrom-test-volume\": \"/mnt\"") { + volumeFound = true + break + } + } + + c.Assert(volumeFound, check.Equals, true) +}