Merge pull request #67 from Random-Liu/add-sandbox-dev-shm

Add sandbox dev shm
This commit is contained in:
Lantao Liu 2017-06-11 02:58:24 -07:00 committed by GitHub
commit fa28558824
7 changed files with 265 additions and 24 deletions

View File

@ -21,9 +21,9 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"golang.org/x/net/context"
"github.com/tonistiigi/fifo" "github.com/tonistiigi/fifo"
"golang.org/x/net/context"
"golang.org/x/sys/unix"
) )
// OS collects system level operations that need to be mocked out // OS collects system level operations that need to be mocked out
@ -35,6 +35,8 @@ type OS interface {
Stat(name string) (os.FileInfo, error) Stat(name string) (os.FileInfo, error)
CopyFile(src, dest string, perm os.FileMode) error CopyFile(src, dest string, perm os.FileMode) error
WriteFile(filename string, data []byte, perm os.FileMode) error WriteFile(filename string, data []byte, perm os.FileMode) error
Mount(source string, target string, fstype string, flags uintptr, data string) error
Unmount(target string, flags int) error
} }
// RealOS is used to dispatch the real system level operations. // RealOS is used to dispatch the real system level operations.
@ -82,3 +84,13 @@ func (RealOS) CopyFile(src, dest string, perm os.FileMode) error {
func (RealOS) WriteFile(filename string, data []byte, perm os.FileMode) error { func (RealOS) WriteFile(filename string, data []byte, perm os.FileMode) error {
return ioutil.WriteFile(filename, data, perm) return ioutil.WriteFile(filename, data, perm)
} }
// Mount will call unix.Mount to mount the file.
func (RealOS) Mount(source string, target string, fstype string, flags uintptr, data string) error {
return unix.Mount(source, target, fstype, flags, data)
}
// Unmount will call unix.Unmount to unmount the file.
func (RealOS) Unmount(target string, flags int) error {
return unix.Unmount(target, flags)
}

View File

@ -26,6 +26,14 @@ import (
osInterface "github.com/kubernetes-incubator/cri-containerd/pkg/os" osInterface "github.com/kubernetes-incubator/cri-containerd/pkg/os"
) )
// CalledDetail is the struct contains called function name and arguments.
type CalledDetail struct {
// Name of the function called.
Name string
// Arguments of the function called.
Arguments []interface{}
}
// FakeOS mocks out certain OS calls to avoid perturbing the filesystem // FakeOS mocks out certain OS calls to avoid perturbing the filesystem
// If a member of the form `*Fn` is set, that function will be called in place // If a member of the form `*Fn` is set, that function will be called in place
// of the real call. // of the real call.
@ -37,6 +45,9 @@ type FakeOS struct {
StatFn func(string) (os.FileInfo, error) StatFn func(string) (os.FileInfo, error)
CopyFileFn func(string, string, os.FileMode) error CopyFileFn func(string, string, os.FileMode) error
WriteFileFn func(string, []byte, os.FileMode) error WriteFileFn func(string, []byte, os.FileMode) error
MountFn func(source string, target string, fstype string, flags uintptr, data string) error
UnmountFn func(target string, flags int) error
calls []CalledDetail
errors map[string]error errors map[string]error
} }
@ -77,6 +88,19 @@ func (f *FakeOS) ClearErrors() {
f.errors = make(map[string]error) f.errors = make(map[string]error)
} }
func (f *FakeOS) appendCalls(name string, args ...interface{}) {
f.Lock()
defer f.Unlock()
f.calls = append(f.calls, CalledDetail{Name: name, Arguments: args})
}
// GetCalls get detail of calls.
func (f *FakeOS) GetCalls() []CalledDetail {
f.Lock()
defer f.Unlock()
return append([]CalledDetail{}, f.calls...)
}
// NewFakeOS creates a FakeOS. // NewFakeOS creates a FakeOS.
func NewFakeOS() *FakeOS { func NewFakeOS() *FakeOS {
return &FakeOS{ return &FakeOS{
@ -86,6 +110,7 @@ func NewFakeOS() *FakeOS {
// MkdirAll is a fake call that invokes MkdirAllFn or just returns nil. // MkdirAll is a fake call that invokes MkdirAllFn or just returns nil.
func (f *FakeOS) MkdirAll(path string, perm os.FileMode) error { func (f *FakeOS) MkdirAll(path string, perm os.FileMode) error {
f.appendCalls("MkdirAll", path, perm)
if err := f.getError("MkdirAll"); err != nil { if err := f.getError("MkdirAll"); err != nil {
return err return err
} }
@ -98,6 +123,7 @@ func (f *FakeOS) MkdirAll(path string, perm os.FileMode) error {
// RemoveAll is a fake call that invokes RemoveAllFn or just returns nil. // RemoveAll is a fake call that invokes RemoveAllFn or just returns nil.
func (f *FakeOS) RemoveAll(path string) error { func (f *FakeOS) RemoveAll(path string) error {
f.appendCalls("RemoveAll", path)
if err := f.getError("RemoveAll"); err != nil { if err := f.getError("RemoveAll"); err != nil {
return err return err
} }
@ -110,6 +136,7 @@ func (f *FakeOS) RemoveAll(path string) error {
// OpenFifo is a fake call that invokes OpenFifoFn or just returns nil. // OpenFifo is a fake call that invokes OpenFifoFn or just returns nil.
func (f *FakeOS) OpenFifo(ctx context.Context, fn string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { func (f *FakeOS) OpenFifo(ctx context.Context, fn string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {
f.appendCalls("OpenFifo", ctx, fn, flag, perm)
if err := f.getError("OpenFifo"); err != nil { if err := f.getError("OpenFifo"); err != nil {
return nil, err return nil, err
} }
@ -122,6 +149,7 @@ func (f *FakeOS) OpenFifo(ctx context.Context, fn string, flag int, perm os.File
// Stat is a fake call that invokes StatFn or just return nil. // Stat is a fake call that invokes StatFn or just return nil.
func (f *FakeOS) Stat(name string) (os.FileInfo, error) { func (f *FakeOS) Stat(name string) (os.FileInfo, error) {
f.appendCalls("Stat", name)
if err := f.getError("Stat"); err != nil { if err := f.getError("Stat"); err != nil {
return nil, err return nil, err
} }
@ -134,6 +162,7 @@ func (f *FakeOS) Stat(name string) (os.FileInfo, error) {
// CopyFile is a fake call that invokes CopyFileFn or just return nil. // CopyFile is a fake call that invokes CopyFileFn or just return nil.
func (f *FakeOS) CopyFile(src, dest string, perm os.FileMode) error { func (f *FakeOS) CopyFile(src, dest string, perm os.FileMode) error {
f.appendCalls("CopyFile", src, dest, perm)
if err := f.getError("CopyFile"); err != nil { if err := f.getError("CopyFile"); err != nil {
return err return err
} }
@ -146,6 +175,7 @@ func (f *FakeOS) CopyFile(src, dest string, perm os.FileMode) error {
// WriteFile is a fake call that invokes WriteFileFn or just return nil. // WriteFile is a fake call that invokes WriteFileFn or just return nil.
func (f *FakeOS) WriteFile(filename string, data []byte, perm os.FileMode) error { func (f *FakeOS) WriteFile(filename string, data []byte, perm os.FileMode) error {
f.appendCalls("WriteFile", filename, data, perm)
if err := f.getError("WriteFile"); err != nil { if err := f.getError("WriteFile"); err != nil {
return err return err
} }
@ -155,3 +185,29 @@ func (f *FakeOS) WriteFile(filename string, data []byte, perm os.FileMode) error
} }
return nil return nil
} }
// Mount is a fake call that invokes MountFn or just return nil.
func (f *FakeOS) Mount(source string, target string, fstype string, flags uintptr, data string) error {
f.appendCalls("Mount", source, target, fstype, flags, data)
if err := f.getError("Mount"); err != nil {
return err
}
if f.MountFn != nil {
return f.MountFn(source, target, fstype, flags, data)
}
return nil
}
// Unmount is a fake call that invokes UnmountFn or just return nil.
func (f *FakeOS) Unmount(target string, flags int) error {
f.appendCalls("Unmount", target, flags)
if err := f.getError("Unmount"); err != nil {
return err
}
if f.UnmountFn != nil {
return f.UnmountFn(target, flags)
}
return nil
}

View File

@ -315,7 +315,15 @@ func (c *criContainerdService) generateContainerMounts(sandboxRootDir string, co
Readonly: securityContext.GetReadonlyRootfs(), Readonly: securityContext.GetReadonlyRootfs(),
}) })
// TODO(random-liu): [P0] Mount sandbox /dev/shm. sandboxDevShm := getSandboxDevShm(sandboxRootDir)
if securityContext.GetNamespaceOptions().GetHostIpc() {
sandboxDevShm = devShm
}
mounts = append(mounts, &runtime.Mount{
ContainerPath: devShm,
HostPath: sandboxDevShm,
Readonly: false,
})
return mounts return mounts
} }
@ -416,6 +424,8 @@ func addOCIDevices(g *generate.Generator, devs []*runtime.Device, privileged boo
} }
// addOCIBindMounts adds bind mounts. // addOCIBindMounts adds bind mounts.
// TODO(random-liu): Figure out whether we need to change all CRI mounts to readonly when
// rootfs is readonly. (https://github.com/moby/moby/blob/master/daemon/oci_linux.go)
func addOCIBindMounts(g *generate.Generator, mounts []*runtime.Mount, privileged bool) { func addOCIBindMounts(g *generate.Generator, mounts []*runtime.Mount, privileged bool) {
for _, mount := range mounts { for _, mount := range mounts {
dst := mount.GetContainerPath() dst := mount.GetContainerPath()

View File

@ -324,6 +324,11 @@ func TestGenerateContainerMounts(t *testing.T) {
HostPath: testSandboxRootDir + "/resolv.conf", HostPath: testSandboxRootDir + "/resolv.conf",
Readonly: true, Readonly: true,
}, },
{
ContainerPath: "/dev/shm",
HostPath: testSandboxRootDir + "/shm",
Readonly: false,
},
}, },
}, },
"should setup rw mount when rootfs is read-write": { "should setup rw mount when rootfs is read-write": {
@ -336,7 +341,34 @@ func TestGenerateContainerMounts(t *testing.T) {
}, },
{ {
ContainerPath: resolvConfPath, ContainerPath: resolvConfPath,
HostPath: getResolvPath(testSandboxRootDir), HostPath: testSandboxRootDir + "/resolv.conf",
Readonly: false,
},
{
ContainerPath: "/dev/shm",
HostPath: testSandboxRootDir + "/shm",
Readonly: false,
},
},
},
"should use host /dev/shm when host ipc is set": {
securityContext: &runtime.LinuxContainerSecurityContext{
NamespaceOptions: &runtime.NamespaceOption{HostIpc: true},
},
expectedMounts: []*runtime.Mount{
{
ContainerPath: "/etc/hosts",
HostPath: testSandboxRootDir + "/hosts",
Readonly: false,
},
{
ContainerPath: resolvConfPath,
HostPath: testSandboxRootDir + "/resolv.conf",
Readonly: false,
},
{
ContainerPath: "/dev/shm",
HostPath: "/dev/shm",
Readonly: false, Readonly: false,
}, },
}, },

View File

@ -58,6 +58,8 @@ const (
// defaultSandboxImage is the image used by sandbox container. // defaultSandboxImage is the image used by sandbox container.
// TODO(random-liu): [P1] Build schema 2 pause image and use it here. // TODO(random-liu): [P1] Build schema 2 pause image and use it here.
defaultSandboxImage = "gcr.io/google.com/noogler-kubernetes/pause-amd64:3.0" defaultSandboxImage = "gcr.io/google.com/noogler-kubernetes/pause-amd64:3.0"
// defaultShmSize is the default size of the sandbox shm.
defaultShmSize = int64(1024 * 1024 * 64)
// relativeRootfsPath is the rootfs path relative to bundle path. // relativeRootfsPath is the rootfs path relative to bundle path.
relativeRootfsPath = "rootfs" relativeRootfsPath = "rootfs"
// defaultRuntime is the runtime to use in containerd. We may support // defaultRuntime is the runtime to use in containerd. We may support
@ -88,6 +90,8 @@ const (
utsNSFormat = "/proc/%v/ns/uts" utsNSFormat = "/proc/%v/ns/uts"
// pidNSFormat is the format of pid namespace of a process. // pidNSFormat is the format of pid namespace of a process.
pidNSFormat = "/proc/%v/ns/pid" pidNSFormat = "/proc/%v/ns/pid"
// devShm is the default path of /dev/shm.
devShm = "/dev/shm"
// etcHosts is the default path of /etc/hosts file. // etcHosts is the default path of /etc/hosts file.
etcHosts = "/etc/hosts" etcHosts = "/etc/hosts"
// resolvConfPath is the abs path of resolv.conf on host or container. // resolvConfPath is the abs path of resolv.conf on host or container.
@ -159,6 +163,11 @@ func getResolvPath(sandboxRoot string) string {
return filepath.Join(sandboxRoot, "resolv.conf") return filepath.Join(sandboxRoot, "resolv.conf")
} }
// getSandboxDevShm returns the shm file path inside the sandbox root directory.
func getSandboxDevShm(sandboxRootDir string) string {
return filepath.Join(sandboxRootDir, "shm")
}
// prepareStreamingPipes prepares stream named pipe for container. returns nil // prepareStreamingPipes prepares stream named pipe for container. returns nil
// streaming handler if corresponding stream path is empty. // streaming handler if corresponding stream path is empty.
func (c *criContainerdService) prepareStreamingPipes(ctx context.Context, stdin, stdout, stderr string) ( func (c *criContainerdService) prepareStreamingPipes(ctx context.Context, stdin, stdout, stderr string) (

View File

@ -19,6 +19,7 @@ package server
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"strings" "strings"
"time" "time"
@ -31,6 +32,7 @@ import (
runtimespec "github.com/opencontainers/runtime-spec/specs-go" runtimespec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/runtime-tools/generate" "github.com/opencontainers/runtime-tools/generate"
"golang.org/x/net/context" "golang.org/x/net/context"
"golang.org/x/sys/unix"
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1" runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
"github.com/kubernetes-incubator/cri-containerd/pkg/metadata" "github.com/kubernetes-incubator/cri-containerd/pkg/metadata"
@ -137,8 +139,11 @@ func (c *criContainerdService) RunPodSandbox(ctx context.Context, r *runtime.Run
if err = c.setupSandboxFiles(sandboxRootDir, config); err != nil { if err = c.setupSandboxFiles(sandboxRootDir, config); err != nil {
return nil, fmt.Errorf("failed to setup sandbox files: %v", err) return nil, fmt.Errorf("failed to setup sandbox files: %v", err)
} }
// No need to cleanup on error, because the whole sandbox root directory will be removed defer func() {
// on error. if retErr != nil {
c.cleanupSandboxFiles(sandboxRootDir, config)
}
}()
// Start sandbox container. // Start sandbox container.
spec, err := c.generateSandboxContainerSpec(id, config, imageMeta.Config) spec, err := c.generateSandboxContainerSpec(id, config, imageMeta.Config)
@ -301,7 +306,7 @@ func (c *criContainerdService) generateSandboxContainerSpec(id string, config *r
func (c *criContainerdService) setupSandboxFiles(rootDir string, config *runtime.PodSandboxConfig) error { func (c *criContainerdService) setupSandboxFiles(rootDir string, config *runtime.PodSandboxConfig) error {
// TODO(random-liu): Consider whether we should maintain /etc/hosts and /etc/resolv.conf in kubelet. // TODO(random-liu): Consider whether we should maintain /etc/hosts and /etc/resolv.conf in kubelet.
sandboxEtcHosts := getSandboxHosts(rootDir) sandboxEtcHosts := getSandboxHosts(rootDir)
if err := c.os.CopyFile(etcHosts, sandboxEtcHosts, 0666); err != nil { if err := c.os.CopyFile(etcHosts, sandboxEtcHosts, 0644); err != nil {
return fmt.Errorf("failed to generate sandbox hosts file %q: %v", sandboxEtcHosts, err) return fmt.Errorf("failed to generate sandbox hosts file %q: %v", sandboxEtcHosts, err)
} }
@ -328,8 +333,22 @@ func (c *criContainerdService) setupSandboxFiles(rootDir string, config *runtime
} }
} }
// TODO(random-liu): [P0] Deal with /dev/shm. Use host for HostIpc, and create and mount for // Setup sandbox /dev/shm.
// non-HostIpc. What about mqueue? if config.GetLinux().GetSecurityContext().GetNamespaceOptions().GetHostIpc() {
if _, err := c.os.Stat(devShm); err != nil {
return fmt.Errorf("host %q is not available for host ipc: %v", devShm, err)
}
} else {
sandboxDevShm := getSandboxDevShm(rootDir)
if err := c.os.MkdirAll(sandboxDevShm, 0700); err != nil {
return fmt.Errorf("failed to create sandbox shm: %v", err)
}
shmproperty := fmt.Sprintf("mode=1777,size=%d", defaultShmSize)
if err := c.os.Mount("shm", sandboxDevShm, "tmpfs", uintptr(unix.MS_NOEXEC|unix.MS_NOSUID|unix.MS_NODEV), shmproperty); err != nil {
return fmt.Errorf("failed to mount sandbox shm: %v", err)
}
}
return nil return nil
} }
@ -356,3 +375,13 @@ func parseDNSOptions(servers, searches, options []string) (string, error) {
return resolvContent, nil return resolvContent, nil
} }
// cleanupSandboxFiles only unmount files, we rely on the removal of sandbox root directory to remove files.
// Each cleanup task should log error instead of returning, so as to keep on cleanup on error.
func (c *criContainerdService) cleanupSandboxFiles(rootDir string, config *runtime.PodSandboxConfig) {
if !config.GetLinux().GetSecurityContext().GetNamespaceOptions().GetHostIpc() {
if err := c.os.Unmount(getSandboxDevShm(rootDir), unix.MNT_DETACH); err != nil && os.IsNotExist(err) {
glog.Errorf("failed to unmount sandbox shm: %v", err)
}
}
}

View File

@ -159,18 +159,112 @@ func TestGenerateSandboxContainerSpec(t *testing.T) {
func TestSetupSandboxFiles(t *testing.T) { func TestSetupSandboxFiles(t *testing.T) {
testRootDir := "test-sandbox-root" testRootDir := "test-sandbox-root"
expectedCopys := [][]interface{}{ for desc, test := range map[string]struct {
{"/etc/hosts", testRootDir + "/hosts", os.FileMode(0666)}, dnsConfig *runtime.DNSConfig
{"/etc/resolv.conf", testRootDir + "/resolv.conf", os.FileMode(0644)}, hostIpc bool
expectedCalls []ostesting.CalledDetail
}{
"should check host /dev/shm existence when hostIpc is true": {
hostIpc: true,
expectedCalls: []ostesting.CalledDetail{
{
Name: "CopyFile",
Arguments: []interface{}{
"/etc/hosts", testRootDir + "/hosts", os.FileMode(0644),
},
},
{
Name: "CopyFile",
Arguments: []interface{}{
"/etc/resolv.conf", testRootDir + "/resolv.conf", os.FileMode(0644),
},
},
{
Name: "Stat",
Arguments: []interface{}{"/dev/shm"},
},
},
},
"should create new /etc/resolv.conf if DNSOptions is set": {
dnsConfig: &runtime.DNSConfig{
Servers: []string{"8.8.8.8"},
Searches: []string{"114.114.114.114"},
Options: []string{"timeout:1"},
},
hostIpc: true,
expectedCalls: []ostesting.CalledDetail{
{
Name: "CopyFile",
Arguments: []interface{}{
"/etc/hosts", testRootDir + "/hosts", os.FileMode(0644),
},
},
{
Name: "WriteFile",
Arguments: []interface{}{
testRootDir + "/resolv.conf", []byte(`search 114.114.114.114
nameserver 8.8.8.8
options timeout:1
`), os.FileMode(0644),
},
},
{
Name: "Stat",
Arguments: []interface{}{"/dev/shm"},
},
},
},
"should create sandbox shm when hostIpc is false": {
hostIpc: false,
expectedCalls: []ostesting.CalledDetail{
{
Name: "CopyFile",
Arguments: []interface{}{
"/etc/hosts", testRootDir + "/hosts", os.FileMode(0644),
},
},
{
Name: "CopyFile",
Arguments: []interface{}{
"/etc/resolv.conf", testRootDir + "/resolv.conf", os.FileMode(0644),
},
},
{
Name: "MkdirAll",
Arguments: []interface{}{
testRootDir + "/shm", os.FileMode(0700),
},
},
{
Name: "Mount",
// Ignore arguments which are too complex to check.
},
},
},
} {
t.Logf("TestCase %q", desc)
c := newTestCRIContainerdService()
cfg := &runtime.PodSandboxConfig{
DnsConfig: test.dnsConfig,
Linux: &runtime.LinuxPodSandboxConfig{
SecurityContext: &runtime.LinuxSandboxSecurityContext{
NamespaceOptions: &runtime.NamespaceOption{
HostIpc: test.hostIpc,
},
},
},
}
c.setupSandboxFiles(testRootDir, cfg)
calls := c.os.(*ostesting.FakeOS).GetCalls()
assert.Len(t, calls, len(test.expectedCalls))
for i, expected := range test.expectedCalls {
if expected.Arguments == nil {
// Ignore arguments.
expected.Arguments = calls[i].Arguments
}
assert.Equal(t, expected, calls[i])
}
} }
c := newTestCRIContainerdService()
var copys [][]interface{}
c.os.(*ostesting.FakeOS).CopyFileFn = func(src string, dest string, perm os.FileMode) error {
copys = append(copys, []interface{}{src, dest, perm})
return nil
}
c.setupSandboxFiles(testRootDir, nil)
assert.Equal(t, expectedCopys, copys, "should copy expected files for sandbox")
} }
func TestRunPodSandbox(t *testing.T) { func TestRunPodSandbox(t *testing.T) {
@ -184,7 +278,6 @@ func TestRunPodSandbox(t *testing.T) {
var pipes []string var pipes []string
fakeOS.MkdirAllFn = func(path string, perm os.FileMode) error { fakeOS.MkdirAllFn = func(path string, perm os.FileMode) error {
dirs = append(dirs, path) dirs = append(dirs, path)
assert.Equal(t, os.FileMode(0755), perm)
return nil return nil
} }
fakeOS.OpenFifoFn = func(ctx context.Context, fn string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { fakeOS.OpenFifoFn = func(ctx context.Context, fn string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {
@ -211,11 +304,11 @@ func TestRunPodSandbox(t *testing.T) {
require.NotNil(t, res) require.NotNil(t, res)
id := res.GetPodSandboxId() id := res.GetPodSandboxId()
assert.Len(t, dirs, 1) sandboxRootDir := getSandboxRootDir(c.rootDir, id)
assert.Equal(t, getSandboxRootDir(c.rootDir, id), dirs[0], "sandbox root directory should be created") assert.Contains(t, dirs[0], sandboxRootDir, "sandbox root directory should be created")
assert.Len(t, pipes, 2) assert.Len(t, pipes, 2)
_, stdout, stderr := getStreamingPipes(getSandboxRootDir(c.rootDir, id)) _, stdout, stderr := getStreamingPipes(sandboxRootDir)
assert.Contains(t, pipes, stdout, "sandbox stdout pipe should be created") assert.Contains(t, pipes, stdout, "sandbox stdout pipe should be created")
assert.Contains(t, pipes, stderr, "sandbox stderr pipe should be created") assert.Contains(t, pipes, stderr, "sandbox stderr pipe should be created")