runc/libcontainer/integration/exec_test.go

1776 lines
43 KiB
Go
Raw Normal View History

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/systemd"
"github.com/opencontainers/runc/libcontainer/configs"
libcontainer: Set 'status' in hook stdin Finish off the work started in a344b2d6 (sync up `HookState` with OCI spec `State`, 2016-12-19, #1201). And drop HookState, since there's no need for a local alias for specs.State. Also set c.initProcess in newInitProcess to support OCIState calls from within initProcess.start(). I think the cyclic references between linuxContainer and initProcess are unfortunate, but didn't want to address that here. I've also left the timing of the Prestart hooks alone, although the spec calls for them to happen before start (not as part of creation) [1,2]. Once the timing gets fixed we can drop the initProcessStartTime hacks which initProcess.start currently needs. I'm not sure why we trigger the prestart hooks in response to both procReady and procHooks. But we've had two prestart rounds in initProcess.start since 2f276498 (Move pre-start hooks after container mounts, 2016-02-17, #568). I've left that alone too. I really think we should have len() guards to avoid computing the state when .Hooks is non-nil but the particular phase we're looking at is empty. Aleksa, however, is adamantly against them [3] citing a risk of sloppy copy/pastes causing the hook slice being len-guarded to diverge from the hook slice being iterated over within the guard. I think that ort of thing is very lo-risk, because: * We shouldn't be copy/pasting this, right? DRY for the win :). * There's only ever a few lines between the guard and the guarded loop. That makes broken copy/pastes easy to catch in review. * We should have test coverage for these. Guarding with the wrong slice is certainly not the only thing you can break with a sloppy copy/paste. But I'm not a maintainer ;). [1]: https://github.com/opencontainers/runtime-spec/blob/v1.0.0/config.md#prestart [2]: https://github.com/opencontainers/runc/issues/1710 [3]: https://github.com/opencontainers/runc/pull/1741#discussion_r233331570 Signed-off-by: W. Trevor King <wking@tremily.us>
2018-02-26 06:47:41 +08:00
"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 {
*: fix go-vet failures Previously, we would get failures with go-vet with test files. % go vet ./... libcontainer/integration/exec_test.go:42: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:43: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:184: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:185: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:1568: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:1569: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:1600: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:1601: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/execin_test.go:92: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/execin_test.go:93: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/execin_test.go:506: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/execin_test.go:507: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields Signed-off-by: Aleksa Sarai <asarai@suse.de>
2016-12-24 06:55:21 +08:00
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 {
*: fix go-vet failures Previously, we would get failures with go-vet with test files. % go vet ./... libcontainer/integration/exec_test.go:42: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:43: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:184: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:185: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:1568: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:1569: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:1600: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:1601: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/execin_test.go:92: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/execin_test.go:93: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/execin_test.go:506: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/execin_test.go:507: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields Signed-off-by: Aleksa Sarai <asarai@suse.de>
2016-12-24 06:55:21 +08:00
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
}
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.UseSystemd() {
t.Skip("Systemd is unsupported")
}
testFreeze(t, true)
}
func testFreeze(t *testing.T, systemd bool) {
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()
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.UseSystemd() {
t.Skip("Systemd is unsupported")
}
testCpuShares(t, true)
}
func testCpuShares(t *testing.T, systemd bool) {
if testing.Short() {
return
}
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.UseSystemd() {
t.Skip("Systemd is unsupported")
}
testPids(t, true)
}
func testPids(t *testing.T, systemd bool) {
if testing.Short() {
return
}
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.UseSystemd() {
t.Skip("Systemd is unsupported")
}
testRunWithKernelMemory(t, true)
}
func testRunWithKernelMemory(t *testing.T, systemd bool) {
if testing.Short() {
return
}
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{
libcontainer: Set 'status' in hook stdin Finish off the work started in a344b2d6 (sync up `HookState` with OCI spec `State`, 2016-12-19, #1201). And drop HookState, since there's no need for a local alias for specs.State. Also set c.initProcess in newInitProcess to support OCIState calls from within initProcess.start(). I think the cyclic references between linuxContainer and initProcess are unfortunate, but didn't want to address that here. I've also left the timing of the Prestart hooks alone, although the spec calls for them to happen before start (not as part of creation) [1,2]. Once the timing gets fixed we can drop the initProcessStartTime hacks which initProcess.start currently needs. I'm not sure why we trigger the prestart hooks in response to both procReady and procHooks. But we've had two prestart rounds in initProcess.start since 2f276498 (Move pre-start hooks after container mounts, 2016-02-17, #568). I've left that alone too. I really think we should have len() guards to avoid computing the state when .Hooks is non-nil but the particular phase we're looking at is empty. Aleksa, however, is adamantly against them [3] citing a risk of sloppy copy/pastes causing the hook slice being len-guarded to diverge from the hook slice being iterated over within the guard. I think that ort of thing is very lo-risk, because: * We shouldn't be copy/pasting this, right? DRY for the win :). * There's only ever a few lines between the guard and the guarded loop. That makes broken copy/pastes easy to catch in review. * We should have test coverage for these. Guarding with the wrong slice is certainly not the only thing you can break with a sloppy copy/paste. But I'm not a maintainer ;). [1]: https://github.com/opencontainers/runtime-spec/blob/v1.0.0/config.md#prestart [2]: https://github.com/opencontainers/runc/issues/1710 [3]: https://github.com/opencontainers/runc/pull/1741#discussion_r233331570 Signed-off-by: W. Trevor King <wking@tremily.us>
2018-02-26 06:47:41 +08:00
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{
libcontainer: Set 'status' in hook stdin Finish off the work started in a344b2d6 (sync up `HookState` with OCI spec `State`, 2016-12-19, #1201). And drop HookState, since there's no need for a local alias for specs.State. Also set c.initProcess in newInitProcess to support OCIState calls from within initProcess.start(). I think the cyclic references between linuxContainer and initProcess are unfortunate, but didn't want to address that here. I've also left the timing of the Prestart hooks alone, although the spec calls for them to happen before start (not as part of creation) [1,2]. Once the timing gets fixed we can drop the initProcessStartTime hacks which initProcess.start currently needs. I'm not sure why we trigger the prestart hooks in response to both procReady and procHooks. But we've had two prestart rounds in initProcess.start since 2f276498 (Move pre-start hooks after container mounts, 2016-02-17, #568). I've left that alone too. I really think we should have len() guards to avoid computing the state when .Hooks is non-nil but the particular phase we're looking at is empty. Aleksa, however, is adamantly against them [3] citing a risk of sloppy copy/pastes causing the hook slice being len-guarded to diverge from the hook slice being iterated over within the guard. I think that ort of thing is very lo-risk, because: * We shouldn't be copy/pasting this, right? DRY for the win :). * There's only ever a few lines between the guard and the guarded loop. That makes broken copy/pastes easy to catch in review. * We should have test coverage for these. Guarding with the wrong slice is certainly not the only thing you can break with a sloppy copy/paste. But I'm not a maintainer ;). [1]: https://github.com/opencontainers/runtime-spec/blob/v1.0.0/config.md#prestart [2]: https://github.com/opencontainers/runc/issues/1710 [3]: https://github.com/opencontainers/runc/pull/1741#discussion_r233331570 Signed-off-by: W. Trevor King <wking@tremily.us>
2018-02-26 06:47:41 +08:00
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{
libcontainer: Set 'status' in hook stdin Finish off the work started in a344b2d6 (sync up `HookState` with OCI spec `State`, 2016-12-19, #1201). And drop HookState, since there's no need for a local alias for specs.State. Also set c.initProcess in newInitProcess to support OCIState calls from within initProcess.start(). I think the cyclic references between linuxContainer and initProcess are unfortunate, but didn't want to address that here. I've also left the timing of the Prestart hooks alone, although the spec calls for them to happen before start (not as part of creation) [1,2]. Once the timing gets fixed we can drop the initProcessStartTime hacks which initProcess.start currently needs. I'm not sure why we trigger the prestart hooks in response to both procReady and procHooks. But we've had two prestart rounds in initProcess.start since 2f276498 (Move pre-start hooks after container mounts, 2016-02-17, #568). I've left that alone too. I really think we should have len() guards to avoid computing the state when .Hooks is non-nil but the particular phase we're looking at is empty. Aleksa, however, is adamantly against them [3] citing a risk of sloppy copy/pastes causing the hook slice being len-guarded to diverge from the hook slice being iterated over within the guard. I think that ort of thing is very lo-risk, because: * We shouldn't be copy/pasting this, right? DRY for the win :). * There's only ever a few lines between the guard and the guarded loop. That makes broken copy/pastes easy to catch in review. * We should have test coverage for these. Guarding with the wrong slice is certainly not the only thing you can break with a sloppy copy/paste. But I'm not a maintainer ;). [1]: https://github.com/opencontainers/runtime-spec/blob/v1.0.0/config.md#prestart [2]: https://github.com/opencontainers/runc/issues/1710 [3]: https://github.com/opencontainers/runc/pull/1741#discussion_r233331570 Signed-off-by: W. Trevor King <wking@tremily.us>
2018-02-26 06:47:41 +08:00
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)
*: fix go-vet failures Previously, we would get failures with go-vet with test files. % go vet ./... libcontainer/integration/exec_test.go:42: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:43: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:184: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:185: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:1568: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:1569: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:1600: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:1601: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/execin_test.go:92: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/execin_test.go:93: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/execin_test.go:506: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/execin_test.go:507: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields Signed-off-by: Aleksa Sarai <asarai@suse.de>
2016-12-24 06:55:21 +08:00
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)
*: fix go-vet failures Previously, we would get failures with go-vet with test files. % go vet ./... libcontainer/integration/exec_test.go:42: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:43: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:184: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:185: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:1568: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:1569: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:1600: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/exec_test.go:1601: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/execin_test.go:92: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/execin_test.go:93: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/execin_test.go:506: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields libcontainer/integration/execin_test.go:507: github.com/opencontainers/runc/libcontainer/configs.IDMap composite literal uses unkeyed fields Signed-off-by: Aleksa Sarai <asarai@suse.de>
2016-12-24 06:55:21 +08:00
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)
}
}