mirror of
https://github.com/containerd/containerd.git
synced 2026-11-06 05:17:02 +00:00
Merge pull request #14135 from containerd/sandboxed-shim
Introduce sandboxed runc shim
This commit is contained in:
38 files changed
+4437
-54
No files matched your search
@@ -607,6 +607,23 @@ jobs:
|
||||
env
|
||||
sudo -E PATH=$PATH ./script/critest.sh "${{github.workspace}}/report"
|
||||
|
||||
# Experimental: the same critest suite through the pause-less sandbox
|
||||
# shim (containerd-shim-sandboxed-runc-v2 selected with runtime_path,
|
||||
# with the shim sandboxer). The user namespace specs skip themselves:
|
||||
# the handler does not advertise user namespaces until the shim
|
||||
# supports them. The sandboxed shim is a Go shim: it runs on the
|
||||
# runc.v2 legs only.
|
||||
- if: matrix.runtime == 'io.containerd.runc.v2'
|
||||
name: cri-tools critest (sandbox shim)
|
||||
env:
|
||||
TEST_RUNTIME: ${{ matrix.runtime }}
|
||||
TEST_RUNTIME_PATH: /usr/local/bin/containerd-shim-sandboxed-runc-v2
|
||||
SANDBOXER: shim
|
||||
CGROUP_DRIVER: ${{ matrix.cgroup_driver }}
|
||||
run: |
|
||||
env
|
||||
sudo -E PATH=$PATH ./script/critest.sh "${{github.workspace}}/report-sandbox-shim"
|
||||
|
||||
# The Rust shim does not implement checkpoint yet.
|
||||
- if: matrix.os != 'ubuntu-24.04-arm' && matrix.runtime != 'io.containerd.runc.v2-rs'
|
||||
name: Checkpoint/Restore Disable via CRI
|
||||
@@ -641,6 +658,8 @@ jobs:
|
||||
*-gotest.json
|
||||
${{github.workspace}}/report/*.xml
|
||||
${{github.workspace}}/report/*.log
|
||||
${{github.workspace}}/report-sandbox-shim/*.xml
|
||||
${{github.workspace}}/report-sandbox-shim/*.log
|
||||
|
||||
integration-vm:
|
||||
name: VM integration
|
||||
|
||||
@@ -272,6 +272,14 @@ bin/containerd-shim-runc-v2: cmd/containerd-shim-runc-v2 FORCE # set !cgo and om
|
||||
@echo "$(WHALE) $@"
|
||||
CGO_ENABLED=${SHIM_CGO_ENABLED} $(GO) build ${GO_BUILD_FLAGS} -o $@ ${SHIM_GO_LDFLAGS} ${SHIM_GO_TAGS} ./cmd/containerd-shim-runc-v2
|
||||
|
||||
bin/containerd-shim-sandboxed-runc-v2: cmd/containerd-shim-sandboxed-runc-v2 FORCE # experimental pause-less sandbox shim, static like containerd-shim-runc-v2
|
||||
@echo "$(WHALE) $@"
|
||||
CGO_ENABLED=${SHIM_CGO_ENABLED} $(GO) build ${GO_BUILD_FLAGS} -o $@ ${SHIM_GO_LDFLAGS} ${SHIM_GO_TAGS} ./cmd/containerd-shim-sandboxed-runc-v2
|
||||
|
||||
bin/containerd-sandbox-init: cmd/containerd-sandbox-init FORCE # PID 1 of a shared pod PID namespace for the sandboxed shim, static like the shims
|
||||
@echo "$(WHALE) $@"
|
||||
CGO_ENABLED=${SHIM_CGO_ENABLED} $(GO) build ${GO_BUILD_FLAGS} -o $@ ${SHIM_GO_LDFLAGS} ${SHIM_GO_TAGS} ./cmd/containerd-sandbox-init
|
||||
|
||||
binaries: $(BINARIES) ## build binaries
|
||||
@echo "$(WHALE) $@"
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
WHALE="+"
|
||||
ONI="-"
|
||||
COMMANDS += containerd-shim-runc-v2
|
||||
COMMANDS += containerd-shim-sandboxed-runc-v2
|
||||
COMMANDS += containerd-sandbox-init
|
||||
|
||||
# check GOOS for cross compile builds
|
||||
ifeq ($(GOOS),linux)
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// containerd-sandbox-init is PID 1 of the PID namespace of a pod that shares
|
||||
// it (shareProcessNamespace). containerd-shim-sandboxed-runc-v2, the
|
||||
// pause-less sandbox shim, starts it through runc for those pods only, and it
|
||||
// runs as a child of the shim. An ordinary pod, one that does not share its
|
||||
// PID namespace, has no sandbox process at all.
|
||||
//
|
||||
// It does what PID 1 of a pod has to do and nothing else: it stays alive so
|
||||
// that the namespace does, it lets the kernel reap the orphans that get
|
||||
// reparented to it, and it cannot be ended from inside the pod.
|
||||
//
|
||||
// The kernel delivers a signal to the init of a PID namespace, from inside
|
||||
// that namespace, only when init has a handler installed for it, and discards
|
||||
// it otherwise. SIGKILL and SIGSTOP reach it from the parent namespace only,
|
||||
// and SIGKILL from the shim is how it ends. The Go runtime installs handlers
|
||||
// for almost every signal and ends the process on most of them. A Go init
|
||||
// that kept those handlers could be killed from a member container with
|
||||
// kill -HUP 1 or kill -SEGV 1, so disableSignals removes every one of them.
|
||||
//
|
||||
// It is also not dumpable, so that the other processes of the pod cannot
|
||||
// inspect or control it: without CAP_SYS_PTRACE they can neither ptrace it
|
||||
// nor open /proc/1/root, /proc/1/mem and the like.
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func main() {
|
||||
run()
|
||||
}
|
||||
|
||||
// run sets the process up and parks it; it never returns.
|
||||
func run() {
|
||||
// runc makes the process non-dumpable, but execve made it dumpable
|
||||
// again, since it changed no credentials.
|
||||
if err := unix.Prctl(unix.PR_SET_DUMPABLE, 0, 0, 0, 0); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
// A single scheduler thread (GOMAXPROCS) and no garbage collector:
|
||||
// there is nothing to run and nothing to collect. Like pause, the
|
||||
// process then sleeps until it is killed.
|
||||
runtime.GOMAXPROCS(1)
|
||||
debug.SetGCPercent(-1)
|
||||
if err := disableSignals(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
debug.FreeOSMemory()
|
||||
for {
|
||||
// A blocking system call rather than select {}: with no other
|
||||
// goroutine the runtime would report a deadlock.
|
||||
unix.Pause()
|
||||
}
|
||||
}
|
||||
|
||||
// disableSignals leaves no signal handler in place, so that no signal sent
|
||||
// from inside the pod runs any code in the process. SIGCHLD is ignored rather
|
||||
// than defaulted so that the kernel reaps the children reparented to PID 1 on
|
||||
// its own, with no wait loop. Ignoring SIGURG gives up asynchronous
|
||||
// preemption, which a process whose only goroutine waits in pause(2) does not
|
||||
// need. os/signal cannot touch the synchronous faults, for which the runtime
|
||||
// installs handlers that exit the process when the signal came from kill(2),
|
||||
// nor SIGPROF, whose handler it keeps for profiling, nor signals 32 and 33,
|
||||
// which the C library reserves and the runtime keeps for itself (33 for
|
||||
// system calls that must run on every thread, which this process never
|
||||
// makes): the faults go back to the kernel default and the others are
|
||||
// ignored, through rt_sigaction.
|
||||
func disableSignals() error {
|
||||
// Signals 32 and 33, reserved by the C library and kept by the Go runtime.
|
||||
const (
|
||||
sigRuntime1 = syscall.Signal(32)
|
||||
sigRuntime2 = syscall.Signal(33)
|
||||
)
|
||||
for n := 1; n <= 64; n++ {
|
||||
sig := syscall.Signal(n)
|
||||
switch sig {
|
||||
case unix.SIGKILL, unix.SIGSTOP:
|
||||
// Uncatchable, and how the shim ends the process.
|
||||
case unix.SIGILL, unix.SIGTRAP, unix.SIGBUS, unix.SIGFPE, unix.SIGSEGV, unix.SIGSTKFLT, unix.SIGSYS:
|
||||
if err := setAction(sig, sigDFL); err != nil {
|
||||
return err
|
||||
}
|
||||
case unix.SIGPROF, sigRuntime1, sigRuntime2:
|
||||
if err := setAction(sig, sigIGN); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
signal.Ignore(sig)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// The dispositions setAction sets.
|
||||
const (
|
||||
sigDFL = 0
|
||||
sigIGN = 1
|
||||
)
|
||||
|
||||
// setAction sets the disposition of sig, with no flags and an empty mask. The
|
||||
// struct sigaction passed is sized for every architecture and starts with the
|
||||
// handler, as it does on every supported one (MIPS puts the flags first and
|
||||
// has a larger sigset); the last argument is the size of the kernel sigset.
|
||||
func setAction(sig syscall.Signal, handler uintptr) error {
|
||||
act := [4]uint64{uint64(handler)}
|
||||
if _, _, errno := unix.RawSyscall6(unix.SYS_RT_SIGACTION, uintptr(sig), uintptr(unsafe.Pointer(&act)), 0, 8, 0, 0); errno != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/containerd/containerd/v2/pkg/testutil"
|
||||
)
|
||||
|
||||
// childEnv makes the test binary run as the init instead of the tests.
|
||||
const childEnv = "CONTAINERD_SANDBOX_INIT_TEST_CHILD"
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if os.Getenv(childEnv) == "1" {
|
||||
run()
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
// initUser is the user the shim runs the init as.
|
||||
const initUser = 65535
|
||||
|
||||
// TestSignals runs the init as PID 1 of a new PID namespace, as the user the
|
||||
// shim runs it as, and sends it every signal but SIGKILL and SIGSTOP, from
|
||||
// the parent namespace: a signal that ended it here would end it from inside
|
||||
// the pod as well (see the package doc).
|
||||
func TestSignals(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
|
||||
cmd := exec.Command(initCopy(t))
|
||||
cmd.Env = append(os.Environ(), childEnv+"=1")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Cloneflags: unix.CLONE_NEWPID,
|
||||
Credential: &syscall.Credential{Uid: initUser, Gid: initUser},
|
||||
}
|
||||
require.NoError(t, cmd.Start())
|
||||
// waitErr is valid once exited is closed.
|
||||
var waitErr error
|
||||
exited := make(chan struct{})
|
||||
go func() {
|
||||
waitErr = cmd.Wait()
|
||||
close(exited)
|
||||
}()
|
||||
t.Cleanup(func() {
|
||||
_ = cmd.Process.Kill()
|
||||
<-exited
|
||||
})
|
||||
pid := cmd.Process.Pid
|
||||
|
||||
// The setup is done once the last signal of the loop, 64, is ignored.
|
||||
require.Eventually(t, func() bool {
|
||||
ignored, _ := signalMasks(t, pid)
|
||||
return ignored&(1<<63) != 0
|
||||
}, 10*time.Second, 10*time.Millisecond, "the init did not finish its signal setup")
|
||||
|
||||
ignored, caught := signalMasks(t, pid)
|
||||
for _, sig := range []syscall.Signal{unix.SIGCHLD, unix.SIGURG, unix.SIGPROF} {
|
||||
assert.NotZero(t, ignored&(1<<(sig-1)), "%s must be ignored", unix.SignalName(sig))
|
||||
}
|
||||
assert.Zero(t, caught, "no signal may have a handler (SigCgt %#x)", caught)
|
||||
|
||||
// The /proc/<pid> entries of a process that is not dumpable belong to
|
||||
// root (the directory itself keeps the owner of the process).
|
||||
var st unix.Stat_t
|
||||
require.NoError(t, unix.Stat(fmt.Sprintf("/proc/%d/status", pid), &st))
|
||||
assert.EqualValues(t, 0, st.Uid, "the init must not be dumpable")
|
||||
|
||||
for n := 1; n <= 64; n++ {
|
||||
sig := syscall.Signal(n)
|
||||
if sig == unix.SIGKILL || sig == unix.SIGSTOP {
|
||||
continue
|
||||
}
|
||||
require.NoError(t, unix.Kill(pid, sig), "signal %d", n)
|
||||
select {
|
||||
case <-exited:
|
||||
t.Fatalf("signal %d (%s) ended the init: %v", n, unix.SignalName(sig), waitErr)
|
||||
case <-time.After(20 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
|
||||
require.NoError(t, unix.Kill(pid, unix.SIGKILL))
|
||||
select {
|
||||
case <-exited:
|
||||
var exitErr *exec.ExitError
|
||||
require.ErrorAs(t, waitErr, &exitErr)
|
||||
assert.Equal(t, unix.SIGKILL, exitErr.Sys().(syscall.WaitStatus).Signal())
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("SIGKILL must end the init")
|
||||
}
|
||||
}
|
||||
|
||||
// initCopy copies the test binary where the init user can run it: the
|
||||
// directories go test builds it in are not traversable for other users.
|
||||
func initCopy(t *testing.T) string {
|
||||
self, err := os.Executable()
|
||||
require.NoError(t, err)
|
||||
dir := t.TempDir()
|
||||
for d := dir; d != os.TempDir(); d = filepath.Dir(d) {
|
||||
require.NoError(t, os.Chmod(d, 0o755))
|
||||
}
|
||||
src, err := os.Open(self)
|
||||
require.NoError(t, err)
|
||||
defer src.Close()
|
||||
path := filepath.Join(dir, "init")
|
||||
dst, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o755)
|
||||
require.NoError(t, err)
|
||||
_, err = io.Copy(dst, src)
|
||||
require.NoError(t, errors.Join(err, dst.Close()))
|
||||
return path
|
||||
}
|
||||
|
||||
// signalMasks reads the ignored and caught signal sets of pid from /proc;
|
||||
// bit n-1 stands for signal n.
|
||||
func signalMasks(t *testing.T, pid int) (ignored, caught uint64) {
|
||||
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid))
|
||||
require.NoError(t, err)
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
field, value, ok := strings.Cut(line, ":")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
value = strings.TrimSpace(value)
|
||||
switch field {
|
||||
case "SigIgn":
|
||||
ignored, err = strconv.ParseUint(value, 16, 64)
|
||||
require.NoError(t, err)
|
||||
case "SigCgt":
|
||||
caught, err = strconv.ParseUint(value, 16, 64)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
return ignored, caught
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// containerd-shim-sandboxed-runc-v2 is an experimental runc shim that
|
||||
// implements the Sandbox API itself, so that a pod needs no pause container:
|
||||
// the shim holds the pod namespaces and owns the pod shared files, and runs
|
||||
// the containers of the pod with the same runc task service as
|
||||
// containerd-shim-runc-v2. It serves the io.containerd.runc.v2 runtime type;
|
||||
// a runtime handler selects it with runtime_path and sandboxer = "shim". See
|
||||
// the sandbox package for details on the design.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/containerd/containerd/v2/pkg/shim"
|
||||
)
|
||||
|
||||
func main() {
|
||||
shim.RunShim(context.Background(), newManager())
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
goruntime "runtime"
|
||||
"slices"
|
||||
"syscall"
|
||||
|
||||
"github.com/containerd/cgroups/v3"
|
||||
cgroupsv2 "github.com/containerd/cgroups/v3/cgroup2"
|
||||
bootapi "github.com/containerd/containerd/api/runtime/bootstrap/v1"
|
||||
"github.com/containerd/containerd/api/types"
|
||||
"github.com/containerd/containerd/api/types/runc/options"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/containerd/typeurl/v2"
|
||||
"github.com/opencontainers/runtime-spec/specs-go/features"
|
||||
|
||||
legacymanager "github.com/containerd/containerd/v2/cmd/containerd-shim-runc-v2/manager"
|
||||
"github.com/containerd/containerd/v2/cmd/containerd-shim-runc-v2/runc"
|
||||
"github.com/containerd/containerd/v2/cmd/containerd-shim-sandboxed-runc-v2/sandbox"
|
||||
"github.com/containerd/containerd/v2/defaults"
|
||||
"github.com/containerd/containerd/v2/pkg/namespaces"
|
||||
"github.com/containerd/containerd/v2/pkg/schedcore"
|
||||
"github.com/containerd/containerd/v2/pkg/shim"
|
||||
"github.com/containerd/containerd/v2/plugins"
|
||||
)
|
||||
|
||||
// manager is a fork of the containerd-shim-runc-v2 manager for the sandbox
|
||||
// flow: "start" runs once per pod, for the sandbox, keyed by the sandbox id
|
||||
// (containers join the shim through the sandbox endpoint, so there is no
|
||||
// grouping by OCI annotations); "delete" also cleans up a sandbox bundle; and
|
||||
// "-info" hides the user namespace feature.
|
||||
//
|
||||
// TODO(mxpv): consolidate with the containerd-shim-runc-v2 manager.
|
||||
type manager struct {
|
||||
// legacy is the containerd-shim-runc-v2 manager. It serves the runtime
|
||||
// info (the same runtime type, the same runc, the same features) and the
|
||||
// cleanup of the task bundles joined to this shim.
|
||||
legacy shim.Shim
|
||||
}
|
||||
|
||||
func newManager() shim.Shim {
|
||||
return &manager{legacy: legacymanager.NewShimManager(plugins.RuntimeRuncV2)}
|
||||
}
|
||||
|
||||
// Name is the runtime type served: containerd reaches this binary through
|
||||
// runtime_path, not through a runtime type of its own.
|
||||
func (m *manager) Name() string {
|
||||
return m.legacy.Name()
|
||||
}
|
||||
|
||||
func newCommand(ctx context.Context, id, containerdAddress string, debug bool) (*exec.Cmd, error) {
|
||||
ns, err := namespaces.NamespaceRequired(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
self, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args := []string{
|
||||
"-namespace", ns,
|
||||
"-id", id,
|
||||
"-address", containerdAddress,
|
||||
}
|
||||
if debug {
|
||||
args = append(args, "-debug")
|
||||
}
|
||||
cmd := exec.Command(self, args...)
|
||||
cmd.Dir = cwd
|
||||
cmd.Env = append(os.Environ(), "GOMAXPROCS=4")
|
||||
cmd.Env = append(cmd.Env, "OTEL_SERVICE_NAME=containerd-shim-"+id)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
type shimSocket struct {
|
||||
addr string
|
||||
s *net.UnixListener
|
||||
f *os.File
|
||||
}
|
||||
|
||||
func (s *shimSocket) Close() {
|
||||
if s.s != nil {
|
||||
s.s.Close()
|
||||
}
|
||||
if s.f != nil {
|
||||
s.f.Close()
|
||||
}
|
||||
_ = shim.RemoveSocket(s.addr)
|
||||
}
|
||||
|
||||
func newShimSocket(ctx context.Context, root, path, id string, debug bool) (*shimSocket, error) {
|
||||
address, err := shim.CreateSocketAddress(ctx, root, path, id, debug)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
socket, err := shim.NewSocket(address)
|
||||
if err != nil {
|
||||
// A socket in use that accepts connections belongs to a shim that is
|
||||
// already running for this sandbox; a stale one is replaced.
|
||||
if !shim.SocketEaddrinuse(err) {
|
||||
return nil, fmt.Errorf("create new shim socket: %w", err)
|
||||
}
|
||||
if !debug && shim.CanConnect(address) {
|
||||
return &shimSocket{addr: address}, errdefs.ErrAlreadyExists
|
||||
}
|
||||
if err := shim.RemoveSocket(address); err != nil {
|
||||
return nil, fmt.Errorf("remove pre-existing socket: %w", err)
|
||||
}
|
||||
if socket, err = shim.NewSocket(address); err != nil {
|
||||
return nil, fmt.Errorf("try create new shim socket 2x: %w", err)
|
||||
}
|
||||
}
|
||||
s := &shimSocket{
|
||||
addr: address,
|
||||
s: socket,
|
||||
}
|
||||
f, err := socket.File()
|
||||
if err != nil {
|
||||
s.Close()
|
||||
return nil, err
|
||||
}
|
||||
s.f = f
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Start starts the shim process for a sandbox bundle. containerd calls it once
|
||||
// per sandbox; the containers of the pod join the running shim through the
|
||||
// bootstrap protocol without invoking "start" again.
|
||||
func (m *manager) Start(ctx context.Context, opts *bootapi.BootstrapParams) (_ *bootapi.BootstrapResult, retErr error) {
|
||||
if cgroups.Mode() != cgroups.Unified {
|
||||
return nil, fmt.Errorf("%s requires cgroup v2 (the unified hierarchy) and this host runs cgroup v1: %w", sandbox.BinaryName, errdefs.ErrNotImplemented)
|
||||
}
|
||||
|
||||
// The working directory is the sandbox bundle.
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var params bootapi.BootstrapResult
|
||||
params.Version = 3
|
||||
params.Protocol = "ttrpc"
|
||||
|
||||
id := opts.GetInstanceID()
|
||||
debugLog := opts.GetLogLevel() <= bootapi.LogLevel_LOG_LEVEL_DEBUG
|
||||
|
||||
cmd, err := newCommand(ctx, id, opts.GetContainerdGrpcAddress(), debugLog)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sockets []*shimSocket
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
for _, s := range sockets {
|
||||
s.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
socketDir := opts.GetSocketDir()
|
||||
if socketDir == "" {
|
||||
socketDir = filepath.Join(defaults.DefaultStateDir, "s")
|
||||
}
|
||||
// The socket is keyed by the sandbox id alone.
|
||||
s, err := newShimSocket(ctx, socketDir, opts.GetContainerdGrpcAddress(), id, false)
|
||||
if err != nil {
|
||||
if errdefs.IsAlreadyExists(err) {
|
||||
params.Address = s.addr
|
||||
return ¶ms, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
sockets = append(sockets, s)
|
||||
cmd.ExtraFiles = append(cmd.ExtraFiles, s.f)
|
||||
|
||||
if debugLog {
|
||||
s, err = newShimSocket(ctx, socketDir, opts.GetContainerdGrpcAddress(), id, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sockets = append(sockets, s)
|
||||
cmd.ExtraFiles = append(cmd.ExtraFiles, s.f)
|
||||
}
|
||||
|
||||
// The shim is spawned from a locked thread so that a core scheduling
|
||||
// cookie created here is inherited by it; the thread is released on
|
||||
// every path.
|
||||
goruntime.LockOSThread()
|
||||
err = func() error {
|
||||
if os.Getenv("SCHED_CORE") != "" {
|
||||
if err := schedcore.Create(schedcore.ProcessGroup); err != nil {
|
||||
return fmt.Errorf("enable sched core support: %w", err)
|
||||
}
|
||||
}
|
||||
return cmd.Start()
|
||||
}()
|
||||
goruntime.UnlockOSThread()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
}()
|
||||
// make sure to wait after start
|
||||
go cmd.Wait()
|
||||
|
||||
var runcOpts options.Options
|
||||
if found, err := opts.FindExtension(&runcOpts); err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch runc options: %w", err)
|
||||
} else if found {
|
||||
// The runc options are persisted in the bundle. The sandbox service
|
||||
// reads them to run containerd-sandbox-init through the same runc,
|
||||
// root and cgroup driver as the containers of the pod, and Stop
|
||||
// reads them to delete that container once the shim is gone.
|
||||
if err := runc.WriteOptions(cwd, &runcOpts); err != nil {
|
||||
return nil, fmt.Errorf("failed to persist the runc options: %w", err)
|
||||
}
|
||||
if err := runc.WriteRuntime(cwd, runcOpts.GetBinaryName()); err != nil {
|
||||
return nil, fmt.Errorf("failed to persist the runtime name: %w", err)
|
||||
}
|
||||
if shimCgroup := runcOpts.GetShimCgroup(); shimCgroup != "" {
|
||||
cg, err := cgroupsv2.Load(shimCgroup)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load cgroup %s: %w", shimCgroup, err)
|
||||
}
|
||||
if err := cg.AddProc(uint64(cmd.Process.Pid)); err != nil {
|
||||
return nil, fmt.Errorf("failed to join cgroup %s: %w", shimCgroup, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := shim.AdjustOOMScore(cmd.Process.Pid); err != nil {
|
||||
return nil, fmt.Errorf("failed to adjust OOM score for shim: %w", err)
|
||||
}
|
||||
|
||||
params.Address = sockets[0].addr
|
||||
return ¶ms, nil
|
||||
}
|
||||
|
||||
// Stop is "shim delete". containerd runs it in a bundle once the shim that
|
||||
// served the bundle is gone: in the sandbox bundle, and in the bundle of every
|
||||
// container task that joined the shim. Stop does not tell the two apart and
|
||||
// runs both cleanups every time:
|
||||
// - The runc task cleanup of containerd-shim-runc-v2 force-deletes the runc
|
||||
// container of that id and detaches its rootfs. In the sandbox bundle of a
|
||||
// shared-PID pod that deletes containerd-sandbox-init; in any other
|
||||
// sandbox bundle it does nothing.
|
||||
// - The sandbox cleanup detaches every mount under the bundle and removes
|
||||
// the pod files. In a task bundle it does nothing.
|
||||
func (m *manager) Stop(ctx context.Context, id string) (shim.StopStatus, error) {
|
||||
status, err := m.legacy.Stop(ctx, id)
|
||||
cwd, cwdErr := os.Getwd()
|
||||
if cwdErr != nil {
|
||||
return status, errors.Join(err, cwdErr)
|
||||
}
|
||||
return status, errors.Join(err, sandbox.Cleanup(ctx, filepath.Join(filepath.Dir(cwd), id)))
|
||||
}
|
||||
|
||||
// Info returns the runtime info of containerd-shim-runc-v2: the same runtime
|
||||
// type, and the features of the same runc minus the user namespace. CRI
|
||||
// advertises user namespace support for a handler from that feature, and this
|
||||
// shim rejects user namespaced pods for now.
|
||||
func (m *manager) Info(ctx context.Context, optionsR io.Reader) (*types.RuntimeInfo, error) {
|
||||
info, err := m.legacy.Info(ctx, optionsR)
|
||||
if err != nil || info.GetFeatures() == nil {
|
||||
return info, err
|
||||
}
|
||||
var feat features.Features
|
||||
if err := typeurl.UnmarshalTo(info.Features, &feat); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode the runc features: %w", err)
|
||||
}
|
||||
if feat.Linux != nil {
|
||||
feat.Linux.Namespaces = slices.DeleteFunc(feat.Linux.Namespaces, func(ns string) bool { return ns == "user" })
|
||||
}
|
||||
if info.Features, err = typeurl.MarshalAnyToProto(&feat); err != nil {
|
||||
return nil, fmt.Errorf("failed to encode the runc features: %w", err)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/containerd/containerd/v2/pkg/namespaces"
|
||||
"github.com/containerd/containerd/v2/plugins"
|
||||
)
|
||||
|
||||
func TestManagerName(t *testing.T) {
|
||||
assert.Equal(t, plugins.RuntimeRuncV2, newManager().Name())
|
||||
}
|
||||
|
||||
func TestStopCleansUpTheBundle(t *testing.T) {
|
||||
// "shim delete" in a sandbox bundle removes what the sandbox created and
|
||||
// leaves what containerd created. The runc task cleanup that runs first
|
||||
// finds no container there and only logs about it.
|
||||
root := t.TempDir()
|
||||
const id = "sandbox-id"
|
||||
bundle := filepath.Join(root, id)
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(bundle, "ns"), 0o700))
|
||||
require.NoError(t, os.Mkdir(filepath.Join(bundle, "rootfs"), 0o700))
|
||||
for _, f := range []string{"hostname", "hosts", "resolv.conf", filepath.Join("ns", "ipc"), filepath.Join("ns", "uts")} {
|
||||
require.NoError(t, os.WriteFile(filepath.Join(bundle, f), nil, 0o600))
|
||||
}
|
||||
t.Chdir(bundle)
|
||||
|
||||
_, err := newManager().Stop(namespaces.WithNamespace(context.Background(), "test"), id)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, f := range []string{"hostname", "hosts", "resolv.conf", "ns"} {
|
||||
_, err := os.Stat(filepath.Join(bundle, f))
|
||||
assert.True(t, os.IsNotExist(err), "%s should be gone", f)
|
||||
}
|
||||
_, err = os.Stat(filepath.Join(bundle, "rootfs"))
|
||||
assert.NoError(t, err, "what containerd created is left alone")
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/moby/sys/mountinfo"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/mount"
|
||||
"github.com/containerd/containerd/v2/internal/cri/sandboxfiles"
|
||||
)
|
||||
|
||||
// Everything the sandbox creates lives under the shim bundle at fixed paths
|
||||
// (see the package doc):
|
||||
//
|
||||
// <bundle>/ns/{ipc,uts} namespace pins, and ns/pid for a shared-PID pod
|
||||
// <bundle>/hostname the pod hostname
|
||||
// <bundle>/hosts a copy of the host /etc/hosts
|
||||
// <bundle>/resolv.conf the pod DNS configuration
|
||||
// <bundle>/shm the pod /dev/shm tmpfs
|
||||
// <bundle>/rootfs the tmpfs rootfs of containerd-sandbox-init, with
|
||||
// its config.json and init.pid beside (shared-PID pods)
|
||||
|
||||
// setupFiles creates the pod shared files and returns the mounts that expose
|
||||
// them to the containers of the pod; CRI picks the sources up from the
|
||||
// sandbox spec.
|
||||
func setupFiles(bundle string, cfg *Config) ([]specs.Mount, error) {
|
||||
var mounts []specs.Mount
|
||||
bind := func(destination, source string) {
|
||||
mounts = append(mounts, specs.Mount{
|
||||
Destination: destination,
|
||||
Type: "bind",
|
||||
Source: source,
|
||||
Options: []string{"rbind"},
|
||||
})
|
||||
}
|
||||
|
||||
hostname := filepath.Join(bundle, sandboxfiles.HostnameFile)
|
||||
if err := os.WriteFile(hostname, sandboxfiles.HostnameContent(cfg.Hostname), 0o644); err != nil {
|
||||
return nil, fmt.Errorf("failed to write %s: %w", hostname, err)
|
||||
}
|
||||
bind(sandboxfiles.EtcHostname, hostname)
|
||||
|
||||
hosts := filepath.Join(bundle, sandboxfiles.HostsFile)
|
||||
if err := copyFile(sandboxfiles.EtcHosts, hosts); err != nil {
|
||||
return nil, fmt.Errorf("failed to create %s: %w", hosts, err)
|
||||
}
|
||||
bind(sandboxfiles.EtcHosts, hosts)
|
||||
|
||||
resolvConf := filepath.Join(bundle, sandboxfiles.ResolvConfFile)
|
||||
if cfg.DNS != nil {
|
||||
content := sandboxfiles.ResolvConfContent(cfg.DNS.GetServers(), cfg.DNS.GetSearches(), cfg.DNS.GetOptions())
|
||||
if err := os.WriteFile(resolvConf, content, 0o644); err != nil {
|
||||
return nil, fmt.Errorf("failed to write %s: %w", resolvConf, err)
|
||||
}
|
||||
} else {
|
||||
// No DNS config means the host resolver configuration, as for pause.
|
||||
if err := copyFile(sandboxfiles.ResolvConfPath, resolvConf); err != nil {
|
||||
return nil, fmt.Errorf("failed to create %s: %w", resolvConf, err)
|
||||
}
|
||||
}
|
||||
bind(sandboxfiles.ResolvConfPath, resolvConf)
|
||||
|
||||
if !cfg.HostIPC {
|
||||
shm := filepath.Join(bundle, sandboxfiles.ShmDir)
|
||||
if err := os.Mkdir(shm, 0o700); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||
return nil, fmt.Errorf("failed to create %s: %w", shm, err)
|
||||
}
|
||||
if err := unix.Mount("shm", shm, "tmpfs", unix.MS_NOEXEC|unix.MS_NOSUID|unix.MS_NODEV, sandboxfiles.ShmMountData(cfg.ShmSize)); err != nil {
|
||||
return nil, fmt.Errorf("failed to mount the pod shm at %s: %w", shm, err)
|
||||
}
|
||||
bind(sandboxfiles.DevShm, shm)
|
||||
}
|
||||
return mounts, nil
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
b, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(dst, b, 0o644)
|
||||
}
|
||||
|
||||
// Cleanup removes everything the sandbox shim created under bundle: it
|
||||
// detaches the mounts at their fixed paths (the namespace pins, the shm tmpfs
|
||||
// and the init rootfs) and removes the pin and shared files. It is idempotent
|
||||
// and relies on no state, so it serves create failure, StopSandbox and "shim
|
||||
// delete" alike; a mount left behind would keep containerd from deleting the
|
||||
// bundle. The init container itself is deleted through runc by its owner
|
||||
// (initProcess.stop, or the runc task cleanup of "shim delete").
|
||||
func Cleanup(ctx context.Context, bundle string) error {
|
||||
var errs []error
|
||||
for _, p := range []string{
|
||||
filepath.Join(bundle, pinDir, ipcPin),
|
||||
filepath.Join(bundle, pinDir, utsPin),
|
||||
filepath.Join(bundle, pinDir, pidPin),
|
||||
filepath.Join(bundle, sandboxfiles.ShmDir),
|
||||
filepath.Join(bundle, initRootfs),
|
||||
} {
|
||||
// Only a mount point is unmounted: umount(2) on a plain file needs
|
||||
// the same privilege as on a mount, and answers EPERM before EINVAL.
|
||||
mounted, err := mountinfo.Mounted(p)
|
||||
if err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
errs = append(errs, fmt.Errorf("failed to check %s: %w", p, err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !mounted {
|
||||
continue
|
||||
}
|
||||
if err := mount.UnmountAll(p, unix.MNT_DETACH); err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to detach %s: %w", p, err))
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
// Nothing is removed while one of the mounts is still in place.
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
for _, name := range []string{pinDir, sandboxfiles.ShmDir, sandboxfiles.HostnameFile, sandboxfiles.HostsFile, sandboxfiles.ResolvConfFile} {
|
||||
if err := os.RemoveAll(filepath.Join(bundle, name)); err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to remove %s: %w", name, err))
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/unix"
|
||||
criruntime "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
|
||||
"github.com/containerd/containerd/v2/pkg/testutil"
|
||||
)
|
||||
|
||||
func TestSetupFiles(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
ctx := context.Background()
|
||||
bundle := t.TempDir()
|
||||
t.Cleanup(func() { _ = Cleanup(ctx, bundle) })
|
||||
|
||||
cfg := &Config{
|
||||
ID: testID,
|
||||
Hostname: "pod-hostname",
|
||||
DNS: &criruntime.DNSConfig{
|
||||
Servers: []string{"10.0.0.10"},
|
||||
Searches: []string{"svc.cluster.local"},
|
||||
Options: []string{"ndots:5"},
|
||||
},
|
||||
ShmSize: 1 << 20,
|
||||
}
|
||||
mounts, err := setupFiles(bundle, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
byDestination := make(map[string]specs.Mount)
|
||||
for _, m := range mounts {
|
||||
byDestination[m.Destination] = m
|
||||
}
|
||||
require.Len(t, byDestination, 4)
|
||||
|
||||
content, err := os.ReadFile(byDestination["/etc/hostname"].Source)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "pod-hostname\n", string(content))
|
||||
|
||||
content, err = os.ReadFile(byDestination["/etc/resolv.conf"].Source)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "search svc.cluster.local\nnameserver 10.0.0.10\noptions ndots:5\n", string(content))
|
||||
|
||||
hostHosts, err := os.ReadFile("/etc/hosts")
|
||||
require.NoError(t, err)
|
||||
content, err = os.ReadFile(byDestination["/etc/hosts"].Source)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, string(hostHosts), string(content), "the pod hosts file starts as a copy of the host one")
|
||||
|
||||
var st unix.Statfs_t
|
||||
require.NoError(t, unix.Statfs(byDestination["/dev/shm"].Source, &st))
|
||||
assert.EqualValues(t, unix.TMPFS_MAGIC, st.Type, "the pod shm is a tmpfs")
|
||||
assert.EqualValues(t, 1<<20, st.Blocks*uint64(st.Bsize), "sized as requested")
|
||||
|
||||
require.NoError(t, Cleanup(ctx, bundle))
|
||||
assertNoMountsUnder(t, bundle)
|
||||
entries, err := os.ReadDir(bundle)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, entries)
|
||||
|
||||
// With the host IPC namespace the pod uses the host /dev/shm: no tmpfs.
|
||||
cfg.HostIPC = true
|
||||
cfg.DNS = nil
|
||||
mounts, err = setupFiles(bundle, cfg)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, mounts, 3)
|
||||
for _, m := range mounts {
|
||||
assert.NotEqual(t, "/dev/shm", m.Destination)
|
||||
}
|
||||
hostResolv, err := os.ReadFile("/etc/resolv.conf")
|
||||
require.NoError(t, err)
|
||||
content, err = os.ReadFile(filepath.Join(bundle, "resolv.conf"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, string(hostResolv), string(content), "no DNS config means the host resolver")
|
||||
}
|
||||
|
||||
func TestCleanupWithoutMounts(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
bundle := t.TempDir()
|
||||
|
||||
// Leftovers of a sandbox whose mounts are already gone (or that crashed
|
||||
// before mounting anything) are removed, and nothing else is touched.
|
||||
require.NoError(t, os.Mkdir(filepath.Join(bundle, pinDir), 0o700))
|
||||
require.NoError(t, os.Mkdir(filepath.Join(bundle, "shm"), 0o700))
|
||||
require.NoError(t, os.Mkdir(filepath.Join(bundle, "rootfs"), 0o700))
|
||||
for _, f := range []string{"hostname", "hosts", "resolv.conf", "ns/ipc", "ns/uts", "address"} {
|
||||
require.NoError(t, os.WriteFile(filepath.Join(bundle, f), []byte("x"), 0o600))
|
||||
}
|
||||
|
||||
require.NoError(t, Cleanup(ctx, bundle))
|
||||
|
||||
entries, err := os.ReadDir(bundle)
|
||||
require.NoError(t, err)
|
||||
var names []string
|
||||
for _, e := range entries {
|
||||
names = append(names, e.Name())
|
||||
}
|
||||
assert.ElementsMatch(t, []string{"address", "rootfs"}, names, "only what containerd owns is left")
|
||||
|
||||
// Idempotent, including on a bundle that no longer exists.
|
||||
require.NoError(t, Cleanup(ctx, bundle))
|
||||
require.NoError(t, Cleanup(ctx, filepath.Join(bundle, "does-not-exist")))
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
api "github.com/containerd/containerd/api/runtime/sandbox/v1"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/containerd/typeurl/v2"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
|
||||
"github.com/containerd/containerd/v2/internal/cri/sandboxfiles"
|
||||
)
|
||||
|
||||
// Config is what the sandbox shim needs to know about a pod. It is derived
|
||||
// from the CRI PodSandboxConfig that CRI passes as the sandbox options.
|
||||
type Config struct {
|
||||
// ID is the sandbox id.
|
||||
ID string
|
||||
// Hostname is the pod hostname, the node hostname when the pod sets none.
|
||||
Hostname string
|
||||
// HostNetwork means the pod uses the host network and UTS namespaces; the
|
||||
// sandbox then holds neither and NetNSPath is empty.
|
||||
HostNetwork bool
|
||||
// HostIPC means the pod uses the host IPC namespace and the host /dev/shm.
|
||||
HostIPC bool
|
||||
// HostPID means the containers of the pod use the host PID namespace.
|
||||
HostPID bool
|
||||
// SharedPID means the pod asked for one PID namespace shared by its
|
||||
// containers (shareProcessNamespace); containerd-sandbox-init is then
|
||||
// run as its PID 1 (see initProcess).
|
||||
SharedPID bool
|
||||
// NetNSPath is the network namespace CRI created for the pod.
|
||||
NetNSPath string
|
||||
// Sysctls is the effective set of namespaced sysctls to apply.
|
||||
Sysctls map[string]string
|
||||
// CgroupParent is the pod cgroup as a cgroup v2 group path: the sandbox
|
||||
// reports its metrics, and places the init of a shared-PID pod under it.
|
||||
// kubelet spells it that way with both cgroup drivers: the systemd
|
||||
// driver hands over the expanded slice path, which CRI shortens to its
|
||||
// base for runc.
|
||||
CgroupParent string
|
||||
// DNS is the pod DNS configuration; nil means the host resolv.conf.
|
||||
DNS *runtime.DNSConfig
|
||||
// ShmSize is the size of the pod /dev/shm tmpfs.
|
||||
ShmSize int64
|
||||
// Annotations are the annotations of the synthesized sandbox spec.
|
||||
Annotations map[string]string
|
||||
}
|
||||
|
||||
// ConfigFromRequest derives the sandbox configuration from a create request.
|
||||
// It rejects what this shim does not support yet with ErrNotImplemented so the
|
||||
// error is explicit rather than a pod that silently lacks isolation.
|
||||
func ConfigFromRequest(r *api.CreateSandboxRequest) (*Config, error) {
|
||||
id := r.GetSandboxID()
|
||||
if r.GetOptions() == nil {
|
||||
return nil, fmt.Errorf("sandbox %q: no options; %s expects the CRI PodSandboxConfig: %w", id, BinaryName, errdefs.ErrInvalidArgument)
|
||||
}
|
||||
// TODO: define a containerd-owned sandbox configuration message for the
|
||||
// handoff (hostname, namespace modes, the effective sysctls including the
|
||||
// enable_unprivileged_ports/enable_unprivileged_icmp defaults CRI applies
|
||||
// to pause, DNS, cgroup parent, labels) and have CRI send it as an
|
||||
// extension of the create request; a sandboxed shim should not depend on
|
||||
// the CRI layer and its PodSandboxConfig, nor re-derive CRI policy.
|
||||
var pc runtime.PodSandboxConfig
|
||||
if err := typeurl.UnmarshalTo(r.GetOptions(), &pc); err != nil {
|
||||
return nil, fmt.Errorf("sandbox %q: options are not a CRI PodSandboxConfig: %w", id, err)
|
||||
}
|
||||
return configFromPodSandboxConfig(id, r.GetNetnsPath(), r.GetAnnotations(), &pc)
|
||||
}
|
||||
|
||||
func configFromPodSandboxConfig(id, netns string, reqAnnotations map[string]string, pc *runtime.PodSandboxConfig) (*Config, error) {
|
||||
nsOpts := pc.GetLinux().GetSecurityContext().GetNamespaceOptions()
|
||||
cfg := &Config{
|
||||
ID: id,
|
||||
Hostname: pc.GetHostname(),
|
||||
HostNetwork: nsOpts.GetNetwork() == runtime.NamespaceMode_NODE,
|
||||
HostIPC: nsOpts.GetIpc() == runtime.NamespaceMode_NODE,
|
||||
HostPID: nsOpts.GetPid() == runtime.NamespaceMode_NODE,
|
||||
SharedPID: nsOpts.GetPid() == runtime.NamespaceMode_POD,
|
||||
NetNSPath: netns,
|
||||
CgroupParent: pc.GetLinux().GetCgroupParent(),
|
||||
DNS: pc.GetDnsConfig(),
|
||||
ShmSize: sandboxfiles.DefaultShmSize,
|
||||
}
|
||||
|
||||
if nsOpts.GetPid() == runtime.NamespaceMode_TARGET {
|
||||
return nil, fmt.Errorf("sandbox %q: PID namespace mode TARGET is not valid for a pod sandbox: %w", id, errdefs.ErrInvalidArgument)
|
||||
}
|
||||
if u := nsOpts.GetUsernsOptions(); u != nil && u.GetMode() != runtime.NamespaceMode_NODE {
|
||||
// The namespaces of a user namespaced pod must be owned by the pod
|
||||
// user namespace, which a multithreaded shim cannot enter; CRI has
|
||||
// to create them. That is a follow-up.
|
||||
return nil, fmt.Errorf("sandbox %q: user namespaces are not supported yet by %s: %w", id, BinaryName, errdefs.ErrNotImplemented)
|
||||
}
|
||||
|
||||
if cfg.HostNetwork {
|
||||
cfg.NetNSPath = ""
|
||||
} else if cfg.NetNSPath == "" {
|
||||
return nil, fmt.Errorf("sandbox %q: no network namespace path for a pod with its own network namespace: %w", id, errdefs.ErrInvalidArgument)
|
||||
}
|
||||
|
||||
if cfg.Hostname == "" {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sandbox %q: failed to get the node hostname: %w", id, err)
|
||||
}
|
||||
cfg.Hostname = hostname
|
||||
}
|
||||
|
||||
sysctls, err := effectiveSysctls(pc.GetLinux().GetSysctls(), cfg.HostNetwork, cfg.HostIPC)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sandbox %q: %w", id, err)
|
||||
}
|
||||
cfg.Sysctls = sysctls
|
||||
cfg.Annotations = reqAnnotations
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// Sysctls are applied by the shim thread that created the pod namespaces (for
|
||||
// pause, runc applied them inside the pause container), so only namespaced
|
||||
// sysctls of namespaces the pod owns can be honored.
|
||||
|
||||
const sysctlRoot = "/proc/sys"
|
||||
|
||||
// defaultSysctls are applied to pods with their own network namespace unless
|
||||
// the pod sets them, matching what containerd applies to pause sandboxes with
|
||||
// its default CRI configuration (enable_unprivileged_ports and
|
||||
// enable_unprivileged_icmp both on).
|
||||
var defaultSysctls = map[string]string{
|
||||
"net.ipv4.ip_unprivileged_port_start": "0",
|
||||
"net.ipv4.ping_group_range": "0 2147483647",
|
||||
}
|
||||
|
||||
// validateSysctl accepts the namespaced sysctls the shim can apply from a host
|
||||
// thread that entered the pod namespaces, and rejects the rest: user.* is
|
||||
// scoped to a user namespace the shim never enters, a sysctl of a namespace
|
||||
// the pod shares with the host would change the host, and anything else is
|
||||
// not namespaced at all. It returns the key in dotted form, the form it
|
||||
// classified and the one that names the file (see sysctlPath).
|
||||
func validateSysctl(key string, hostNetwork, hostIPC bool) (string, error) {
|
||||
_, dotted, err := sysctlPath(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch {
|
||||
case strings.HasPrefix(dotted, "net."):
|
||||
if hostNetwork {
|
||||
return "", fmt.Errorf("sysctl %q is not allowed for a pod using the host network: %w", key, errdefs.ErrInvalidArgument)
|
||||
}
|
||||
case strings.HasPrefix(dotted, "kernel.shm"), strings.HasPrefix(dotted, "kernel.msg"), dotted == "kernel.sem", strings.HasPrefix(dotted, "fs.mqueue."):
|
||||
if hostIPC {
|
||||
return "", fmt.Errorf("sysctl %q is not allowed for a pod using the host IPC namespace: %w", key, errdefs.ErrInvalidArgument)
|
||||
}
|
||||
case dotted == "kernel.hostname", dotted == "kernel.domainname":
|
||||
if hostNetwork {
|
||||
return "", fmt.Errorf("sysctl %q is not allowed for a pod using the host network (and UTS) namespace: %w", key, errdefs.ErrInvalidArgument)
|
||||
}
|
||||
case strings.HasPrefix(dotted, "user."):
|
||||
return "", fmt.Errorf("sysctl %q: user.* sysctls are not supported by %s: %w", key, BinaryName, errdefs.ErrNotImplemented)
|
||||
default:
|
||||
return "", fmt.Errorf("sysctl %q is not namespaced and cannot be set for a pod: %w", key, errdefs.ErrInvalidArgument)
|
||||
}
|
||||
return dotted, nil
|
||||
}
|
||||
|
||||
// effectiveSysctls validates the pod sysctls and adds the defaults. The keys
|
||||
// of the result are in dotted form.
|
||||
func effectiveSysctls(pod map[string]string, hostNetwork, hostIPC bool) (map[string]string, error) {
|
||||
out := make(map[string]string, len(pod)+len(defaultSysctls))
|
||||
for k, v := range pod {
|
||||
dotted, err := validateSysctl(k, hostNetwork, hostIPC)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[dotted] = v
|
||||
}
|
||||
if !hostNetwork {
|
||||
for k, v := range defaultSysctls {
|
||||
if _, ok := out[k]; !ok {
|
||||
out[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// sysctlPath maps a sysctl key to its file under /proc/sys and returns the key
|
||||
// in dotted form, the form validateSysctl classifies. Keys are spelled as in
|
||||
// sysctl.d(5), which is what kubelet passes through and runc accepts: the
|
||||
// first separator, "." or "/", separates the components and the other
|
||||
// character is literal. The rp_filter of the interface eth0.100 is therefore
|
||||
// "net.ipv4.conf.eth0/100.rp_filter" or "net/ipv4/conf/eth0.100/rp_filter",
|
||||
// the file /proc/sys/net/ipv4/conf/eth0.100/rp_filter. Every component must
|
||||
// be a name: with "." or ".." the file would be of another class than the
|
||||
// dotted form says.
|
||||
func sysctlPath(key string) (path, dotted string, err error) {
|
||||
swap := func(r rune) rune {
|
||||
switch r {
|
||||
case '.':
|
||||
return '/'
|
||||
case '/':
|
||||
return '.'
|
||||
}
|
||||
return r
|
||||
}
|
||||
dotted = key
|
||||
if i := strings.IndexAny(key, "./"); i >= 0 && key[i] == '/' {
|
||||
dotted = strings.Map(swap, key)
|
||||
}
|
||||
rel := strings.Map(swap, dotted)
|
||||
for c := range strings.SplitSeq(rel, "/") {
|
||||
if c == "" || c == "." || c == ".." || strings.ContainsRune(c, 0) {
|
||||
return "", "", fmt.Errorf("invalid sysctl key %q: %w", key, errdefs.ErrInvalidArgument)
|
||||
}
|
||||
}
|
||||
return sysctlRoot + "/" + rel, dotted, nil
|
||||
}
|
||||
|
||||
// buildSpec synthesizes the sandbox spec CRI and NRI consume. It is not the
|
||||
// spec of a process: it describes the namespaces the sandbox holds, from what
|
||||
// was actually pinned, the sysctls applied to them, and the pod shared files
|
||||
// as mounts. A namespace the pod shares with the host has no entry. Linux is
|
||||
// always set, which is how CRI tells a sandbox without a process from an
|
||||
// older Sandbox API shim that returns no spec.
|
||||
//
|
||||
// The PID namespace entry has a path only for a pod that shares its PID
|
||||
// namespace (NamespaceMode_POD, the CRI default and kubelet's
|
||||
// shareProcessNamespace): pidNS, the pinned namespace containerd-sandbox-init
|
||||
// is PID 1 of. Otherwise the sandbox holds no PID namespace, and the entry, without a
|
||||
// path, declares that each container gets a new one of its own; CRI honors
|
||||
// the declaration. A host PID pod has no entry.
|
||||
func buildSpec(cfg *Config, pins Pins, mounts []specs.Mount, pidNS string) *specs.Spec {
|
||||
s := &specs.Spec{
|
||||
Version: specs.Version,
|
||||
Hostname: cfg.Hostname,
|
||||
Annotations: cfg.Annotations,
|
||||
Mounts: mounts,
|
||||
Linux: &specs.Linux{
|
||||
Sysctl: cfg.Sysctls,
|
||||
Namespaces: podNamespaces(cfg, pins),
|
||||
},
|
||||
}
|
||||
switch {
|
||||
case cfg.HostPID:
|
||||
case pidNS != "":
|
||||
s.Linux.Namespaces = append(s.Linux.Namespaces, specs.LinuxNamespace{Type: specs.PIDNamespace, Path: pidNS})
|
||||
default:
|
||||
s.Linux.Namespaces = append(s.Linux.Namespaces, specs.LinuxNamespace{Type: specs.PIDNamespace})
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// podNamespaces are the network, IPC and UTS namespaces the sandbox holds,
|
||||
// which the sandbox spec lists and containerd-sandbox-init joins.
|
||||
func podNamespaces(cfg *Config, pins Pins) []specs.LinuxNamespace {
|
||||
var ns []specs.LinuxNamespace
|
||||
if cfg.NetNSPath != "" {
|
||||
ns = append(ns, specs.LinuxNamespace{Type: specs.NetworkNamespace, Path: cfg.NetNSPath})
|
||||
}
|
||||
if pins.IPC != "" {
|
||||
ns = append(ns, specs.LinuxNamespace{Type: specs.IPCNamespace, Path: pins.IPC})
|
||||
}
|
||||
if pins.UTS != "" {
|
||||
ns = append(ns, specs.LinuxNamespace{Type: specs.UTSNamespace, Path: pins.UTS})
|
||||
}
|
||||
return ns
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
api "github.com/containerd/containerd/api/runtime/sandbox/v1"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/containerd/typeurl/v2"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
testID = "sandbox-id"
|
||||
testNetNS = "/var/run/netns/cni-test"
|
||||
)
|
||||
|
||||
func testPodConfig() *runtime.PodSandboxConfig {
|
||||
return &runtime.PodSandboxConfig{
|
||||
Metadata: &runtime.PodSandboxMetadata{
|
||||
Name: "pod",
|
||||
Uid: "pod-uid",
|
||||
Namespace: "pod-ns",
|
||||
},
|
||||
Hostname: "pod-hostname",
|
||||
LogDirectory: "/var/log/pods/pod",
|
||||
DnsConfig: &runtime.DNSConfig{
|
||||
Servers: []string{"10.0.0.10"},
|
||||
},
|
||||
Linux: &runtime.LinuxPodSandboxConfig{
|
||||
CgroupParent: "/kubepods/pod",
|
||||
SecurityContext: &runtime.LinuxSandboxSecurityContext{
|
||||
NamespaceOptions: &runtime.NamespaceOption{},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createRequest(t *testing.T, pc *runtime.PodSandboxConfig, netns string) *api.CreateSandboxRequest {
|
||||
t.Helper()
|
||||
req := &api.CreateSandboxRequest{
|
||||
SandboxID: testID,
|
||||
BundlePath: "/bundle",
|
||||
NetnsPath: netns,
|
||||
Annotations: map[string]string{"extra": "annotation"},
|
||||
}
|
||||
if pc != nil {
|
||||
any, err := typeurl.MarshalAnyToProto(pc)
|
||||
require.NoError(t, err)
|
||||
req.Options = any
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func TestConfigFromRequest(t *testing.T) {
|
||||
t.Run("options are required", func(t *testing.T) {
|
||||
_, err := ConfigFromRequest(createRequest(t, nil, testNetNS))
|
||||
require.Error(t, err)
|
||||
assert.True(t, errdefs.IsInvalidArgument(err))
|
||||
})
|
||||
|
||||
t.Run("pod namespaces", func(t *testing.T) {
|
||||
cfg, err := ConfigFromRequest(createRequest(t, testPodConfig(), testNetNS))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testID, cfg.ID)
|
||||
assert.Equal(t, "pod-hostname", cfg.Hostname)
|
||||
assert.False(t, cfg.HostNetwork)
|
||||
assert.False(t, cfg.HostIPC)
|
||||
assert.False(t, cfg.HostPID)
|
||||
assert.Equal(t, testNetNS, cfg.NetNSPath)
|
||||
assert.Equal(t, "/kubepods/pod", cfg.CgroupParent)
|
||||
assert.Equal(t, []string{"10.0.0.10"}, cfg.DNS.GetServers())
|
||||
assert.Equal(t, defaultSysctls, cfg.Sysctls, "the pause defaults apply to a pod with its own network namespace")
|
||||
assert.Equal(t, map[string]string{"extra": "annotation"}, cfg.Annotations, "the annotations of the request")
|
||||
})
|
||||
|
||||
t.Run("pod network requires a network namespace path", func(t *testing.T) {
|
||||
_, err := ConfigFromRequest(createRequest(t, testPodConfig(), ""))
|
||||
require.Error(t, err)
|
||||
assert.True(t, errdefs.IsInvalidArgument(err))
|
||||
})
|
||||
|
||||
t.Run("host network", func(t *testing.T) {
|
||||
pc := testPodConfig()
|
||||
pc.Linux.SecurityContext.NamespaceOptions.Network = runtime.NamespaceMode_NODE
|
||||
cfg, err := ConfigFromRequest(createRequest(t, pc, ""))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, cfg.HostNetwork)
|
||||
assert.Empty(t, cfg.NetNSPath)
|
||||
assert.Empty(t, cfg.Sysctls, "no network defaults for a pod in the host network namespace")
|
||||
|
||||
pc.Linux.Sysctls = map[string]string{"net.ipv4.ip_forward": "1"}
|
||||
_, err = ConfigFromRequest(createRequest(t, pc, ""))
|
||||
require.Error(t, err, "a net sysctl would change the host")
|
||||
assert.True(t, errdefs.IsInvalidArgument(err))
|
||||
})
|
||||
|
||||
t.Run("host ipc", func(t *testing.T) {
|
||||
pc := testPodConfig()
|
||||
pc.Linux.SecurityContext.NamespaceOptions.Ipc = runtime.NamespaceMode_NODE
|
||||
cfg, err := ConfigFromRequest(createRequest(t, pc, testNetNS))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, cfg.HostIPC)
|
||||
|
||||
pc.Linux.Sysctls = map[string]string{"kernel.shm_rmid_forced": "1"}
|
||||
_, err = ConfigFromRequest(createRequest(t, pc, testNetNS))
|
||||
require.Error(t, err, "an IPC sysctl would change the host")
|
||||
assert.True(t, errdefs.IsInvalidArgument(err))
|
||||
})
|
||||
|
||||
t.Run("host pid", func(t *testing.T) {
|
||||
pc := testPodConfig()
|
||||
pc.Linux.SecurityContext.NamespaceOptions.Pid = runtime.NamespaceMode_NODE
|
||||
cfg, err := ConfigFromRequest(createRequest(t, pc, testNetNS))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, cfg.HostPID)
|
||||
})
|
||||
|
||||
t.Run("pod sysctls win over the defaults", func(t *testing.T) {
|
||||
pc := testPodConfig()
|
||||
pc.Linux.Sysctls = map[string]string{
|
||||
"net.ipv4.ip_unprivileged_port_start": "1024",
|
||||
"kernel.shm_rmid_forced": "1",
|
||||
"fs.mqueue.msg_max": "20",
|
||||
}
|
||||
cfg, err := ConfigFromRequest(createRequest(t, pc, testNetNS))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[string]string{
|
||||
"net.ipv4.ip_unprivileged_port_start": "1024",
|
||||
"net.ipv4.ping_group_range": "0 2147483647",
|
||||
"kernel.shm_rmid_forced": "1",
|
||||
"fs.mqueue.msg_max": "20",
|
||||
}, cfg.Sysctls)
|
||||
})
|
||||
|
||||
t.Run("slash separated sysctls", func(t *testing.T) {
|
||||
// kubelet passes sysctl names with either separator through as
|
||||
// written; they are classified and kept in dotted form.
|
||||
pc := testPodConfig()
|
||||
pc.Linux.Sysctls = map[string]string{"net/ipv4/ping_group_range": "1 1", "kernel/shm_rmid_forced": "1"}
|
||||
cfg, err := ConfigFromRequest(createRequest(t, pc, testNetNS))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[string]string{
|
||||
"net.ipv4.ip_unprivileged_port_start": "0",
|
||||
"net.ipv4.ping_group_range": "1 1",
|
||||
"kernel.shm_rmid_forced": "1",
|
||||
}, cfg.Sysctls)
|
||||
|
||||
pc.Linux.SecurityContext.NamespaceOptions.Ipc = runtime.NamespaceMode_NODE
|
||||
_, err = ConfigFromRequest(createRequest(t, pc, testNetNS))
|
||||
require.Error(t, err, "an IPC sysctl would change the host, however it is spelled")
|
||||
assert.True(t, errdefs.IsInvalidArgument(err))
|
||||
})
|
||||
|
||||
t.Run("shared pid namespace is accepted but not shared", func(t *testing.T) {
|
||||
// NamespaceMode_POD is the zero value, so it is what a pod config
|
||||
// without explicit namespace options (crictl, critest) asks for.
|
||||
pc := testPodConfig()
|
||||
pc.Linux.SecurityContext.NamespaceOptions.Pid = runtime.NamespaceMode_POD
|
||||
cfg, err := ConfigFromRequest(createRequest(t, pc, testNetNS))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, cfg.SharedPID)
|
||||
|
||||
pc.Linux.SecurityContext.NamespaceOptions.Pid = runtime.NamespaceMode_CONTAINER
|
||||
cfg, err = ConfigFromRequest(createRequest(t, pc, testNetNS))
|
||||
require.NoError(t, err)
|
||||
assert.False(t, cfg.SharedPID)
|
||||
})
|
||||
|
||||
t.Run("target pid namespace is invalid for a sandbox", func(t *testing.T) {
|
||||
pc := testPodConfig()
|
||||
pc.Linux.SecurityContext.NamespaceOptions.Pid = runtime.NamespaceMode_TARGET
|
||||
_, err := ConfigFromRequest(createRequest(t, pc, testNetNS))
|
||||
require.Error(t, err)
|
||||
assert.True(t, errdefs.IsInvalidArgument(err))
|
||||
})
|
||||
|
||||
t.Run("user namespaces are not supported yet", func(t *testing.T) {
|
||||
pc := testPodConfig()
|
||||
pc.Linux.SecurityContext.NamespaceOptions.UsernsOptions = &runtime.UserNamespace{Mode: runtime.NamespaceMode_POD}
|
||||
_, err := ConfigFromRequest(createRequest(t, pc, testNetNS))
|
||||
require.Error(t, err)
|
||||
assert.True(t, errdefs.IsNotImplemented(err))
|
||||
|
||||
pc.Linux.SecurityContext.NamespaceOptions.UsernsOptions = &runtime.UserNamespace{Mode: runtime.NamespaceMode_NODE}
|
||||
_, err = ConfigFromRequest(createRequest(t, pc, testNetNS))
|
||||
require.NoError(t, err, "the host user namespace is the default")
|
||||
})
|
||||
|
||||
t.Run("unsupported sysctls", func(t *testing.T) {
|
||||
pc := testPodConfig()
|
||||
pc.Linux.Sysctls = map[string]string{"user.max_user_namespaces": "10"}
|
||||
_, err := ConfigFromRequest(createRequest(t, pc, testNetNS))
|
||||
require.Error(t, err)
|
||||
assert.True(t, errdefs.IsNotImplemented(err), "user.* needs the pod user namespace")
|
||||
|
||||
pc.Linux.Sysctls = map[string]string{"vm.swappiness": "10"}
|
||||
_, err = ConfigFromRequest(createRequest(t, pc, testNetNS))
|
||||
require.Error(t, err)
|
||||
assert.True(t, errdefs.IsInvalidArgument(err), "not namespaced")
|
||||
|
||||
pc.Linux.Sysctls = map[string]string{".": "x"}
|
||||
_, err = ConfigFromRequest(createRequest(t, pc, testNetNS))
|
||||
require.Error(t, err)
|
||||
assert.True(t, errdefs.IsInvalidArgument(err), "not a key")
|
||||
})
|
||||
|
||||
t.Run("hostname defaults to the node hostname", func(t *testing.T) {
|
||||
pc := testPodConfig()
|
||||
pc.Hostname = ""
|
||||
cfg, err := ConfigFromRequest(createRequest(t, pc, testNetNS))
|
||||
require.NoError(t, err)
|
||||
hostname, err := os.Hostname()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, hostname, cfg.Hostname)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSysctlPath(t *testing.T) {
|
||||
// Either separator, as in sysctl.d(5): the first one separates the
|
||||
// components and the other character is literal, which is how a dotted
|
||||
// interface name is spelled. The dotted form is what gets classified.
|
||||
for key, want := range map[string][2]string{
|
||||
"net.ipv4.ip_unprivileged_port_start": {"/proc/sys/net/ipv4/ip_unprivileged_port_start", "net.ipv4.ip_unprivileged_port_start"},
|
||||
"net/ipv4/ip_unprivileged_port_start": {"/proc/sys/net/ipv4/ip_unprivileged_port_start", "net.ipv4.ip_unprivileged_port_start"},
|
||||
"net.ipv4.conf.eth0/100.rp_filter": {"/proc/sys/net/ipv4/conf/eth0.100/rp_filter", "net.ipv4.conf.eth0/100.rp_filter"},
|
||||
"net/ipv4/conf/eth0.100/rp_filter": {"/proc/sys/net/ipv4/conf/eth0.100/rp_filter", "net.ipv4.conf.eth0/100.rp_filter"},
|
||||
"kernel/shm_rmid_forced": {"/proc/sys/kernel/shm_rmid_forced", "kernel.shm_rmid_forced"},
|
||||
} {
|
||||
p, dotted, err := sysctlPath(key)
|
||||
require.NoError(t, err, "key %q", key)
|
||||
assert.Equal(t, want[0], p, "key %q", key)
|
||||
assert.Equal(t, want[1], dotted, "key %q", key)
|
||||
}
|
||||
|
||||
// No component may be empty, "." or "..": the file would not be under
|
||||
// the directory the dotted form names. "net.//.kernel.shm_rmid_forced"
|
||||
// (net/../kernel/shm_rmid_forced with slashes) is an IPC sysctl spelled
|
||||
// as a net one. A NUL byte cannot be part of a path either.
|
||||
for _, key := range []string{"", ".", "..", "/", "net.", ".net.ipv4.ip_forward", "net..ipv4.ip_forward", "net.//.kernel.shm_rmid_forced", "net/../kernel/shm_rmid_forced", "net./../kernel.shm_rmid_forced", "net.\x00"} {
|
||||
_, _, err := sysctlPath(key)
|
||||
assert.ErrorIs(t, err, errdefs.ErrInvalidArgument, "key %q", key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSysctl(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
key string
|
||||
hostNetwork, hostIPC bool
|
||||
dotted string
|
||||
}{
|
||||
{key: "net.ipv4.ip_forward", dotted: "net.ipv4.ip_forward"},
|
||||
{key: "kernel.shm_rmid_forced", dotted: "kernel.shm_rmid_forced"},
|
||||
{key: "kernel.sem", dotted: "kernel.sem"},
|
||||
{key: "fs.mqueue.msg_max", dotted: "fs.mqueue.msg_max"},
|
||||
{key: "kernel.hostname", dotted: "kernel.hostname"},
|
||||
{key: "kernel/shm_rmid_forced", dotted: "kernel.shm_rmid_forced"},
|
||||
{key: "net/ipv4/conf/eth0.100/rp_filter", dotted: "net.ipv4.conf.eth0/100.rp_filter"},
|
||||
// IPC sysctls are fine with the host network, net sysctls with the
|
||||
// host IPC namespace.
|
||||
{key: "kernel.shm_rmid_forced", hostNetwork: true, dotted: "kernel.shm_rmid_forced"},
|
||||
{key: "net.ipv4.ip_forward", hostIPC: true, dotted: "net.ipv4.ip_forward"},
|
||||
} {
|
||||
dotted, err := validateSysctl(tc.key, tc.hostNetwork, tc.hostIPC)
|
||||
require.NoError(t, err, "key %q", tc.key)
|
||||
assert.Equal(t, tc.dotted, dotted, "key %q", tc.key)
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
key string
|
||||
hostNetwork, hostIPC bool
|
||||
wantErr error
|
||||
}{
|
||||
{key: "net.ipv4.ip_forward", hostNetwork: true, wantErr: errdefs.ErrInvalidArgument},
|
||||
{key: "net/ipv4/ip_forward", hostNetwork: true, wantErr: errdefs.ErrInvalidArgument},
|
||||
// The UTS namespace is shared with the host along with the network one.
|
||||
{key: "kernel.hostname", hostNetwork: true, wantErr: errdefs.ErrInvalidArgument},
|
||||
{key: "kernel.domainname", hostNetwork: true, wantErr: errdefs.ErrInvalidArgument},
|
||||
{key: "kernel.shm_rmid_forced", hostIPC: true, wantErr: errdefs.ErrInvalidArgument},
|
||||
{key: "kernel/shm_rmid_forced", hostIPC: true, wantErr: errdefs.ErrInvalidArgument},
|
||||
{key: "kernel.msgmax", hostIPC: true, wantErr: errdefs.ErrInvalidArgument},
|
||||
{key: "fs.mqueue.msg_max", hostIPC: true, wantErr: errdefs.ErrInvalidArgument},
|
||||
// Like runc, only kernel.sem itself counts as an IPC sysctl.
|
||||
{key: "kernel.sem_next_id", wantErr: errdefs.ErrInvalidArgument},
|
||||
{key: "user.max_user_namespaces", wantErr: errdefs.ErrNotImplemented},
|
||||
{key: "vm.swappiness", wantErr: errdefs.ErrInvalidArgument},
|
||||
{key: "net.\x00", wantErr: errdefs.ErrInvalidArgument},
|
||||
} {
|
||||
_, err := validateSysctl(tc.key, tc.hostNetwork, tc.hostIPC)
|
||||
assert.ErrorIs(t, err, tc.wantErr, "key %q, host network %v, host IPC %v", tc.key, tc.hostNetwork, tc.hostIPC)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSpec(t *testing.T) {
|
||||
pins := Pins{IPC: "/bundle/ns/ipc", UTS: "/bundle/ns/uts"}
|
||||
mounts := []specs.Mount{{Destination: "/etc/hosts", Type: "bind", Source: "/bundle/hosts", Options: []string{"rbind"}}}
|
||||
sysctls := map[string]string{"net.ipv4.ip_forward": "1"}
|
||||
annots := map[string]string{"io.kubernetes.cri.sandbox-id": testID}
|
||||
|
||||
t.Run("pod namespaces", func(t *testing.T) {
|
||||
cfg := &Config{ID: testID, Hostname: "pod", NetNSPath: testNetNS, Sysctls: sysctls, Annotations: annots}
|
||||
spec := buildSpec(cfg, pins, mounts, "")
|
||||
assert.Equal(t, specs.Version, spec.Version)
|
||||
assert.Equal(t, "pod", spec.Hostname)
|
||||
assert.Equal(t, annots, spec.Annotations)
|
||||
assert.Equal(t, mounts, spec.Mounts)
|
||||
require.NotNil(t, spec.Linux)
|
||||
assert.Equal(t, sysctls, spec.Linux.Sysctl)
|
||||
assert.Equal(t, []specs.LinuxNamespace{
|
||||
{Type: specs.NetworkNamespace, Path: testNetNS},
|
||||
{Type: specs.IPCNamespace, Path: pins.IPC},
|
||||
{Type: specs.UTSNamespace, Path: pins.UTS},
|
||||
{Type: specs.PIDNamespace},
|
||||
}, spec.Linux.Namespaces)
|
||||
})
|
||||
|
||||
t.Run("shared PID namespace", func(t *testing.T) {
|
||||
cfg := &Config{ID: testID, Hostname: "pod", NetNSPath: testNetNS, SharedPID: true}
|
||||
spec := buildSpec(cfg, pins, nil, "/proc/42/ns/pid")
|
||||
assert.Equal(t, []specs.LinuxNamespace{
|
||||
{Type: specs.NetworkNamespace, Path: testNetNS},
|
||||
{Type: specs.IPCNamespace, Path: pins.IPC},
|
||||
{Type: specs.UTSNamespace, Path: pins.UTS},
|
||||
{Type: specs.PIDNamespace, Path: "/proc/42/ns/pid"},
|
||||
}, spec.Linux.Namespaces)
|
||||
})
|
||||
|
||||
t.Run("host network keeps only the IPC namespace", func(t *testing.T) {
|
||||
cfg := &Config{ID: testID, Hostname: "node", HostNetwork: true}
|
||||
spec := buildSpec(cfg, Pins{IPC: pins.IPC}, nil, "")
|
||||
assert.Equal(t, []specs.LinuxNamespace{{Type: specs.IPCNamespace, Path: pins.IPC}, {Type: specs.PIDNamespace}}, spec.Linux.Namespaces)
|
||||
})
|
||||
|
||||
t.Run("host ipc keeps the network and UTS namespaces", func(t *testing.T) {
|
||||
cfg := &Config{ID: testID, Hostname: "pod", HostIPC: true, NetNSPath: testNetNS}
|
||||
spec := buildSpec(cfg, Pins{UTS: pins.UTS}, nil, "")
|
||||
assert.Equal(t, []specs.LinuxNamespace{
|
||||
{Type: specs.NetworkNamespace, Path: testNetNS},
|
||||
{Type: specs.UTSNamespace, Path: pins.UTS},
|
||||
{Type: specs.PIDNamespace},
|
||||
}, spec.Linux.Namespaces)
|
||||
})
|
||||
|
||||
t.Run("all host namespaces still carry a Linux section", func(t *testing.T) {
|
||||
cfg := &Config{ID: testID, Hostname: "node", HostNetwork: true, HostIPC: true, HostPID: true}
|
||||
spec := buildSpec(cfg, Pins{}, nil, "")
|
||||
require.NotNil(t, spec.Linux, "CRI tells a processless sandbox by the Linux section")
|
||||
assert.Empty(t, spec.Linux.Namespaces, "no PID namespace declaration for a host PID pod")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package sandbox implements the Sandbox API (runtime.sandbox.v1) for a pod
|
||||
// that has no pause container.
|
||||
//
|
||||
// # Configuration
|
||||
//
|
||||
// The shim serves the io.containerd.runc.v2 runtime type from a different
|
||||
// binary, so CRI treats the handler exactly as runc (runc options, features,
|
||||
// cgroup driver); the handler names the binary and the sandboxer:
|
||||
//
|
||||
// [plugins."io.containerd.cri.v1.runtime".containerd.runtimes.runc]
|
||||
// runtime_type = "io.containerd.runc.v2"
|
||||
// runtime_path = "/usr/local/bin/containerd-shim-sandboxed-runc-v2"
|
||||
// sandboxer = "shim"
|
||||
// disable_pause_image_pull = true
|
||||
//
|
||||
// # What a pause sandbox does today
|
||||
//
|
||||
// With the podsandbox controller a pod sandbox is a container: CRI pulls the
|
||||
// pause image, builds an OCI spec for it (hostname, namespaces, sysctls,
|
||||
// cgroup, security options), creates the pod shared files under the CRI state
|
||||
// directory (<state>/sandboxes/<id>/{hostname,hosts,resolv.conf,shm}) and runs
|
||||
// the pause process through the runc shim. The pause process exists only to
|
||||
// keep the pod network, IPC and UTS namespaces alive: containers of the pod
|
||||
// join /proc/<pause pid>/ns/{net,ipc,uts} and CRI bind-mounts the shared
|
||||
// files, which it created itself, into every container.
|
||||
//
|
||||
// # What this package does instead
|
||||
//
|
||||
// The shim is the sandbox. CreateSandbox receives the CRI PodSandboxConfig
|
||||
// (as the sandbox options) and the network namespace CRI created and
|
||||
// configured with CNI, and sets the pod up, without a process unless the pod
|
||||
// shares its PID namespace:
|
||||
//
|
||||
// - Namespaces (namespaces.go). A locked OS thread enters the CRI network
|
||||
// namespace, unshares an IPC and a UTS namespace, sets the pod hostname,
|
||||
// writes the pod sysctls (net.* land in the pod network namespace,
|
||||
// kernel.shm*, kernel.msg*, kernel.sem and fs.mqueue.* in the pod IPC
|
||||
// namespace) and bind-mounts its /proc/self/task/<tid>/ns/{ipc,uts} onto
|
||||
// files under <bundle>/ns. The thread is then discarded; the bind mounts,
|
||||
// the pins, keep the namespaces alive the same way CRI keeps the network
|
||||
// namespace alive under /var/run/netns. A namespace the pod shares with
|
||||
// the host (hostNetwork, hostIPC) is neither created nor pinned.
|
||||
//
|
||||
// - Shared files (bundle.go). The shim writes hostname, copies the host
|
||||
// hosts file, writes resolv.conf from the pod DNS config (or copies the
|
||||
// host one) and mounts the pod shm tmpfs, all under its bundle instead of
|
||||
// the CRI state directory, and lists them as Mounts of the sandbox spec.
|
||||
//
|
||||
// - Shared PID namespace (init.go). A pod that shares its PID namespace
|
||||
// (shareProcessNamespace) needs a durable PID 1 for it, the role pause
|
||||
// plays: the shim runs containerd-sandbox-init through runc, in the pod
|
||||
// cgroup and the pod namespaces, and the spec names its PID namespace
|
||||
// for the containers to join. Every other pod has no sandbox process.
|
||||
//
|
||||
// - Spec (buildSpec in config.go). StartSandbox returns pid 0 and a
|
||||
// synthesized OCI spec that carries the namespace paths, the sysctls, the
|
||||
// shared file mounts and the annotations of the request. It describes what
|
||||
// the sandbox holds; there is no process it could describe.
|
||||
//
|
||||
// CRI consumes that spec where it used the pause pid before: a sandbox
|
||||
// reporting pid 0 with a Linux section in its spec joins containers to the
|
||||
// namespace paths of the spec (a namespace missing from the spec is the host
|
||||
// one when the pod asked for it, and an error otherwise), and bind-mounts the
|
||||
// shared files from the spec mount sources when nothing exists at the fixed
|
||||
// CRI paths. Sandboxes with a pid, and older Sandbox API shims returning no
|
||||
// spec, keep the pid derived paths unchanged.
|
||||
//
|
||||
// # Lifetime and cleanup
|
||||
//
|
||||
// One shim process serves one pod: its sandbox and, through the wrapped task
|
||||
// service of containerd-shim-runc-v2 (task.go in package main), the runc tasks
|
||||
// of its containers. StopSandbox releases what the sandbox holds and
|
||||
// ShutdownSandbox ends the shim; CRI stops and removes the containers of a
|
||||
// pod before either. Everything the shim creates lives under its bundle at
|
||||
// fixed paths, so Cleanup needs no state and runs the same way on create
|
||||
// failure, on stop and from "shim delete".
|
||||
//
|
||||
// # Not supported yet
|
||||
//
|
||||
// User namespaced pods need their namespaces created inside the
|
||||
// pod user namespace, which a multithreaded shim cannot enter; they are
|
||||
// rejected, as are user.* sysctls. The shim requires cgroup v2 and does not
|
||||
// run rootless. SELinux labels are not applied yet: the pod label allocation
|
||||
// moves into the CRI layer with
|
||||
// https://github.com/containerd/containerd/pull/14108, after which the shim
|
||||
// applies the labels it is handed (see the TODO in CreateSandbox).
|
||||
package sandbox
|
||||
@@ -0,0 +1,328 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/errdefs"
|
||||
gorunc "github.com/containerd/go-runc"
|
||||
"github.com/containerd/log"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/containerd/containerd/v2/cmd/containerd-shim-runc-v2/process"
|
||||
"github.com/containerd/containerd/v2/cmd/containerd-shim-runc-v2/runc"
|
||||
"github.com/containerd/containerd/v2/contrib/seccomp"
|
||||
"github.com/containerd/containerd/v2/core/containers"
|
||||
"github.com/containerd/containerd/v2/pkg/namespaces"
|
||||
"github.com/containerd/containerd/v2/pkg/oci"
|
||||
"github.com/containerd/containerd/v2/pkg/sys/reaper"
|
||||
)
|
||||
|
||||
// InitBinary is the PID 1 program of a pod that shares its PID namespace,
|
||||
// built from cmd/containerd-sandbox-init and installed next to the shim.
|
||||
const InitBinary = "containerd-sandbox-init"
|
||||
|
||||
const (
|
||||
// initPath is where the program is mounted inside its container.
|
||||
initPath = "/" + InitBinary
|
||||
// initRootfs is the rootfs of the init container, relative to the
|
||||
// sandbox bundle: an empty tmpfs.
|
||||
initRootfs = "rootfs"
|
||||
// pidPin is the pin of the PID namespace the init is PID 1 of, beside
|
||||
// the IPC and UTS pins.
|
||||
pidPin = "pid"
|
||||
// initUser is the user the init runs as: not root, and not nobody
|
||||
// (65534), which workloads commonly run as.
|
||||
initUser = 65535
|
||||
// initOOMScoreAdj is what CRI gives a pause container.
|
||||
initOOMScoreAdj = -998
|
||||
// initStopTimeout bounds the wait for the init to be reaped after
|
||||
// SIGKILL.
|
||||
initStopTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// initProcess is the containerd-sandbox-init runc container of a pod that
|
||||
// shares its PID namespace (see the package doc). Its container id is the
|
||||
// sandbox id, which is what "shim delete" force-deletes once the shim is
|
||||
// gone.
|
||||
type initProcess struct {
|
||||
runtime *gorunc.Runc
|
||||
id string
|
||||
pid int
|
||||
// pidNS is the pin of the PID namespace, the path the containers join.
|
||||
pidNS string
|
||||
// exited is closed once the exit of the process has been reaped; exit
|
||||
// is valid from then on.
|
||||
exited chan struct{}
|
||||
exit gorunc.Exit
|
||||
}
|
||||
|
||||
// initBinary locates containerd-sandbox-init: next to the shim binary, then
|
||||
// in PATH.
|
||||
func initBinary() (string, error) {
|
||||
if self, err := os.Executable(); err == nil {
|
||||
p := filepath.Join(filepath.Dir(self), InitBinary)
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
p, err := exec.LookPath(InitBinary)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s is not installed next to %s or in PATH: %w", InitBinary, BinaryName, errdefs.ErrFailedPrecondition)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// cgroupsPath places the init container under the pod cgroup the way CRI
|
||||
// places the containers of the pod: a systemd slice becomes runc's
|
||||
// "slice:prefix:name", a cgroupfs path gets the container id appended.
|
||||
func cgroupsPath(parent, id string) string {
|
||||
if base := path.Base(parent); strings.HasSuffix(base, ".slice") {
|
||||
return base + ":cri-containerd:" + id
|
||||
}
|
||||
return filepath.Join(parent, id)
|
||||
}
|
||||
|
||||
// initSpec is the OCI spec of the init container. The rootfs holds nothing
|
||||
// but the program; the process has no capabilities, cannot gain privileges,
|
||||
// runs under containerd's default seccomp profile and is scheduled and
|
||||
// OOM-scored as a pause container is. Its /proc, that of the pod PID
|
||||
// namespace, has the masked and read-only paths of a default container spec,
|
||||
// as that of a pause container has. It joins the pod namespaces the sandbox
|
||||
// holds and creates the PID namespace. The hostname is left alone: it is that
|
||||
// of the joined UTS namespace.
|
||||
//
|
||||
// TODO: the pod SELinux label is not applied yet, as for the files the shim
|
||||
// creates (see the SELinux TODO in CreateSandbox).
|
||||
func initSpec(ctx context.Context, cfg *Config, pins Pins, binary string) (*specs.Spec, error) {
|
||||
defaults, err := oci.GenerateSpec(ctx, nil, &containers.Container{ID: cfg.ID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oomScoreAdj := initOOMScoreAdj
|
||||
// The CPU shares of a pause container (WithDefaultSandboxShares).
|
||||
shares := uint64(2)
|
||||
s := &specs.Spec{
|
||||
Version: specs.Version,
|
||||
Process: &specs.Process{
|
||||
User: specs.User{UID: initUser, GID: initUser},
|
||||
Args: []string{initPath},
|
||||
Cwd: "/",
|
||||
Capabilities: &specs.LinuxCapabilities{},
|
||||
NoNewPrivileges: true,
|
||||
OOMScoreAdj: &oomScoreAdj,
|
||||
},
|
||||
Root: &specs.Root{Path: initRootfs, Readonly: true},
|
||||
Mounts: []specs.Mount{
|
||||
{Destination: "/proc", Type: "proc", Source: "proc", Options: []string{"nosuid", "noexec", "nodev"}},
|
||||
{Destination: "/dev", Type: "tmpfs", Source: "tmpfs", Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"}},
|
||||
{Destination: initPath, Type: "bind", Source: binary, Options: []string{"bind", "ro", "nosuid", "nodev"}},
|
||||
},
|
||||
Linux: &specs.Linux{
|
||||
Resources: &specs.LinuxResources{CPU: &specs.LinuxCPU{Shares: &shares}},
|
||||
Namespaces: append([]specs.LinuxNamespace{{Type: specs.PIDNamespace}, {Type: specs.MountNamespace}}, podNamespaces(cfg, pins)...),
|
||||
MaskedPaths: defaults.Linux.MaskedPaths,
|
||||
ReadonlyPaths: defaults.Linux.ReadonlyPaths,
|
||||
},
|
||||
}
|
||||
if cfg.CgroupParent != "" {
|
||||
s.Linux.CgroupsPath = cgroupsPath(cfg.CgroupParent, cfg.ID)
|
||||
}
|
||||
// The profile depends on the capabilities of the process, set above.
|
||||
s.Linux.Seccomp = seccomp.DefaultProfile(s)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// createInit runs "runc create" for the init container in the sandbox
|
||||
// bundle: the rootfs is a tmpfs mounted at <bundle>/rootfs, so that the
|
||||
// mount points runc creates in it leave nothing behind, and the spec is
|
||||
// written as <bundle>/config.json. From here on the process exists and is
|
||||
// PID 1 of the new PID namespace, pinned at <bundle>/ns/pid, waiting in runc
|
||||
// for StartSandbox to exec the program. On error nothing is left running;
|
||||
// the Cleanup the caller runs detaches the rootfs and the pin.
|
||||
func createInit(ctx context.Context, bundle string, cfg *Config, pins Pins) (_ *initProcess, retErr error) {
|
||||
ns, err := namespaces.NamespaceRequired(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
binary, err := initBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The runc options of the runtime handler, persisted by "shim start":
|
||||
// the same runc, root and cgroup driver as the containers of the pod.
|
||||
opts, err := runc.ReadOptions(bundle)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read the runc options: %w", err)
|
||||
}
|
||||
rootfs := filepath.Join(bundle, initRootfs)
|
||||
if err := unix.Mount("tmpfs", rootfs, "tmpfs", unix.MS_NOSUID|unix.MS_NODEV, "mode=0755,size=1m"); err != nil {
|
||||
return nil, fmt.Errorf("failed to mount the init rootfs at %s: %w", rootfs, err)
|
||||
}
|
||||
spec, err := initSpec(ctx, cfg, pins, binary)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config, err := json.Marshal(spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(bundle, "config.json"), config, 0o600); err != nil {
|
||||
return nil, fmt.Errorf("failed to write the init spec: %w", err)
|
||||
}
|
||||
|
||||
p := &initProcess{
|
||||
runtime: process.NewRunc(opts.GetRoot(), bundle, ns, opts.GetBinaryName(), opts.GetSystemdCgroup()),
|
||||
id: cfg.ID,
|
||||
exited: make(chan struct{}),
|
||||
}
|
||||
// Subscribed before the process exists so that its exit cannot be
|
||||
// missed; the watcher learns the pid once runc reports it.
|
||||
ec := reaper.Default.Subscribe()
|
||||
pidCh := make(chan int, 1)
|
||||
go p.watch(ec, pidCh)
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
close(pidCh)
|
||||
// A failed create may have left runc state and a process.
|
||||
if err := p.runtime.Delete(ctx, p.id, &gorunc.DeleteOpts{Force: true}); err != nil {
|
||||
log.G(ctx).WithError(err).WithField("id", p.id).Debugf("failed to delete %s after a failed create", InitBinary)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
nullIO, err := gorunc.NewNullIO()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer nullIO.Close()
|
||||
pidFile := filepath.Join(bundle, process.InitPidFile)
|
||||
if err := p.runtime.Create(ctx, p.id, bundle, &gorunc.CreateOpts{
|
||||
IO: nullIO,
|
||||
PidFile: pidFile,
|
||||
NoPivot: opts.GetNoPivotRoot(),
|
||||
NoNewKeyring: opts.GetNoNewKeyring(),
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed to create the %s container: %w", InitBinary, err)
|
||||
}
|
||||
if p.pid, err = gorunc.ReadPidFile(pidFile); err != nil {
|
||||
return nil, fmt.Errorf("failed to read the %s pid: %w", InitBinary, err)
|
||||
}
|
||||
pidCh <- p.pid
|
||||
// Pinned like the IPC and UTS namespaces, so that the path the
|
||||
// containers join never names whatever process reuses the pid.
|
||||
if err := os.MkdirAll(filepath.Join(bundle, pinDir), 0o700); err != nil {
|
||||
return nil, fmt.Errorf("failed to create namespace pin directory: %w", err)
|
||||
}
|
||||
p.pidNS = filepath.Join(bundle, pinDir, pidPin)
|
||||
if err := pin(fmt.Sprintf("/proc/%d/ns/pid", p.pid), p.pidNS); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// watch delivers the exit of the init through exited. It drains the reaper
|
||||
// subscription throughout (a subscriber that falls behind slows every reap
|
||||
// in the shim) and remembers the exits seen before the pid is known, since
|
||||
// the process can be reaped between "runc create" and the pid file read.
|
||||
func (p *initProcess) watch(ec chan gorunc.Exit, pidCh <-chan int) {
|
||||
defer reaper.Default.Unsubscribe(ec)
|
||||
pid := -1
|
||||
seen := map[int]gorunc.Exit{}
|
||||
for {
|
||||
select {
|
||||
case id, ok := <-pidCh:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if e, ok := seen[id]; ok {
|
||||
p.finish(e)
|
||||
return
|
||||
}
|
||||
pid, seen = id, nil
|
||||
case e, ok := <-ec:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case pid < 0:
|
||||
seen[e.Pid] = e
|
||||
case e.Pid == pid:
|
||||
p.finish(e)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *initProcess) finish(e gorunc.Exit) {
|
||||
p.exit = e
|
||||
close(p.exited)
|
||||
}
|
||||
|
||||
// stop ends the init with SIGKILL, the one signal it does not ignore, waits
|
||||
// for its exit to be reaped and deletes its runc container. A process that
|
||||
// already exited on its own is only deleted; the exit is the same either
|
||||
// way. The wait is bounded by initStopTimeout.
|
||||
func (p *initProcess) stop(ctx context.Context) (gorunc.Exit, error) {
|
||||
exit, err := p.kill(ctx)
|
||||
if err != nil {
|
||||
return exit, err
|
||||
}
|
||||
return exit, p.delete(ctx)
|
||||
}
|
||||
|
||||
// kill ends the init with SIGKILL, unless it has exited already, and waits
|
||||
// for its exit, at most initStopTimeout.
|
||||
func (p *initProcess) kill(ctx context.Context) (gorunc.Exit, error) {
|
||||
select {
|
||||
case <-p.exited:
|
||||
default:
|
||||
if err := p.runtime.Kill(ctx, p.id, int(unix.SIGKILL), nil); err != nil {
|
||||
// The process may have exited in the meantime; the wait tells.
|
||||
log.G(ctx).WithError(err).WithField("id", p.id).Debugf("failed to kill %s", InitBinary)
|
||||
}
|
||||
select {
|
||||
case <-p.exited:
|
||||
case <-time.After(initStopTimeout):
|
||||
return gorunc.Exit{}, fmt.Errorf("%s (pid %d) did not exit after SIGKILL", InitBinary, p.pid)
|
||||
case <-ctx.Done():
|
||||
return gorunc.Exit{}, ctx.Err()
|
||||
}
|
||||
}
|
||||
return p.exit, nil
|
||||
}
|
||||
|
||||
// delete removes the runc container of the init. Deleting it again is fine.
|
||||
func (p *initProcess) delete(ctx context.Context) error {
|
||||
if err := p.runtime.Delete(ctx, p.id, &gorunc.DeleteOpts{Force: true}); err != nil {
|
||||
return fmt.Errorf("failed to delete the %s container: %w", InitBinary, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
gorunc "github.com/containerd/go-runc"
|
||||
"github.com/moby/sys/mountinfo"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/containerd/containerd/v2/contrib/seccomp"
|
||||
"github.com/containerd/containerd/v2/core/containers"
|
||||
"github.com/containerd/containerd/v2/pkg/namespaces"
|
||||
"github.com/containerd/containerd/v2/pkg/oci"
|
||||
"github.com/containerd/containerd/v2/pkg/sys/reaper"
|
||||
"github.com/containerd/containerd/v2/pkg/testutil"
|
||||
)
|
||||
|
||||
func TestCgroupsPath(t *testing.T) {
|
||||
assert.Equal(t, "/kubepods/burstable/pod1/"+testID, cgroupsPath("/kubepods/burstable/pod1", testID))
|
||||
assert.Equal(t, "kubepods-burstable-pod1.slice:cri-containerd:"+testID, cgroupsPath("/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod1.slice", testID))
|
||||
assert.Equal(t, "test.slice:cri-containerd:"+testID, cgroupsPath("/test.slice", testID))
|
||||
}
|
||||
|
||||
func TestInitSpec(t *testing.T) {
|
||||
ctx := namespaces.WithNamespace(context.Background(), "testing")
|
||||
cfg := &Config{ID: testID, NetNSPath: testNetNS, CgroupParent: "/kubepods/pod1", SharedPID: true}
|
||||
pins := Pins{IPC: "/bundle/ns/ipc", UTS: "/bundle/ns/uts"}
|
||||
binary := "/usr/local/bin/" + InitBinary
|
||||
spec, err := initSpec(ctx, cfg, pins, binary)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, spec.Process)
|
||||
assert.Equal(t, []string{"/" + InitBinary}, spec.Process.Args)
|
||||
assert.Equal(t, specs.User{UID: initUser, GID: initUser}, spec.Process.User)
|
||||
assert.True(t, spec.Process.NoNewPrivileges)
|
||||
assert.Equal(t, &specs.LinuxCapabilities{}, spec.Process.Capabilities, "no capabilities")
|
||||
require.NotNil(t, spec.Process.OOMScoreAdj)
|
||||
assert.Equal(t, initOOMScoreAdj, *spec.Process.OOMScoreAdj)
|
||||
assert.Equal(t, &specs.Root{Path: "rootfs", Readonly: true}, spec.Root)
|
||||
assert.Equal(t, specs.Mount{Destination: "/" + InitBinary, Type: "bind", Source: binary, Options: []string{"bind", "ro", "nosuid", "nodev"}}, spec.Mounts[len(spec.Mounts)-1])
|
||||
assert.Equal(t, []specs.LinuxNamespace{
|
||||
{Type: specs.PIDNamespace},
|
||||
{Type: specs.MountNamespace},
|
||||
{Type: specs.NetworkNamespace, Path: testNetNS},
|
||||
{Type: specs.IPCNamespace, Path: pins.IPC},
|
||||
{Type: specs.UTSNamespace, Path: pins.UTS},
|
||||
}, spec.Linux.Namespaces)
|
||||
assert.Equal(t, "/kubepods/pod1/"+testID, spec.Linux.CgroupsPath)
|
||||
assert.Empty(t, spec.Hostname, "the hostname of the joined UTS namespace is left alone")
|
||||
|
||||
defaults, err := oci.GenerateSpec(ctx, nil, &containers.Container{ID: testID})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, defaults.Linux.MaskedPaths, spec.Linux.MaskedPaths)
|
||||
assert.Equal(t, defaults.Linux.ReadonlyPaths, spec.Linux.ReadonlyPaths)
|
||||
assert.Contains(t, spec.Linux.ReadonlyPaths, "/proc/sys")
|
||||
assert.Equal(t, seccomp.DefaultProfile(spec), spec.Linux.Seccomp)
|
||||
|
||||
t.Run("host namespaces", func(t *testing.T) {
|
||||
spec, err := initSpec(ctx, &Config{ID: testID, HostNetwork: true, HostIPC: true, SharedPID: true}, Pins{}, binary)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []specs.LinuxNamespace{{Type: specs.PIDNamespace}, {Type: specs.MountNamespace}}, spec.Linux.Namespaces)
|
||||
assert.Empty(t, spec.Linux.CgroupsPath)
|
||||
})
|
||||
}
|
||||
|
||||
// startReaper makes the test process reap its children the way the shim
|
||||
// does; the runc commands and the exit tracking of the init rely on it.
|
||||
func startReaper(t *testing.T) {
|
||||
require.NoError(t, reaper.SetSubreaper(1))
|
||||
monitor := gorunc.Monitor
|
||||
gorunc.Monitor = reaper.Default
|
||||
ch := make(chan os.Signal, 32)
|
||||
signal.Notify(ch, unix.SIGCHLD)
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ch:
|
||||
_ = reaper.Reap()
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
t.Cleanup(func() {
|
||||
signal.Stop(ch)
|
||||
close(done)
|
||||
gorunc.Monitor = monitor
|
||||
_ = reaper.SetSubreaper(0)
|
||||
})
|
||||
}
|
||||
|
||||
// newInit creates the init of a pod in the host network and IPC namespaces
|
||||
// in a fresh sandbox bundle, stopped and cleaned up with the test. The test
|
||||
// needs root, runc and the installed init binary.
|
||||
func newInit(t *testing.T) (context.Context, string, *initProcess) {
|
||||
testutil.RequiresRoot(t)
|
||||
if _, err := exec.LookPath("runc"); err != nil {
|
||||
t.Skip("runc is not installed")
|
||||
}
|
||||
if _, err := initBinary(); err != nil {
|
||||
t.Skip(err.Error())
|
||||
}
|
||||
startReaper(t)
|
||||
|
||||
ctx := namespaces.WithNamespace(context.Background(), "testing")
|
||||
bundle := t.TempDir()
|
||||
require.NoError(t, os.Mkdir(filepath.Join(bundle, initRootfs), 0o711))
|
||||
t.Cleanup(func() { _ = Cleanup(ctx, bundle) })
|
||||
cfg := &Config{ID: fmt.Sprintf("sandbox-init-test-%d", os.Getpid()), HostNetwork: true, HostIPC: true, SharedPID: true, CgroupParent: "/containerd-test"}
|
||||
p, err := createInit(ctx, bundle, cfg, Pins{})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _, _ = p.stop(ctx) })
|
||||
return ctx, bundle, p
|
||||
}
|
||||
|
||||
// startInit execs the program of p and waits for it to run.
|
||||
func startInit(ctx context.Context, t *testing.T, p *initProcess) {
|
||||
require.NoError(t, p.runtime.Start(ctx, p.id))
|
||||
require.Eventually(t, func() bool { return running(p.pid) }, 5*time.Second, 20*time.Millisecond)
|
||||
}
|
||||
|
||||
// running reports whether the init program is running as pid: the comm of
|
||||
// the process is the binary name, truncated by the kernel.
|
||||
func running(pid int) bool {
|
||||
comm, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
|
||||
return err == nil && strings.HasPrefix(InitBinary, strings.TrimSpace(string(comm)))
|
||||
}
|
||||
|
||||
func TestInitLifecycle(t *testing.T) {
|
||||
ctx, bundle, p := newInit(t)
|
||||
assert.Greater(t, p.pid, 0)
|
||||
mounted, err := mountinfo.Mounted(p.pidNS)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, mounted, "the PID namespace is pinned")
|
||||
assert.False(t, running(p.pid), "created, not started")
|
||||
|
||||
startInit(ctx, t, p)
|
||||
|
||||
exit, err := p.stop(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 128+int(unix.SIGKILL), exit.Status)
|
||||
assert.NoFileExists(t, fmt.Sprintf("/proc/%d", p.pid))
|
||||
|
||||
// Stopping again deletes what is already deleted and reports the same exit.
|
||||
again, err := p.stop(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, exit, again)
|
||||
|
||||
require.NoError(t, Cleanup(ctx, bundle))
|
||||
for _, p := range []string{filepath.Join(bundle, initRootfs), p.pidNS} {
|
||||
mounted, err := mountinfo.Mounted(p)
|
||||
if err == nil {
|
||||
assert.False(t, mounted, "%s is detached", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitExit(t *testing.T) {
|
||||
ctx, _, p := newInit(t)
|
||||
startInit(ctx, t, p)
|
||||
|
||||
// An init killed from outside the shim is noticed and its exit kept.
|
||||
require.NoError(t, unix.Kill(p.pid, unix.SIGKILL))
|
||||
select {
|
||||
case <-p.exited:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("the exit of the init was not seen")
|
||||
}
|
||||
exit, err := p.stop(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 128+int(unix.SIGKILL), exit.Status)
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Pod namespaces without a process; see the package doc. The approach follows
|
||||
// nerdbox's shared resources implementation (github.com/containerd/nerdbox,
|
||||
// internal/vminit/sharedresources).
|
||||
|
||||
const (
|
||||
// pinDir is the bundle directory holding the namespace pins.
|
||||
pinDir = "ns"
|
||||
ipcPin = "ipc"
|
||||
utsPin = "uts"
|
||||
)
|
||||
|
||||
// Pins are the bind-mounted namespace files a sandbox holds. An empty path
|
||||
// means the pod uses the host namespace of that type.
|
||||
type Pins struct {
|
||||
IPC string
|
||||
UTS string
|
||||
}
|
||||
|
||||
// setupNamespaces creates and pins the pod namespaces under the bundle and
|
||||
// applies the hostname and sysctls to them. On error the caller runs Cleanup.
|
||||
func setupNamespaces(bundle string, cfg *Config) (Pins, error) {
|
||||
ipc, uts := !cfg.HostIPC, !cfg.HostNetwork
|
||||
if !ipc && !uts {
|
||||
// Nothing to hold, and the sysctl policy leaves nothing to apply to
|
||||
// namespaces the pod shares with the host.
|
||||
return Pins{}, nil
|
||||
}
|
||||
dir := filepath.Join(bundle, pinDir)
|
||||
if err := os.Mkdir(dir, 0o700); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||
return Pins{}, fmt.Errorf("failed to create namespace pin directory: %w", err)
|
||||
}
|
||||
|
||||
var pins Pins
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
// The thread is locked while it is in the pod namespaces and put
|
||||
// back into the host namespaces before it is released, whether the
|
||||
// work succeeded or not: a thread Go cannot discard (the main thread)
|
||||
// must never stay in the pod. The pins keep the namespaces alive.
|
||||
runtime.LockOSThread()
|
||||
host, err := openThreadNamespaces()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
err = func() error {
|
||||
if cfg.NetNSPath != "" {
|
||||
// Entered first so that net sysctls apply to the pod network
|
||||
// namespace.
|
||||
if err := setns(cfg.NetNSPath, unix.CLONE_NEWNET); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var flags int
|
||||
if ipc {
|
||||
flags |= unix.CLONE_NEWIPC
|
||||
}
|
||||
if uts {
|
||||
flags |= unix.CLONE_NEWUTS
|
||||
}
|
||||
if err := unix.Unshare(flags); err != nil {
|
||||
return fmt.Errorf("failed to create pod namespaces: %w", err)
|
||||
}
|
||||
if uts {
|
||||
if err := unix.Sethostname([]byte(cfg.Hostname)); err != nil {
|
||||
return fmt.Errorf("failed to set hostname %q: %w", cfg.Hostname, err)
|
||||
}
|
||||
}
|
||||
for _, k := range slices.Sorted(maps.Keys(cfg.Sysctls)) {
|
||||
if err := writeSysctl(k, cfg.Sysctls[k]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
tid := unix.Gettid()
|
||||
if ipc {
|
||||
p := filepath.Join(dir, ipcPin)
|
||||
if err := pin(threadNamespacePath(tid, "ipc"), p); err != nil {
|
||||
return err
|
||||
}
|
||||
pins.IPC = p
|
||||
}
|
||||
if uts {
|
||||
p := filepath.Join(dir, utsPin)
|
||||
if err := pin(threadNamespacePath(tid, "uts"), p); err != nil {
|
||||
return err
|
||||
}
|
||||
pins.UTS = p
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if rerr := host.restore(); rerr != nil {
|
||||
// The thread stays locked and is discarded with the goroutine.
|
||||
errCh <- errors.Join(err, rerr)
|
||||
return
|
||||
}
|
||||
runtime.UnlockOSThread()
|
||||
errCh <- err
|
||||
}()
|
||||
if err := <-errCh; err != nil {
|
||||
return Pins{}, err
|
||||
}
|
||||
return pins, nil
|
||||
}
|
||||
|
||||
// threadNamespaces holds the namespace files of a thread before it moves,
|
||||
// so that it can be moved back.
|
||||
type threadNamespaces struct {
|
||||
net, ipc, uts int
|
||||
}
|
||||
|
||||
func openThreadNamespaces() (*threadNamespaces, error) {
|
||||
t := &threadNamespaces{net: -1, ipc: -1, uts: -1}
|
||||
for _, ns := range []struct {
|
||||
name string
|
||||
fd *int
|
||||
}{{"net", &t.net}, {"ipc", &t.ipc}, {"uts", &t.uts}} {
|
||||
fd, err := unix.Open("/proc/thread-self/ns/"+ns.name, unix.O_RDONLY|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
t.close()
|
||||
return nil, fmt.Errorf("failed to open the %s namespace of the thread: %w", ns.name, err)
|
||||
}
|
||||
*ns.fd = fd
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// restore moves the thread back into its original namespaces and closes them.
|
||||
func (t *threadNamespaces) restore() error {
|
||||
defer t.close()
|
||||
var errs []error
|
||||
for _, ns := range []struct {
|
||||
fd int
|
||||
flag int
|
||||
which string
|
||||
}{{t.uts, unix.CLONE_NEWUTS, "uts"}, {t.ipc, unix.CLONE_NEWIPC, "ipc"}, {t.net, unix.CLONE_NEWNET, "net"}} {
|
||||
if err := unix.Setns(ns.fd, ns.flag); err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to move the thread back into the host %s namespace: %w", ns.which, err))
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func (t *threadNamespaces) close() {
|
||||
for _, fd := range []int{t.net, t.ipc, t.uts} {
|
||||
if fd >= 0 {
|
||||
unix.Close(fd)
|
||||
}
|
||||
}
|
||||
t.net, t.ipc, t.uts = -1, -1, -1
|
||||
}
|
||||
|
||||
// setns moves the calling thread into the namespace pinned at path.
|
||||
func setns(path string, nstype int) error {
|
||||
fd, err := unix.Open(path, unix.O_RDONLY|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open namespace %s: %w", path, err)
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
if err := unix.Setns(fd, nstype); err != nil {
|
||||
return fmt.Errorf("failed to enter namespace %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// threadNamespacePath is the namespace file of one thread. /proc/self/ns
|
||||
// would name the namespaces of the main thread, not of the thread that
|
||||
// changed them.
|
||||
func threadNamespacePath(tid int, nsType string) string {
|
||||
return fmt.Sprintf("/proc/%d/task/%d/ns/%s", os.Getpid(), tid, nsType)
|
||||
}
|
||||
|
||||
// pin bind-mounts the namespace file src onto a new empty file at target so
|
||||
// that the namespace persists without a process in it.
|
||||
func pin(src, target string) error {
|
||||
f, err := os.OpenFile(target, os.O_RDONLY|os.O_CREATE|os.O_EXCL, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create namespace pin %s: %w", target, err)
|
||||
}
|
||||
f.Close()
|
||||
if err := unix.Mount(src, target, "none", unix.MS_BIND, ""); err != nil {
|
||||
os.Remove(target)
|
||||
return fmt.Errorf("failed to pin namespace %s at %s: %w", src, target, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeSysctl sets a sysctl for the namespaces of the calling thread: the
|
||||
// files under /proc/sys/net resolve against the network namespace of the
|
||||
// caller and the IPC ones against its IPC namespace, so this must run on the
|
||||
// thread that entered the pod namespaces.
|
||||
func writeSysctl(key, value string) error {
|
||||
p, _, err := sysctlPath(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(p, []byte(value), 0o644); err != nil {
|
||||
return fmt.Errorf("failed to set sysctl %s=%q: %w", key, value, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/moby/sys/mountinfo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/containerd/containerd/v2/pkg/testutil"
|
||||
)
|
||||
|
||||
// inNamespace runs fn on a thread that entered the namespace pinned at pin
|
||||
// and returns what it reads. The thread is locked and discarded afterwards,
|
||||
// like the one that created the namespace.
|
||||
func inNamespace(t *testing.T, pin string, nsType int, fn func() (string, error)) string {
|
||||
t.Helper()
|
||||
var (
|
||||
result string
|
||||
err error
|
||||
)
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
runtime.LockOSThread()
|
||||
var fd int
|
||||
if fd, err = unix.Open(pin, unix.O_RDONLY|unix.O_CLOEXEC, 0); err != nil {
|
||||
return
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
if err = unix.Setns(fd, nsType); err != nil {
|
||||
return
|
||||
}
|
||||
result, err = fn()
|
||||
}()
|
||||
<-done
|
||||
require.NoError(t, err, "in namespace %s", pin)
|
||||
return result
|
||||
}
|
||||
|
||||
func inode(t *testing.T, path string) uint64 {
|
||||
t.Helper()
|
||||
var st unix.Stat_t
|
||||
require.NoError(t, unix.Stat(path, &st))
|
||||
return st.Ino
|
||||
}
|
||||
|
||||
func assertNoMountsUnder(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
mounts, err := mountinfo.GetMounts(mountinfo.PrefixFilter(dir))
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, mounts, "no mounts left under %s", dir)
|
||||
}
|
||||
|
||||
func TestSetupNamespaces(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
ctx := context.Background()
|
||||
bundle := t.TempDir()
|
||||
t.Cleanup(func() { _ = Cleanup(ctx, bundle) })
|
||||
|
||||
pins, err := setupNamespaces(bundle, &Config{
|
||||
Hostname: "sandbox-test",
|
||||
Sysctls: map[string]string{"kernel.shm_rmid_forced": "1"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, filepath.Join(bundle, pinDir, ipcPin), pins.IPC)
|
||||
assert.Equal(t, filepath.Join(bundle, pinDir, utsPin), pins.UTS)
|
||||
|
||||
// The pins are namespace files distinct from the host namespaces.
|
||||
for _, tc := range []struct{ pin, nsType string }{{pins.IPC, "ipc"}, {pins.UTS, "uts"}} {
|
||||
var st unix.Statfs_t
|
||||
require.NoError(t, unix.Statfs(tc.pin, &st))
|
||||
assert.EqualValues(t, unix.NSFS_MAGIC, st.Type, "%s pin is an nsfs mount", tc.nsType)
|
||||
assert.NotEqual(t, inode(t, "/proc/self/ns/"+tc.nsType), inode(t, tc.pin), "%s namespace is not the host one", tc.nsType)
|
||||
}
|
||||
|
||||
// The pod UTS namespace carries the pod hostname; the host keeps its own.
|
||||
hostHostname, err := os.Hostname()
|
||||
require.NoError(t, err)
|
||||
podHostname := inNamespace(t, pins.UTS, unix.CLONE_NEWUTS, func() (string, error) {
|
||||
var u unix.Utsname
|
||||
if err := unix.Uname(&u); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return unix.ByteSliceToString(u.Nodename[:]), nil
|
||||
})
|
||||
assert.Equal(t, "sandbox-test", podHostname)
|
||||
current, err := os.Hostname()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, hostHostname, current)
|
||||
|
||||
// The IPC sysctl was applied inside the pod IPC namespace.
|
||||
value := inNamespace(t, pins.IPC, unix.CLONE_NEWIPC, func() (string, error) {
|
||||
b, err := os.ReadFile("/proc/sys/kernel/shm_rmid_forced")
|
||||
return strings.TrimSpace(string(b)), err
|
||||
})
|
||||
assert.Equal(t, "1", value)
|
||||
|
||||
// Cleanup detaches the pins and removes them; a second run is a no-op.
|
||||
require.NoError(t, Cleanup(ctx, bundle))
|
||||
for _, p := range []string{pins.IPC, pins.UTS, filepath.Join(bundle, pinDir)} {
|
||||
_, err := os.Stat(p)
|
||||
assert.True(t, os.IsNotExist(err), "%s should be gone", p)
|
||||
}
|
||||
assertNoMountsUnder(t, bundle)
|
||||
require.NoError(t, Cleanup(ctx, bundle))
|
||||
}
|
||||
|
||||
func TestSetupNamespacesHostModes(t *testing.T) {
|
||||
testutil.RequiresRoot(t)
|
||||
bundle := t.TempDir()
|
||||
|
||||
// A pod in the host network and IPC namespaces holds nothing.
|
||||
pins, err := setupNamespaces(bundle, &Config{HostNetwork: true, HostIPC: true})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, Pins{}, pins)
|
||||
_, err = os.Stat(filepath.Join(bundle, pinDir))
|
||||
assert.True(t, os.IsNotExist(err), "no pin directory without pins")
|
||||
|
||||
// Host network with a pod IPC namespace: only the IPC pin exists.
|
||||
pins, err = setupNamespaces(bundle, &Config{HostNetwork: true})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = Cleanup(context.Background(), bundle) })
|
||||
assert.NotEmpty(t, pins.IPC)
|
||||
assert.Empty(t, pins.UTS)
|
||||
_, err = os.Stat(filepath.Join(bundle, pinDir, utsPin))
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cgroupsv2 "github.com/containerd/cgroups/v3/cgroup2"
|
||||
api "github.com/containerd/containerd/api/runtime/sandbox/v1"
|
||||
"github.com/containerd/containerd/api/types"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/containerd/errdefs/pkg/errgrpc"
|
||||
"github.com/containerd/log"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/containerd/plugin"
|
||||
"github.com/containerd/plugin/registry"
|
||||
"github.com/containerd/ttrpc"
|
||||
"github.com/containerd/typeurl/v2"
|
||||
"github.com/moby/sys/userns"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
|
||||
"github.com/containerd/containerd/v2/pkg/protobuf"
|
||||
"github.com/containerd/containerd/v2/pkg/shutdown"
|
||||
"github.com/containerd/containerd/v2/plugins"
|
||||
)
|
||||
|
||||
// BinaryName is the name of this shim, used in messages. It serves the
|
||||
// io.containerd.runc.v2 runtime type; a runtime handler reaches it through
|
||||
// runtime_path.
|
||||
const BinaryName = "containerd-shim-sandboxed-runc-v2"
|
||||
|
||||
func init() {
|
||||
registry.Register(&plugin.Registration{
|
||||
Type: plugins.TTRPCPlugin,
|
||||
ID: "sandbox",
|
||||
Requires: []plugin.Type{
|
||||
plugins.InternalPlugin,
|
||||
},
|
||||
InitFn: func(ic *plugin.InitContext) (any, error) {
|
||||
ss, err := ic.GetByID(plugins.InternalPlugin, "shutdown")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewService(ss.(shutdown.Service)), nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Service implements the sandbox ttrpc service. containerd starts one shim per
|
||||
// sandbox, so a Service hosts at most one.
|
||||
type Service struct {
|
||||
shutdown shutdown.Service
|
||||
|
||||
mu sync.Mutex
|
||||
sandbox *instance
|
||||
}
|
||||
|
||||
type instance struct {
|
||||
cfg *Config
|
||||
bundle string
|
||||
pins Pins
|
||||
spec *anypb.Any
|
||||
createdAt time.Time
|
||||
// init is PID 1 of the pod PID namespace, nil unless the pod shares it.
|
||||
init *initProcess
|
||||
|
||||
started bool
|
||||
stopped bool
|
||||
exitStatus uint32
|
||||
exitedAt time.Time
|
||||
// done is closed once the sandbox is stopped.
|
||||
done chan struct{}
|
||||
// released is set once the init container, the pins and the files are
|
||||
// gone; a stopped sandbox may still hold them if releasing failed.
|
||||
released bool
|
||||
}
|
||||
|
||||
var _ api.TTRPCSandboxService = (*Service)(nil)
|
||||
|
||||
// NewService returns a sandbox service. sd ends the shim on ShutdownSandbox.
|
||||
func NewService(sd shutdown.Service) *Service {
|
||||
return &Service{shutdown: sd}
|
||||
}
|
||||
|
||||
// RegisterTTRPC implements shim.TTRPCService.
|
||||
func (s *Service) RegisterTTRPC(server *ttrpc.Server) error {
|
||||
api.RegisterTTRPCSandboxService(server, s)
|
||||
return nil
|
||||
}
|
||||
|
||||
// findSandbox returns the hosted sandbox if it is the one asked for. The
|
||||
// caller holds s.mu.
|
||||
func (s *Service) findSandbox(id string) (*instance, error) {
|
||||
if s.sandbox == nil || s.sandbox.cfg.ID != id {
|
||||
return nil, fmt.Errorf("sandbox %q: %w", id, errdefs.ErrNotFound)
|
||||
}
|
||||
return s.sandbox, nil
|
||||
}
|
||||
|
||||
// CreateSandbox sets the pod up: namespaces, hostname, sysctls and shared
|
||||
// files. Nothing is left behind on failure.
|
||||
func (s *Service) CreateSandbox(ctx context.Context, r *api.CreateSandboxRequest) (_ *api.CreateSandboxResponse, retErr error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.sandbox != nil {
|
||||
return nil, errgrpc.ToGRPCf(errdefs.ErrAlreadyExists, "shim already hosts sandbox %q", s.sandbox.cfg.ID)
|
||||
}
|
||||
if userns.RunningInUserNS() {
|
||||
return nil, errgrpc.ToGRPCf(errdefs.ErrNotImplemented, "%s does not support rootless containerd", BinaryName)
|
||||
}
|
||||
bundle := r.GetBundlePath()
|
||||
if bundle == "" {
|
||||
return nil, errgrpc.ToGRPCf(errdefs.ErrInvalidArgument, "sandbox %q: no bundle path", r.GetSandboxID())
|
||||
}
|
||||
cfg, err := ConfigFromRequest(r)
|
||||
if err != nil {
|
||||
return nil, errgrpc.ToGRPC(err)
|
||||
}
|
||||
log.G(ctx).WithField("id", cfg.ID).Warnf("%s is experimental: the pod sandbox runs without a pause container", BinaryName)
|
||||
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
if err := Cleanup(ctx, bundle); err != nil {
|
||||
log.G(ctx).WithError(err).WithField("id", cfg.ID).Warn("failed to clean up after a failed sandbox creation")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
pins, err := setupNamespaces(bundle, cfg)
|
||||
if err != nil {
|
||||
return nil, errgrpc.ToGRPCf(err, "failed to set up the pod namespaces")
|
||||
}
|
||||
// TODO: SELinux. The shim allocates no pod process label and CRI does not
|
||||
// allocate one for a Sandbox API shim yet, so on an enforcing host the
|
||||
// containers of the pod without explicit SELinux options get independent
|
||||
// MCS labels and relabel the shared files and /dev/shm in turn, and the
|
||||
// pod-level SELinux options are not applied. Waits for
|
||||
// https://github.com/containerd/containerd/pull/14108, which allocates the
|
||||
// pod label in the CRI layer; the shim then applies the labels it is
|
||||
// handed to the files it creates.
|
||||
mounts, err := setupFiles(bundle, cfg)
|
||||
if err != nil {
|
||||
return nil, errgrpc.ToGRPCf(err, "failed to set up the pod files")
|
||||
}
|
||||
var (
|
||||
initP *initProcess
|
||||
pidNS string
|
||||
)
|
||||
if cfg.SharedPID {
|
||||
// Created last, once the pins it joins exist; if anything below
|
||||
// fails it is stopped before the deferred Cleanup runs.
|
||||
if initP, err = createInit(ctx, bundle, cfg, pins); err != nil {
|
||||
return nil, errgrpc.ToGRPCf(err, "failed to create the pod PID namespace")
|
||||
}
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
if _, err := initP.stop(ctx); err != nil {
|
||||
log.G(ctx).WithError(err).WithField("id", cfg.ID).Warnf("failed to stop %s after a failed sandbox creation", InitBinary)
|
||||
}
|
||||
}
|
||||
}()
|
||||
pidNS = initP.pidNS
|
||||
}
|
||||
spec, err := typeurl.MarshalAnyToProto(buildSpec(cfg, pins, mounts, pidNS))
|
||||
if err != nil {
|
||||
return nil, errgrpc.ToGRPCf(err, "failed to marshal the sandbox spec")
|
||||
}
|
||||
|
||||
inst := &instance{
|
||||
cfg: cfg,
|
||||
bundle: bundle,
|
||||
pins: pins,
|
||||
spec: spec,
|
||||
createdAt: time.Now(),
|
||||
init: initP,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
s.sandbox = inst
|
||||
if initP != nil {
|
||||
// The watcher outlives this request: it keeps the namespace and
|
||||
// log fields of the request without its cancellation.
|
||||
go s.watchInit(context.WithoutCancel(ctx), inst)
|
||||
}
|
||||
return &api.CreateSandboxResponse{}, nil
|
||||
}
|
||||
|
||||
// watchInit ends the sandbox when its init exits on its own: the kernel
|
||||
// kills every process in a PID namespace whose PID 1 is gone, so the pod is
|
||||
// over, as it is when a pause container dies. StopSandbox, which kills the
|
||||
// init itself, records the exit first and leaves nothing to do here.
|
||||
func (s *Service) watchInit(ctx context.Context, inst *instance) {
|
||||
<-inst.init.exited
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if inst.stopped {
|
||||
return
|
||||
}
|
||||
log.G(ctx).WithField("id", inst.cfg.ID).Warnf("%s exited with status %d, stopping the sandbox", InitBinary, inst.init.exit.Status)
|
||||
if err := s.stop(ctx, inst); err != nil {
|
||||
log.G(ctx).WithError(err).WithField("id", inst.cfg.ID).Error("failed to stop the sandbox after its init exited")
|
||||
}
|
||||
}
|
||||
|
||||
// StartSandbox marks the sandbox running. There is no process to start
|
||||
// unless the pod shares its PID namespace, in which case the program of its
|
||||
// PID 1 is exec'd now. The pid reported is 0 either way: the sandbox is
|
||||
// path-backed and the spec describes what it holds, that PID namespace
|
||||
// included. Starting it again returns the same response.
|
||||
func (s *Service) StartSandbox(ctx context.Context, r *api.StartSandboxRequest) (*api.StartSandboxResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
inst, err := s.findSandbox(r.GetSandboxID())
|
||||
if err != nil {
|
||||
return nil, errgrpc.ToGRPC(err)
|
||||
}
|
||||
if inst.stopped {
|
||||
return nil, errgrpc.ToGRPCf(errdefs.ErrFailedPrecondition, "sandbox %q is stopped", inst.cfg.ID)
|
||||
}
|
||||
if !inst.started && inst.init != nil {
|
||||
if err := inst.init.runtime.Start(ctx, inst.init.id); err != nil {
|
||||
return nil, errgrpc.ToGRPCf(err, "failed to start %s", InitBinary)
|
||||
}
|
||||
}
|
||||
inst.started = true
|
||||
return &api.StartSandboxResponse{
|
||||
Pid: 0,
|
||||
CreatedAt: protobuf.ToTimestamp(inst.createdAt),
|
||||
Spec: inst.spec,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Platform implements the sandbox service: the sandbox runs on the host.
|
||||
func (s *Service) Platform(ctx context.Context, r *api.PlatformRequest) (*api.PlatformResponse, error) {
|
||||
return &api.PlatformResponse{
|
||||
Platform: types.OCIPlatformToProto([]ocispec.Platform{platforms.DefaultSpec()})[0],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StopSandbox releases the pod namespaces and files, and kills the init of a
|
||||
// shared-PID pod, which ends whatever is left in the pod PID namespace.
|
||||
// Containers still running keep the other namespaces they joined until they
|
||||
// exit; CRI stops them first. Stopping twice is fine.
|
||||
func (s *Service) StopSandbox(ctx context.Context, r *api.StopSandboxRequest) (*api.StopSandboxResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
inst, err := s.findSandbox(r.GetSandboxID())
|
||||
if err != nil {
|
||||
return nil, errgrpc.ToGRPC(err)
|
||||
}
|
||||
if err := s.stop(ctx, inst); err != nil {
|
||||
return nil, errgrpc.ToGRPC(err)
|
||||
}
|
||||
return &api.StopSandboxResponse{}, nil
|
||||
}
|
||||
|
||||
// stop marks the sandbox stopped and releases what it holds. The init of a
|
||||
// shared-PID pod is killed first, and its exit becomes the exit of the
|
||||
// sandbox. Once the init is gone the sandbox is stopped, and Status and Wait
|
||||
// report it, even if the release then fails: the pod PID namespace died with
|
||||
// its init. A release that failed is retried by the next stop. The caller
|
||||
// holds s.mu.
|
||||
func (s *Service) stop(ctx context.Context, inst *instance) error {
|
||||
if !inst.stopped {
|
||||
exitedAt := time.Now()
|
||||
if inst.init != nil {
|
||||
exit, err := inst.init.kill(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stop sandbox %q: %w", inst.cfg.ID, err)
|
||||
}
|
||||
inst.exitStatus, exitedAt = uint32(exit.Status), exit.Timestamp
|
||||
}
|
||||
inst.stopped, inst.exitedAt = true, exitedAt
|
||||
close(inst.done)
|
||||
}
|
||||
if inst.released {
|
||||
return nil
|
||||
}
|
||||
if inst.init != nil {
|
||||
if err := inst.init.delete(ctx); err != nil {
|
||||
return fmt.Errorf("failed to stop sandbox %q: %w", inst.cfg.ID, err)
|
||||
}
|
||||
}
|
||||
if err := Cleanup(ctx, inst.bundle); err != nil {
|
||||
return fmt.Errorf("failed to clean up sandbox %q: %w", inst.cfg.ID, err)
|
||||
}
|
||||
inst.released = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitSandbox blocks until the sandbox is stopped. The exit status is that
|
||||
// of the init of a shared-PID pod, 0 for a sandbox without a process.
|
||||
func (s *Service) WaitSandbox(ctx context.Context, r *api.WaitSandboxRequest) (*api.WaitSandboxResponse, error) {
|
||||
s.mu.Lock()
|
||||
inst, err := s.findSandbox(r.GetSandboxID())
|
||||
s.mu.Unlock()
|
||||
if err != nil {
|
||||
return nil, errgrpc.ToGRPC(err)
|
||||
}
|
||||
select {
|
||||
case <-inst.done:
|
||||
case <-ctx.Done():
|
||||
return nil, errgrpc.ToGRPC(ctx.Err())
|
||||
}
|
||||
return &api.WaitSandboxResponse{
|
||||
ExitStatus: inst.exitStatus,
|
||||
ExitedAt: protobuf.ToTimestamp(inst.exitedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SandboxStatus reports the lifecycle state: ready from StartSandbox until the
|
||||
// sandbox is stopped, not ready before and after. The verbose info carries
|
||||
// only shim specific keys; the CRI server builds its own verbose document.
|
||||
func (s *Service) SandboxStatus(ctx context.Context, r *api.SandboxStatusRequest) (*api.SandboxStatusResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
inst, err := s.findSandbox(r.GetSandboxID())
|
||||
if err != nil {
|
||||
return nil, errgrpc.ToGRPC(err)
|
||||
}
|
||||
resp := &api.SandboxStatusResponse{
|
||||
SandboxID: inst.cfg.ID,
|
||||
Pid: 0,
|
||||
State: runtime.PodSandboxState_SANDBOX_NOTREADY.String(),
|
||||
CreatedAt: protobuf.ToTimestamp(inst.createdAt),
|
||||
}
|
||||
switch {
|
||||
case inst.stopped:
|
||||
resp.ExitedAt = protobuf.ToTimestamp(inst.exitedAt)
|
||||
case inst.started:
|
||||
resp.State = runtime.PodSandboxState_SANDBOX_READY.String()
|
||||
}
|
||||
if r.GetVerbose() {
|
||||
var initPid string
|
||||
if inst.init != nil {
|
||||
initPid = strconv.Itoa(inst.init.pid)
|
||||
}
|
||||
resp.Info = make(map[string]string, 5)
|
||||
for _, kv := range [][2]string{
|
||||
{BinaryName + "/netns", inst.cfg.NetNSPath},
|
||||
{BinaryName + "/ipcns", inst.pins.IPC},
|
||||
{BinaryName + "/utsns", inst.pins.UTS},
|
||||
{BinaryName + "/hostname", inst.cfg.Hostname},
|
||||
{BinaryName + "/init-pid", initPid},
|
||||
} {
|
||||
if kv[1] != "" {
|
||||
resp.Info[kv[0]] = kv[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// PingSandbox implements the sandbox service.
|
||||
func (s *Service) PingSandbox(ctx context.Context, r *api.PingRequest) (*api.PingResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if _, err := s.findSandbox(r.GetSandboxID()); err != nil {
|
||||
return nil, errgrpc.ToGRPC(err)
|
||||
}
|
||||
return &api.PingResponse{}, nil
|
||||
}
|
||||
|
||||
// ShutdownSandbox stops the sandbox if needed, forgets it and ends the shim.
|
||||
// The containers of the pod are gone by then: CRI removes them before it
|
||||
// removes the pod. Shutting down twice, or a shim that hosts no sandbox (its
|
||||
// creation failed), is fine; a request for another sandbox than the hosted
|
||||
// one is NotFound and leaves it running.
|
||||
func (s *Service) ShutdownSandbox(ctx context.Context, r *api.ShutdownSandboxRequest) (*api.ShutdownSandboxResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.sandbox != nil {
|
||||
inst, err := s.findSandbox(r.GetSandboxID())
|
||||
if err != nil {
|
||||
return nil, errgrpc.ToGRPC(err)
|
||||
}
|
||||
if err := s.stop(ctx, inst); err != nil {
|
||||
return nil, errgrpc.ToGRPC(err)
|
||||
}
|
||||
s.sandbox = nil
|
||||
}
|
||||
s.shutdown.Shutdown()
|
||||
return &api.ShutdownSandboxResponse{}, nil
|
||||
}
|
||||
|
||||
// SandboxMetrics reports the cgroup v2 stats of the pod cgroup, encoded the
|
||||
// way CRI expects sandbox metrics (a *cgroup2/stats.Metrics). The sandbox has
|
||||
// no process of its own (the init of a shared-PID pod sits in the pod cgroup
|
||||
// like any container), so the aggregate pod cgroup is the only meaningful
|
||||
// source. CRI polls this every second; the cgroup files are read outside the
|
||||
// lock.
|
||||
func (s *Service) SandboxMetrics(ctx context.Context, r *api.SandboxMetricsRequest) (*api.SandboxMetricsResponse, error) {
|
||||
s.mu.Lock()
|
||||
inst, err := s.findSandbox(r.GetSandboxID())
|
||||
s.mu.Unlock()
|
||||
if err != nil {
|
||||
return nil, errgrpc.ToGRPC(err)
|
||||
}
|
||||
id, cgroupParent := inst.cfg.ID, inst.cfg.CgroupParent
|
||||
if cgroupParent == "" {
|
||||
return nil, errgrpc.ToGRPCf(errdefs.ErrNotFound, "sandbox %q has no cgroup parent", id)
|
||||
}
|
||||
cg, err := cgroupsv2.Load(cgroupParent)
|
||||
if err != nil {
|
||||
return nil, errgrpc.ToGRPCf(err, "failed to load sandbox cgroup %q", cgroupParent)
|
||||
}
|
||||
stats, err := cg.StatFiltered(cgroupsv2.StatCPU | cgroupsv2.StatMemory)
|
||||
if err != nil {
|
||||
return nil, errgrpc.ToGRPCf(err, "failed to read sandbox cgroup %q", cgroupParent)
|
||||
}
|
||||
data, err := typeurl.MarshalAnyToProto(stats)
|
||||
if err != nil {
|
||||
return nil, errgrpc.ToGRPCf(err, "failed to marshal sandbox metrics")
|
||||
}
|
||||
return &api.SandboxMetricsResponse{Metrics: &types.Metric{
|
||||
Timestamp: protobuf.ToTimestamp(time.Now()),
|
||||
ID: id,
|
||||
Data: data,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
// UpdateSandbox is not implemented: the sandbox has no resources of its own.
|
||||
func (s *Service) UpdateSandbox(ctx context.Context, r *api.UpdateSandboxRequest) (*api.UpdateSandboxResponse, error) {
|
||||
return nil, errgrpc.ToGRPCf(errdefs.ErrNotImplemented, "%s does not implement UpdateSandbox", BinaryName)
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
api "github.com/containerd/containerd/api/runtime/sandbox/v1"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/containerd/errdefs/pkg/errgrpc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
|
||||
"github.com/containerd/containerd/v2/pkg/shutdown"
|
||||
)
|
||||
|
||||
func TestServiceWithoutSandbox(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
_, sd := shutdown.WithShutdown(ctx)
|
||||
s := NewService(sd)
|
||||
|
||||
// Every sandbox scoped call is NotFound until a sandbox is created.
|
||||
_, err := s.StartSandbox(ctx, &api.StartSandboxRequest{SandboxID: testID})
|
||||
assert.True(t, errdefs.IsNotFound(errgrpc.ToNative(err)), "start: %v", err)
|
||||
_, err = s.StopSandbox(ctx, &api.StopSandboxRequest{SandboxID: testID})
|
||||
assert.True(t, errdefs.IsNotFound(errgrpc.ToNative(err)), "stop: %v", err)
|
||||
_, err = s.SandboxStatus(ctx, &api.SandboxStatusRequest{SandboxID: testID})
|
||||
assert.True(t, errdefs.IsNotFound(errgrpc.ToNative(err)), "status: %v", err)
|
||||
_, err = s.PingSandbox(ctx, &api.PingRequest{SandboxID: testID})
|
||||
assert.True(t, errdefs.IsNotFound(errgrpc.ToNative(err)), "ping: %v", err)
|
||||
_, err = s.WaitSandbox(ctx, &api.WaitSandboxRequest{SandboxID: testID})
|
||||
assert.True(t, errdefs.IsNotFound(errgrpc.ToNative(err)), "wait: %v", err)
|
||||
_, err = s.SandboxMetrics(ctx, &api.SandboxMetricsRequest{SandboxID: testID})
|
||||
assert.True(t, errdefs.IsNotFound(errgrpc.ToNative(err)), "metrics: %v", err)
|
||||
|
||||
_, err = s.UpdateSandbox(ctx, &api.UpdateSandboxRequest{SandboxID: testID})
|
||||
assert.True(t, errdefs.IsNotImplemented(errgrpc.ToNative(err)), "update: %v", err)
|
||||
|
||||
resp, err := s.Platform(ctx, &api.PlatformRequest{SandboxID: testID})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "linux", resp.GetPlatform().GetOS())
|
||||
|
||||
// Shutting down, even an unknown sandbox, ends the shim.
|
||||
select {
|
||||
case <-sd.Done():
|
||||
t.Fatal("the shim must not shut down before ShutdownSandbox")
|
||||
default:
|
||||
}
|
||||
_, err = s.ShutdownSandbox(ctx, &api.ShutdownSandboxRequest{SandboxID: testID})
|
||||
require.NoError(t, err)
|
||||
select {
|
||||
case <-sd.Done():
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("ShutdownSandbox must end the shim")
|
||||
}
|
||||
}
|
||||
|
||||
// TestStopRetriesRelease checks that a sandbox is stopped, and Wait returns,
|
||||
// even when releasing what it holds fails, and that the next stop retries the
|
||||
// release.
|
||||
func TestStopRetriesRelease(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
bundle := t.TempDir()
|
||||
// A file where the pin directory belongs makes Cleanup fail.
|
||||
pins := filepath.Join(bundle, pinDir)
|
||||
require.NoError(t, os.WriteFile(pins, nil, 0o600))
|
||||
inst := &instance{cfg: &Config{ID: testID}, bundle: bundle, done: make(chan struct{})}
|
||||
s := &Service{}
|
||||
|
||||
require.Error(t, s.stop(ctx, inst))
|
||||
assert.True(t, inst.stopped)
|
||||
assert.False(t, inst.released)
|
||||
select {
|
||||
case <-inst.done:
|
||||
default:
|
||||
t.Fatal("a stopped sandbox must end Wait even if its release failed")
|
||||
}
|
||||
|
||||
require.NoError(t, os.Remove(pins))
|
||||
require.NoError(t, s.stop(ctx, inst))
|
||||
assert.True(t, inst.released)
|
||||
require.NoError(t, s.stop(ctx, inst), "stopping a released sandbox is a no-op")
|
||||
}
|
||||
|
||||
func TestSandboxStatusBeforeStart(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
_, sd := shutdown.WithShutdown(ctx)
|
||||
s := NewService(sd)
|
||||
s.sandbox = &instance{cfg: &Config{ID: testID}, done: make(chan struct{})}
|
||||
|
||||
// A created sandbox is not ready until it is started.
|
||||
status, err := s.SandboxStatus(ctx, &api.SandboxStatusRequest{SandboxID: testID})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, runtime.PodSandboxState_SANDBOX_NOTREADY.String(), status.GetState())
|
||||
assert.Nil(t, status.GetExitedAt())
|
||||
|
||||
_, err = s.StartSandbox(ctx, &api.StartSandboxRequest{SandboxID: testID})
|
||||
require.NoError(t, err)
|
||||
status, err = s.SandboxStatus(ctx, &api.SandboxStatusRequest{SandboxID: testID})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, runtime.PodSandboxState_SANDBOX_READY.String(), status.GetState())
|
||||
}
|
||||
|
||||
func TestShutdownSandboxOther(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
_, sd := shutdown.WithShutdown(ctx)
|
||||
s := NewService(sd)
|
||||
s.sandbox = &instance{cfg: &Config{ID: testID}, done: make(chan struct{})}
|
||||
|
||||
// A shutdown meant for another sandbox must neither stop the hosted one
|
||||
// nor end the shim.
|
||||
_, err := s.ShutdownSandbox(ctx, &api.ShutdownSandboxRequest{SandboxID: "other"})
|
||||
assert.True(t, errdefs.IsNotFound(errgrpc.ToNative(err)), "shutdown: %v", err)
|
||||
require.NotNil(t, s.sandbox)
|
||||
assert.False(t, s.sandbox.stopped)
|
||||
select {
|
||||
case <-sd.Done():
|
||||
t.Fatal("the shim must not shut down for another sandbox")
|
||||
default:
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
taskAPI "github.com/containerd/containerd/api/runtime/task/v3"
|
||||
"github.com/containerd/plugin"
|
||||
"github.com/containerd/plugin/registry"
|
||||
"github.com/containerd/ttrpc"
|
||||
|
||||
"github.com/containerd/containerd/v2/cmd/containerd-shim-runc-v2/task"
|
||||
ptypes "github.com/containerd/containerd/v2/pkg/protobuf/types"
|
||||
"github.com/containerd/containerd/v2/pkg/shim"
|
||||
"github.com/containerd/containerd/v2/pkg/shutdown"
|
||||
"github.com/containerd/containerd/v2/plugins"
|
||||
)
|
||||
|
||||
// The containers of the pod run through the task service of
|
||||
// containerd-shim-runc-v2, the very same implementation: this file only wraps
|
||||
// it. Nothing about how containers are created, started, exec'd or deleted
|
||||
// changes.
|
||||
//
|
||||
// The one thing the wrapper overrides is who ends the shim. containerd sends
|
||||
// the task Shutdown RPC after every task deletion, and the shared task
|
||||
// service exits the shim as soon as no container is left. That is right for a
|
||||
// shim that exists for its tasks and wrong for a shim whose sandbox outlives
|
||||
// them: a pod with no containers is still a pod. Here Shutdown is
|
||||
// acknowledged and ignored; the shim exits when its sandbox is shut down
|
||||
// (see the sandbox package).
|
||||
//
|
||||
// TODO: consolidate with containerd-shim-runc-v2 so that both shims register
|
||||
// the same task service without a wrapper, e.g. by letting the host of the
|
||||
// shared service decide the shim lifetime. Wrapping the shutdown.Service the
|
||||
// shared service receives (a no-op Shutdown, everything else delegated) would
|
||||
// work as well; the RPC wrapper is kept because it states the intent in one
|
||||
// place.
|
||||
|
||||
func init() {
|
||||
registry.Register(&plugin.Registration{
|
||||
Type: plugins.TTRPCPlugin,
|
||||
ID: "task",
|
||||
Requires: []plugin.Type{
|
||||
plugins.EventPlugin,
|
||||
plugins.InternalPlugin,
|
||||
},
|
||||
InitFn: func(ic *plugin.InitContext) (any, error) {
|
||||
pp, err := ic.GetByID(plugins.EventPlugin, "publisher")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ss, err := ic.GetByID(plugins.InternalPlugin, "shutdown")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
svc, err := task.NewTaskService(ic.Context, pp.(shim.Publisher), ss.(shutdown.Service))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &taskService{TTRPCTaskService: svc}, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// taskService is the task service of containerd-shim-runc-v2 with the shim
|
||||
// lifetime taken out of its hands. Every RPC but Shutdown is the embedded one.
|
||||
type taskService struct {
|
||||
taskAPI.TTRPCTaskService
|
||||
}
|
||||
|
||||
var _ shim.TTRPCService = (*taskService)(nil)
|
||||
|
||||
// RegisterTTRPC implements shim.TTRPCService.
|
||||
func (s *taskService) RegisterTTRPC(server *ttrpc.Server) error {
|
||||
taskAPI.RegisterTTRPCTaskService(server, s)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown acknowledges the request and keeps the shim running: the sandbox
|
||||
// decides when the shim exits.
|
||||
func (s *taskService) Shutdown(context.Context, *taskAPI.ShutdownRequest) (*ptypes.Empty, error) {
|
||||
return &ptypes.Empty{}, nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//go:build linux
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
taskAPI "github.com/containerd/containerd/api/runtime/task/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
ptypes "github.com/containerd/containerd/v2/pkg/protobuf/types"
|
||||
)
|
||||
|
||||
// shuttingDown counts the Shutdown calls that reach the wrapped task service.
|
||||
type shuttingDown struct {
|
||||
taskAPI.TTRPCTaskService
|
||||
calls int
|
||||
}
|
||||
|
||||
func (s *shuttingDown) Shutdown(context.Context, *taskAPI.ShutdownRequest) (*ptypes.Empty, error) {
|
||||
s.calls++
|
||||
return &ptypes.Empty{}, nil
|
||||
}
|
||||
|
||||
func TestShutdownKeepsTheShimRunning(t *testing.T) {
|
||||
inner := &shuttingDown{}
|
||||
svc := &taskService{TTRPCTaskService: inner}
|
||||
|
||||
// containerd sends Shutdown after every task deletion; the shared task
|
||||
// service would exit the shim once no container is left. The sandbox
|
||||
// shim must stay up for its pod.
|
||||
_, err := svc.Shutdown(context.Background(), &taskAPI.ShutdownRequest{ID: "container"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, inner.calls, "the shared task service must not see the shutdown")
|
||||
}
|
||||
@@ -19,10 +19,12 @@ package sandbox
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd/api/types"
|
||||
"github.com/containerd/containerd/v2/core/mount"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/containerd/typeurl/v2"
|
||||
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
@@ -36,10 +38,28 @@ type CreateOptions struct {
|
||||
Options typeurl.Any
|
||||
NetNSPath string
|
||||
Annotations map[string]string
|
||||
// RuntimePath is an absolute path to the shim binary to start for the
|
||||
// sandbox instead of resolving it from the runtime name, the same way a
|
||||
// task can be created with a runtime path.
|
||||
RuntimePath string
|
||||
}
|
||||
|
||||
type CreateOpt func(*CreateOptions) error
|
||||
|
||||
// WithRuntimePath starts the sandbox with the shim binary at the given
|
||||
// absolute path instead of the one resolved from the runtime name. An empty
|
||||
// path keeps the runtime name; a relative one is rejected, since the shim
|
||||
// manager would take it for a runtime name.
|
||||
func WithRuntimePath(path string) CreateOpt {
|
||||
return func(co *CreateOptions) error {
|
||||
if path != "" && !filepath.IsAbs(path) {
|
||||
return fmt.Errorf("runtime path %q is not absolute: %w", path, errdefs.ErrInvalidArgument)
|
||||
}
|
||||
co.RuntimePath = path
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithRootFS is used to create a sandbox with the provided rootfs mount
|
||||
func WithRootFS(m []mount.Mount) CreateOpt {
|
||||
return func(co *CreateOptions) error {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWithRuntimePath(t *testing.T) {
|
||||
var opts CreateOptions
|
||||
require.NoError(t, WithRuntimePath("")(&opts))
|
||||
assert.Empty(t, opts.RuntimePath, "an empty path keeps the runtime name")
|
||||
|
||||
// An absolute path of this platform (a leading slash is not absolute on
|
||||
// Windows).
|
||||
abs := filepath.Join(t.TempDir(), "containerd-shim-example-v1")
|
||||
require.NoError(t, WithRuntimePath(abs)(&opts))
|
||||
assert.Equal(t, abs, opts.RuntimePath)
|
||||
|
||||
err := WithRuntimePath("containerd-shim-example-v1")(&opts)
|
||||
require.Error(t, err, "a relative path would be taken for a runtime name")
|
||||
assert.True(t, errdefs.IsInvalidArgument(err))
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/containerd/containerd/api/types"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/containerd/errdefs/pkg/errgrpc"
|
||||
"github.com/containerd/log"
|
||||
"github.com/containerd/typeurl/v2"
|
||||
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
@@ -47,7 +48,17 @@ func NewSandboxController(client api.ControllerClient, name string) sandbox.Cont
|
||||
func (s *remoteSandboxController) Create(ctx context.Context, sandboxInfo sandbox.Sandbox, opts ...sandbox.CreateOpt) error {
|
||||
var options sandbox.CreateOptions
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
if err := opt(&options); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if options.RuntimePath != "" {
|
||||
// ControllerCreateRequest has no field for it; the remote controller
|
||||
// starts whatever the runtime name resolves to.
|
||||
log.G(ctx).WithFields(log.Fields{
|
||||
"runtime_path": options.RuntimePath,
|
||||
"runtime": sandboxInfo.Runtime.Name,
|
||||
}).Warn("runtime path is not forwarded to a remote sandbox controller, which resolves the runtime name instead")
|
||||
}
|
||||
apiSandbox := sandbox.ToProto(&sandboxInfo)
|
||||
_, err := s.client.Create(ctx, &api.ControllerCreateRequest{
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/sandbox"
|
||||
)
|
||||
|
||||
// A failing create option ends Create before the request is sent: the
|
||||
// controller has no client here, so getting past the options would panic.
|
||||
func TestCreateOptionError(t *testing.T) {
|
||||
c := NewSandboxController(nil, "remote")
|
||||
err := c.Create(context.Background(), sandbox.Sandbox{ID: "sb"}, sandbox.WithRuntimePath("containerd-shim-example-v1"))
|
||||
require.ErrorIs(t, err, errdefs.ErrInvalidArgument)
|
||||
}
|
||||
@@ -344,17 +344,103 @@ func WithNamespacePath(t runtimespec.LinuxNamespaceType, nsPath string) oci.Spec
|
||||
}
|
||||
}
|
||||
|
||||
// WithPodNamespaces sets the pod namespaces for the container
|
||||
// PodNamespacePaths are the paths of the pod namespaces a container joins.
|
||||
//
|
||||
// A pod sandbox is either PID-backed (a process such as pause is a member of
|
||||
// every pod namespace, and the paths are derived from its pid with
|
||||
// PodNamespacePathsFromPid) or path-backed (the sandbox holds the namespaces
|
||||
// without a process and reports their paths, typically bind-mounted namespace
|
||||
// files). An empty path means the sandbox does not hold that namespace.
|
||||
type PodNamespacePaths struct {
|
||||
Network string
|
||||
IPC string
|
||||
UTS string
|
||||
// PID is the pod PID namespace, joined only when the pod shares it
|
||||
// (NamespaceMode_POD or NamespaceMode_TARGET). The container keeps its own
|
||||
// PID namespace otherwise. It is empty for a host PID pod on a sandbox
|
||||
// without a process: the containers are then in the host PID namespace.
|
||||
PID string
|
||||
// PrivatePID is set by a sandbox that holds no PID namespace and declares
|
||||
// that each container gets a new one, even when the pod asked to share
|
||||
// (a sandbox without a process has no PID 1 to hold a shared namespace).
|
||||
// It is an explicit declaration in the sandbox spec, not a fallback.
|
||||
PrivatePID bool
|
||||
User string
|
||||
}
|
||||
|
||||
// PodNamespacePathsFromPid derives the pod namespace paths from the pid of a
|
||||
// process that is a member of every pod namespace, e.g. the pause container.
|
||||
// targetPid is the process whose PID namespace the container joins when the
|
||||
// pod shares one; it differs from sandboxPid for NamespaceMode_TARGET.
|
||||
func PodNamespacePathsFromPid(sandboxPid, targetPid uint32) PodNamespacePaths {
|
||||
return PodNamespacePaths{
|
||||
Network: GetNetworkNamespace(sandboxPid),
|
||||
IPC: GetIPCNamespace(sandboxPid),
|
||||
UTS: GetUTSNamespace(sandboxPid),
|
||||
PID: GetPIDNamespace(targetPid),
|
||||
User: GetUserNamespace(sandboxPid),
|
||||
}
|
||||
}
|
||||
|
||||
// WithPodNamespaces sets the pod namespaces for the container from the pid of
|
||||
// the sandbox process. See WithPodNamespacePaths.
|
||||
func WithPodNamespaces(config *runtime.LinuxContainerSecurityContext, sandboxPid uint32, targetPid uint32, uids, gids []runtimespec.LinuxIDMapping) oci.SpecOpts {
|
||||
return WithPodNamespacePaths(config, config.GetNamespaceOptions(), PodNamespacePathsFromPid(sandboxPid, targetPid), uids, gids)
|
||||
}
|
||||
|
||||
// WithPodNamespacePaths makes the container join the pod namespaces at the
|
||||
// given paths. The network, IPC and UTS namespaces of a container are always
|
||||
// those of its pod, whatever the container config says (with a pause
|
||||
// container the container joined the namespaces of pause, which was itself in
|
||||
// the host namespaces the pod asked for); pod is the namespace options of the
|
||||
// pod sandbox and decides which of them are host namespaces. The PID and user
|
||||
// namespaces follow the container config as before; a container sharing the
|
||||
// pod PID namespace (NamespaceMode_POD) is in the host one when the pod is.
|
||||
//
|
||||
// Every namespace the container is expected to share with the pod must have a
|
||||
// path; a missing path is an error rather than a fallback to a fresh or host
|
||||
// namespace, so that a broken sandbox cannot silently weaken isolation. The
|
||||
// entry of a host namespace (NamespaceMode_NODE) is removed from the spec
|
||||
// when the sandbox provides no path for it.
|
||||
func WithPodNamespacePaths(config *runtime.LinuxContainerSecurityContext, pod *runtime.NamespaceOption, paths PodNamespacePaths, uids, gids []runtimespec.LinuxIDMapping) oci.SpecOpts {
|
||||
namespaces := config.GetNamespaceOptions()
|
||||
|
||||
opts := []oci.SpecOpts{
|
||||
oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: runtimespec.NetworkNamespace, Path: GetNetworkNamespace(sandboxPid)}),
|
||||
oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: runtimespec.IPCNamespace, Path: GetIPCNamespace(sandboxPid)}),
|
||||
oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: runtimespec.UTSNamespace, Path: GetUTSNamespace(sandboxPid)}),
|
||||
var opts []oci.SpecOpts
|
||||
join := func(t runtimespec.LinuxNamespaceType, path string, host bool) error {
|
||||
switch {
|
||||
case path != "":
|
||||
opts = append(opts, oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: t, Path: path}))
|
||||
case host:
|
||||
opts = append(opts, WithoutNamespace(t))
|
||||
default:
|
||||
return fmt.Errorf("pod sandbox provides no %s namespace for the container to join", t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if namespaces.GetPid() != runtime.NamespaceMode_CONTAINER {
|
||||
opts = append(opts, oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: runtimespec.PIDNamespace, Path: GetPIDNamespace(targetPid)}))
|
||||
|
||||
hostNetwork := pod.GetNetwork() == runtime.NamespaceMode_NODE
|
||||
if err := join(runtimespec.NetworkNamespace, paths.Network, hostNetwork); err != nil {
|
||||
return failSpecOpt(err)
|
||||
}
|
||||
if err := join(runtimespec.IPCNamespace, paths.IPC, pod.GetIpc() == runtime.NamespaceMode_NODE); err != nil {
|
||||
return failSpecOpt(err)
|
||||
}
|
||||
// There is no CRI option for the UTS namespace: a pod shares it unless it
|
||||
// uses the host network, in which case it uses the host UTS namespace too.
|
||||
if err := join(runtimespec.UTSNamespace, paths.UTS, hostNetwork); err != nil {
|
||||
return failSpecOpt(err)
|
||||
}
|
||||
// The default spec already gives the container its own PID namespace,
|
||||
// which is what NamespaceMode_CONTAINER asks for and what a sandbox that
|
||||
// declared it holds no PID namespace to share provides. A container that
|
||||
// shares the pod PID namespace is in the host one when the pod is (pause
|
||||
// was): a sandbox without a process provides no path for it.
|
||||
pidMode := namespaces.GetPid()
|
||||
if pidMode != runtime.NamespaceMode_CONTAINER && (pidMode != runtime.NamespaceMode_POD || !paths.PrivatePID) {
|
||||
hostPID := pidMode == runtime.NamespaceMode_NODE || (pidMode == runtime.NamespaceMode_POD && pod.GetPid() == runtime.NamespaceMode_NODE)
|
||||
if err := join(runtimespec.PIDNamespace, paths.PID, hostPID); err != nil {
|
||||
return failSpecOpt(err)
|
||||
}
|
||||
}
|
||||
|
||||
if namespaces.GetUsernsOptions() != nil {
|
||||
@@ -362,7 +448,9 @@ func WithPodNamespaces(config *runtime.LinuxContainerSecurityContext, sandboxPid
|
||||
case runtime.NamespaceMode_NODE:
|
||||
// Nothing to do. Not adding userns field uses the node userns.
|
||||
case runtime.NamespaceMode_POD:
|
||||
opts = append(opts, oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: runtimespec.UserNamespace, Path: GetUserNamespace(sandboxPid)}))
|
||||
if err := join(runtimespec.UserNamespace, paths.User, false); err != nil {
|
||||
return failSpecOpt(err)
|
||||
}
|
||||
opts = append(opts, oci.WithUserNamespace(uids, gids))
|
||||
}
|
||||
}
|
||||
@@ -370,6 +458,12 @@ func WithPodNamespaces(config *runtime.LinuxContainerSecurityContext, sandboxPid
|
||||
return oci.Compose(opts...)
|
||||
}
|
||||
|
||||
func failSpecOpt(err error) oci.SpecOpts {
|
||||
return func(context.Context, oci.Client, *containers.Container, *runtimespec.Spec) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// netNSFormat is the format of network namespace of a process.
|
||||
netNSFormat = "/proc/%v/ns/net"
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package opts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
runtimespec "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
|
||||
"github.com/containerd/containerd/v2/pkg/oci"
|
||||
)
|
||||
|
||||
// defaultNamespaces are the namespaces of the default OCI spec: private, no
|
||||
// path, before the pod namespaces are applied.
|
||||
func defaultNamespaces() *runtimespec.Spec {
|
||||
return &runtimespec.Spec{Linux: &runtimespec.Linux{Namespaces: []runtimespec.LinuxNamespace{
|
||||
{Type: runtimespec.PIDNamespace},
|
||||
{Type: runtimespec.IPCNamespace},
|
||||
{Type: runtimespec.UTSNamespace},
|
||||
{Type: runtimespec.MountNamespace},
|
||||
{Type: runtimespec.NetworkNamespace},
|
||||
}}}
|
||||
}
|
||||
|
||||
func applyNamespaces(t *testing.T, opt oci.SpecOpts) (*runtimespec.Spec, error) {
|
||||
t.Helper()
|
||||
spec := defaultNamespaces()
|
||||
err := opt(context.Background(), nil, nil, spec)
|
||||
return spec, err
|
||||
}
|
||||
|
||||
func namespacePaths(spec *runtimespec.Spec) map[runtimespec.LinuxNamespaceType]string {
|
||||
paths := make(map[runtimespec.LinuxNamespaceType]string)
|
||||
for _, ns := range spec.Linux.Namespaces {
|
||||
paths[ns.Type] = ns.Path
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func securityContext(network, ipc, pid runtime.NamespaceMode) *runtime.LinuxContainerSecurityContext {
|
||||
return &runtime.LinuxContainerSecurityContext{NamespaceOptions: &runtime.NamespaceOption{
|
||||
Network: network,
|
||||
Ipc: ipc,
|
||||
Pid: pid,
|
||||
}}
|
||||
}
|
||||
|
||||
func TestWithPodNamespacePathsFromPid(t *testing.T) {
|
||||
// A sandbox with a process (pause) keeps today's construction exactly,
|
||||
// whatever the requested modes: the container joins the namespaces of the
|
||||
// sandbox process, which is itself in the host namespaces the pod asked for.
|
||||
for name, sc := range map[string]*runtime.LinuxContainerSecurityContext{
|
||||
"pod namespaces": securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_POD),
|
||||
"host namespaces": securityContext(runtime.NamespaceMode_NODE, runtime.NamespaceMode_NODE, runtime.NamespaceMode_NODE),
|
||||
"container pid": securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_CONTAINER),
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
legacy, err := applyNamespaces(t, WithPodNamespaces(sc, 42, 42, nil, nil))
|
||||
require.NoError(t, err)
|
||||
byPath, err := applyNamespaces(t, WithPodNamespacePaths(sc, sc.GetNamespaceOptions(), PodNamespacePathsFromPid(42, 42), nil, nil))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, legacy, byPath)
|
||||
|
||||
paths := namespacePaths(byPath)
|
||||
assert.Equal(t, "/proc/42/ns/net", paths[runtimespec.NetworkNamespace])
|
||||
assert.Equal(t, "/proc/42/ns/ipc", paths[runtimespec.IPCNamespace])
|
||||
assert.Equal(t, "/proc/42/ns/uts", paths[runtimespec.UTSNamespace])
|
||||
if sc.GetNamespaceOptions().GetPid() == runtime.NamespaceMode_CONTAINER {
|
||||
assert.Equal(t, "", paths[runtimespec.PIDNamespace], "the container keeps its own PID namespace")
|
||||
} else {
|
||||
assert.Equal(t, "/proc/42/ns/pid", paths[runtimespec.PIDNamespace])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("target pid", func(t *testing.T) {
|
||||
sc := securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_TARGET)
|
||||
spec, err := applyNamespaces(t, WithPodNamespacePaths(sc, sc.GetNamespaceOptions(), PodNamespacePathsFromPid(42, 7), nil, nil))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "/proc/7/ns/pid", namespacePaths(spec)[runtimespec.PIDNamespace])
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithPodNamespacePaths(t *testing.T) {
|
||||
pathBacked := PodNamespacePaths{
|
||||
Network: "/var/run/netns/cni-1",
|
||||
IPC: "/run/sandbox/ns/ipc",
|
||||
UTS: "/run/sandbox/ns/uts",
|
||||
}
|
||||
|
||||
t.Run("pod namespaces by path", func(t *testing.T) {
|
||||
sc := securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_CONTAINER)
|
||||
spec, err := applyNamespaces(t, WithPodNamespacePaths(sc, sc.GetNamespaceOptions(), pathBacked, nil, nil))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[runtimespec.LinuxNamespaceType]string{
|
||||
runtimespec.NetworkNamespace: pathBacked.Network,
|
||||
runtimespec.IPCNamespace: pathBacked.IPC,
|
||||
runtimespec.UTSNamespace: pathBacked.UTS,
|
||||
runtimespec.PIDNamespace: "",
|
||||
runtimespec.MountNamespace: "",
|
||||
}, namespacePaths(spec))
|
||||
})
|
||||
|
||||
t.Run("host namespaces without paths are removed from the spec", func(t *testing.T) {
|
||||
sc := securityContext(runtime.NamespaceMode_NODE, runtime.NamespaceMode_NODE, runtime.NamespaceMode_NODE)
|
||||
spec, err := applyNamespaces(t, WithPodNamespacePaths(sc, sc.GetNamespaceOptions(), PodNamespacePaths{}, nil, nil))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[runtimespec.LinuxNamespaceType]string{
|
||||
runtimespec.MountNamespace: "",
|
||||
}, namespacePaths(spec), "only the private mount namespace is left")
|
||||
})
|
||||
|
||||
t.Run("a sandbox declaring private PID namespaces", func(t *testing.T) {
|
||||
// A pod asking to share its PID namespace on a sandbox that holds
|
||||
// none: the sandbox said so explicitly, the container keeps its own.
|
||||
sc := securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_POD)
|
||||
paths := pathBacked
|
||||
paths.PrivatePID = true
|
||||
spec, err := applyNamespaces(t, WithPodNamespacePaths(sc, sc.GetNamespaceOptions(), paths, nil, nil))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", namespacePaths(spec)[runtimespec.PIDNamespace])
|
||||
assert.Contains(t, namespacePaths(spec), runtimespec.PIDNamespace)
|
||||
|
||||
// The declaration does not cover the host PID namespace.
|
||||
sc = securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_NODE)
|
||||
spec, err = applyNamespaces(t, WithPodNamespacePaths(sc, sc.GetNamespaceOptions(), paths, nil, nil))
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, namespacePaths(spec), runtimespec.PIDNamespace)
|
||||
|
||||
// Nor a container joining the PID namespace of another container.
|
||||
sc = securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_TARGET)
|
||||
paths.PID = "/proc/7/ns/pid"
|
||||
spec, err = applyNamespaces(t, WithPodNamespacePaths(sc, sc.GetNamespaceOptions(), paths, nil, nil))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, paths.PID, namespacePaths(spec)[runtimespec.PIDNamespace])
|
||||
})
|
||||
|
||||
t.Run("host pid pod", func(t *testing.T) {
|
||||
// A sandbox without a process holds no PID namespace for a host PID
|
||||
// pod and provides no path: a container sharing the pod PID namespace
|
||||
// (NamespaceMode_POD, the zero value) is in the host one like the
|
||||
// pod, as it was with pause; NamespaceMode_CONTAINER keeps its own.
|
||||
pod := &runtime.NamespaceOption{Network: runtime.NamespaceMode_POD, Ipc: runtime.NamespaceMode_POD, Pid: runtime.NamespaceMode_NODE}
|
||||
for _, mode := range []runtime.NamespaceMode{runtime.NamespaceMode_POD, runtime.NamespaceMode_NODE} {
|
||||
sc := securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, mode)
|
||||
spec, err := applyNamespaces(t, WithPodNamespacePaths(sc, pod, pathBacked, nil, nil))
|
||||
require.NoError(t, err, "pid mode %s", mode)
|
||||
assert.NotContains(t, namespacePaths(spec), runtimespec.PIDNamespace, "pid mode %s", mode)
|
||||
}
|
||||
sc := securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_CONTAINER)
|
||||
spec, err := applyNamespaces(t, WithPodNamespacePaths(sc, pod, pathBacked, nil, nil))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", namespacePaths(spec)[runtimespec.PIDNamespace])
|
||||
assert.Contains(t, namespacePaths(spec), runtimespec.PIDNamespace)
|
||||
})
|
||||
|
||||
t.Run("host network with pod ipc", func(t *testing.T) {
|
||||
sc := securityContext(runtime.NamespaceMode_NODE, runtime.NamespaceMode_POD, runtime.NamespaceMode_CONTAINER)
|
||||
spec, err := applyNamespaces(t, WithPodNamespacePaths(sc, sc.GetNamespaceOptions(), PodNamespacePaths{IPC: pathBacked.IPC}, nil, nil))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[runtimespec.LinuxNamespaceType]string{
|
||||
runtimespec.IPCNamespace: pathBacked.IPC,
|
||||
runtimespec.PIDNamespace: "",
|
||||
runtimespec.MountNamespace: "",
|
||||
}, namespacePaths(spec), "no network and no UTS namespace entry")
|
||||
})
|
||||
|
||||
t.Run("the pod decides the host namespaces, not the container config", func(t *testing.T) {
|
||||
// A container config that says nothing (NamespaceMode_POD, the zero
|
||||
// value) in a host network pod: the container is in the host network
|
||||
// and UTS namespaces like the pod, as it was with pause.
|
||||
container := securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_CONTAINER)
|
||||
pod := &runtime.NamespaceOption{Network: runtime.NamespaceMode_NODE, Ipc: runtime.NamespaceMode_POD}
|
||||
spec, err := applyNamespaces(t, WithPodNamespacePaths(container, pod, PodNamespacePaths{IPC: pathBacked.IPC}, nil, nil))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[runtimespec.LinuxNamespaceType]string{
|
||||
runtimespec.IPCNamespace: pathBacked.IPC,
|
||||
runtimespec.PIDNamespace: "",
|
||||
runtimespec.MountNamespace: "",
|
||||
}, namespacePaths(spec))
|
||||
})
|
||||
|
||||
// A pod namespace the sandbox does not provide is an error, never a
|
||||
// silent fallback to a fresh or host namespace.
|
||||
for name, tc := range map[string]struct {
|
||||
sc *runtime.LinuxContainerSecurityContext
|
||||
paths PodNamespacePaths
|
||||
}{
|
||||
"missing network": {securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_CONTAINER), PodNamespacePaths{IPC: pathBacked.IPC, UTS: pathBacked.UTS}},
|
||||
"missing ipc": {securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_CONTAINER), PodNamespacePaths{Network: pathBacked.Network, UTS: pathBacked.UTS}},
|
||||
"missing uts": {securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_CONTAINER), PodNamespacePaths{Network: pathBacked.Network, IPC: pathBacked.IPC}},
|
||||
"missing pid": {securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_POD), pathBacked},
|
||||
"missing target pid": {securityContext(runtime.NamespaceMode_POD, runtime.NamespaceMode_POD, runtime.NamespaceMode_TARGET), PodNamespacePaths{
|
||||
Network: pathBacked.Network,
|
||||
IPC: pathBacked.IPC,
|
||||
UTS: pathBacked.UTS,
|
||||
PrivatePID: true,
|
||||
}},
|
||||
"missing user": {&runtime.LinuxContainerSecurityContext{NamespaceOptions: &runtime.NamespaceOption{
|
||||
Network: runtime.NamespaceMode_POD,
|
||||
Ipc: runtime.NamespaceMode_POD,
|
||||
Pid: runtime.NamespaceMode_CONTAINER,
|
||||
UsernsOptions: &runtime.UserNamespace{Mode: runtime.NamespaceMode_POD},
|
||||
}}, pathBacked},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, err := applyNamespaces(t, WithPodNamespacePaths(tc.sc, tc.sc.GetNamespaceOptions(), tc.paths, nil, nil))
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "provides no")
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("pod user namespace by path", func(t *testing.T) {
|
||||
sc := &runtime.LinuxContainerSecurityContext{NamespaceOptions: &runtime.NamespaceOption{
|
||||
Network: runtime.NamespaceMode_POD,
|
||||
Ipc: runtime.NamespaceMode_POD,
|
||||
Pid: runtime.NamespaceMode_CONTAINER,
|
||||
UsernsOptions: &runtime.UserNamespace{Mode: runtime.NamespaceMode_POD},
|
||||
}}
|
||||
paths := pathBacked
|
||||
paths.User = "/run/sandbox/ns/user"
|
||||
uids := []runtimespec.LinuxIDMapping{{ContainerID: 0, HostID: 65536, Size: 65536}}
|
||||
spec, err := applyNamespaces(t, WithPodNamespacePaths(sc, sc.GetNamespaceOptions(), paths, uids, uids))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, paths.User, namespacePaths(spec)[runtimespec.UserNamespace])
|
||||
assert.Equal(t, uids, spec.Linux.UIDMappings)
|
||||
assert.Equal(t, uids, spec.Linux.GIDMappings)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package sandboxfiles builds the content of the files a pod sandbox shares
|
||||
// with every container of the pod: /etc/hostname, /etc/hosts, /etc/resolv.conf
|
||||
// and the /dev/shm tmpfs.
|
||||
//
|
||||
// The package is deliberately a leaf: it has no dependency on the CRI server
|
||||
// or on any sandbox implementation, so that both the in-process podsandbox
|
||||
// controller and a shim that implements the Sandbox API can produce the same
|
||||
// files. Where the files live and how they reach the containers is decided by
|
||||
// the caller.
|
||||
package sandboxfiles
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// HostnameFile is the file name of the pod hostname file.
|
||||
HostnameFile = "hostname"
|
||||
// HostsFile is the file name of the pod hosts file.
|
||||
HostsFile = "hosts"
|
||||
// ResolvConfFile is the file name of the pod resolv.conf.
|
||||
ResolvConfFile = "resolv.conf"
|
||||
// ShmDir is the directory name of the pod /dev/shm tmpfs.
|
||||
ShmDir = "shm"
|
||||
|
||||
// EtcHostname is the path of the hostname file inside a container.
|
||||
EtcHostname = "/etc/hostname"
|
||||
// EtcHosts is the path of the hosts file on the host and inside a container.
|
||||
EtcHosts = "/etc/hosts"
|
||||
// ResolvConfPath is the path of resolv.conf on the host and inside a container.
|
||||
ResolvConfPath = "/etc/resolv.conf"
|
||||
// DevShm is the path of the shared memory tmpfs on the host and inside a container.
|
||||
DevShm = "/dev/shm"
|
||||
|
||||
// DefaultShmSize is the default size of the pod /dev/shm tmpfs.
|
||||
DefaultShmSize = int64(1024 * 1024 * 64)
|
||||
)
|
||||
|
||||
// HostnameContent returns the content of the pod hostname file.
|
||||
func HostnameContent(hostname string) []byte {
|
||||
return []byte(hostname + "\n")
|
||||
}
|
||||
|
||||
// ResolvConfContent renders resolv.conf content from CRI DNS options. When no
|
||||
// option is specified the result is empty.
|
||||
func ResolvConfContent(servers, searches, options []string) []byte {
|
||||
var b strings.Builder
|
||||
|
||||
if len(searches) > 0 {
|
||||
fmt.Fprintf(&b, "search %s\n", strings.Join(searches, " "))
|
||||
}
|
||||
|
||||
if len(servers) > 0 {
|
||||
fmt.Fprintf(&b, "nameserver %s\n", strings.Join(servers, "\nnameserver "))
|
||||
}
|
||||
|
||||
if len(options) > 0 {
|
||||
fmt.Fprintf(&b, "options %s\n", strings.Join(options, " "))
|
||||
}
|
||||
|
||||
return []byte(b.String())
|
||||
}
|
||||
|
||||
// ShmMountData returns the tmpfs mount data for a pod /dev/shm of the given
|
||||
// size. A non-positive size falls back to DefaultShmSize.
|
||||
func ShmMountData(size int64) string {
|
||||
if size <= 0 {
|
||||
size = DefaultShmSize
|
||||
}
|
||||
return fmt.Sprintf("mode=1777,size=%d", size)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sandboxfiles
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestResolvConfContent(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
desc string
|
||||
servers []string
|
||||
searches []string
|
||||
options []string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "empty dns options should return empty content",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
desc: "non-empty dns options should return correct content",
|
||||
servers: []string{"8.8.8.8", "8.8.4.4"},
|
||||
searches: []string{"114.114.114.114"},
|
||||
options: []string{"timeout:1"},
|
||||
expected: "search 114.114.114.114\nnameserver 8.8.8.8\nnameserver 8.8.4.4\noptions timeout:1\n",
|
||||
},
|
||||
{
|
||||
desc: "servers only",
|
||||
servers: []string{"1.1.1.1"},
|
||||
expected: "nameserver 1.1.1.1\n",
|
||||
},
|
||||
} {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, string(ResolvConfContent(test.servers, test.searches, test.options)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostnameContent(t *testing.T) {
|
||||
assert.Equal(t, "pod-1\n", string(HostnameContent("pod-1")))
|
||||
}
|
||||
|
||||
func TestShmMountData(t *testing.T) {
|
||||
assert.Equal(t, "mode=1777,size=67108864", ShmMountData(0))
|
||||
assert.Equal(t, "mode=1777,size=67108864", ShmMountData(-1))
|
||||
assert.Equal(t, "mode=1777,size=1024", ShmMountData(1024))
|
||||
}
|
||||
@@ -78,6 +78,15 @@ func (c *criService) CreateContainer(ctx context.Context, r *runtime.CreateConta
|
||||
if sandbox.Status.Get().State != sandboxstore.StateReady {
|
||||
return nil, fmt.Errorf("sandbox container %q is not running", sandboxID)
|
||||
}
|
||||
// A sandbox without a process (pid 0) describes the namespaces and files it
|
||||
// holds in the spec it returned on start; containers are wired up from it.
|
||||
var sandboxSpec *runtimespec.Spec
|
||||
if sandboxPid == 0 {
|
||||
sandboxSpec, err = c.sandboxSpec(ctx, sandboxID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
span.SetAttributes(
|
||||
tracing.Attribute("sandbox.id", sandboxID),
|
||||
tracing.Attribute("sandbox.pid", sandboxPid),
|
||||
@@ -153,6 +162,7 @@ func (c *criService) CreateContainer(ctx context.Context, r *runtime.CreateConta
|
||||
podSandboxConfig: sandboxConfig,
|
||||
sandboxRuntimeHandler: sandbox.Metadata.RuntimeHandler,
|
||||
sandboxPid: sandboxPid,
|
||||
sandboxSpec: sandboxSpec,
|
||||
NetNSPath: sandbox.NetNSPath,
|
||||
containerName: containerName,
|
||||
containerdImage: &containerdImage,
|
||||
@@ -183,6 +193,10 @@ type createContainerRequest struct {
|
||||
containerdImage *containerd.Image
|
||||
meta *containerstore.Metadata
|
||||
start time.Time
|
||||
// sandboxSpec is the spec returned by the sandbox controller on start. It is
|
||||
// only loaded for sandboxes without a process (sandboxPid == 0), where it is
|
||||
// the source of the pod namespace paths and shared file mounts.
|
||||
sandboxSpec *runtimespec.Spec
|
||||
}
|
||||
|
||||
func (c *criService) createContainer(r *createContainerRequest) (_ string, retErr error) {
|
||||
@@ -275,6 +289,7 @@ func (c *criService) createContainer(r *createContainerRequest) (_ string, retEr
|
||||
volumeMounts,
|
||||
ociRuntime,
|
||||
runtimeHandler,
|
||||
r.sandboxSpec,
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate container %q spec: %w", r.containerID, err)
|
||||
@@ -612,6 +627,7 @@ func (c *criService) buildContainerSpec(
|
||||
extraMounts []*runtime.Mount,
|
||||
ociRuntime criconfig.Runtime,
|
||||
runtimeHandler *runtime.RuntimeHandler,
|
||||
sandboxSpec *runtimespec.Spec,
|
||||
) (_ *runtimespec.Spec, retErr error) {
|
||||
var (
|
||||
specOpts []oci.SpecOpts
|
||||
@@ -627,12 +643,13 @@ func (c *criService) buildContainerSpec(
|
||||
case isLinux:
|
||||
// Generate container mounts.
|
||||
// No mounts are passed for other platforms.
|
||||
linuxMounts := c.linuxContainerMounts(sandboxID, config)
|
||||
linuxMounts := c.linuxContainerMounts(sandboxID, config, sandboxSpec)
|
||||
|
||||
specOpts, err = c.buildLinuxSpec(
|
||||
id,
|
||||
sandboxID,
|
||||
sandboxPid,
|
||||
sandboxSpec,
|
||||
containerName,
|
||||
imageName,
|
||||
config,
|
||||
@@ -680,6 +697,7 @@ func (c *criService) buildLinuxSpec(
|
||||
id string,
|
||||
sandboxID string,
|
||||
sandboxPid uint32,
|
||||
sandboxSpec *runtimespec.Spec,
|
||||
containerName string,
|
||||
imageName string,
|
||||
config *runtime.ContainerConfig,
|
||||
@@ -922,7 +940,7 @@ func (c *criService) buildLinuxSpec(
|
||||
|
||||
specOpts = append(specOpts,
|
||||
customopts.WithOOMScoreAdj(config, c.config.RestrictOOMScoreAdj),
|
||||
customopts.WithPodNamespaces(securityContext, sandboxPid, targetPid, uids, gids),
|
||||
customopts.WithPodNamespacePaths(securityContext, sandboxConfig.GetLinux().GetSecurityContext().GetNamespaceOptions(), podNamespacePaths(sandboxPid, targetPid, sandboxSpec), uids, gids),
|
||||
customopts.WithSupplementalGroups(supplementalGroups),
|
||||
)
|
||||
specOpts = append(
|
||||
@@ -1084,9 +1102,94 @@ func (c *criService) buildDarwinSpec(
|
||||
return specOpts, nil
|
||||
}
|
||||
|
||||
// sandboxSpec loads the OCI spec the sandbox controller returned when the
|
||||
// sandbox was started. It is nil when the controller returned none.
|
||||
func (c *criService) sandboxSpec(ctx context.Context, sandboxID string) (*runtimespec.Spec, error) {
|
||||
sandboxInfo, err := c.client.SandboxStore().Get(ctx, sandboxID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get sandbox %q from metadata store: %w", sandboxID, err)
|
||||
}
|
||||
if sandboxInfo.Spec == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var spec runtimespec.Spec
|
||||
if err := typeurl.UnmarshalTo(sandboxInfo.Spec, &spec); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal spec of sandbox %q: %w", sandboxID, err)
|
||||
}
|
||||
return &spec, nil
|
||||
}
|
||||
|
||||
// podNamespacePaths resolves the namespaces a container joins.
|
||||
//
|
||||
// A PID-backed sandbox (pause) is a member of every pod namespace, so the
|
||||
// paths are derived from its pid exactly as before. A sandbox without a
|
||||
// process holds the pod namespaces itself and lists their paths in the Linux
|
||||
// section of the spec it returned on start; a namespace missing from that
|
||||
// spec is one the sandbox does not hold, which WithPodNamespacePaths turns into
|
||||
// the host namespace when the pod asked for it and into an error otherwise.
|
||||
//
|
||||
// A sandbox that reports no process and returns no spec (or none with a Linux
|
||||
// section) is a pre-existing Sandbox API shim that describes neither (VM based
|
||||
// shims ignore the paths CRI generates); those keep the historical pid derived
|
||||
// construction so that their behavior does not change.
|
||||
func podNamespacePaths(sandboxPid, targetPid uint32, sandboxSpec *runtimespec.Spec) customopts.PodNamespacePaths {
|
||||
if sandboxPid != 0 || sandboxSpec == nil || sandboxSpec.Linux == nil {
|
||||
return customopts.PodNamespacePathsFromPid(sandboxPid, targetPid)
|
||||
}
|
||||
var paths customopts.PodNamespacePaths
|
||||
for _, ns := range sandboxSpec.Linux.Namespaces {
|
||||
switch ns.Type {
|
||||
case runtimespec.NetworkNamespace:
|
||||
paths.Network = ns.Path
|
||||
case runtimespec.IPCNamespace:
|
||||
paths.IPC = ns.Path
|
||||
case runtimespec.UTSNamespace:
|
||||
paths.UTS = ns.Path
|
||||
case runtimespec.PIDNamespace:
|
||||
// A PID namespace entry without a path is the sandbox declaring
|
||||
// that it holds none and that each container gets a new one.
|
||||
paths.PID = ns.Path
|
||||
paths.PrivatePID = ns.Path == ""
|
||||
case runtimespec.UserNamespace:
|
||||
paths.User = ns.Path
|
||||
}
|
||||
}
|
||||
// A container targeting another container's PID namespace joins that
|
||||
// container, not the sandbox.
|
||||
if targetPid != sandboxPid {
|
||||
paths.PID = customopts.GetPIDNamespace(targetPid)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
// sandboxSharedFile returns the host path of a pod shared file, i.e. a file
|
||||
// the sandbox provides to every container of the pod at destination (for
|
||||
// example /etc/hosts). The fixed location under the CRI sandbox directory
|
||||
// wins when it exists; otherwise the mount the sandbox declared for that
|
||||
// destination in its spec is used, which is how a sandbox implementation that
|
||||
// owns the files, such as a Sandbox API shim, hands them over. The result is
|
||||
// empty when neither exists.
|
||||
func (c *criService) sandboxSharedFile(fixedPath, destination string, sandboxSpec *runtimespec.Spec) string {
|
||||
if _, err := c.os.Stat(fixedPath); err == nil {
|
||||
return fixedPath
|
||||
}
|
||||
if sandboxSpec == nil {
|
||||
return ""
|
||||
}
|
||||
for _, m := range sandboxSpec.Mounts {
|
||||
if m.Destination != destination || m.Source == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := c.os.Stat(m.Source); err == nil {
|
||||
return m.Source
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// linuxContainerMounts sets up necessary container system file mounts
|
||||
// including /dev/shm, /etc/hosts and /etc/resolv.conf.
|
||||
func (c *criService) linuxContainerMounts(sandboxID string, config *runtime.ContainerConfig) []*runtime.Mount {
|
||||
func (c *criService) linuxContainerMounts(sandboxID string, config *runtime.ContainerConfig, sandboxSpec *runtimespec.Spec) []*runtime.Mount {
|
||||
var mounts []*runtime.Mount
|
||||
securityContext := config.GetLinux().GetSecurityContext()
|
||||
var uidMappings, gidMappings []*runtime.IDMapping
|
||||
@@ -1101,8 +1204,7 @@ func (c *criService) linuxContainerMounts(sandboxID string, config *runtime.Cont
|
||||
// do not mount this in that case.
|
||||
// TODO(random-liu): Remove the check and always mount this when
|
||||
// containerd 1.1 and 1.2 are deprecated.
|
||||
hostpath := c.getSandboxHostname(sandboxID)
|
||||
if _, err := c.os.Stat(hostpath); err == nil {
|
||||
if hostpath := c.sandboxSharedFile(c.getSandboxHostname(sandboxID), etcHostname, sandboxSpec); hostpath != "" {
|
||||
mounts = append(mounts, &runtime.Mount{
|
||||
ContainerPath: etcHostname,
|
||||
HostPath: hostpath,
|
||||
@@ -1115,10 +1217,9 @@ func (c *criService) linuxContainerMounts(sandboxID string, config *runtime.Cont
|
||||
}
|
||||
|
||||
if !isInCRIMounts(etcHosts, config.GetMounts()) {
|
||||
hostpath := c.getSandboxHosts(sandboxID)
|
||||
// /etc/hosts could be delegated to remote sandbox controller. That file isn't required to be existed
|
||||
// in host side for some sandbox runtimes. Skip it if we don't need it.
|
||||
if _, err := c.os.Stat(hostpath); err == nil {
|
||||
if hostpath := c.sandboxSharedFile(c.getSandboxHosts(sandboxID), etcHosts, sandboxSpec); hostpath != "" {
|
||||
mounts = append(mounts, &runtime.Mount{
|
||||
ContainerPath: etcHosts,
|
||||
HostPath: hostpath,
|
||||
@@ -1133,10 +1234,9 @@ func (c *criService) linuxContainerMounts(sandboxID string, config *runtime.Cont
|
||||
// Mount sandbox resolv.config.
|
||||
// TODO: Need to figure out whether we should always mount it as read-only
|
||||
if !isInCRIMounts(resolvConfPath, config.GetMounts()) {
|
||||
hostpath := c.getResolvPath(sandboxID)
|
||||
// The ownership of /etc/resolv.conf could be delegated to remote sandbox controller. That file isn't
|
||||
// required to be existed in host side for some sandbox runtimes. Skip it if we don't need it.
|
||||
if _, err := c.os.Stat(hostpath); err == nil {
|
||||
if hostpath := c.sandboxSharedFile(c.getResolvPath(sandboxID), resolvConfPath, sandboxSpec); hostpath != "" {
|
||||
mounts = append(mounts, &runtime.Mount{
|
||||
ContainerPath: resolvConfPath,
|
||||
HostPath: hostpath,
|
||||
@@ -1152,6 +1252,8 @@ func (c *criService) linuxContainerMounts(sandboxID string, config *runtime.Cont
|
||||
sandboxDevShm := c.getSandboxDevShm(sandboxID)
|
||||
if securityContext.GetNamespaceOptions().GetIpc() == runtime.NamespaceMode_NODE {
|
||||
sandboxDevShm = devShm
|
||||
} else if source := c.sandboxSharedFile(sandboxDevShm, devShm, sandboxSpec); source != "" {
|
||||
sandboxDevShm = source
|
||||
}
|
||||
// The ownership of /dev/shm could be delegated to remote sandbox controller. That file isn't required
|
||||
// to be existed in host side for some sandbox runtimes. Skip it if we don't need it.
|
||||
|
||||
@@ -251,7 +251,7 @@ func TestContainerCapabilities(t *testing.T) {
|
||||
c.allCaps = allCaps
|
||||
|
||||
containerConfig.Linux.SecurityContext.Capabilities = test.capability
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
if selinux.GetEnabled() {
|
||||
@@ -286,7 +286,7 @@ func TestContainerSpecTty(t *testing.T) {
|
||||
c := newTestCRIService()
|
||||
for _, tty := range []bool{true, false} {
|
||||
containerConfig.Tty = tty
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
require.NoError(t, err)
|
||||
specCheck(t, testID, testSandboxID, testPid, spec)
|
||||
assert.Equal(t, tty, spec.Process.Terminal)
|
||||
@@ -313,7 +313,7 @@ func TestContainerSpecDefaultPath(t *testing.T) {
|
||||
imageConfig.Env = append(imageConfig.Env, pathenv)
|
||||
expected = pathenv
|
||||
}
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
require.NoError(t, err)
|
||||
specCheck(t, testID, testSandboxID, testPid, spec)
|
||||
assert.Contains(t, spec.Process.Env, expected)
|
||||
@@ -330,7 +330,7 @@ func TestContainerSpecReadonlyRootfs(t *testing.T) {
|
||||
c := newTestCRIService()
|
||||
for _, readonly := range []bool{true, false} {
|
||||
containerConfig.Linux.SecurityContext.ReadonlyRootfs = readonly
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
require.NoError(t, err)
|
||||
specCheck(t, testID, testSandboxID, testPid, spec)
|
||||
assert.Equal(t, readonly, spec.Root.Readonly)
|
||||
@@ -364,7 +364,7 @@ func TestContainerSpecWithExtraMounts(t *testing.T) {
|
||||
Readonly: false,
|
||||
},
|
||||
}
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, extraMounts, ociRuntime, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, extraMounts, ociRuntime, nil, nil)
|
||||
require.NoError(t, err)
|
||||
specCheck(t, testID, testSandboxID, testPid, spec)
|
||||
var mounts, sysMounts []runtimespec.Mount
|
||||
@@ -430,7 +430,7 @@ func TestContainerAndSandboxPrivileged(t *testing.T) {
|
||||
sandboxConfig.Linux.SecurityContext = &runtime.LinuxSandboxSecurityContext{
|
||||
Privileged: test.sandboxPrivileged,
|
||||
}
|
||||
_, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
_, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
if test.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
@@ -470,7 +470,7 @@ func TestPrivilegedBindMount(t *testing.T) {
|
||||
containerConfig.Linux.SecurityContext.Privileged = test.privileged
|
||||
sandboxConfig.Linux.SecurityContext.Privileged = test.privileged
|
||||
|
||||
spec, err := c.buildContainerSpec(currentPlatform, t.Name(), testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, t.Name(), testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
|
||||
assert.NoError(t, err)
|
||||
if test.expectedSysFSRO {
|
||||
@@ -542,7 +542,7 @@ func TestCgroupNamespace(t *testing.T) {
|
||||
containerConfig.Linux.SecurityContext.Privileged = tt.privileged
|
||||
sandboxConfig.Linux.SecurityContext.Privileged = tt.privileged
|
||||
|
||||
spec, err := c.buildContainerSpec(currentPlatform, t.Name(), testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, t.Name(), testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
hasCgroupNS := false
|
||||
@@ -711,7 +711,7 @@ func TestPidNamespace(t *testing.T) {
|
||||
} {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
containerConfig.Linux.SecurityContext.NamespaceOptions = &runtime.NamespaceOption{Pid: test.pidNS}
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, spec.Linux.Namespaces, test.expected)
|
||||
})
|
||||
@@ -885,7 +885,7 @@ func TestUserNamespace(t *testing.T) {
|
||||
sandboxUserns = test.sandboxUserNS
|
||||
}
|
||||
sandboxConfig.Linux.SecurityContext.NamespaceOptions = &runtime.NamespaceOption{UsernsOptions: sandboxUserns}
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
|
||||
if test.err {
|
||||
require.Error(t, err)
|
||||
@@ -915,7 +915,7 @@ func TestNoDefaultRunMount(t *testing.T) {
|
||||
ociRuntime := config.Runtime{}
|
||||
c := newTestCRIService()
|
||||
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
for _, mount := range spec.Mounts {
|
||||
assert.NotEqual(t, "/run", mount.Destination)
|
||||
@@ -1008,7 +1008,7 @@ func TestMaskedAndReadonlyPaths(t *testing.T) {
|
||||
sandboxConfig.Linux.SecurityContext = &runtime.LinuxSandboxSecurityContext{
|
||||
Privileged: test.privileged,
|
||||
}
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
require.NoError(t, err)
|
||||
if !test.privileged { // specCheck presumes an unprivileged container
|
||||
specCheck(t, testID, testSandboxID, testPid, spec)
|
||||
@@ -1060,7 +1060,7 @@ func TestHostname(t *testing.T) {
|
||||
sandboxConfig.Linux.SecurityContext = &runtime.LinuxSandboxSecurityContext{
|
||||
NamespaceOptions: &runtime.NamespaceOption{Network: test.networkNs},
|
||||
}
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
require.NoError(t, err)
|
||||
specCheck(t, testID, testSandboxID, testPid, spec)
|
||||
assert.Contains(t, spec.Process.Env, test.expectedEnv)
|
||||
@@ -1169,7 +1169,7 @@ additional-group-for-root:x:22222:root
|
||||
containerConfig.Linux.SecurityContext = test.securityContext
|
||||
imageConfig.User = test.imageConfigUser
|
||||
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
spec.Root.Path = tempRootDir // simulating /etc/{passwd, group}
|
||||
@@ -1244,7 +1244,7 @@ func TestNonRootUserAndDevices(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
spec, err := c.buildContainerSpec(currentPlatform, t.Name(), testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, config.Runtime{}, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, t.Name(), testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, config.Runtime{}, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.expectedDeviceUID, *spec.Linux.Devices[0].UID)
|
||||
@@ -1317,7 +1317,7 @@ func TestPrivilegedDevices(t *testing.T) {
|
||||
PrivilegedWithoutHostDevices: test.privilegedWithoutHostDevices,
|
||||
PrivilegedWithoutHostDevicesAllDevicesAllowed: test.privilegedWithoutHostDevicesAllDevicesAllowed,
|
||||
}
|
||||
spec, err := c.buildContainerSpec(currentPlatform, t.Name(), testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, t.Name(), testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
hostDevicesRaw, err := oci.HostDevices()
|
||||
@@ -1372,7 +1372,7 @@ func TestBaseOCISpec(t *testing.T) {
|
||||
testPid := uint32(1234)
|
||||
containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData()
|
||||
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
specCheck(t, testID, testSandboxID, testPid, spec)
|
||||
@@ -1700,7 +1700,7 @@ containerEdits:
|
||||
},
|
||||
} {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
specCheck(t, testID, testSandboxID, testPid, spec)
|
||||
@@ -1816,7 +1816,7 @@ func TestUserNamespaceWithHostNetwork(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, spec)
|
||||
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
runtimespec "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
|
||||
customopts "github.com/containerd/containerd/v2/internal/cri/opts"
|
||||
ostesting "github.com/containerd/containerd/v2/pkg/os/testing"
|
||||
)
|
||||
|
||||
// pathBackedSandboxSpec is what a sandbox without a process returns on start:
|
||||
// the namespaces it holds and the pod shared files it owns.
|
||||
func pathBackedSandboxSpec() *runtimespec.Spec {
|
||||
return &runtimespec.Spec{
|
||||
Linux: &runtimespec.Linux{
|
||||
Namespaces: []runtimespec.LinuxNamespace{
|
||||
{Type: runtimespec.NetworkNamespace, Path: "/var/run/netns/cni-1"},
|
||||
{Type: runtimespec.IPCNamespace, Path: "/run/sandbox/ns/ipc"},
|
||||
{Type: runtimespec.UTSNamespace, Path: "/run/sandbox/ns/uts"},
|
||||
},
|
||||
},
|
||||
Mounts: []runtimespec.Mount{
|
||||
{Destination: "/etc/hostname", Type: "bind", Source: "/run/sandbox/hostname"},
|
||||
{Destination: "/etc/hosts", Type: "bind", Source: "/run/sandbox/hosts"},
|
||||
{Destination: "/etc/resolv.conf", Type: "bind", Source: "/run/sandbox/resolv.conf"},
|
||||
{Destination: "/dev/shm", Type: "bind", Source: "/run/sandbox/shm"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestPodNamespacePaths(t *testing.T) {
|
||||
spec := pathBackedSandboxSpec()
|
||||
for name, tc := range map[string]struct {
|
||||
sandboxPid, targetPid uint32
|
||||
spec *runtimespec.Spec
|
||||
expected customopts.PodNamespacePaths
|
||||
}{
|
||||
"a sandbox process wins over the spec": {
|
||||
sandboxPid: 42, targetPid: 42, spec: spec,
|
||||
expected: customopts.PodNamespacePathsFromPid(42, 42),
|
||||
},
|
||||
"no process and no spec is a legacy sandbox api shim": {
|
||||
expected: customopts.PodNamespacePathsFromPid(0, 0),
|
||||
},
|
||||
"no process and a spec without a linux section is legacy too": {
|
||||
spec: &runtimespec.Spec{},
|
||||
expected: customopts.PodNamespacePathsFromPid(0, 0),
|
||||
},
|
||||
"no process and a spec with namespaces is path backed": {
|
||||
spec: spec,
|
||||
expected: customopts.PodNamespacePaths{
|
||||
Network: "/var/run/netns/cni-1",
|
||||
IPC: "/run/sandbox/ns/ipc",
|
||||
UTS: "/run/sandbox/ns/uts",
|
||||
},
|
||||
},
|
||||
"a path backed sandbox in the host namespaces provides none": {
|
||||
spec: &runtimespec.Spec{Linux: &runtimespec.Linux{}},
|
||||
expected: customopts.PodNamespacePaths{},
|
||||
},
|
||||
"a pid namespace entry without a path declares private pid namespaces": {
|
||||
spec: &runtimespec.Spec{Linux: &runtimespec.Linux{Namespaces: []runtimespec.LinuxNamespace{
|
||||
{Type: runtimespec.NetworkNamespace, Path: "/var/run/netns/cni-1"},
|
||||
{Type: runtimespec.PIDNamespace},
|
||||
}}},
|
||||
expected: customopts.PodNamespacePaths{Network: "/var/run/netns/cni-1", PrivatePID: true},
|
||||
},
|
||||
"a target container pid namespace is joined by pid": {
|
||||
targetPid: 7, spec: spec,
|
||||
expected: customopts.PodNamespacePaths{
|
||||
Network: "/var/run/netns/cni-1",
|
||||
IPC: "/run/sandbox/ns/ipc",
|
||||
UTS: "/run/sandbox/ns/uts",
|
||||
PID: "/proc/7/ns/pid",
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.expected, podNamespacePaths(tc.sandboxPid, tc.targetPid, tc.spec))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinuxContainerMountsFromSandboxSpec(t *testing.T) {
|
||||
const testSandboxID = "test-sandbox-id"
|
||||
config := &runtime.ContainerConfig{
|
||||
Metadata: &runtime.ContainerMetadata{Name: "test-name", Attempt: 1},
|
||||
Linux: &runtime.LinuxContainerConfig{
|
||||
SecurityContext: &runtime.LinuxContainerSecurityContext{},
|
||||
},
|
||||
}
|
||||
exists := func(paths ...string) func(string) (os.FileInfo, error) {
|
||||
set := make(map[string]struct{}, len(paths))
|
||||
for _, p := range paths {
|
||||
set[p] = struct{}{}
|
||||
}
|
||||
return func(p string) (os.FileInfo, error) {
|
||||
if _, ok := set[p]; ok {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
}
|
||||
sharedMount := func(containerPath, hostPath string) *runtime.Mount {
|
||||
return &runtime.Mount{ContainerPath: containerPath, HostPath: hostPath, SelinuxRelabel: true}
|
||||
}
|
||||
|
||||
t.Run("the sandbox spec provides the pod shared files", func(t *testing.T) {
|
||||
c := newTestCRIService()
|
||||
c.os.(*ostesting.FakeOS).StatFn = exists("/run/sandbox/hostname", "/run/sandbox/hosts", "/run/sandbox/resolv.conf", "/run/sandbox/shm")
|
||||
assert.Equal(t, []*runtime.Mount{
|
||||
sharedMount(etcHostname, "/run/sandbox/hostname"),
|
||||
sharedMount(etcHosts, "/run/sandbox/hosts"),
|
||||
sharedMount(resolvConfPath, "/run/sandbox/resolv.conf"),
|
||||
sharedMount(devShm, "/run/sandbox/shm"),
|
||||
}, c.linuxContainerMounts(testSandboxID, config, pathBackedSandboxSpec()))
|
||||
})
|
||||
|
||||
t.Run("the fixed sandbox directory wins over the spec", func(t *testing.T) {
|
||||
c := newTestCRIService()
|
||||
c.os.(*ostesting.FakeOS).StatFn = exists(c.getSandboxHosts(testSandboxID), "/run/sandbox/hosts")
|
||||
assert.Equal(t, []*runtime.Mount{
|
||||
sharedMount(etcHosts, c.getSandboxHosts(testSandboxID)),
|
||||
}, c.linuxContainerMounts(testSandboxID, config, pathBackedSandboxSpec()))
|
||||
})
|
||||
|
||||
t.Run("a spec source that does not exist is not mounted", func(t *testing.T) {
|
||||
c := newTestCRIService()
|
||||
c.os.(*ostesting.FakeOS).StatFn = exists()
|
||||
assert.Empty(t, c.linuxContainerMounts(testSandboxID, config, pathBackedSandboxSpec()))
|
||||
})
|
||||
|
||||
t.Run("host ipc uses the host shm", func(t *testing.T) {
|
||||
c := newTestCRIService()
|
||||
c.os.(*ostesting.FakeOS).StatFn = exists(devShm, "/run/sandbox/shm")
|
||||
hostIPC := &runtime.ContainerConfig{
|
||||
Metadata: config.Metadata,
|
||||
Linux: &runtime.LinuxContainerConfig{
|
||||
SecurityContext: &runtime.LinuxContainerSecurityContext{
|
||||
NamespaceOptions: &runtime.NamespaceOption{Ipc: runtime.NamespaceMode_NODE},
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, []*runtime.Mount{
|
||||
{ContainerPath: devShm, HostPath: devShm, SelinuxRelabel: false},
|
||||
}, c.linuxContainerMounts(testSandboxID, hostIPC, pathBackedSandboxSpec()))
|
||||
})
|
||||
}
|
||||
@@ -70,7 +70,7 @@ func TestGeneralContainerSpec(t *testing.T) {
|
||||
c := newTestCRIService()
|
||||
testSandboxID := "sandbox-id"
|
||||
testContainerName := "container-name"
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
require.NoError(t, err)
|
||||
specCheck(t, testID, testSandboxID, testPid, spec)
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func TestPodAnnotationPassthroughContainerSpec(t *testing.T) {
|
||||
PodAnnotations: test.podAnnotations,
|
||||
}
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName,
|
||||
containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, spec)
|
||||
specCheck(t, testID, testSandboxID, testPid, spec)
|
||||
@@ -508,7 +508,7 @@ func TestContainerAnnotationPassthroughContainerSpec(t *testing.T) {
|
||||
ContainerAnnotations: test.containerAnnotations,
|
||||
}
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName,
|
||||
containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, spec)
|
||||
specCheck(t, testID, testSandboxID, testPid, spec)
|
||||
@@ -759,7 +759,7 @@ func TestLinuxContainerMounts(t *testing.T) {
|
||||
}
|
||||
c := newTestCRIService()
|
||||
c.os.(*ostesting.FakeOS).StatFn = test.statFn
|
||||
mounts := c.linuxContainerMounts(testSandboxID, config)
|
||||
mounts := c.linuxContainerMounts(testSandboxID, config, nil)
|
||||
assert.Equal(t, test.expectedMounts, mounts, test.desc)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ func TestContainerWindowsNetworkNamespace(t *testing.T) {
|
||||
c := newTestCRIService()
|
||||
|
||||
containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData()
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, nsPath, testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, config.Runtime{}, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, nsPath, testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, config.Runtime{}, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, spec)
|
||||
specCheck(t, testID, testSandboxID, testPid, spec)
|
||||
@@ -187,7 +187,7 @@ func TestMountCleanPath(t *testing.T) {
|
||||
ContainerPath: "c:/test/container-path",
|
||||
HostPath: "c:/test/host-path",
|
||||
})
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, nsPath, testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, config.Runtime{}, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, nsPath, testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, config.Runtime{}, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, spec)
|
||||
specCheck(t, testID, testSandboxID, testPid, spec)
|
||||
@@ -207,7 +207,7 @@ func TestMountNamedPipe(t *testing.T) {
|
||||
ContainerPath: `\\.\pipe\foo`,
|
||||
HostPath: `\\.\pipe\foo`,
|
||||
})
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, nsPath, testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, config.Runtime{}, nil)
|
||||
spec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, nsPath, testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, config.Runtime{}, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, spec)
|
||||
specCheck(t, testID, testSandboxID, testPid, spec)
|
||||
@@ -258,7 +258,7 @@ func TestHostProcessRequirements(t *testing.T) {
|
||||
sandboxConfig.Windows.SecurityContext = &runtime.WindowsSandboxSecurityContext{
|
||||
HostProcess: test.sandboxHostProcess,
|
||||
}
|
||||
_, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil)
|
||||
_, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, "", testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime, nil, nil)
|
||||
if test.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
@@ -355,7 +355,7 @@ func TestEntrypointAndCmdForArgsEscaped(t *testing.T) {
|
||||
Args: test.args,
|
||||
Windows: &runtime.WindowsContainerConfig{},
|
||||
}
|
||||
runtimeSpec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, nsPath, testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, config.Runtime{}, nil)
|
||||
runtimeSpec, err := c.buildContainerSpec(currentPlatform, testID, testSandboxID, testPid, nsPath, testContainerName, testImageName, containerConfig, sandboxConfig, imageConfig, nil, config.Runtime{}, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, runtimeSpec)
|
||||
|
||||
|
||||
@@ -304,7 +304,7 @@ func (c *criService) RunPodSandbox(ctx context.Context, r *runtime.RunPodSandbox
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.sandboxService.CreateSandbox(ctx, sandboxInfo, sb.WithOptions(config), sb.WithNetNSPath(sandbox.NetNSPath)); err != nil {
|
||||
if err := c.sandboxService.CreateSandbox(ctx, sandboxInfo, sb.WithOptions(config), sb.WithNetNSPath(sandbox.NetNSPath), sb.WithRuntimePath(ociRuntime.Path)); err != nil {
|
||||
return nil, fmt.Errorf("failed to create sandbox %q: %w", id, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
@@ -27,6 +28,7 @@ import (
|
||||
"github.com/containerd/log"
|
||||
"github.com/containerd/plugin"
|
||||
"github.com/containerd/plugin/registry"
|
||||
"github.com/containerd/ttrpc"
|
||||
"github.com/containerd/typeurl/v2"
|
||||
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
@@ -102,10 +104,22 @@ type controllerLocal struct {
|
||||
var _ sandbox.Controller = (*controllerLocal)(nil)
|
||||
|
||||
func (c *controllerLocal) cleanupShim(ctx context.Context, sandboxID string, svc runtimeAPI.TTRPCSandboxService) {
|
||||
// Let the shim exit, then we can clean up the bundle after.
|
||||
if _, sErr := svc.ShutdownSandbox(ctx, &runtimeAPI.ShutdownSandboxRequest{
|
||||
// The caller's context may be the reason Create or Start failed, canceled
|
||||
// or past its deadline; the cleanup must still run, so it keeps the values
|
||||
// of that context (namespace, logger) but not its cancellation, and each
|
||||
// step gets its own deadline instead.
|
||||
ctx = context.WithoutCancel(ctx)
|
||||
|
||||
// Let the shim exit, then we can clean up the bundle after. The deadline
|
||||
// leaves room for the shim to stop what it runs for the sandbox, which
|
||||
// takes its own bounded wait. A closed connection means the shim exited
|
||||
// before answering, as for Shutdown.
|
||||
sctx, scancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
_, sErr := svc.ShutdownSandbox(sctx, &runtimeAPI.ShutdownSandboxRequest{
|
||||
SandboxID: sandboxID,
|
||||
}); sErr != nil {
|
||||
})
|
||||
scancel()
|
||||
if sErr != nil && !errors.Is(sErr, ttrpc.ErrClosed) {
|
||||
log.G(ctx).WithError(sErr).WithField("sandboxID", sandboxID).
|
||||
Error("failed to shutdown sandbox")
|
||||
}
|
||||
@@ -124,7 +138,9 @@ func (c *controllerLocal) Create(ctx context.Context, info sandbox.Sandbox, opts
|
||||
var coptions sandbox.CreateOptions
|
||||
sandboxID := info.ID
|
||||
for _, opt := range opts {
|
||||
opt(&coptions)
|
||||
if err := opt(&coptions); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := c.shims.Get(ctx, sandboxID); err == nil {
|
||||
@@ -141,10 +157,16 @@ func (c *controllerLocal) Create(ctx context.Context, info sandbox.Sandbox, opts
|
||||
}
|
||||
}()
|
||||
|
||||
// The shim manager accepts an absolute binary path in place of a
|
||||
// runtime name.
|
||||
runtimeName := info.Runtime.Name
|
||||
if coptions.RuntimePath != "" {
|
||||
runtimeName = coptions.RuntimePath
|
||||
}
|
||||
shim, err := c.shims.Start(ctx, sandboxID, bundle, runtime.CreateOpts{
|
||||
Spec: info.Spec,
|
||||
RuntimeOptions: info.Runtime.Options,
|
||||
Runtime: info.Runtime.Name,
|
||||
Runtime: runtimeName,
|
||||
TaskOptions: nil,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -252,8 +274,11 @@ func (c *controllerLocal) Shutdown(ctx context.Context, sandboxID string) error
|
||||
return err
|
||||
}
|
||||
|
||||
// A shim may exit as soon as it has shut its sandbox down, before the
|
||||
// response is read; a closed connection means the shutdown happened, as
|
||||
// for the task Shutdown RPC.
|
||||
_, err = svc.ShutdownSandbox(ctx, &runtimeAPI.ShutdownSandboxRequest{SandboxID: sandboxID})
|
||||
if err != nil {
|
||||
if err != nil && !errors.Is(err, ttrpc.ErrClosed) {
|
||||
return fmt.Errorf("failed to shutdown sandbox: %w", errgrpc.ToNative(err))
|
||||
}
|
||||
|
||||
@@ -331,7 +356,7 @@ func (c *controllerLocal) Metrics(ctx context.Context, sandboxID string) (*types
|
||||
req := &runtimeAPI.SandboxMetricsRequest{SandboxID: sandboxID}
|
||||
resp, err := sb.SandboxMetrics(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errgrpc.ToNative(err)
|
||||
}
|
||||
return resp.Metrics, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/sandbox"
|
||||
)
|
||||
|
||||
// A failing create option ends Create before any shim is involved: the
|
||||
// controller has no shim manager here, so getting past the options would panic.
|
||||
func TestCreateOptionError(t *testing.T) {
|
||||
c := &controllerLocal{}
|
||||
err := c.Create(context.Background(), sandbox.Sandbox{ID: "sb"}, sandbox.WithRuntimePath("containerd-shim-example-v1"))
|
||||
require.ErrorIs(t, err, errdefs.ErrInvalidArgument)
|
||||
}
|
||||
@@ -50,6 +50,17 @@ function cleanup() {
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p ${BDIR}/{root,state}
|
||||
|
||||
# SANDBOXER is the sandbox controller of the runtime handler: "podsandbox"
|
||||
# (the pause container, the default) or "shim" (the Sandbox API of the shim,
|
||||
# which needs no pause image).
|
||||
SANDBOXER="${SANDBOXER:-podsandbox}"
|
||||
SANDBOX_CONFIG="sandboxer = \"${SANDBOXER}\""
|
||||
if [ "${SANDBOXER}" = "shim" ]; then
|
||||
SANDBOX_CONFIG="${SANDBOX_CONFIG}
|
||||
disable_pause_image_pull = true"
|
||||
fi
|
||||
|
||||
containerd_config="${CONTAINERD_CONFIG_FILE:-${BDIR}/config.toml}"
|
||||
if [[ -z "${CONTAINERD_CONFIG_FILE:-}" ]]; then
|
||||
cat > "${containerd_config}" <<EOF
|
||||
@@ -57,6 +68,7 @@ version = 2
|
||||
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
|
||||
runtime_type = "${TEST_RUNTIME}"
|
||||
runtime_path = "${TEST_RUNTIME_PATH:-}"
|
||||
${SANDBOX_CONFIG}
|
||||
[plugins."io.containerd.snapshotter.v1.overlayfs"]
|
||||
# slow_chown is needed to avoid an error with kernel < 5.19:
|
||||
# > "snapshotter \"overlayfs\" doesn't support idmap mounts on this host,
|
||||
|
||||
Reference in new issue
Block a user