diff --git a/container_linux_test.go b/container_linux_test.go index 6e53187ad..10e79f04e 100644 --- a/container_linux_test.go +++ b/container_linux_test.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "runtime" + "strings" "sync" "syscall" "testing" @@ -364,3 +365,80 @@ func TestContainerAttach(t *testing.T) { t.Errorf("expected output %q but received %q", expected, output) } } + +func TestContainerUsername(t *testing.T) { + t.Parallel() + + 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() + + if runtime.GOOS != "windows" { + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Error(err) + return + } + } + direct, err := NewDirectIO(ctx, false) + if err != nil { + t.Error(err) + return + } + defer direct.Delete() + var ( + wg sync.WaitGroup + buf = bytes.NewBuffer(nil) + ) + wg.Add(1) + go func() { + defer wg.Done() + io.Copy(buf, direct.Stdout) + }() + + // squid user in the alpine image has a uid of 31 + container, err := client.NewContainer(ctx, id, + withNewSnapshot(id, image), + WithNewSpec(withImageConfig(image), WithUsername("squid"), WithProcessArgs("id", "-u")), + ) + if err != nil { + t.Error(err) + return + } + defer container.Delete(ctx, WithSnapshotCleanup) + + task, err := container.NewTask(ctx, direct.IOCreate) + if err != nil { + t.Error(err) + return + } + defer task.Delete(ctx) + + statusC, err := task.Wait(ctx) + if err != nil { + t.Error(err) + return + } + + if err := task.Start(ctx); err != nil { + t.Error(err) + return + } + <-statusC + + wg.Wait() + + output := strings.TrimSuffix(buf.String(), "\n") + if output != "31" { + t.Errorf("expected squid uid to be 31 but received %q", output) + } +} diff --git a/spec_opts_unix.go b/spec_opts_unix.go index 86f3420c8..7e0cef518 100644 --- a/spec_opts_unix.go +++ b/spec_opts_unix.go @@ -6,10 +6,14 @@ import ( "context" "encoding/json" "fmt" + "io/ioutil" + "os" "path/filepath" "strconv" "strings" + "golang.org/x/sys/unix" + "github.com/containerd/containerd/containers" "github.com/containerd/containerd/content" "github.com/containerd/containerd/images" @@ -17,6 +21,7 @@ import ( "github.com/containerd/containerd/typeurl" "github.com/opencontainers/image-spec/identity" "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/opencontainers/runc/libcontainer/user" specs "github.com/opencontainers/runtime-spec/specs-go" ) @@ -100,6 +105,10 @@ func WithImageConfig(i Image) SpecOpts { case 1: v, err := strconv.ParseUint(parts[0], 0, 10) if err != nil { + // if we cannot parse as a uint they try to see if it is a username + if err := WithUsername(config.User)(ctx, client, c, s); err != nil { + return err + } return err } uid, gid = uint32(v), uint32(v) @@ -319,3 +328,50 @@ func WithUserIDs(uid, gid uint32) SpecOpts { return nil } } + +// WithUsername sets the correct UID and GID for the container +// based on the the image's /etc/passwd contents. +// id is the snapshot id that is used +func WithUsername(username string) SpecOpts { + return func(ctx context.Context, client *Client, c *containers.Container, s *specs.Spec) error { + if c.Snapshotter == "" { + return fmt.Errorf("no snapshotter set for container") + } + if c.RootFS == "" { + return fmt.Errorf("rootfs not created for container") + } + snapshotter := client.SnapshotService(c.Snapshotter) + mounts, err := snapshotter.Mounts(ctx, c.RootFS) + if err != nil { + return err + } + root, err := ioutil.TempDir("", "ctd-username") + if err != nil { + return err + } + defer os.RemoveAll(root) + for _, m := range mounts { + if err := m.Mount(root); err != nil { + return err + } + } + defer unix.Unmount(root, 0) + f, err := os.Open(filepath.Join(root, "/etc/passwd")) + if err != nil { + return err + } + defer f.Close() + users, err := user.ParsePasswdFilter(f, func(u user.User) bool { + return u.Name == username + }) + if err != nil { + return err + } + if len(users) == 0 { + return fmt.Errorf("no users found for %s", username) + } + u := users[0] + s.Process.User.UID, s.Process.User.GID = uint32(u.Uid), uint32(u.Gid) + return nil + } +}