From 45b00455932bda9270853ef12853a5e4677bd97a Mon Sep 17 00:00:00 2001 From: Lantao Liu Date: Fri, 30 Mar 2018 20:06:25 +0000 Subject: [PATCH] Add oci.WithUser helper function. Signed-off-by: Lantao Liu --- container_linux_test.go | 74 +++++++++++++++++++++++ oci/spec_opts_unix.go | 128 ++++++++++++++++++++++++++++++---------- 2 files changed, 170 insertions(+), 32 deletions(-) diff --git a/container_linux_test.go b/container_linux_test.go index fbaed4292..d479d47d4 100644 --- a/container_linux_test.go +++ b/container_linux_test.go @@ -475,6 +475,80 @@ func TestContainerUsername(t *testing.T) { } } +func TestContainerUser(t *testing.T) { + t.Parallel() + t.Run("UserNameAndGroupName", func(t *testing.T) { testContainerUser(t, "squid:squid", "31:31") }) + t.Run("UserIDAndGroupName", func(t *testing.T) { testContainerUser(t, "1001:squid", "1001:31") }) + t.Run("UserNameAndGroupID", func(t *testing.T) { testContainerUser(t, "squid:1002", "31:1002") }) + t.Run("UserIDAndGroupID", func(t *testing.T) { testContainerUser(t, "1001:1002", "1001:1002") }) +} + +func testContainerUser(t *testing.T, userstr, expectedOutput string) { + client, err := newClient(t, address) + if err != nil { + t.Fatal(err) + } + defer client.Close() + + var ( + image Image + ctx, cancel = testContext() + id = t.Name() + ) + defer cancel() + + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) + } + direct, err := newDirectIO(ctx) + if err != nil { + t.Fatal(err) + } + defer direct.Delete() + var ( + wg sync.WaitGroup + buf = bytes.NewBuffer(nil) + ) + wg.Add(1) + go func() { + defer wg.Done() + io.Copy(buf, direct.Stdout) + }() + + container, err := client.NewContainer(ctx, id, + WithNewSnapshot(id, image), + WithNewSpec(oci.WithImageConfig(image), oci.WithUser(userstr), oci.WithProcessArgs("sh", "-c", "echo $(id -u):$(id -g)")), + ) + if err != nil { + t.Fatal(err) + } + defer container.Delete(ctx, WithSnapshotCleanup) + + task, err := container.NewTask(ctx, direct.IOCreate) + if err != nil { + t.Fatal(err) + } + defer task.Delete(ctx) + + statusC, err := task.Wait(ctx) + if err != nil { + t.Fatal(err) + } + + if err := task.Start(ctx); err != nil { + t.Fatal(err) + } + <-statusC + + wg.Wait() + + output := strings.TrimSuffix(buf.String(), "\n") + if output != expectedOutput { + t.Errorf("expected uid:gid to be %q, but received %q", expectedOutput, output) + } +} + func TestContainerAttachProcess(t *testing.T) { t.Parallel() diff --git a/oci/spec_opts_unix.go b/oci/spec_opts_unix.go index 87d313add..e8ce21d52 100644 --- a/oci/spec_opts_unix.go +++ b/oci/spec_opts_unix.go @@ -118,32 +118,7 @@ func WithImageConfig(image Image) SpecOpts { } s.Process.Cwd = cwd if config.User != "" { - // According to OCI Image Spec v1.0.0, the following are valid for Linux: - // user, uid, user:group, uid:gid, uid:group, user:gid - parts := strings.Split(config.User, ":") - switch len(parts) { - case 1: - v, err := strconv.Atoi(parts[0]) - if err != nil { - // if we cannot parse as a uint they try to see if it is a username - return WithUsername(config.User)(ctx, client, c, s) - } - return WithUserID(uint32(v))(ctx, client, c, s) - case 2: - // TODO: support username and groupname - v, err := strconv.Atoi(parts[0]) - if err != nil { - return errors.Wrapf(err, "parse uid %s", parts[0]) - } - uid := uint32(v) - if v, err = strconv.Atoi(parts[1]); err != nil { - return errors.Wrapf(err, "parse gid %s", parts[1]) - } - gid := uint32(v) - s.Process.User.UID, s.Process.User.GID = uid, gid - default: - return fmt.Errorf("invalid USER value %s", config.User) - } + return WithUser(config.User)(ctx, client, c, s) } return nil } @@ -259,6 +234,82 @@ func WithNamespacedCgroup() SpecOpts { } } +// WithUser accepts a valid user string in OCI Image Spec v1.0.0: +// user, uid, user:group, uid:gid, uid:group, user:gid +// and set the correct UID and GID for container. +func WithUser(userstr string) SpecOpts { + return func(ctx context.Context, client Client, c *containers.Container, s *specs.Spec) error { + parts := strings.Split(userstr, ":") + switch len(parts) { + case 1: + v, err := strconv.Atoi(parts[0]) + if err != nil { + // if we cannot parse as a uint they try to see if it is a username + return WithUsername(userstr)(ctx, client, c, s) + } + return WithUserID(uint32(v))(ctx, client, c, s) + case 2: + var username, groupname string + var uid, gid uint32 + v, err := strconv.Atoi(parts[0]) + if err != nil { + username = parts[0] + } else { + uid = uint32(v) + } + if v, err = strconv.Atoi(parts[1]); err != nil { + groupname = parts[1] + } else { + gid = uint32(v) + } + if username == "" && groupname == "" { + s.Process.User.UID, s.Process.User.GID = uid, gid + return nil + } + f := func(root string) error { + if username != "" { + uid, _, err = getUIDGIDFromPath(root, func(u user.User) bool { + return u.Name == username + }) + if err != nil { + return err + } + } + if groupname != "" { + gid, err = getGIDFromPath(root, func(g user.Group) bool { + return g.Name == groupname + }) + if err != nil { + return err + } + } + s.Process.User.UID, s.Process.User.GID = uid, gid + return nil + } + if c.Snapshotter == "" && c.SnapshotKey == "" { + if !isRootfsAbs(s.Root.Path) { + return errors.New("rootfs absolute path is required") + } + return f(s.Root.Path) + } + if c.Snapshotter == "" { + return errors.New("no snapshotter set for container") + } + if c.SnapshotKey == "" { + return errors.New("rootfs snapshot not created for container") + } + snapshotter := client.SnapshotService(c.Snapshotter) + mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey) + if err != nil { + return err + } + return mount.WithTempMount(ctx, mounts, f) + default: + return fmt.Errorf("invalid USER value %s", userstr) + } + } +} + // WithUIDGID allows the UID and GID for the Process to be set func WithUIDGID(uid, gid uint32) SpecOpts { return func(_ context.Context, _ Client, _ *containers.Container, s *specs.Spec) error { @@ -398,12 +449,7 @@ func getUIDGIDFromPath(root string, filter func(user.User) bool) (uid, gid uint3 if err != nil { return 0, 0, err } - f, err := os.Open(ppath) - if err != nil { - return 0, 0, err - } - defer f.Close() - users, err := user.ParsePasswdFilter(f, filter) + users, err := user.ParsePasswdFileFilter(ppath, filter) if err != nil { return 0, 0, err } @@ -414,6 +460,24 @@ func getUIDGIDFromPath(root string, filter func(user.User) bool) (uid, gid uint3 return uint32(u.Uid), uint32(u.Gid), nil } +var errNoGroupsFound = errors.New("no groups found") + +func getGIDFromPath(root string, filter func(user.Group) bool) (gid uint32, err error) { + gpath, err := fs.RootPath(root, "/etc/group") + if err != nil { + return 0, err + } + groups, err := user.ParseGroupFileFilter(gpath, filter) + if err != nil { + return 0, err + } + if len(groups) == 0 { + return 0, errNoGroupsFound + } + g := groups[0] + return uint32(g.Gid), nil +} + func isRootfsAbs(root string) bool { return filepath.IsAbs(root) }