integration: Add userns test with volumes

Signed-off-by: Rodrigo Campos <rodrigoca@microsoft.com>
This commit is contained in:
Rodrigo Campos 2023-07-21 13:30:51 +02:00
parent ab5b43fe80
commit 24aa808fe2
2 changed files with 153 additions and 5 deletions

View File

@ -298,6 +298,21 @@ func WithVolumeMount(hostPath, containerPath string) ContainerOpts {
} }
} }
func WithIDMapVolumeMount(hostPath, containerPath string, uidMaps, gidMaps []*runtime.IDMapping) ContainerOpts {
return func(c *runtime.ContainerConfig) {
hostPath, _ = filepath.Abs(hostPath)
containerPath, _ = filepath.Abs(containerPath)
mount := &runtime.Mount{
HostPath: hostPath,
ContainerPath: containerPath,
SelinuxRelabel: selinux.GetEnabled(),
UidMappings: uidMaps,
GidMappings: gidMaps,
}
c.Mounts = append(c.Mounts, mount)
}
}
func WithWindowsUsername(username string) ContainerOpts { func WithWindowsUsername(username string) ContainerOpts {
return func(c *runtime.ContainerConfig) { return func(c *runtime.ContainerConfig) {
if c.Windows == nil { if c.Windows == nil {

View File

@ -19,7 +19,9 @@ package integration
import ( import (
"fmt" "fmt"
"os" "os"
"os/user"
"path/filepath" "path/filepath"
"strings"
"syscall" "syscall"
"testing" "testing"
"time" "time"
@ -28,17 +30,121 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
exec "golang.org/x/sys/execabs" exec "golang.org/x/sys/execabs"
"golang.org/x/sys/unix"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1" runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
) )
const (
defaultRoot = "/var/lib/containerd-test"
)
func supportsUserNS() bool {
if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
return false
}
return true
}
func supportsIDMap(path string) bool {
treeFD, err := unix.OpenTree(-1, path, uint(unix.OPEN_TREE_CLONE|unix.OPEN_TREE_CLOEXEC))
if err != nil {
return false
}
defer unix.Close(treeFD)
// We want to test if idmap mounts are supported.
// So we use just some random mapping, it doesn't really matter which one.
// For the helper command, we just need something that is alive while we
// test this, a sleep 5 will do it.
cmd := exec.Command("sleep", "5")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUSER,
UidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: 65536, Size: 65536}},
GidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: 65536, Size: 65536}},
}
if err := cmd.Start(); err != nil {
return false
}
defer func() {
_ = cmd.Process.Kill()
_ = cmd.Wait()
}()
usernsFD := fmt.Sprintf("/proc/%d/ns/user", cmd.Process.Pid)
var usernsFile *os.File
if usernsFile, err = os.Open(usernsFD); err != nil {
return false
}
defer usernsFile.Close()
attr := unix.MountAttr{
Attr_set: unix.MOUNT_ATTR_IDMAP,
Userns_fd: uint64(usernsFile.Fd()),
}
if err := unix.MountSetattr(treeFD, "", unix.AT_EMPTY_PATH, &attr); err != nil {
return false
}
return true
}
// traversePath gives 755 permissions for all elements in tPath below
// os.TempDir() and errors out if elements above it don't have read+exec
// permissions for others. tPath MUST be a descendant of os.TempDir(). The path
// returned by testing.TempDir() usually is.
func traversePath(tPath string) error {
// Check the assumption that the argument is under os.TempDir().
tempBase := os.TempDir()
if !strings.HasPrefix(tPath, tempBase) {
return fmt.Errorf("traversePath: %q is not a descendant of %q", tPath, tempBase)
}
var path string
for _, p := range strings.SplitAfter(tPath, "/") {
path = path + p
stats, err := os.Stat(path)
if err != nil {
return err
}
perm := stats.Mode().Perm()
if perm&0o5 == 0o5 {
continue
}
if strings.HasPrefix(tempBase, path) {
return fmt.Errorf("traversePath: directory %q MUST have read+exec permissions for others", path)
}
if err := os.Chmod(path, perm|0o755); err != nil {
return err
}
}
return nil
}
func TestPodUserNS(t *testing.T) { func TestPodUserNS(t *testing.T) {
containerID := uint32(0) containerID := uint32(0)
hostID := uint32(65536) hostID := uint32(65536)
size := uint32(65536) size := uint32(65536)
idmap := []*runtime.IDMapping{
{
ContainerId: containerID,
HostId: hostID,
Length: size,
},
}
volumeHostPath := t.TempDir()
if err := traversePath(volumeHostPath); err != nil {
t.Fatalf("failed to setup volume host path: %v", err)
}
for name, test := range map[string]struct { for name, test := range map[string]struct {
sandboxOpts []PodSandboxOpts sandboxOpts []PodSandboxOpts
containerOpts []ContainerOpts containerOpts []ContainerOpts
checkOutput func(t *testing.T, output string) checkOutput func(t *testing.T, output string)
hostVolumes bool // whether to config uses host Volumes
expectErr bool expectErr bool
}{ }{
"userns uid mapping": { "userns uid mapping": {
@ -85,6 +191,31 @@ func TestPodUserNS(t *testing.T) {
assert.Contains(t, output, "=0=0=") assert.Contains(t, output, "=0=0=")
}, },
}, },
"volumes permissions": {
sandboxOpts: []PodSandboxOpts{
WithPodUserNs(containerID, hostID, size),
},
hostVolumes: true,
containerOpts: []ContainerOpts{
WithUserNamespace(containerID, hostID, size),
WithIDMapVolumeMount(volumeHostPath, "/mnt", idmap, idmap),
// Prints numeric UID and GID for path.
// For example, if UID and GID is 0 it will print: =0=0=
// We add the "=" signs so we use can assert.Contains() and be sure
// the UID/GID is 0 and not things like 100 (that contain 0).
// We can't use assert.Equal() easily as it contains timestamp, etc.
WithCommand("stat", "-c", "'=%u=%g='", "/mnt/"),
},
checkOutput: func(t *testing.T, output string) {
// The UID and GID should be the current user if chown/remap is done correctly.
uid := "0"
user, err := user.Current()
if user != nil && err == nil {
uid = user.Uid
}
assert.Contains(t, output, "="+uid+"="+uid+"=")
},
},
"fails with several mappings": { "fails with several mappings": {
sandboxOpts: []PodSandboxOpts{ sandboxOpts: []PodSandboxOpts{
WithPodUserNs(containerID, hostID, size), WithPodUserNs(containerID, hostID, size),
@ -94,12 +225,14 @@ func TestPodUserNS(t *testing.T) {
}, },
} { } {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
cmd := exec.Command("true") if !supportsUserNS() {
cmd.SysProcAttr = &syscall.SysProcAttr{ t.Skip("User namespaces are not supported")
Cloneflags: syscall.CLONE_NEWUSER,
} }
if err := cmd.Run(); err != nil { if !supportsIDMap(defaultRoot) {
t.Skip("skipping test: user namespaces are unavailable") t.Skipf("ID mappings are not supported on: %v", defaultRoot)
}
if test.hostVolumes && !supportsIDMap(volumeHostPath) {
t.Skipf("ID mappings are not supported host volume filesystem: %v", volumeHostPath)
} }
testPodLogDir := t.TempDir() testPodLogDir := t.TempDir()