Add user namespace support to client
Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
This commit is contained in:
parent
c3872b848f
commit
a0a5cc7787
13
apparmor.go
Normal file
13
apparmor.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// +build linux
|
||||||
|
|
||||||
|
package containerd
|
||||||
|
|
||||||
|
import specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||||
|
|
||||||
|
// WithApparmor sets the provided apparmor profile to the spec
|
||||||
|
func WithApparmorProfile(profile string) SpecOpts {
|
||||||
|
return func(s *specs.Spec) error {
|
||||||
|
s.Process.ApparmorProfile = profile
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
@ -889,3 +889,81 @@ func TestContainerExecNoBinaryExists(t *testing.T) {
|
|||||||
}
|
}
|
||||||
<-finished
|
<-finished
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUserNamespaces(t *testing.T) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec, err := generateSpec(
|
||||||
|
withImageConfig(ctx, image),
|
||||||
|
withExitStatus(7),
|
||||||
|
withUserNamespace(0, 1000, 10000),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
container, err := client.NewContainer(ctx, id,
|
||||||
|
WithSpec(spec),
|
||||||
|
withRemappedSnapshot(id, image, 1000, 1000),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer container.Delete(ctx, WithSnapshotCleanup)
|
||||||
|
|
||||||
|
task, err := container.NewTask(ctx, Stdio)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer task.Delete(ctx)
|
||||||
|
|
||||||
|
statusC := make(chan uint32, 1)
|
||||||
|
go func() {
|
||||||
|
status, err := task.Wait(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
statusC <- status
|
||||||
|
}()
|
||||||
|
|
||||||
|
if pid := task.Pid(); pid <= 0 {
|
||||||
|
t.Errorf("invalid task pid %d", pid)
|
||||||
|
}
|
||||||
|
if err := task.Start(ctx); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
task.Delete(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
status := <-statusC
|
||||||
|
if status != 7 {
|
||||||
|
t.Errorf("expected status 7 from wait but received %d", status)
|
||||||
|
}
|
||||||
|
if status, err = task.Delete(ctx); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if status != 7 {
|
||||||
|
t.Errorf("expected status 7 from delete but received %d", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -49,3 +49,7 @@ func withImageConfig(ctx context.Context, i Image) SpecOpts {
|
|||||||
func withNewSnapshot(id string, i Image) NewContainerOpts {
|
func withNewSnapshot(id string, i Image) NewContainerOpts {
|
||||||
return WithNewSnapshot(id, i)
|
return WithNewSnapshot(id, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var withUserNamespace = WithUserNamespace
|
||||||
|
|
||||||
|
var withRemappedSnapshot = WithRemappedSnapshot
|
||||||
|
@ -63,3 +63,15 @@ func withNewSnapshot(id string, i Image) NewContainerOpts {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func withUserNamespace(u, g, s uint32) SpecOpts {
|
||||||
|
return func(s *specs.Spec) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withRemappedSnapshot(id string, i Image, u, g uint32) NewContainerOpts {
|
||||||
|
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -21,7 +21,7 @@ func loadBundle(path, namespace string) *bundle {
|
|||||||
|
|
||||||
// newBundle creates a new bundle on disk at the provided path for the given id
|
// newBundle creates a new bundle on disk at the provided path for the given id
|
||||||
func newBundle(path, namespace, id string, spec []byte) (b *bundle, err error) {
|
func newBundle(path, namespace, id string, spec []byte) (b *bundle, err error) {
|
||||||
if err := os.MkdirAll(path, 0700); err != nil {
|
if err := os.MkdirAll(path, 0711); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
path = filepath.Join(path, id)
|
path = filepath.Join(path, id)
|
||||||
@ -30,10 +30,10 @@ func newBundle(path, namespace, id string, spec []byte) (b *bundle, err error) {
|
|||||||
os.RemoveAll(path)
|
os.RemoveAll(path)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if err := os.Mkdir(path, 0700); err != nil {
|
if err := os.Mkdir(path, 0711); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := os.Mkdir(filepath.Join(path, "rootfs"), 0700); err != nil {
|
if err := os.Mkdir(filepath.Join(path, "rootfs"), 0711); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
f, err := os.Create(filepath.Join(path, configFilename))
|
f, err := os.Create(filepath.Join(path, configFilename))
|
||||||
|
@ -68,7 +68,7 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(ic *plugin.InitContext) (interface{}, error) {
|
func New(ic *plugin.InitContext) (interface{}, error) {
|
||||||
if err := os.MkdirAll(ic.Root, 0700); err != nil {
|
if err := os.MkdirAll(ic.Root, 0711); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
monitor, err := ic.Get(plugin.TaskMonitorPlugin)
|
monitor, err := ic.Get(plugin.TaskMonitorPlugin)
|
||||||
|
@ -120,7 +120,6 @@ func newInitProcess(context context.Context, path, namespace string, r *shimapi.
|
|||||||
}
|
}
|
||||||
defer os.Remove(socket.Path())
|
defer os.Remove(socket.Path())
|
||||||
} else {
|
} else {
|
||||||
// TODO: get uid/gid
|
|
||||||
if io, err = runc.NewPipeIO(0, 0); err != nil {
|
if io, err = runc.NewPipeIO(0, 0); err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to create OCI runtime io pipes")
|
return nil, errors.Wrap(err, "failed to create OCI runtime io pipes")
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ func New(ctx context.Context, config *Config) (*Server, error) {
|
|||||||
if config.Root == "" {
|
if config.Root == "" {
|
||||||
return nil, errors.New("root must be specified")
|
return nil, errors.New("root must be specified")
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(config.Root, 0700); err != nil {
|
if err := os.MkdirAll(config.Root, 0711); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := apply(ctx, config); err != nil {
|
if err := apply(ctx, config); err != nil {
|
||||||
@ -168,7 +168,7 @@ func loadPlugins(config *Config) ([]*plugin.Registration, error) {
|
|||||||
Type: plugin.MetadataPlugin,
|
Type: plugin.MetadataPlugin,
|
||||||
ID: "bolt",
|
ID: "bolt",
|
||||||
Init: func(ic *plugin.InitContext) (interface{}, error) {
|
Init: func(ic *plugin.InitContext) (interface{}, error) {
|
||||||
if err := os.MkdirAll(ic.Root, 0700); err != nil {
|
if err := os.MkdirAll(ic.Root, 0711); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return bolt.Open(filepath.Join(ic.Root, "meta.db"), 0644, nil)
|
return bolt.Open(filepath.Join(ic.Root, "meta.db"), 0644, nil)
|
||||||
|
@ -255,12 +255,12 @@ func (o *snapshotter) createSnapshot(ctx context.Context, kind snapshot.Kind, ke
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err = os.MkdirAll(filepath.Join(td, "fs"), 0711); err != nil {
|
if err = os.MkdirAll(filepath.Join(td, "fs"), 0755); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if kind == snapshot.KindActive {
|
if kind == snapshot.KindActive {
|
||||||
if err = os.MkdirAll(filepath.Join(td, "work"), 0700); err != nil {
|
if err = os.MkdirAll(filepath.Join(td, "work"), 0711); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
spec.go
2
spec.go
@ -2,8 +2,10 @@ package containerd
|
|||||||
|
|
||||||
import specs "github.com/opencontainers/runtime-spec/specs-go"
|
import specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||||
|
|
||||||
|
// SpecOpts sets spec specific information to a newly generated OCI spec
|
||||||
type SpecOpts func(s *specs.Spec) error
|
type SpecOpts func(s *specs.Spec) error
|
||||||
|
|
||||||
|
// WithProcessArgs replaces the args on the generated spec
|
||||||
func WithProcessArgs(args ...string) SpecOpts {
|
func WithProcessArgs(args ...string) SpecOpts {
|
||||||
return func(s *specs.Spec) error {
|
return func(s *specs.Spec) error {
|
||||||
s.Process.Args = args
|
s.Process.Args = args
|
||||||
|
173
spec_unix.go
173
spec_unix.go
@ -6,12 +6,21 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
"github.com/containerd/containerd/containers"
|
"github.com/containerd/containerd/containers"
|
||||||
|
"github.com/containerd/containerd/fs"
|
||||||
"github.com/containerd/containerd/images"
|
"github.com/containerd/containerd/images"
|
||||||
|
"github.com/containerd/containerd/mount"
|
||||||
"github.com/containerd/containerd/typeurl"
|
"github.com/containerd/containerd/typeurl"
|
||||||
|
"github.com/opencontainers/image-spec/identity"
|
||||||
"github.com/opencontainers/image-spec/specs-go/v1"
|
"github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||||
)
|
)
|
||||||
@ -78,7 +87,6 @@ func createDefaultSpec() (*specs.Spec, error) {
|
|||||||
Permitted: defaltCaps(),
|
Permitted: defaltCaps(),
|
||||||
Inheritable: defaltCaps(),
|
Inheritable: defaltCaps(),
|
||||||
Effective: defaltCaps(),
|
Effective: defaltCaps(),
|
||||||
Ambient: defaltCaps(),
|
|
||||||
},
|
},
|
||||||
Rlimits: []specs.POSIXRlimit{
|
Rlimits: []specs.POSIXRlimit{
|
||||||
{
|
{
|
||||||
@ -130,24 +138,6 @@ func createDefaultSpec() (*specs.Spec, error) {
|
|||||||
Source: "tmpfs",
|
Source: "tmpfs",
|
||||||
Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"},
|
Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Destination: "/etc/resolv.conf",
|
|
||||||
Type: "bind",
|
|
||||||
Source: "/etc/resolv.conf",
|
|
||||||
Options: []string{"rbind", "ro"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Destination: "/etc/hosts",
|
|
||||||
Type: "bind",
|
|
||||||
Source: "/etc/hosts",
|
|
||||||
Options: []string{"rbind", "ro"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Destination: "/etc/localtime",
|
|
||||||
Type: "bind",
|
|
||||||
Source: "/etc/localtime",
|
|
||||||
Options: []string{"rbind", "ro"},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Linux: &specs.Linux{
|
Linux: &specs.Linux{
|
||||||
// TODO (@crosbymichael) make sure we don't have have two containers in the same cgroup
|
// TODO (@crosbymichael) make sure we don't have have two containers in the same cgroup
|
||||||
@ -272,6 +262,7 @@ func WithImageConfig(ctx context.Context, i Image) SpecOpts {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithSpec sets the provided spec for a new container
|
||||||
func WithSpec(spec *specs.Spec) NewContainerOpts {
|
func WithSpec(spec *specs.Spec) NewContainerOpts {
|
||||||
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
||||||
any, err := typeurl.MarshalAny(spec)
|
any, err := typeurl.MarshalAny(spec)
|
||||||
@ -283,9 +274,153 @@ func WithSpec(spec *specs.Spec) NewContainerOpts {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithResources sets the provided resources on the spec for task updates
|
||||||
func WithResources(resources *specs.LinuxResources) UpdateTaskOpts {
|
func WithResources(resources *specs.LinuxResources) UpdateTaskOpts {
|
||||||
return func(ctx context.Context, client *Client, r *UpdateTaskInfo) error {
|
return func(ctx context.Context, client *Client, r *UpdateTaskInfo) error {
|
||||||
r.Resources = resources
|
r.Resources = resources
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithNoNewPrivileges sets no_new_privileges on the process for the container
|
||||||
|
func WithNoNewPrivileges(s *specs.Spec) error {
|
||||||
|
s.Process.NoNewPrivileges = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithHostHosts(s *specs.Spec) error {
|
||||||
|
s.Mounts = append(s.Mounts, specs.Mount{
|
||||||
|
Destination: "/etc/hosts",
|
||||||
|
Type: "bind",
|
||||||
|
Source: "/etc/hosts",
|
||||||
|
Options: []string{"rbind", "ro"},
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithHostResoveconf(s *specs.Spec) error {
|
||||||
|
s.Mounts = append(s.Mounts, specs.Mount{
|
||||||
|
Destination: "/etc/resolv.conf",
|
||||||
|
Type: "bind",
|
||||||
|
Source: "/etc/resolv.conf",
|
||||||
|
Options: []string{"rbind", "ro"},
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithHostLocaltime(s *specs.Spec) error {
|
||||||
|
s.Mounts = append(s.Mounts, specs.Mount{
|
||||||
|
Destination: "/etc/localtime",
|
||||||
|
Type: "bind",
|
||||||
|
Source: "/etc/localtime",
|
||||||
|
Options: []string{"rbind", "ro"},
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithUserNamespace sets the uid and gid mappings for the task
|
||||||
|
// this can be called multiple times to add more mappings to the generated spec
|
||||||
|
func WithUserNamespace(container, host, size uint32) SpecOpts {
|
||||||
|
return func(s *specs.Spec) error {
|
||||||
|
var hasUserns bool
|
||||||
|
for _, ns := range s.Linux.Namespaces {
|
||||||
|
if ns.Type == specs.UserNamespace {
|
||||||
|
hasUserns = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasUserns {
|
||||||
|
s.Linux.Namespaces = append(s.Linux.Namespaces, specs.LinuxNamespace{
|
||||||
|
Type: specs.UserNamespace,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
mapping := specs.LinuxIDMapping{
|
||||||
|
ContainerID: container,
|
||||||
|
HostID: host,
|
||||||
|
Size: size,
|
||||||
|
}
|
||||||
|
s.Linux.UIDMappings = append(s.Linux.UIDMappings, mapping)
|
||||||
|
s.Linux.GIDMappings = append(s.Linux.GIDMappings, mapping)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRemappedSnapshot creates a new snapshot and remaps the uid/gid for the
|
||||||
|
// filesystem to be used by a container with user namespaces
|
||||||
|
func WithRemappedSnapshot(id string, i Image, uid, gid uint32) NewContainerOpts {
|
||||||
|
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
||||||
|
diffIDs, err := i.(*image).i.RootFS(ctx, client.ContentStore())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
snapshotter = client.SnapshotService(c.Snapshotter)
|
||||||
|
parent = identity.ChainID(diffIDs).String()
|
||||||
|
usernsID = fmt.Sprintf("%s-%d-%d", parent, uid, gid)
|
||||||
|
)
|
||||||
|
if _, err := snapshotter.Stat(ctx, usernsID); err == nil {
|
||||||
|
if _, err := snapshotter.Prepare(ctx, id, usernsID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.RootFS = id
|
||||||
|
c.Image = i.Name()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mounts, err := snapshotter.Prepare(ctx, usernsID+"-remap", parent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := remapRootFS(mounts, uid, gid); err != nil {
|
||||||
|
snapshotter.Remove(ctx, usernsID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := snapshotter.Commit(ctx, usernsID, usernsID+"-remap"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := snapshotter.Prepare(ctx, id, usernsID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.RootFS = id
|
||||||
|
c.Image = i.Name()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func remapRootFS(mounts []mount.Mount, uid, gid uint32) error {
|
||||||
|
root, err := ioutil.TempDir("", "ctd-remap")
|
||||||
|
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)
|
||||||
|
return filepath.Walk(root, incrementFS(root, uid, gid))
|
||||||
|
}
|
||||||
|
|
||||||
|
func incrementFS(root string, uidInc, gidInc uint32) filepath.WalkFunc {
|
||||||
|
return func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if root == path {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
stat = info.Sys().(*syscall.Stat_t)
|
||||||
|
u, g = int(stat.Uid + uidInc), int(stat.Gid + gidInc)
|
||||||
|
symlink = info.Mode()&os.ModeSymlink != 0
|
||||||
|
)
|
||||||
|
// make sure we resolve links inside the root for symlinks
|
||||||
|
if path, err = fs.RootPath(root, strings.TrimPrefix(path, root)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.Chown(path, u, g); err != nil && !symlink {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -20,7 +20,6 @@ func TestGenerateSpec(t *testing.T) {
|
|||||||
// check for matching caps
|
// check for matching caps
|
||||||
defaults := defaltCaps()
|
defaults := defaltCaps()
|
||||||
for _, cl := range [][]string{
|
for _, cl := range [][]string{
|
||||||
s.Process.Capabilities.Ambient,
|
|
||||||
s.Process.Capabilities.Bounding,
|
s.Process.Capabilities.Bounding,
|
||||||
s.Process.Capabilities.Permitted,
|
s.Process.Capabilities.Permitted,
|
||||||
s.Process.Capabilities.Inheritable,
|
s.Process.Capabilities.Inheritable,
|
||||||
|
Loading…
Reference in New Issue
Block a user