diff --git a/apis/server/container_bridge.go b/apis/server/container_bridge.go index 696a829ea..3d7e48657 100644 --- a/apis/server/container_bridge.go +++ b/apis/server/container_bridge.go @@ -327,6 +327,11 @@ func (s *Server) getContainer(ctx context.Context, rw http.ResponseWriter, req * } } + container.Mounts = []types.MountPoint{} + for _, mp := range meta.Mounts { + container.Mounts = append(container.Mounts, *mp) + } + return EncodeResponse(rw, http.StatusOK, container) } diff --git a/apis/swagger.yml b/apis/swagger.yml index c8c8f5822..c73f5199b 100644 --- a/apis/swagger.yml +++ b/apis/swagger.yml @@ -2838,6 +2838,8 @@ definitions: properties: Type: type: "string" + ID: + type: "string" Name: type: "string" Source: @@ -2850,6 +2852,12 @@ definitions: type: "string" RW: type: "boolean" + CopyData: + type: "boolean" + Named: + type: "boolean" + Replace: + type: "string" Propagation: type: "string" diff --git a/apis/types/mount_point.go b/apis/types/mount_point.go index 0d93b31db..0f904365c 100644 --- a/apis/types/mount_point.go +++ b/apis/types/mount_point.go @@ -17,24 +17,36 @@ import ( type MountPoint struct { + // copy data + CopyData bool `json:"CopyData,omitempty"` + // destination Destination string `json:"Destination,omitempty"` // driver Driver string `json:"Driver,omitempty"` + // ID + ID string `json:"ID,omitempty"` + // mode Mode string `json:"Mode,omitempty"` // name Name string `json:"Name,omitempty"` + // named + Named bool `json:"Named,omitempty"` + // propagation Propagation string `json:"Propagation,omitempty"` // r w RW bool `json:"RW,omitempty"` + // replace + Replace string `json:"Replace,omitempty"` + // source Source string `json:"Source,omitempty"` @@ -42,18 +54,26 @@ type MountPoint struct { Type string `json:"Type,omitempty"` } +/* polymorph MountPoint CopyData false */ + /* polymorph MountPoint Destination false */ /* polymorph MountPoint Driver false */ +/* polymorph MountPoint ID false */ + /* polymorph MountPoint Mode false */ /* polymorph MountPoint Name false */ +/* polymorph MountPoint Named false */ + /* polymorph MountPoint Propagation false */ /* polymorph MountPoint RW false */ +/* polymorph MountPoint Replace false */ + /* polymorph MountPoint Source false */ /* polymorph MountPoint Type false */ diff --git a/cli/common_flags.go b/cli/common_flags.go index 69791d7b9..8fa37fd0f 100644 --- a/cli/common_flags.go +++ b/cli/common_flags.go @@ -77,7 +77,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") + 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.StringVarP(&c.workdir, "workdir", "w", "", "Set the working directory in a container") diff --git a/daemon/mgr/container.go b/daemon/mgr/container.go index 449f475f7..f062e4068 100644 --- a/daemon/mgr/container.go +++ b/daemon/mgr/container.go @@ -339,11 +339,6 @@ func (mgr *ContainerManager) Create(ctx context.Context, name string, config *ty return nil, errors.Wrap(errtypes.ErrAlreadyExisted, "container name: "+name) } - // parse volume config - if err := mgr.parseVolumes(ctx, id, config); err != nil { - return nil, errors.Wrap(err, "failed to parse volume argument") - } - // check the image existed or not, and convert image id to image ref image, err := mgr.ImageMgr.GetImage(ctx, config.Image) if err != nil { @@ -392,6 +387,11 @@ func (mgr *ContainerManager) Create(ctx context.Context, name string, config *ty HostConfig: config.HostConfig, } + // parse volume config + if err := mgr.parseBinds(ctx, meta); err != nil { + return nil, errors.Wrap(err, "failed to parse volume argument") + } + // set container basefs mgr.setBaseFS(ctx, meta, id) @@ -1277,92 +1277,150 @@ func (mgr *ContainerManager) execExitedAndRelease(id string, m *ctrd.Message) er return nil } -func (mgr *ContainerManager) parseVolumes(ctx context.Context, id string, c *types.ContainerCreateConfig) error { - logrus.Debugf("bind volumes: %v", c.HostConfig.Binds) +func (mgr *ContainerManager) bindVolume(ctx context.Context, name string, meta *ContainerMeta) (string, string, error) { + id := meta.ID - if c.Volumes == nil { - c.Volumes = make(map[string]interface{}) + ref := "" + driver := "local" + v, err := mgr.VolumeMgr.Get(ctx, name) + if err != nil || v == nil { + opts := map[string]string{ + "backend": "local", + } + if err := mgr.VolumeMgr.Create(ctx, name, meta.HostConfig.VolumeDriver, opts, nil); err != nil { + logrus.Errorf("failed to create volume: %s, err: %v", name, err) + return "", "", errors.Wrap(err, "failed to create volume") + } + } else { + ref = v.Option("ref") + driver = v.Driver() + } + + option := map[string]string{} + if ref == "" { + option["ref"] = id + } else { + option["ref"] = ref + "," + id + } + if _, err := mgr.VolumeMgr.Attach(ctx, name, option); err != nil { + logrus.Errorf("failed to attach volume: %s, err: %v", name, err) + return "", "", errors.Wrap(err, "failed to attach volume") } + mountPath, err := mgr.VolumeMgr.Path(ctx, name) + if err != nil { + logrus.Errorf("failed to get the mount path of volume: %s, err: %v", name, err) + return "", "", errors.Wrap(err, "failed to get volume mount path") + } + + return mountPath, driver, nil +} + +func (mgr *ContainerManager) parseBinds(ctx context.Context, meta *ContainerMeta) error { + logrus.Debugf("bind volumes: %v", meta.HostConfig.Binds) + + var err error + + if meta.Config.Volumes == nil { + meta.Config.Volumes = make(map[string]interface{}) + } + + if meta.Mounts == nil { + meta.Mounts = make([]*types.MountPoint, 0) + } + + defer func() { + if err != nil { + if err := mgr.detachVolumes(ctx, meta); err != nil { + logrus.Errorf("failed to detach volume, err: %v", err) + } + } + }() + // TODO: parse c.HostConfig.VolumesFrom - for i, b := range c.HostConfig.Binds { + for _, b := range meta.HostConfig.Binds { + var parts []string // TODO: when caused error, how to rollback. - arr, err := checkBind(b) + parts, err = checkBind(b) if err != nil { return err } - source := "" - destination := "" - switch len(arr) { + + mode := "" + mp := new(types.MountPoint) + + switch len(parts) { case 1: - source = "" - destination = arr[0] - case 2, 3: - source = arr[0] - destination = arr[1] + mp.Source = "" + mp.Destination = parts[0] + case 2: + mp.Source = parts[0] + mp.Destination = parts[1] + case 3: + mp.Source = parts[0] + mp.Destination = parts[1] + mode = parts[2] default: return errors.Errorf("unknown bind: %s", b) } - if source == "" { - source = randomid.Generate() + if mp.Source == "" { + mp.Source = randomid.Generate() } - if !path.IsAbs(source) { - ref := "" - v, err := mgr.VolumeMgr.Get(ctx, source) - if err != nil || v == nil { - opts := map[string]string{ - "backend": "local", - } - if err := mgr.VolumeMgr.Create(ctx, source, c.HostConfig.VolumeDriver, opts, nil); err != nil { - logrus.Errorf("failed to create volume: %s, err: %v", source, err) - return errors.Wrap(err, "failed to create volume") + + err = parseBindMode(mp, mode) + if err != nil { + logrus.Errorf("failed to parse bind mode: %s, err: %v", mode, err) + return err + } + + if !path.IsAbs(mp.Source) { + // volume bind. + name := mp.Source + if _, exist := meta.Config.Volumes[name]; !exist { + mp.Name = name + mp.Source, mp.Driver, err = mgr.bindVolume(ctx, name, meta) + if err != nil { + logrus.Errorf("failed to bind volume: %s, err: %v", name, err) + return errors.Wrap(err, "failed to bind volume") } - } else { - ref = v.Option("ref") + meta.Config.Volumes[mp.Name] = mp.Destination } - option := map[string]string{} - if ref == "" { - option["ref"] = id - } else { - option["ref"] = ref + "," + id - } - if _, err := mgr.VolumeMgr.Attach(ctx, source, option); err != nil { - logrus.Errorf("failed to attach volume: %s, err: %v", source, err) - return errors.Wrap(err, "failed to attach volume") - } + if mp.Replace != "" { + mp.Source, err = mgr.VolumeMgr.Path(ctx, name) + if err != nil { + return err + } + + switch mp.Replace { + case "dr": + mp.Source = path.Join(mp.Source, mp.Destination) + case "rr": + mp.Source = path.Join(mp.Source, randomid.Generate()) + } - mountPath, err := mgr.VolumeMgr.Path(ctx, source) - if err != nil { - logrus.Errorf("failed to get the mount path of volume: %s, err: %v", source, err) - return errors.Wrap(err, "failed to get volume mount path") + mp.Name = "" + mp.Named = false + mp.Driver = "" } + } - c.Volumes[source] = destination - source = mountPath - } else if _, err := os.Stat(source); err != nil { + if _, err = os.Stat(mp.Source); err != nil { + // host directory bind into container. if !os.IsNotExist(err) { - return errors.Errorf("failed to stat %q: %v", source, err) + return errors.Errorf("failed to stat %q: %v", mp.Source, err) } // Create the host path if it doesn't exist. - if err := os.MkdirAll(source, 0755); err != nil { - return errors.Errorf("failed to mkdir %q: %v", source, err) + if err = os.MkdirAll(mp.Source, 0755); err != nil { + return errors.Errorf("failed to mkdir %q: %v", mp.Source, err) } } - switch len(arr) { - case 1: - b = fmt.Sprintf("%s:%s", source, arr[0]) - case 2, 3: - arr[0] = source - b = strings.Join(arr, ":") - default: - } - - c.HostConfig.Binds[i] = b + meta.Mounts = append(meta.Mounts, mp) } + return nil } @@ -1463,3 +1521,49 @@ func checkBind(b string) ([]string, error) { 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/daemon/mgr/container_test.go b/daemon/mgr/container_test.go index 2c010553d..3d59ef2b6 100644 --- a/daemon/mgr/container_test.go +++ b/daemon/mgr/container_test.go @@ -4,6 +4,8 @@ import ( "fmt" "testing" + "github.com/alibaba/pouch/apis/types" + "github.com/stretchr/testify/assert" ) @@ -39,3 +41,36 @@ func TestCheckBind(t *testing.T) { } } } + +func TestParseBindMode(t *testing.T) { + assert := assert.New(t) + + type parsed struct { + mode string + expectMountPoint *types.MountPoint + err bool + expectErr error + } + + parseds := []parsed{ + {mode: "dr", expectMountPoint: &types.MountPoint{Mode: "dr", RW: true, CopyData: true}, err: false, expectErr: nil}, + {mode: "nocopy", expectMountPoint: &types.MountPoint{Mode: "nocopy", RW: true, CopyData: false}, err: false, expectErr: nil}, + {mode: "ro", expectMountPoint: &types.MountPoint{Mode: "ro", RW: false, CopyData: true}, err: false, expectErr: nil}, + {mode: "", expectMountPoint: &types.MountPoint{Mode: "", RW: true, CopyData: true}, err: false, expectErr: nil}, + {mode: "dr,rr", err: true, expectErr: fmt.Errorf("invalid bind mode: dr,rr")}, + {mode: "unknown", err: true, expectErr: fmt.Errorf("unknown bind mode: unknown")}, + } + + for _, p := range parseds { + mp := &types.MountPoint{} + err := parseBindMode(mp, p.mode) + if p.err { + assert.Equal(err, p.expectErr) + } else { + assert.NoError(err, p.expectErr) + assert.Equal(p.expectMountPoint.Mode, mp.Mode) + assert.Equal(p.expectMountPoint.RW, mp.RW) + assert.Equal(p.expectMountPoint.CopyData, mp.CopyData) + } + } +} diff --git a/daemon/mgr/spec_volume.go b/daemon/mgr/spec_volume.go index 5a751dc24..68a05c671 100644 --- a/daemon/mgr/spec_volume.go +++ b/daemon/mgr/spec_volume.go @@ -3,7 +3,6 @@ package mgr import ( "context" "fmt" - "strings" specs "github.com/opencontainers/runtime-spec/specs-go" ) @@ -14,28 +13,43 @@ func setupMounts(ctx context.Context, c *ContainerMeta, spec *SpecWrapper) error if c.HostConfig == nil { return nil } - for _, v := range c.HostConfig.Binds { - sd := strings.Split(v, ":") - lensd := len(sd) - if lensd < 2 || lensd > 3 { - return fmt.Errorf("unknown bind: %s", v) + for _, mp := range c.Mounts { + // check duplicate mountpoint + for _, sm := range mounts { + if sm.Destination == mp.Destination { + return fmt.Errorf("duplicate mount point: %s", mp.Destination) + } } - opt := []string{"rbind"} - if lensd == 3 { - opt = append(opt, strings.Split(sd[2], ",")...) - // Set rootfs propagation, default setting is private. - if strings.Contains(sd[2], "rshared") { - s.Linux.RootfsPropagation = "rshared" + + pg := mp.Propagation + rootfspg := s.Linux.RootfsPropagation + // Set rootfs propagation, default setting is private. + switch pg { + case "shared", "rshared": + if rootfspg != "shared" && rootfspg != "rshared" { + s.Linux.RootfsPropagation = "shared" } - if strings.Contains(sd[2], "rslave") && s.Linux.RootfsPropagation != "rshared" { + case "slave", "rslave": + if rootfspg != "shared" && rootfspg != "rshared" && rootfspg != "slave" && rootfspg != "rslave" { s.Linux.RootfsPropagation = "rslave" } } + + opts := []string{"rbind"} + if !mp.RW { + opts = append(opts, "ro") + } + if pg != "" { + opts = append(opts, pg) + } + + // TODO: support copy data. + mounts = append(mounts, specs.Mount{ - Destination: sd[1], - Source: sd[0], + Source: mp.Source, + Destination: mp.Destination, Type: "bind", - Options: opt, + Options: opts, }) } s.Mounts = mounts diff --git a/test/cli_volume_test.go b/test/cli_volume_test.go index e4f5a2303..035103183 100644 --- a/test/cli_volume_test.go +++ b/test/cli_volume_test.go @@ -9,6 +9,7 @@ import ( "github.com/alibaba/pouch/apis/types" "github.com/alibaba/pouch/test/command" "github.com/alibaba/pouch/test/environment" + "github.com/alibaba/pouch/test/request" "github.com/go-check/check" "github.com/gotestyourself/gotestyourself/icmd" @@ -203,3 +204,37 @@ func (suite *PouchVolumeSuite) TestVolumeUsingByContainer(c *check.C) { command.PouchRun("rm", "-f", funcname).Assert(c, icmd.Success) command.PouchRun("volume", "rm", volumeName).Assert(c, icmd.Success) } + +// TestVolumeBindReplaceMode tests the volume "direct replace(dr)" mode. +func (suite *PouchVolumeSuite) TestVolumeBindReplaceMode(c *check.C) { + pc, _, _, _ := runtime.Caller(0) + tmpname := strings.Split(runtime.FuncForPC(pc).Name(), ".") + var funcname string + for i := range tmpname { + funcname = tmpname[i] + } + + volumeName := "volume_" + funcname + command.PouchRun("volume", "create", "--name", volumeName).Assert(c, icmd.Success) + command.PouchRun("run", "-d", "-v", volumeName+":/mnt", "-v", volumeName+":/home:dr", "--name", funcname, busyboxImage, "top").Assert(c, icmd.Success) + defer func() { + command.PouchRun("volume", "rm", volumeName) + command.PouchRun("rm", "-f", funcname) + }() + + resp, err := request.Get("/containers/" + funcname + "/json") + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, 200) + + got := types.ContainerJSON{} + err = request.DecodeBody(&got, resp.Body) + c.Assert(err, check.IsNil) + + found := false + for _, m := range got.Mounts { + if m.Replace == "dr" && m.Mode == "dr" && m.Source == "/mnt/local/volume_TestVolumeBindReplaceMode/home" { + found = true + } + } + c.Assert(found, check.Equals, true) +}