integration: Add userns test with volumes
Signed-off-by: Rodrigo Campos <rodrigoca@microsoft.com>
This commit is contained in:
parent
ab5b43fe80
commit
24aa808fe2
@ -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 {
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user