1794 lines
44 KiB
Go
1794 lines
44 KiB
Go
package integration
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/opencontainers/runc/libcontainer"
|
|
"github.com/opencontainers/runc/libcontainer/cgroups"
|
|
"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
|
|
"github.com/opencontainers/runc/libcontainer/configs"
|
|
"github.com/opencontainers/runtime-spec/specs-go"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
func TestExecPS(t *testing.T) {
|
|
testExecPS(t, false)
|
|
}
|
|
|
|
func TestUsernsExecPS(t *testing.T) {
|
|
if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
|
|
t.Skip("userns is unsupported")
|
|
}
|
|
testExecPS(t, true)
|
|
}
|
|
|
|
func testExecPS(t *testing.T, userns bool) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
config := newTemplateConfig(rootfs)
|
|
if userns {
|
|
config.UidMappings = []configs.IDMap{{HostID: 0, ContainerID: 0, Size: 1000}}
|
|
config.GidMappings = []configs.IDMap{{HostID: 0, ContainerID: 0, Size: 1000}}
|
|
config.Namespaces = append(config.Namespaces, configs.Namespace{Type: configs.NEWUSER})
|
|
}
|
|
|
|
buffers, exitCode, err := runContainer(config, "", "ps", "-o", "pid,user,comm")
|
|
if err != nil {
|
|
t.Fatalf("%s: %s", buffers, err)
|
|
}
|
|
if exitCode != 0 {
|
|
t.Fatalf("exit code not 0. code %d stderr %q", exitCode, buffers.Stderr)
|
|
}
|
|
lines := strings.Split(buffers.Stdout.String(), "\n")
|
|
if len(lines) < 2 {
|
|
t.Fatalf("more than one process running for output %q", buffers.Stdout.String())
|
|
}
|
|
expected := `1 root ps`
|
|
actual := strings.Trim(lines[1], "\n ")
|
|
if actual != expected {
|
|
t.Fatalf("expected output %q but received %q", expected, actual)
|
|
}
|
|
}
|
|
|
|
func TestIPCPrivate(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
l, err := os.Readlink("/proc/1/ns/ipc")
|
|
ok(t, err)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
buffers, exitCode, err := runContainer(config, "", "readlink", "/proc/self/ns/ipc")
|
|
ok(t, err)
|
|
|
|
if exitCode != 0 {
|
|
t.Fatalf("exit code not 0. code %d stderr %q", exitCode, buffers.Stderr)
|
|
}
|
|
|
|
if actual := strings.Trim(buffers.Stdout.String(), "\n"); actual == l {
|
|
t.Fatalf("ipc link should be private to the container but equals host %q %q", actual, l)
|
|
}
|
|
}
|
|
|
|
func TestIPCHost(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
l, err := os.Readlink("/proc/1/ns/ipc")
|
|
ok(t, err)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
config.Namespaces.Remove(configs.NEWIPC)
|
|
buffers, exitCode, err := runContainer(config, "", "readlink", "/proc/self/ns/ipc")
|
|
ok(t, err)
|
|
|
|
if exitCode != 0 {
|
|
t.Fatalf("exit code not 0. code %d stderr %q", exitCode, buffers.Stderr)
|
|
}
|
|
|
|
if actual := strings.Trim(buffers.Stdout.String(), "\n"); actual != l {
|
|
t.Fatalf("ipc link not equal to host link %q %q", actual, l)
|
|
}
|
|
}
|
|
|
|
func TestIPCJoinPath(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
l, err := os.Readlink("/proc/1/ns/ipc")
|
|
ok(t, err)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
config.Namespaces.Add(configs.NEWIPC, "/proc/1/ns/ipc")
|
|
|
|
buffers, exitCode, err := runContainer(config, "", "readlink", "/proc/self/ns/ipc")
|
|
ok(t, err)
|
|
|
|
if exitCode != 0 {
|
|
t.Fatalf("exit code not 0. code %d stderr %q", exitCode, buffers.Stderr)
|
|
}
|
|
|
|
if actual := strings.Trim(buffers.Stdout.String(), "\n"); actual != l {
|
|
t.Fatalf("ipc link not equal to host link %q %q", actual, l)
|
|
}
|
|
}
|
|
|
|
func TestIPCBadPath(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
config.Namespaces.Add(configs.NEWIPC, "/proc/1/ns/ipcc")
|
|
|
|
_, _, err = runContainer(config, "", "true")
|
|
if err == nil {
|
|
t.Fatal("container succeeded with bad ipc path")
|
|
}
|
|
}
|
|
|
|
func TestRlimit(t *testing.T) {
|
|
testRlimit(t, false)
|
|
}
|
|
|
|
func TestUsernsRlimit(t *testing.T) {
|
|
if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
|
|
t.Skip("userns is unsupported")
|
|
}
|
|
|
|
testRlimit(t, true)
|
|
}
|
|
|
|
func testRlimit(t *testing.T, userns bool) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
if userns {
|
|
config.UidMappings = []configs.IDMap{{HostID: 0, ContainerID: 0, Size: 1000}}
|
|
config.GidMappings = []configs.IDMap{{HostID: 0, ContainerID: 0, Size: 1000}}
|
|
config.Namespaces = append(config.Namespaces, configs.Namespace{Type: configs.NEWUSER})
|
|
}
|
|
|
|
// ensure limit is lower than what the config requests to test that in a user namespace
|
|
// the Setrlimit call happens early enough that we still have permissions to raise the limit.
|
|
ok(t, unix.Setrlimit(unix.RLIMIT_NOFILE, &unix.Rlimit{
|
|
Max: 1024,
|
|
Cur: 1024,
|
|
}))
|
|
|
|
out, _, err := runContainer(config, "", "/bin/sh", "-c", "ulimit -n")
|
|
ok(t, err)
|
|
if limit := strings.TrimSpace(out.Stdout.String()); limit != "1025" {
|
|
t.Fatalf("expected rlimit to be 1025, got %s", limit)
|
|
}
|
|
}
|
|
|
|
func TestEnter(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
if cgroups.IsCgroup2UnifiedMode() {
|
|
t.Skip("cgroup v2 is not supported")
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
|
|
container, err := newContainerWithName("test", config)
|
|
ok(t, err)
|
|
defer container.Destroy()
|
|
|
|
// Execute a first process in the container
|
|
stdinR, stdinW, err := os.Pipe()
|
|
ok(t, err)
|
|
|
|
var stdout, stdout2 bytes.Buffer
|
|
|
|
pconfig := libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"sh", "-c", "cat && readlink /proc/self/ns/pid"},
|
|
Env: standardEnvironment,
|
|
Stdin: stdinR,
|
|
Stdout: &stdout,
|
|
Init: true,
|
|
}
|
|
err = container.Run(&pconfig)
|
|
stdinR.Close()
|
|
defer stdinW.Close()
|
|
ok(t, err)
|
|
pid, err := pconfig.Pid()
|
|
ok(t, err)
|
|
|
|
// Execute another process in the container
|
|
stdinR2, stdinW2, err := os.Pipe()
|
|
ok(t, err)
|
|
pconfig2 := libcontainer.Process{
|
|
Cwd: "/",
|
|
Env: standardEnvironment,
|
|
}
|
|
pconfig2.Args = []string{"sh", "-c", "cat && readlink /proc/self/ns/pid"}
|
|
pconfig2.Stdin = stdinR2
|
|
pconfig2.Stdout = &stdout2
|
|
|
|
err = container.Run(&pconfig2)
|
|
stdinR2.Close()
|
|
defer stdinW2.Close()
|
|
ok(t, err)
|
|
|
|
pid2, err := pconfig2.Pid()
|
|
ok(t, err)
|
|
|
|
processes, err := container.Processes()
|
|
ok(t, err)
|
|
|
|
n := 0
|
|
for i := range processes {
|
|
if processes[i] == pid || processes[i] == pid2 {
|
|
n++
|
|
}
|
|
}
|
|
if n != 2 {
|
|
t.Fatal("unexpected number of processes", processes, pid, pid2)
|
|
}
|
|
|
|
// Wait processes
|
|
stdinW2.Close()
|
|
waitProcess(&pconfig2, t)
|
|
|
|
stdinW.Close()
|
|
waitProcess(&pconfig, t)
|
|
|
|
// Check that both processes live in the same pidns
|
|
pidns := string(stdout.Bytes())
|
|
ok(t, err)
|
|
|
|
pidns2 := string(stdout2.Bytes())
|
|
ok(t, err)
|
|
|
|
if pidns != pidns2 {
|
|
t.Fatal("The second process isn't in the required pid namespace", pidns, pidns2)
|
|
}
|
|
}
|
|
|
|
func TestProcessEnv(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
|
|
container, err := newContainerWithName("test", config)
|
|
ok(t, err)
|
|
defer container.Destroy()
|
|
|
|
var stdout bytes.Buffer
|
|
pconfig := libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"sh", "-c", "env"},
|
|
Env: []string{
|
|
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
"HOSTNAME=integration",
|
|
"TERM=xterm",
|
|
"FOO=BAR",
|
|
},
|
|
Stdin: nil,
|
|
Stdout: &stdout,
|
|
Init: true,
|
|
}
|
|
err = container.Run(&pconfig)
|
|
ok(t, err)
|
|
|
|
// Wait for process
|
|
waitProcess(&pconfig, t)
|
|
|
|
outputEnv := string(stdout.Bytes())
|
|
|
|
// Check that the environment has the key/value pair we added
|
|
if !strings.Contains(outputEnv, "FOO=BAR") {
|
|
t.Fatal("Environment doesn't have the expected FOO=BAR key/value pair: ", outputEnv)
|
|
}
|
|
|
|
// Make sure that HOME is set
|
|
if !strings.Contains(outputEnv, "HOME=/root") {
|
|
t.Fatal("Environment doesn't have HOME set: ", outputEnv)
|
|
}
|
|
}
|
|
|
|
func TestProcessEmptyCaps(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
config.Capabilities = nil
|
|
|
|
container, err := newContainerWithName("test", config)
|
|
ok(t, err)
|
|
defer container.Destroy()
|
|
|
|
var stdout bytes.Buffer
|
|
pconfig := libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"sh", "-c", "cat /proc/self/status"},
|
|
Env: standardEnvironment,
|
|
Stdin: nil,
|
|
Stdout: &stdout,
|
|
Init: true,
|
|
}
|
|
err = container.Run(&pconfig)
|
|
ok(t, err)
|
|
|
|
// Wait for process
|
|
waitProcess(&pconfig, t)
|
|
|
|
outputStatus := string(stdout.Bytes())
|
|
|
|
lines := strings.Split(outputStatus, "\n")
|
|
|
|
effectiveCapsLine := ""
|
|
for _, l := range lines {
|
|
line := strings.TrimSpace(l)
|
|
if strings.Contains(line, "CapEff:") {
|
|
effectiveCapsLine = line
|
|
break
|
|
}
|
|
}
|
|
|
|
if effectiveCapsLine == "" {
|
|
t.Fatal("Couldn't find effective caps: ", outputStatus)
|
|
}
|
|
}
|
|
|
|
func TestProcessCaps(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
|
|
container, err := newContainerWithName("test", config)
|
|
ok(t, err)
|
|
defer container.Destroy()
|
|
|
|
var stdout bytes.Buffer
|
|
pconfig := libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"sh", "-c", "cat /proc/self/status"},
|
|
Env: standardEnvironment,
|
|
Stdin: nil,
|
|
Stdout: &stdout,
|
|
Capabilities: &configs.Capabilities{},
|
|
Init: true,
|
|
}
|
|
pconfig.Capabilities.Bounding = append(config.Capabilities.Bounding, "CAP_NET_ADMIN")
|
|
pconfig.Capabilities.Permitted = append(config.Capabilities.Permitted, "CAP_NET_ADMIN")
|
|
pconfig.Capabilities.Effective = append(config.Capabilities.Effective, "CAP_NET_ADMIN")
|
|
pconfig.Capabilities.Inheritable = append(config.Capabilities.Inheritable, "CAP_NET_ADMIN")
|
|
err = container.Run(&pconfig)
|
|
ok(t, err)
|
|
|
|
// Wait for process
|
|
waitProcess(&pconfig, t)
|
|
|
|
outputStatus := string(stdout.Bytes())
|
|
|
|
lines := strings.Split(outputStatus, "\n")
|
|
|
|
effectiveCapsLine := ""
|
|
for _, l := range lines {
|
|
line := strings.TrimSpace(l)
|
|
if strings.Contains(line, "CapEff:") {
|
|
effectiveCapsLine = line
|
|
break
|
|
}
|
|
}
|
|
|
|
if effectiveCapsLine == "" {
|
|
t.Fatal("Couldn't find effective caps: ", outputStatus)
|
|
}
|
|
|
|
parts := strings.Split(effectiveCapsLine, ":")
|
|
effectiveCapsStr := strings.TrimSpace(parts[1])
|
|
|
|
effectiveCaps, err := strconv.ParseUint(effectiveCapsStr, 16, 64)
|
|
if err != nil {
|
|
t.Fatal("Could not parse effective caps", err)
|
|
}
|
|
|
|
var netAdminMask uint64
|
|
var netAdminBit uint
|
|
netAdminBit = 12 // from capability.h
|
|
netAdminMask = 1 << netAdminBit
|
|
if effectiveCaps&netAdminMask != netAdminMask {
|
|
t.Fatal("CAP_NET_ADMIN is not set as expected")
|
|
}
|
|
}
|
|
|
|
func TestAdditionalGroups(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
|
|
container, err := newContainerWithName("test", config)
|
|
ok(t, err)
|
|
defer container.Destroy()
|
|
|
|
var stdout bytes.Buffer
|
|
pconfig := libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"sh", "-c", "id", "-Gn"},
|
|
Env: standardEnvironment,
|
|
Stdin: nil,
|
|
Stdout: &stdout,
|
|
AdditionalGroups: []string{"plugdev", "audio"},
|
|
Init: true,
|
|
}
|
|
err = container.Run(&pconfig)
|
|
ok(t, err)
|
|
|
|
// Wait for process
|
|
waitProcess(&pconfig, t)
|
|
|
|
outputGroups := string(stdout.Bytes())
|
|
|
|
// Check that the groups output has the groups that we specified
|
|
if !strings.Contains(outputGroups, "audio") {
|
|
t.Fatalf("Listed groups do not contain the audio group as expected: %v", outputGroups)
|
|
}
|
|
|
|
if !strings.Contains(outputGroups, "plugdev") {
|
|
t.Fatalf("Listed groups do not contain the plugdev group as expected: %v", outputGroups)
|
|
}
|
|
}
|
|
|
|
func TestFreeze(t *testing.T) {
|
|
testFreeze(t, false)
|
|
}
|
|
|
|
func TestSystemdFreeze(t *testing.T) {
|
|
if !systemd.IsRunningSystemd() {
|
|
t.Skip("Systemd is unsupported")
|
|
}
|
|
testFreeze(t, true)
|
|
}
|
|
|
|
func testFreeze(t *testing.T, systemd bool) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
if cgroups.IsCgroup2UnifiedMode() {
|
|
t.Skip("cgroup v2 is not supported")
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
container, err := newContainerWithName("test", config)
|
|
ok(t, err)
|
|
defer container.Destroy()
|
|
|
|
stdinR, stdinW, err := os.Pipe()
|
|
ok(t, err)
|
|
|
|
pconfig := &libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"cat"},
|
|
Env: standardEnvironment,
|
|
Stdin: stdinR,
|
|
Init: true,
|
|
}
|
|
err = container.Run(pconfig)
|
|
stdinR.Close()
|
|
defer stdinW.Close()
|
|
ok(t, err)
|
|
|
|
err = container.Pause()
|
|
ok(t, err)
|
|
state, err := container.Status()
|
|
ok(t, err)
|
|
err = container.Resume()
|
|
ok(t, err)
|
|
if state != libcontainer.Paused {
|
|
t.Fatal("Unexpected state: ", state)
|
|
}
|
|
|
|
stdinW.Close()
|
|
waitProcess(pconfig, t)
|
|
}
|
|
|
|
func TestCpuShares(t *testing.T) {
|
|
testCpuShares(t, false)
|
|
}
|
|
|
|
func TestCpuSharesSystemd(t *testing.T) {
|
|
if !systemd.IsRunningSystemd() {
|
|
t.Skip("Systemd is unsupported")
|
|
}
|
|
testCpuShares(t, true)
|
|
}
|
|
|
|
func testCpuShares(t *testing.T, systemd bool) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
if cgroups.IsCgroup2UnifiedMode() {
|
|
t.Skip("cgroup v2 is not supported")
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
if systemd {
|
|
config.Cgroups.Parent = "system.slice"
|
|
}
|
|
config.Cgroups.Resources.CpuShares = 1
|
|
|
|
_, _, err = runContainer(config, "", "ps")
|
|
if err == nil {
|
|
t.Fatalf("runContainer should failed with invalid CpuShares")
|
|
}
|
|
}
|
|
|
|
func TestPids(t *testing.T) {
|
|
testPids(t, false)
|
|
}
|
|
|
|
func TestPidsSystemd(t *testing.T) {
|
|
if !systemd.IsRunningSystemd() {
|
|
t.Skip("Systemd is unsupported")
|
|
}
|
|
testPids(t, true)
|
|
}
|
|
|
|
func testPids(t *testing.T, systemd bool) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
if cgroups.IsCgroup2UnifiedMode() {
|
|
t.Skip("cgroup v2 is not supported")
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
if systemd {
|
|
config.Cgroups.Parent = "system.slice"
|
|
}
|
|
config.Cgroups.Resources.PidsLimit = -1
|
|
|
|
// Running multiple processes.
|
|
_, ret, err := runContainer(config, "", "/bin/sh", "-c", "/bin/true | /bin/true | /bin/true | /bin/true")
|
|
if err != nil && strings.Contains(err.Error(), "no such directory for pids.max") {
|
|
t.Skip("PIDs cgroup is unsupported")
|
|
}
|
|
ok(t, err)
|
|
|
|
if ret != 0 {
|
|
t.Fatalf("expected fork() to succeed with no pids limit")
|
|
}
|
|
|
|
// Enforce a permissive limit. This needs to be fairly hand-wavey due to the
|
|
// issues with running Go binaries with pids restrictions (see below).
|
|
config.Cgroups.Resources.PidsLimit = 64
|
|
_, ret, err = runContainer(config, "", "/bin/sh", "-c", `
|
|
/bin/true | /bin/true | /bin/true | /bin/true | /bin/true | /bin/true | bin/true | /bin/true |
|
|
/bin/true | /bin/true | /bin/true | /bin/true | /bin/true | /bin/true | bin/true | /bin/true |
|
|
/bin/true | /bin/true | /bin/true | /bin/true | /bin/true | /bin/true | bin/true | /bin/true |
|
|
/bin/true | /bin/true | /bin/true | /bin/true | /bin/true | /bin/true | bin/true | /bin/true`)
|
|
if err != nil && strings.Contains(err.Error(), "no such directory for pids.max") {
|
|
t.Skip("PIDs cgroup is unsupported")
|
|
}
|
|
ok(t, err)
|
|
|
|
if ret != 0 {
|
|
t.Fatalf("expected fork() to succeed with permissive pids limit")
|
|
}
|
|
|
|
// Enforce a restrictive limit. 64 * /bin/true + 1 * shell should cause this
|
|
// to fail reliability.
|
|
config.Cgroups.Resources.PidsLimit = 64
|
|
out, _, err := runContainer(config, "", "/bin/sh", "-c", `
|
|
/bin/true | /bin/true | /bin/true | /bin/true | /bin/true | /bin/true | bin/true | /bin/true |
|
|
/bin/true | /bin/true | /bin/true | /bin/true | /bin/true | /bin/true | bin/true | /bin/true |
|
|
/bin/true | /bin/true | /bin/true | /bin/true | /bin/true | /bin/true | bin/true | /bin/true |
|
|
/bin/true | /bin/true | /bin/true | /bin/true | /bin/true | /bin/true | bin/true | /bin/true |
|
|
/bin/true | /bin/true | /bin/true | /bin/true | /bin/true | /bin/true | bin/true | /bin/true |
|
|
/bin/true | /bin/true | /bin/true | /bin/true | /bin/true | /bin/true | bin/true | /bin/true |
|
|
/bin/true | /bin/true | /bin/true | /bin/true | /bin/true | /bin/true | bin/true | /bin/true |
|
|
/bin/true | /bin/true | /bin/true | /bin/true | /bin/true | /bin/true | bin/true | /bin/true`)
|
|
if err != nil && strings.Contains(err.Error(), "no such directory for pids.max") {
|
|
t.Skip("PIDs cgroup is unsupported")
|
|
}
|
|
if err != nil && !strings.Contains(out.String(), "sh: can't fork") {
|
|
ok(t, err)
|
|
}
|
|
|
|
if err == nil {
|
|
t.Fatalf("expected fork() to fail with restrictive pids limit")
|
|
}
|
|
|
|
// Minimal restrictions are not really supported, due to quirks in using Go
|
|
// due to the fact that it spawns random processes. While we do our best with
|
|
// late setting cgroup values, it's just too unreliable with very small pids.max.
|
|
// As such, we don't test that case. YMMV.
|
|
}
|
|
|
|
func TestRunWithKernelMemory(t *testing.T) {
|
|
testRunWithKernelMemory(t, false)
|
|
}
|
|
|
|
func TestRunWithKernelMemorySystemd(t *testing.T) {
|
|
if !systemd.IsRunningSystemd() {
|
|
t.Skip("Systemd is unsupported")
|
|
}
|
|
testRunWithKernelMemory(t, true)
|
|
}
|
|
|
|
func testRunWithKernelMemory(t *testing.T, systemd bool) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
if cgroups.IsCgroup2UnifiedMode() {
|
|
t.Skip("cgroup v2 is not supported")
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
if systemd {
|
|
config.Cgroups.Parent = "system.slice"
|
|
}
|
|
config.Cgroups.Resources.KernelMemory = 52428800
|
|
|
|
_, _, err = runContainer(config, "", "ps")
|
|
if err != nil {
|
|
t.Fatalf("runContainer failed with kernel memory limit: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestContainerState(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer remove(rootfs)
|
|
|
|
l, err := os.Readlink("/proc/1/ns/ipc")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
config.Namespaces = configs.Namespaces([]configs.Namespace{
|
|
{Type: configs.NEWNS},
|
|
{Type: configs.NEWUTS},
|
|
// host for IPC
|
|
//{Type: configs.NEWIPC},
|
|
{Type: configs.NEWPID},
|
|
{Type: configs.NEWNET},
|
|
})
|
|
|
|
container, err := newContainerWithName("test", config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer container.Destroy()
|
|
|
|
stdinR, stdinW, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p := &libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"cat"},
|
|
Env: standardEnvironment,
|
|
Stdin: stdinR,
|
|
Init: true,
|
|
}
|
|
err = container.Run(p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stdinR.Close()
|
|
defer stdinW.Close()
|
|
|
|
st, err := container.State()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
l1, err := os.Readlink(st.NamespacePaths[configs.NEWIPC])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if l1 != l {
|
|
t.Fatal("Container using non-host ipc namespace")
|
|
}
|
|
stdinW.Close()
|
|
waitProcess(p, t)
|
|
}
|
|
|
|
func TestPassExtraFiles(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer remove(rootfs)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
|
|
container, err := newContainerWithName("test", config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer container.Destroy()
|
|
|
|
var stdout bytes.Buffer
|
|
pipeout1, pipein1, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pipeout2, pipein2, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
process := libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"sh", "-c", "cd /proc/$$/fd; echo -n *; echo -n 1 >3; echo -n 2 >4"},
|
|
Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
|
|
ExtraFiles: []*os.File{pipein1, pipein2},
|
|
Stdin: nil,
|
|
Stdout: &stdout,
|
|
Init: true,
|
|
}
|
|
err = container.Run(&process)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
waitProcess(&process, t)
|
|
|
|
out := string(stdout.Bytes())
|
|
// fd 5 is the directory handle for /proc/$$/fd
|
|
if out != "0 1 2 3 4 5" {
|
|
t.Fatalf("expected to have the file descriptors '0 1 2 3 4 5' passed to init, got '%s'", out)
|
|
}
|
|
var buf = []byte{0}
|
|
_, err = pipeout1.Read(buf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out1 := string(buf)
|
|
if out1 != "1" {
|
|
t.Fatalf("expected first pipe to receive '1', got '%s'", out1)
|
|
}
|
|
|
|
_, err = pipeout2.Read(buf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out2 := string(buf)
|
|
if out2 != "2" {
|
|
t.Fatalf("expected second pipe to receive '2', got '%s'", out2)
|
|
}
|
|
}
|
|
|
|
func TestMountCmds(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer remove(rootfs)
|
|
|
|
tmpDir, err := ioutil.TempDir("", "tmpdir")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
config.Mounts = append(config.Mounts, &configs.Mount{
|
|
Source: tmpDir,
|
|
Destination: "/tmp",
|
|
Device: "bind",
|
|
Flags: unix.MS_BIND | unix.MS_REC,
|
|
PremountCmds: []configs.Command{
|
|
{Path: "touch", Args: []string{filepath.Join(tmpDir, "hello")}},
|
|
{Path: "touch", Args: []string{filepath.Join(tmpDir, "world")}},
|
|
},
|
|
PostmountCmds: []configs.Command{
|
|
{Path: "cp", Args: []string{filepath.Join(rootfs, "tmp", "hello"), filepath.Join(rootfs, "tmp", "hello-backup")}},
|
|
{Path: "cp", Args: []string{filepath.Join(rootfs, "tmp", "world"), filepath.Join(rootfs, "tmp", "world-backup")}},
|
|
},
|
|
})
|
|
|
|
container, err := newContainerWithName("test", config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer container.Destroy()
|
|
|
|
pconfig := libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"sh", "-c", "env"},
|
|
Env: standardEnvironment,
|
|
Init: true,
|
|
}
|
|
err = container.Run(&pconfig)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Wait for process
|
|
waitProcess(&pconfig, t)
|
|
|
|
entries, err := ioutil.ReadDir(tmpDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
expected := []string{"hello", "hello-backup", "world", "world-backup"}
|
|
for i, e := range entries {
|
|
if e.Name() != expected[i] {
|
|
t.Errorf("Got(%s), expect %s", e.Name(), expected[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSysctl(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
config.Sysctl = map[string]string{
|
|
"kernel.shmmni": "8192",
|
|
}
|
|
|
|
container, err := newContainerWithName("test", config)
|
|
ok(t, err)
|
|
defer container.Destroy()
|
|
|
|
var stdout bytes.Buffer
|
|
pconfig := libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"sh", "-c", "cat /proc/sys/kernel/shmmni"},
|
|
Env: standardEnvironment,
|
|
Stdin: nil,
|
|
Stdout: &stdout,
|
|
Init: true,
|
|
}
|
|
err = container.Run(&pconfig)
|
|
ok(t, err)
|
|
|
|
// Wait for process
|
|
waitProcess(&pconfig, t)
|
|
|
|
shmmniOutput := strings.TrimSpace(string(stdout.Bytes()))
|
|
if shmmniOutput != "8192" {
|
|
t.Fatalf("kernel.shmmni property expected to be 8192, but is %s", shmmniOutput)
|
|
}
|
|
}
|
|
|
|
func TestMountCgroupRO(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
config := newTemplateConfig(rootfs)
|
|
|
|
config.Mounts = append(config.Mounts, &configs.Mount{
|
|
Destination: "/sys/fs/cgroup",
|
|
Device: "cgroup",
|
|
Flags: defaultMountFlags | unix.MS_RDONLY,
|
|
})
|
|
|
|
buffers, exitCode, err := runContainer(config, "", "mount")
|
|
if err != nil {
|
|
t.Fatalf("%s: %s", buffers, err)
|
|
}
|
|
if exitCode != 0 {
|
|
t.Fatalf("exit code not 0. code %d stderr %q", exitCode, buffers.Stderr)
|
|
}
|
|
mountInfo := buffers.Stdout.String()
|
|
lines := strings.Split(mountInfo, "\n")
|
|
for _, l := range lines {
|
|
if strings.HasPrefix(l, "tmpfs on /sys/fs/cgroup") {
|
|
if !strings.Contains(l, "ro") ||
|
|
!strings.Contains(l, "nosuid") ||
|
|
!strings.Contains(l, "nodev") ||
|
|
!strings.Contains(l, "noexec") {
|
|
t.Fatalf("Mode expected to contain 'ro,nosuid,nodev,noexec': %s", l)
|
|
}
|
|
if !strings.Contains(l, "mode=755") {
|
|
t.Fatalf("Mode expected to contain 'mode=755': %s", l)
|
|
}
|
|
continue
|
|
}
|
|
if !strings.HasPrefix(l, "cgroup") {
|
|
continue
|
|
}
|
|
if !strings.Contains(l, "ro") ||
|
|
!strings.Contains(l, "nosuid") ||
|
|
!strings.Contains(l, "nodev") ||
|
|
!strings.Contains(l, "noexec") {
|
|
t.Fatalf("Mode expected to contain 'ro,nosuid,nodev,noexec': %s", l)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMountCgroupRW(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
config := newTemplateConfig(rootfs)
|
|
|
|
config.Mounts = append(config.Mounts, &configs.Mount{
|
|
Destination: "/sys/fs/cgroup",
|
|
Device: "cgroup",
|
|
Flags: defaultMountFlags,
|
|
})
|
|
|
|
buffers, exitCode, err := runContainer(config, "", "mount")
|
|
if err != nil {
|
|
t.Fatalf("%s: %s", buffers, err)
|
|
}
|
|
if exitCode != 0 {
|
|
t.Fatalf("exit code not 0. code %d stderr %q", exitCode, buffers.Stderr)
|
|
}
|
|
mountInfo := buffers.Stdout.String()
|
|
lines := strings.Split(mountInfo, "\n")
|
|
for _, l := range lines {
|
|
if strings.HasPrefix(l, "tmpfs on /sys/fs/cgroup") {
|
|
if !strings.Contains(l, "rw") ||
|
|
!strings.Contains(l, "nosuid") ||
|
|
!strings.Contains(l, "nodev") ||
|
|
!strings.Contains(l, "noexec") {
|
|
t.Fatalf("Mode expected to contain 'rw,nosuid,nodev,noexec': %s", l)
|
|
}
|
|
if !strings.Contains(l, "mode=755") {
|
|
t.Fatalf("Mode expected to contain 'mode=755': %s", l)
|
|
}
|
|
continue
|
|
}
|
|
if !strings.HasPrefix(l, "cgroup") {
|
|
continue
|
|
}
|
|
if !strings.Contains(l, "rw") ||
|
|
!strings.Contains(l, "nosuid") ||
|
|
!strings.Contains(l, "nodev") ||
|
|
!strings.Contains(l, "noexec") {
|
|
t.Fatalf("Mode expected to contain 'rw,nosuid,nodev,noexec': %s", l)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOomScoreAdj(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
config.OomScoreAdj = ptrInt(200)
|
|
|
|
container, err := newContainerWithName("test", config)
|
|
ok(t, err)
|
|
defer container.Destroy()
|
|
|
|
var stdout bytes.Buffer
|
|
pconfig := libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"sh", "-c", "cat /proc/self/oom_score_adj"},
|
|
Env: standardEnvironment,
|
|
Stdin: nil,
|
|
Stdout: &stdout,
|
|
Init: true,
|
|
}
|
|
err = container.Run(&pconfig)
|
|
ok(t, err)
|
|
|
|
// Wait for process
|
|
waitProcess(&pconfig, t)
|
|
outputOomScoreAdj := strings.TrimSpace(string(stdout.Bytes()))
|
|
|
|
// Check that the oom_score_adj matches the value that was set as part of config.
|
|
if outputOomScoreAdj != strconv.Itoa(*config.OomScoreAdj) {
|
|
t.Fatalf("Expected oom_score_adj %d; got %q", *config.OomScoreAdj, outputOomScoreAdj)
|
|
}
|
|
}
|
|
|
|
func TestHook(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
|
|
bundle, err := newTestBundle()
|
|
ok(t, err)
|
|
defer remove(bundle)
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
expectedBundle := bundle
|
|
config.Labels = append(config.Labels, fmt.Sprintf("bundle=%s", expectedBundle))
|
|
|
|
getRootfsFromBundle := func(bundle string) (string, error) {
|
|
f, err := os.Open(filepath.Join(bundle, "config.json"))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var config configs.Config
|
|
if err = json.NewDecoder(f).Decode(&config); err != nil {
|
|
return "", err
|
|
}
|
|
return config.Rootfs, nil
|
|
}
|
|
|
|
config.Hooks = &configs.Hooks{
|
|
Prestart: []configs.Hook{
|
|
configs.NewFunctionHook(func(s *specs.State) error {
|
|
if s.Bundle != expectedBundle {
|
|
t.Fatalf("Expected prestart hook bundlePath '%s'; got '%s'", expectedBundle, s.Bundle)
|
|
}
|
|
|
|
root, err := getRootfsFromBundle(s.Bundle)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f, err := os.Create(filepath.Join(root, "test"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return f.Close()
|
|
}),
|
|
},
|
|
Poststart: []configs.Hook{
|
|
configs.NewFunctionHook(func(s *specs.State) error {
|
|
if s.Bundle != expectedBundle {
|
|
t.Fatalf("Expected poststart hook bundlePath '%s'; got '%s'", expectedBundle, s.Bundle)
|
|
}
|
|
|
|
root, err := getRootfsFromBundle(s.Bundle)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ioutil.WriteFile(filepath.Join(root, "test"), []byte("hello world"), 0755)
|
|
}),
|
|
},
|
|
Poststop: []configs.Hook{
|
|
configs.NewFunctionHook(func(s *specs.State) error {
|
|
if s.Bundle != expectedBundle {
|
|
t.Fatalf("Expected poststop hook bundlePath '%s'; got '%s'", expectedBundle, s.Bundle)
|
|
}
|
|
|
|
root, err := getRootfsFromBundle(s.Bundle)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.RemoveAll(filepath.Join(root, "test"))
|
|
}),
|
|
},
|
|
}
|
|
|
|
// write config of json format into config.json under bundle
|
|
f, err := os.OpenFile(filepath.Join(bundle, "config.json"), os.O_CREATE|os.O_RDWR, 0644)
|
|
ok(t, err)
|
|
ok(t, json.NewEncoder(f).Encode(config))
|
|
|
|
container, err := newContainerWithName("test", config)
|
|
ok(t, err)
|
|
|
|
var stdout bytes.Buffer
|
|
pconfig := libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"sh", "-c", "ls /test"},
|
|
Env: standardEnvironment,
|
|
Stdin: nil,
|
|
Stdout: &stdout,
|
|
Init: true,
|
|
}
|
|
err = container.Run(&pconfig)
|
|
ok(t, err)
|
|
|
|
// Wait for process
|
|
waitProcess(&pconfig, t)
|
|
|
|
outputLs := string(stdout.Bytes())
|
|
|
|
// Check that the ls output has the expected file touched by the prestart hook
|
|
if !strings.Contains(outputLs, "/test") {
|
|
container.Destroy()
|
|
t.Fatalf("ls output doesn't have the expected file: %s", outputLs)
|
|
}
|
|
|
|
// Check that the file is written by the poststart hook
|
|
testFilePath := filepath.Join(rootfs, "test")
|
|
contents, err := ioutil.ReadFile(testFilePath)
|
|
if err != nil {
|
|
t.Fatalf("cannot read file '%s': %s", testFilePath, err)
|
|
}
|
|
if string(contents) != "hello world" {
|
|
t.Fatalf("Expected test file to contain 'hello world'; got '%s'", string(contents))
|
|
}
|
|
|
|
if err := container.Destroy(); err != nil {
|
|
t.Fatalf("container destroy %s", err)
|
|
}
|
|
fi, err := os.Stat(filepath.Join(rootfs, "test"))
|
|
if err == nil || !os.IsNotExist(err) {
|
|
t.Fatalf("expected file to not exist, got %s", fi.Name())
|
|
}
|
|
}
|
|
|
|
func TestSTDIOPermissions(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
config := newTemplateConfig(rootfs)
|
|
buffers, exitCode, err := runContainer(config, "", "sh", "-c", "echo hi > /dev/stderr")
|
|
ok(t, err)
|
|
if exitCode != 0 {
|
|
t.Fatalf("exit code not 0. code %d stderr %q", exitCode, buffers.Stderr)
|
|
}
|
|
|
|
if actual := strings.Trim(buffers.Stderr.String(), "\n"); actual != "hi" {
|
|
t.Fatalf("stderr should equal be equal %q %q", actual, "hi")
|
|
}
|
|
}
|
|
|
|
func unmountOp(path string) error {
|
|
return unix.Unmount(path, unix.MNT_DETACH)
|
|
}
|
|
|
|
// Launch container with rootfsPropagation in rslave mode. Also
|
|
// bind mount a volume /mnt1host at /mnt1cont at the time of launch. Now do
|
|
// another mount on host (/mnt1host/mnt2host) and this new mount should
|
|
// propagate to container (/mnt1cont/mnt2host)
|
|
func TestRootfsPropagationSlaveMount(t *testing.T) {
|
|
var mountPropagated bool
|
|
var dir1cont string
|
|
var dir2cont string
|
|
|
|
dir1cont = "/root/mnt1cont"
|
|
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
config := newTemplateConfig(rootfs)
|
|
|
|
config.RootPropagation = unix.MS_SLAVE | unix.MS_REC
|
|
|
|
// Bind mount a volume
|
|
dir1host, err := ioutil.TempDir("", "mnt1host")
|
|
ok(t, err)
|
|
defer os.RemoveAll(dir1host)
|
|
|
|
// Make this dir a "shared" mount point. This will make sure a
|
|
// slave relationship can be established in container.
|
|
err = unix.Mount(dir1host, dir1host, "bind", unix.MS_BIND|unix.MS_REC, "")
|
|
ok(t, err)
|
|
err = unix.Mount("", dir1host, "", unix.MS_SHARED|unix.MS_REC, "")
|
|
ok(t, err)
|
|
defer unmountOp(dir1host)
|
|
|
|
config.Mounts = append(config.Mounts, &configs.Mount{
|
|
Source: dir1host,
|
|
Destination: dir1cont,
|
|
Device: "bind",
|
|
Flags: unix.MS_BIND | unix.MS_REC})
|
|
|
|
container, err := newContainerWithName("testSlaveMount", config)
|
|
ok(t, err)
|
|
defer container.Destroy()
|
|
|
|
stdinR, stdinW, err := os.Pipe()
|
|
ok(t, err)
|
|
|
|
pconfig := &libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"cat"},
|
|
Env: standardEnvironment,
|
|
Stdin: stdinR,
|
|
Init: true,
|
|
}
|
|
|
|
err = container.Run(pconfig)
|
|
stdinR.Close()
|
|
defer stdinW.Close()
|
|
ok(t, err)
|
|
|
|
// Create mnt1host/mnt2host and bind mount itself on top of it. This
|
|
// should be visible in container.
|
|
dir2host, err := ioutil.TempDir(dir1host, "mnt2host")
|
|
ok(t, err)
|
|
defer os.RemoveAll(dir2host)
|
|
|
|
err = unix.Mount(dir2host, dir2host, "bind", unix.MS_BIND, "")
|
|
defer unmountOp(dir2host)
|
|
ok(t, err)
|
|
|
|
// Run "cat /proc/self/mountinfo" in container and look at mount points.
|
|
var stdout2 bytes.Buffer
|
|
|
|
stdinR2, stdinW2, err := os.Pipe()
|
|
ok(t, err)
|
|
|
|
pconfig2 := &libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"cat", "/proc/self/mountinfo"},
|
|
Env: standardEnvironment,
|
|
Stdin: stdinR2,
|
|
Stdout: &stdout2,
|
|
}
|
|
|
|
err = container.Run(pconfig2)
|
|
stdinR2.Close()
|
|
defer stdinW2.Close()
|
|
ok(t, err)
|
|
|
|
stdinW2.Close()
|
|
waitProcess(pconfig2, t)
|
|
stdinW.Close()
|
|
waitProcess(pconfig, t)
|
|
|
|
mountPropagated = false
|
|
dir2cont = filepath.Join(dir1cont, filepath.Base(dir2host))
|
|
|
|
propagationInfo := string(stdout2.Bytes())
|
|
lines := strings.Split(propagationInfo, "\n")
|
|
for _, l := range lines {
|
|
linefields := strings.Split(l, " ")
|
|
if len(linefields) < 5 {
|
|
continue
|
|
}
|
|
|
|
if linefields[4] == dir2cont {
|
|
mountPropagated = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if mountPropagated != true {
|
|
t.Fatalf("Mount on host %s did not propagate in container at %s\n", dir2host, dir2cont)
|
|
}
|
|
}
|
|
|
|
// Launch container with rootfsPropagation 0 so no propagation flags are
|
|
// applied. Also bind mount a volume /mnt1host at /mnt1cont at the time of
|
|
// launch. Now do a mount in container (/mnt1cont/mnt2cont) and this new
|
|
// mount should propagate to host (/mnt1host/mnt2cont)
|
|
|
|
func TestRootfsPropagationSharedMount(t *testing.T) {
|
|
var dir1cont string
|
|
var dir2cont string
|
|
|
|
dir1cont = "/root/mnt1cont"
|
|
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
config := newTemplateConfig(rootfs)
|
|
config.RootPropagation = unix.MS_PRIVATE
|
|
|
|
// Bind mount a volume
|
|
dir1host, err := ioutil.TempDir("", "mnt1host")
|
|
ok(t, err)
|
|
defer os.RemoveAll(dir1host)
|
|
|
|
// Make this dir a "shared" mount point. This will make sure a
|
|
// shared relationship can be established in container.
|
|
err = unix.Mount(dir1host, dir1host, "bind", unix.MS_BIND|unix.MS_REC, "")
|
|
ok(t, err)
|
|
err = unix.Mount("", dir1host, "", unix.MS_SHARED|unix.MS_REC, "")
|
|
ok(t, err)
|
|
defer unmountOp(dir1host)
|
|
|
|
config.Mounts = append(config.Mounts, &configs.Mount{
|
|
Source: dir1host,
|
|
Destination: dir1cont,
|
|
Device: "bind",
|
|
Flags: unix.MS_BIND | unix.MS_REC})
|
|
|
|
container, err := newContainerWithName("testSharedMount", config)
|
|
ok(t, err)
|
|
defer container.Destroy()
|
|
|
|
stdinR, stdinW, err := os.Pipe()
|
|
ok(t, err)
|
|
|
|
pconfig := &libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"cat"},
|
|
Env: standardEnvironment,
|
|
Stdin: stdinR,
|
|
Init: true,
|
|
}
|
|
|
|
err = container.Run(pconfig)
|
|
stdinR.Close()
|
|
defer stdinW.Close()
|
|
ok(t, err)
|
|
|
|
// Create mnt1host/mnt2cont. This will become visible inside container
|
|
// at mnt1cont/mnt2cont. Bind mount itself on top of it. This
|
|
// should be visible on host now.
|
|
dir2host, err := ioutil.TempDir(dir1host, "mnt2cont")
|
|
ok(t, err)
|
|
defer os.RemoveAll(dir2host)
|
|
|
|
dir2cont = filepath.Join(dir1cont, filepath.Base(dir2host))
|
|
|
|
// Mount something in container and see if it is visible on host.
|
|
var stdout2 bytes.Buffer
|
|
|
|
stdinR2, stdinW2, err := os.Pipe()
|
|
ok(t, err)
|
|
|
|
pconfig2 := &libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"mount", "--bind", dir2cont, dir2cont},
|
|
Env: standardEnvironment,
|
|
Stdin: stdinR2,
|
|
Stdout: &stdout2,
|
|
Capabilities: &configs.Capabilities{},
|
|
}
|
|
|
|
// Provide CAP_SYS_ADMIN
|
|
pconfig2.Capabilities.Bounding = append(config.Capabilities.Bounding, "CAP_SYS_ADMIN")
|
|
pconfig2.Capabilities.Permitted = append(config.Capabilities.Permitted, "CAP_SYS_ADMIN")
|
|
pconfig2.Capabilities.Effective = append(config.Capabilities.Effective, "CAP_SYS_ADMIN")
|
|
pconfig2.Capabilities.Inheritable = append(config.Capabilities.Inheritable, "CAP_SYS_ADMIN")
|
|
|
|
err = container.Run(pconfig2)
|
|
stdinR2.Close()
|
|
defer stdinW2.Close()
|
|
ok(t, err)
|
|
|
|
// Wait for process
|
|
stdinW2.Close()
|
|
waitProcess(pconfig2, t)
|
|
stdinW.Close()
|
|
waitProcess(pconfig, t)
|
|
|
|
defer unmountOp(dir2host)
|
|
|
|
// Check if mount is visible on host or not.
|
|
out, err := exec.Command("findmnt", "-n", "-f", "-oTARGET", dir2host).CombinedOutput()
|
|
outtrim := strings.TrimSpace(string(out))
|
|
if err != nil {
|
|
t.Logf("findmnt error %q: %q", err, outtrim)
|
|
}
|
|
|
|
if string(outtrim) != dir2host {
|
|
t.Fatalf("Mount in container on %s did not propagate to host on %s. finmnt output=%s", dir2cont, dir2host, outtrim)
|
|
}
|
|
}
|
|
|
|
func TestPIDHost(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
l, err := os.Readlink("/proc/1/ns/pid")
|
|
ok(t, err)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
config.Namespaces.Remove(configs.NEWPID)
|
|
buffers, exitCode, err := runContainer(config, "", "readlink", "/proc/self/ns/pid")
|
|
ok(t, err)
|
|
|
|
if exitCode != 0 {
|
|
t.Fatalf("exit code not 0. code %d stderr %q", exitCode, buffers.Stderr)
|
|
}
|
|
|
|
if actual := strings.Trim(buffers.Stdout.String(), "\n"); actual != l {
|
|
t.Fatalf("ipc link not equal to host link %q %q", actual, l)
|
|
}
|
|
}
|
|
|
|
func TestInitJoinPID(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
// Execute a long-running container
|
|
container1, err := newContainer(newTemplateConfig(rootfs))
|
|
ok(t, err)
|
|
defer container1.Destroy()
|
|
|
|
stdinR1, stdinW1, err := os.Pipe()
|
|
ok(t, err)
|
|
init1 := &libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"cat"},
|
|
Env: standardEnvironment,
|
|
Stdin: stdinR1,
|
|
Init: true,
|
|
}
|
|
err = container1.Run(init1)
|
|
stdinR1.Close()
|
|
defer stdinW1.Close()
|
|
ok(t, err)
|
|
|
|
// get the state of the first container
|
|
state1, err := container1.State()
|
|
ok(t, err)
|
|
pidns1 := state1.NamespacePaths[configs.NEWPID]
|
|
|
|
// Run a container inside the existing pidns but with different cgroups
|
|
config2 := newTemplateConfig(rootfs)
|
|
config2.Namespaces.Add(configs.NEWPID, pidns1)
|
|
config2.Cgroups.Path = "integration/test2"
|
|
container2, err := newContainerWithName("testCT2", config2)
|
|
ok(t, err)
|
|
defer container2.Destroy()
|
|
|
|
stdinR2, stdinW2, err := os.Pipe()
|
|
ok(t, err)
|
|
init2 := &libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"cat"},
|
|
Env: standardEnvironment,
|
|
Stdin: stdinR2,
|
|
Init: true,
|
|
}
|
|
err = container2.Run(init2)
|
|
stdinR2.Close()
|
|
defer stdinW2.Close()
|
|
ok(t, err)
|
|
// get the state of the second container
|
|
state2, err := container2.State()
|
|
ok(t, err)
|
|
|
|
ns1, err := os.Readlink(fmt.Sprintf("/proc/%d/ns/pid", state1.InitProcessPid))
|
|
ok(t, err)
|
|
ns2, err := os.Readlink(fmt.Sprintf("/proc/%d/ns/pid", state2.InitProcessPid))
|
|
ok(t, err)
|
|
if ns1 != ns2 {
|
|
t.Errorf("pidns(%s), wanted %s", ns2, ns1)
|
|
}
|
|
|
|
// check that namespaces are not the same
|
|
if reflect.DeepEqual(state2.NamespacePaths, state1.NamespacePaths) {
|
|
t.Errorf("Namespaces(%v), original %v", state2.NamespacePaths,
|
|
state1.NamespacePaths)
|
|
}
|
|
// check that pidns is joined correctly. The initial container process list
|
|
// should contain the second container's init process
|
|
buffers := newStdBuffers()
|
|
ps := &libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"ps"},
|
|
Env: standardEnvironment,
|
|
Stdout: buffers.Stdout,
|
|
}
|
|
err = container1.Run(ps)
|
|
ok(t, err)
|
|
waitProcess(ps, t)
|
|
|
|
// Stop init processes one by one. Stop the second container should
|
|
// not stop the first.
|
|
stdinW2.Close()
|
|
waitProcess(init2, t)
|
|
stdinW1.Close()
|
|
waitProcess(init1, t)
|
|
|
|
out := strings.TrimSpace(buffers.Stdout.String())
|
|
// output of ps inside the initial PID namespace should have
|
|
// 1 line of header,
|
|
// 2 lines of init processes,
|
|
// 1 line of ps process
|
|
if len(strings.Split(out, "\n")) != 4 {
|
|
t.Errorf("unexpected running process, output %q", out)
|
|
}
|
|
}
|
|
|
|
func TestInitJoinNetworkAndUser(t *testing.T) {
|
|
if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
|
|
t.Skip("userns is unsupported")
|
|
}
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
// Execute a long-running container
|
|
config1 := newTemplateConfig(rootfs)
|
|
config1.UidMappings = []configs.IDMap{{HostID: 0, ContainerID: 0, Size: 1000}}
|
|
config1.GidMappings = []configs.IDMap{{HostID: 0, ContainerID: 0, Size: 1000}}
|
|
config1.Namespaces = append(config1.Namespaces, configs.Namespace{Type: configs.NEWUSER})
|
|
container1, err := newContainer(config1)
|
|
ok(t, err)
|
|
defer container1.Destroy()
|
|
|
|
stdinR1, stdinW1, err := os.Pipe()
|
|
ok(t, err)
|
|
init1 := &libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"cat"},
|
|
Env: standardEnvironment,
|
|
Stdin: stdinR1,
|
|
Init: true,
|
|
}
|
|
err = container1.Run(init1)
|
|
stdinR1.Close()
|
|
defer stdinW1.Close()
|
|
ok(t, err)
|
|
|
|
// get the state of the first container
|
|
state1, err := container1.State()
|
|
ok(t, err)
|
|
netns1 := state1.NamespacePaths[configs.NEWNET]
|
|
userns1 := state1.NamespacePaths[configs.NEWUSER]
|
|
|
|
// Run a container inside the existing pidns but with different cgroups
|
|
rootfs2, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs2)
|
|
|
|
config2 := newTemplateConfig(rootfs2)
|
|
config2.UidMappings = []configs.IDMap{{HostID: 0, ContainerID: 0, Size: 1000}}
|
|
config2.GidMappings = []configs.IDMap{{HostID: 0, ContainerID: 0, Size: 1000}}
|
|
config2.Namespaces.Add(configs.NEWNET, netns1)
|
|
config2.Namespaces.Add(configs.NEWUSER, userns1)
|
|
config2.Cgroups.Path = "integration/test2"
|
|
container2, err := newContainerWithName("testCT2", config2)
|
|
ok(t, err)
|
|
defer container2.Destroy()
|
|
|
|
stdinR2, stdinW2, err := os.Pipe()
|
|
ok(t, err)
|
|
init2 := &libcontainer.Process{
|
|
Cwd: "/",
|
|
Args: []string{"cat"},
|
|
Env: standardEnvironment,
|
|
Stdin: stdinR2,
|
|
Init: true,
|
|
}
|
|
err = container2.Run(init2)
|
|
stdinR2.Close()
|
|
defer stdinW2.Close()
|
|
ok(t, err)
|
|
|
|
// get the state of the second container
|
|
state2, err := container2.State()
|
|
ok(t, err)
|
|
|
|
for _, ns := range []string{"net", "user"} {
|
|
ns1, err := os.Readlink(fmt.Sprintf("/proc/%d/ns/%s", state1.InitProcessPid, ns))
|
|
ok(t, err)
|
|
ns2, err := os.Readlink(fmt.Sprintf("/proc/%d/ns/%s", state2.InitProcessPid, ns))
|
|
ok(t, err)
|
|
if ns1 != ns2 {
|
|
t.Errorf("%s(%s), wanted %s", ns, ns2, ns1)
|
|
}
|
|
}
|
|
|
|
// check that namespaces are not the same
|
|
if reflect.DeepEqual(state2.NamespacePaths, state1.NamespacePaths) {
|
|
t.Errorf("Namespaces(%v), original %v", state2.NamespacePaths,
|
|
state1.NamespacePaths)
|
|
}
|
|
// Stop init processes one by one. Stop the second container should
|
|
// not stop the first.
|
|
stdinW2.Close()
|
|
waitProcess(init2, t)
|
|
stdinW1.Close()
|
|
waitProcess(init1, t)
|
|
}
|
|
|
|
func TestTmpfsCopyUp(t *testing.T) {
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
|
|
config.Mounts = append(config.Mounts, &configs.Mount{
|
|
Source: "tmpfs",
|
|
Destination: "/etc",
|
|
Device: "tmpfs",
|
|
Extensions: configs.EXT_COPYUP,
|
|
})
|
|
|
|
container, err := newContainerWithName("test", config)
|
|
ok(t, err)
|
|
defer container.Destroy()
|
|
|
|
var stdout bytes.Buffer
|
|
pconfig := libcontainer.Process{
|
|
Args: []string{"ls", "/etc/passwd"},
|
|
Env: standardEnvironment,
|
|
Stdin: nil,
|
|
Stdout: &stdout,
|
|
Init: true,
|
|
}
|
|
err = container.Run(&pconfig)
|
|
ok(t, err)
|
|
|
|
// Wait for process
|
|
waitProcess(&pconfig, t)
|
|
|
|
outputLs := string(stdout.Bytes())
|
|
|
|
// Check that the ls output has /etc/passwd
|
|
if !strings.Contains(outputLs, "/etc/passwd") {
|
|
t.Fatalf("/etc/passwd not copied up as expected: %v", outputLs)
|
|
}
|
|
}
|
|
|
|
func TestCGROUPPrivate(t *testing.T) {
|
|
if _, err := os.Stat("/proc/self/ns/cgroup"); os.IsNotExist(err) {
|
|
t.Skip("cgroupns is unsupported")
|
|
}
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
l, err := os.Readlink("/proc/1/ns/cgroup")
|
|
ok(t, err)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
config.Namespaces.Add(configs.NEWCGROUP, "")
|
|
buffers, exitCode, err := runContainer(config, "", "readlink", "/proc/self/ns/cgroup")
|
|
ok(t, err)
|
|
|
|
if exitCode != 0 {
|
|
t.Fatalf("exit code not 0. code %d stderr %q", exitCode, buffers.Stderr)
|
|
}
|
|
|
|
if actual := strings.Trim(buffers.Stdout.String(), "\n"); actual == l {
|
|
t.Fatalf("cgroup link should be private to the container but equals host %q %q", actual, l)
|
|
}
|
|
}
|
|
|
|
func TestCGROUPHost(t *testing.T) {
|
|
if _, err := os.Stat("/proc/self/ns/cgroup"); os.IsNotExist(err) {
|
|
t.Skip("cgroupns is unsupported")
|
|
}
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
|
|
rootfs, err := newRootfs()
|
|
ok(t, err)
|
|
defer remove(rootfs)
|
|
|
|
l, err := os.Readlink("/proc/1/ns/cgroup")
|
|
ok(t, err)
|
|
|
|
config := newTemplateConfig(rootfs)
|
|
buffers, exitCode, err := runContainer(config, "", "readlink", "/proc/self/ns/cgroup")
|
|
ok(t, err)
|
|
|
|
if exitCode != 0 {
|
|
t.Fatalf("exit code not 0. code %d stderr %q", exitCode, buffers.Stderr)
|
|
}
|
|
|
|
if actual := strings.Trim(buffers.Stdout.String(), "\n"); actual != l {
|
|
t.Fatalf("cgroup link not equal to host link %q %q", actual, l)
|
|
}
|
|
}
|