diff --git a/cmd/ctr/commands/run/run_unix.go b/cmd/ctr/commands/run/run_unix.go index b733984f807c..fc110685ac43 100644 --- a/cmd/ctr/commands/run/run_unix.go +++ b/cmd/ctr/commands/run/run_unix.go @@ -20,7 +20,9 @@ package run import ( gocontext "context" + "fmt" "path/filepath" + "strconv" "strings" "github.com/containerd/containerd" @@ -44,6 +46,14 @@ var platformRunFlags = []cli.Flag{ Name: "runc-systemd-cgroup", Usage: "start runc with systemd cgroup manager", }, + cli.StringFlag{ + Name: "uidmap", + Usage: "run inside a user namespace with the specified UID mapping range; specified with the format `container-uid:host-uid:length`", + }, + cli.StringFlag{ + Name: "gidmap", + Usage: "run inside a user namespace with the specified GID mapping range; specified with the format `container-gid:host-gid:length`", + }, } // NewContainer creates a new container @@ -115,12 +125,30 @@ func NewContainer(ctx gocontext.Context, client *containerd.Client, context *cli opts = append(opts, oci.WithImageConfig(image)) cOpts = append(cOpts, containerd.WithImage(image), - containerd.WithSnapshotter(snapshotter), + containerd.WithSnapshotter(snapshotter)) + if uidmap, gidmap := context.String("uidmap"), context.String("gidmap"); uidmap != "" && gidmap != "" { + uidMap, err := parseIDMapping(uidmap) + if err != nil { + return nil, err + } + gidMap, err := parseIDMapping(gidmap) + if err != nil { + return nil, err + } + opts = append(opts, + oci.WithUserNamespace([]specs.LinuxIDMapping{uidMap}, []specs.LinuxIDMapping{gidMap})) + if context.Bool("read-only") { + cOpts = append(cOpts, containerd.WithRemappedSnapshotView(id, image, uidMap.HostID, gidMap.HostID)) + } else { + cOpts = append(cOpts, containerd.WithRemappedSnapshot(id, image, uidMap.HostID, gidMap.HostID)) + } + } else { // Even when "read-only" is set, we don't use KindView snapshot here. (#1495) // We pass writable snapshot to the OCI runtime, and the runtime remounts it as read-only, // after creating some mount points on demand. - containerd.WithNewSnapshot(id, image), - containerd.WithImageStopSignal(image, "SIGTERM")) + cOpts = append(cOpts, containerd.WithNewSnapshot(id, image)) + } + cOpts = append(cOpts, containerd.WithImageStopSignal(image, "SIGTERM")) } if context.Bool("read-only") { opts = append(opts, oci.WithRootFSReadonly()) @@ -210,10 +238,51 @@ func NewContainer(ctx gocontext.Context, client *containerd.Client, context *cli } func getNewTaskOpts(context *cli.Context) []containerd.NewTaskOpts { + var ( + tOpts []containerd.NewTaskOpts + ) if context.Bool("no-pivot") { - return []containerd.NewTaskOpts{containerd.WithNoPivotRoot} + tOpts = append(tOpts, containerd.WithNoPivotRoot) + } + if uidmap := context.String("uidmap"); uidmap != "" { + uidMap, err := parseIDMapping(uidmap) + if err != nil { + fmt.Printf("warning: expected to parse uidmap: %s\n", err.Error()) + } + tOpts = append(tOpts, containerd.WithUIDOwner(uidMap.HostID)) + } + if gidmap := context.String("gidmap"); gidmap != "" { + gidMap, err := parseIDMapping(gidmap) + if err != nil { + fmt.Printf("warning: expected to parse uidmap: %s\n", err.Error()) + } + tOpts = append(tOpts, containerd.WithGIDOwner(gidMap.HostID)) + } + return tOpts +} + +func parseIDMapping(mapping string) (specs.LinuxIDMapping, error) { + parts := strings.Split(mapping, ":") + if len(parts) != 3 { + return specs.LinuxIDMapping{}, errors.New("user namespace mappings require the format `container-id:host-id:size`") + } + cID, err := strconv.ParseUint(parts[0], 0, 16) + if err != nil { + return specs.LinuxIDMapping{}, errors.Wrapf(err, "invalid container id for user namespace remapping") + } + hID, err := strconv.ParseUint(parts[1], 0, 16) + if err != nil { + return specs.LinuxIDMapping{}, errors.Wrapf(err, "invalid host id for user namespace remapping") + } + size, err := strconv.ParseUint(parts[2], 0, 16) + if err != nil { + return specs.LinuxIDMapping{}, errors.Wrapf(err, "invalid size for user namespace remapping") } - return nil + return specs.LinuxIDMapping{ + ContainerID: uint32(cID), + HostID: uint32(hID), + Size: uint32(size), + }, nil } func validNamespace(ns string) bool { diff --git a/task_opts_unix.go b/task_opts_unix.go index 8b498d47efc7..a710b358fce4 100644 --- a/task_opts_unix.go +++ b/task_opts_unix.go @@ -103,3 +103,55 @@ func WithShimCgroup(path string) NewTaskOpts { return nil } } + +// WithUIDOwner allows console I/O to work with the remapped UID in user namespace +func WithUIDOwner(uid uint32) NewTaskOpts { + return func(ctx context.Context, c *Client, ti *TaskInfo) error { + if CheckRuntime(ti.Runtime(), "io.containerd.runc") { + if ti.Options == nil { + ti.Options = &options.Options{} + } + opts, ok := ti.Options.(*options.Options) + if !ok { + return errors.New("invalid v2 shim create options format") + } + opts.IoUid = uid + } else { + if ti.Options == nil { + ti.Options = &runctypes.CreateOptions{} + } + opts, ok := ti.Options.(*runctypes.CreateOptions) + if !ok { + return errors.New("could not cast TaskInfo Options to CreateOptions") + } + opts.IoUid = uid + } + return nil + } +} + +// WithGIDOwner allows console I/O to work with the remapped GID in user namespace +func WithGIDOwner(gid uint32) NewTaskOpts { + return func(ctx context.Context, c *Client, ti *TaskInfo) error { + if CheckRuntime(ti.Runtime(), "io.containerd.runc") { + if ti.Options == nil { + ti.Options = &options.Options{} + } + opts, ok := ti.Options.(*options.Options) + if !ok { + return errors.New("invalid v2 shim create options format") + } + opts.IoGid = gid + } else { + if ti.Options == nil { + ti.Options = &runctypes.CreateOptions{} + } + opts, ok := ti.Options.(*runctypes.CreateOptions) + if !ok { + return errors.New("could not cast TaskInfo Options to CreateOptions") + } + opts.IoGid = gid + } + return nil + } +}