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