gvisor/pkg/shim/service.go

1033 lines
29 KiB
Go

// Copyright 2018 The gVisor 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
//
// https://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 shim implements Containerd Shim v2 interface.
package shim
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
"github.com/BurntSushi/toml"
"github.com/containerd/cgroups"
cgroupsstats "github.com/containerd/cgroups/stats/v1"
"github.com/containerd/console"
"github.com/containerd/containerd/api/events"
"github.com/containerd/containerd/api/types/task"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/namespaces"
"github.com/containerd/containerd/pkg/process"
"github.com/containerd/containerd/pkg/stdio"
"github.com/containerd/containerd/runtime"
"github.com/containerd/containerd/runtime/linux/runctypes"
"github.com/containerd/containerd/runtime/v2/shim"
taskAPI "github.com/containerd/containerd/runtime/v2/task"
"github.com/containerd/containerd/sys/reaper"
"github.com/containerd/typeurl"
"github.com/gogo/protobuf/types"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/cleanup"
"gvisor.dev/gvisor/pkg/shim/runtimeoptions/v14"
"gvisor.dev/gvisor/pkg/shim/proc"
"gvisor.dev/gvisor/pkg/shim/runsc"
"gvisor.dev/gvisor/pkg/shim/runtimeoptions"
"gvisor.dev/gvisor/pkg/shim/utils"
"gvisor.dev/gvisor/runsc/specutils"
)
var (
empty = &types.Empty{}
bufPool = sync.Pool{
New: func() interface{} {
buffer := make([]byte, 32<<10)
return &buffer
},
}
)
var _ = (taskAPI.TaskService)(&service{})
const (
// configFile is the default config file name. For containerd 1.2,
// we assume that a config.toml should exist in the runtime root.
configFile = "config.toml"
// shimAddressPath is the relative path to a file that contains the address
// to the shim UDS. See service.shimAddress.
shimAddressPath = "address"
cgroupParentAnnotation = "dev.gvisor.spec.cgroup-parent"
)
// New returns a new shim service that can be used via GRPC.
func New(ctx context.Context, id string, publisher shim.Publisher, cancel func()) (shim.Shim, error) {
var opts shim.Opts
if ctxOpts := ctx.Value(shim.OptsKey{}); ctxOpts != nil {
opts = ctxOpts.(shim.Opts)
}
ep, err := newOOMEpoller(publisher)
if err != nil {
return nil, err
}
go ep.run(ctx)
s := &service{
id: id,
processes: make(map[string]process.Process),
events: make(chan interface{}, 128),
ec: proc.ExitCh,
oomPoller: ep,
cancel: cancel,
genericOptions: opts,
}
go s.processExits(ctx)
runsc.Monitor = &runsc.LogMonitor{Next: reaper.Default}
if err := s.initPlatform(); err != nil {
cancel()
return nil, fmt.Errorf("failed to initialized platform behavior: %w", err)
}
go s.forward(ctx, publisher)
if address, err := shim.ReadAddress(shimAddressPath); err == nil {
s.shimAddress = address
}
return s, nil
}
// service is the shim implementation of a remote shim over GRPC. It runs in 2
// different modes:
// 1. Service: process runs for the life time of the container and receives
// calls described in shimapi.TaskService interface.
// 2. Tool: process is short lived and runs only to perform the requested
// operations and then exits. It implements the direct functions in
// shim.Shim interface.
//
// When the service is running, it saves a json file with state information so
// that commands sent to the tool can load the state and perform the operation.
type service struct {
mu sync.Mutex
// id is the container ID.
id string
// bundle is a path provided by the caller on container creation. Store
// because it's needed in commands that don't receive bundle in the request.
bundle string
// task is the main process that is running the container.
task *proc.Init
// processes maps ExecId to processes running through exec.
processes map[string]process.Process
events chan interface{}
// platform handles operations related to the console.
platform stdio.Platform
// genericOptions are options that come from the shim interface and are common
// to all shims.
genericOptions shim.Opts
// opts are configuration options specific for this shim.
opts options
// ex gets notified whenever the container init process or an exec'd process
// exits from inside the sandbox.
ec chan proc.Exit
// oomPoller monitors the sandbox's cgroup for OOM notifications.
oomPoller *epoller
// cancel is a function that needs to be called before the shim stops. The
// function is provided by the caller to New().
cancel func()
// shimAddress is the location of the UDS used to communicate to containerd.
shimAddress string
}
func (s *service) newCommand(ctx context.Context, containerdBinary, containerdAddress string) (*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,
"-address", containerdAddress,
"-publish-binary", containerdBinary,
}
if s.genericOptions.Debug {
args = append(args, "-debug")
}
cmd := exec.Command(self, args...)
cmd.Dir = cwd
cmd.Env = append(os.Environ(), "GOMAXPROCS=2")
cmd.SysProcAttr = &unix.SysProcAttr{
Setpgid: true,
}
return cmd, nil
}
func (s *service) StartShim(ctx context.Context, id, containerdBinary, containerdAddress, containerdTTRPCAddress string) (string, error) {
log.L.Debugf("StartShim, id: %s, binary: %q, address: %q", id, containerdBinary, containerdAddress)
cmd, err := s.newCommand(ctx, containerdBinary, containerdAddress)
if err != nil {
return "", err
}
address, err := shim.SocketAddress(ctx, containerdAddress, id)
if err != nil {
return "", err
}
socket, err := shim.NewSocket(address)
if err != nil {
// The only time where this would happen is if there is a bug and the socket
// was not cleaned up in the cleanup method of the shim or we are using the
// grouping functionality where the new process should be run with the same
// shim as an existing container.
if !shim.SocketEaddrinuse(err) {
return "", fmt.Errorf("create new shim socket: %w", err)
}
if shim.CanConnect(address) {
if err := shim.WriteAddress(shimAddressPath, address); err != nil {
return "", fmt.Errorf("write existing socket for shim: %w", err)
}
return address, nil
}
if err := shim.RemoveSocket(address); err != nil {
return "", fmt.Errorf("remove pre-existing socket: %w", err)
}
if socket, err = shim.NewSocket(address); err != nil {
return "", fmt.Errorf("try create new shim socket 2x: %w", err)
}
}
cu := cleanup.Make(func() {
socket.Close()
_ = shim.RemoveSocket(address)
})
defer cu.Clean()
f, err := socket.File()
if err != nil {
return "", err
}
cmd.ExtraFiles = append(cmd.ExtraFiles, f)
log.L.Debugf("Executing: %q %s", cmd.Path, cmd.Args)
if err := cmd.Start(); err != nil {
f.Close()
return "", err
}
cu.Add(func() { cmd.Process.Kill() })
// make sure to wait after start
go cmd.Wait()
if err := shim.WritePidFile("shim.pid", cmd.Process.Pid); err != nil {
return "", err
}
if err := shim.WriteAddress(shimAddressPath, address); err != nil {
return "", err
}
if err := shim.SetScore(cmd.Process.Pid); err != nil {
return "", fmt.Errorf("failed to set OOM Score on shim: %w", err)
}
cu.Release()
return address, nil
}
// Cleanup is called from another process (need to reload state) to stop the
// container and undo all operations done in Create().
func (s *service) Cleanup(ctx context.Context) (*taskAPI.DeleteResponse, error) {
log.L.Debugf("Cleanup")
path, err := os.Getwd()
if err != nil {
return nil, err
}
ns, err := namespaces.NamespaceRequired(ctx)
if err != nil {
return nil, err
}
var st state
if err := st.load(path); err != nil {
return nil, err
}
r := proc.NewRunsc(s.opts.Root, path, ns, st.Options.BinaryName, nil)
if err := r.Delete(ctx, s.id, &runsc.DeleteOpts{
Force: true,
}); err != nil {
log.L.Infof("failed to remove runc container: %v", err)
}
if err := mount.UnmountAll(st.Rootfs, 0); err != nil {
log.L.Infof("failed to cleanup rootfs mount: %v", err)
}
return &taskAPI.DeleteResponse{
ExitedAt: time.Now(),
ExitStatus: 128 + uint32(unix.SIGKILL),
}, nil
}
// Create creates a new initial process and container with the underlying OCI
// runtime.
func (s *service) Create(ctx context.Context, r *taskAPI.CreateTaskRequest) (*taskAPI.CreateTaskResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
// Save the main task id and bundle to the shim for additional requests.
s.id = r.ID
s.bundle = r.Bundle
ns, err := namespaces.NamespaceRequired(ctx)
if err != nil {
return nil, fmt.Errorf("create namespace: %w", err)
}
// Read from root for now.
if r.Options != nil {
v, err := typeurl.UnmarshalAny(r.Options)
if err != nil {
return nil, err
}
var path string
switch o := v.(type) {
case *runctypes.CreateOptions: // containerd 1.2.x
s.opts.IoUID = o.IoUid
s.opts.IoGID = o.IoGid
s.opts.ShimCgroup = o.ShimCgroup
case *runctypes.RuncOptions: // containerd 1.2.x
root := proc.RunscRoot
if o.RuntimeRoot != "" {
root = o.RuntimeRoot
}
s.opts.BinaryName = o.Runtime
path = filepath.Join(root, configFile)
if _, err := os.Stat(path); err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf("stat config file %q: %w", path, err)
}
// A config file in runtime root is not required.
path = ""
}
case *runtimeoptions.Options: // containerd 1.5+
if o.ConfigPath == "" {
break
}
if o.TypeUrl != optionsType {
return nil, fmt.Errorf("unsupported option type %q", o.TypeUrl)
}
path = o.ConfigPath
case *v14.Options: // containerd 1.4-
if o.ConfigPath == "" {
break
}
if o.TypeUrl != optionsType {
return nil, fmt.Errorf("unsupported option type %q", o.TypeUrl)
}
path = o.ConfigPath
default:
return nil, fmt.Errorf("unsupported option type %q", r.Options.TypeUrl)
}
if path != "" {
if _, err = toml.DecodeFile(path, &s.opts); err != nil {
return nil, fmt.Errorf("decode config file %q: %w", path, err)
}
}
}
if len(s.opts.LogLevel) != 0 {
lvl, err := logrus.ParseLevel(s.opts.LogLevel)
if err != nil {
return nil, err
}
logrus.SetLevel(lvl)
}
if len(s.opts.LogPath) != 0 {
logPath := runsc.FormatShimLogPath(s.opts.LogPath, s.id)
if err := os.MkdirAll(filepath.Dir(logPath), 0777); err != nil {
return nil, fmt.Errorf("failed to create log dir: %w", err)
}
logFile, err := os.Create(logPath)
if err != nil {
return nil, fmt.Errorf("failed to create log file: %w", err)
}
log.L.Debugf("Starting mirror log at %q", logPath)
std := logrus.StandardLogger()
std.SetOutput(io.MultiWriter(std.Out, logFile))
log.L.Debugf("Create shim")
log.L.Debugf("***************************")
log.L.Debugf("Args: %s", os.Args)
log.L.Debugf("PID: %d", os.Getpid())
log.L.Debugf("ID: %s", s.id)
log.L.Debugf("Options: %+v", s.opts)
log.L.Debugf("Bundle: %s", r.Bundle)
log.L.Debugf("Terminal: %t", r.Terminal)
log.L.Debugf("stdin: %s", r.Stdin)
log.L.Debugf("stdout: %s", r.Stdout)
log.L.Debugf("stderr: %s", r.Stderr)
log.L.Debugf("***************************")
if log.L.Logger.IsLevelEnabled(logrus.DebugLevel) {
setDebugSigHandler()
}
}
// Save state before any action is taken to ensure Cleanup() will have all
// the information it needs to undo the operations.
st := state{
Rootfs: filepath.Join(r.Bundle, "rootfs"),
Options: s.opts,
}
if err := st.save(r.Bundle); err != nil {
return nil, err
}
if err := os.Mkdir(st.Rootfs, 0711); err != nil && !os.IsExist(err) {
return nil, err
}
// Convert from types.Mount to proc.Mount.
var mounts []proc.Mount
for _, m := range r.Rootfs {
mounts = append(mounts, proc.Mount{
Type: m.Type,
Source: m.Source,
Target: m.Target,
Options: m.Options,
})
}
// Cleans up all mounts in case of failure.
cu := cleanup.Make(func() {
if err := mount.UnmountAll(st.Rootfs, 0); err != nil {
log.L.Infof("failed to cleanup rootfs mount: %v", err)
}
})
defer cu.Clean()
for _, rm := range mounts {
m := &mount.Mount{
Type: rm.Type,
Source: rm.Source,
Options: rm.Options,
}
if err := m.Mount(st.Rootfs); err != nil {
return nil, fmt.Errorf("failed to mount rootfs component %v: %w", m, err)
}
}
config := &proc.CreateConfig{
ID: r.ID,
Bundle: r.Bundle,
Runtime: s.opts.BinaryName,
Rootfs: mounts,
Terminal: r.Terminal,
Stdin: r.Stdin,
Stdout: r.Stdout,
Stderr: r.Stderr,
}
process, err := newInit(r.Bundle, filepath.Join(r.Bundle, "work"), ns, s.platform, config, &s.opts, st.Rootfs)
if err != nil {
return nil, utils.ErrToGRPC(err)
}
if err := process.Create(ctx, config); err != nil {
return nil, utils.ErrToGRPC(err)
}
// Set up OOM notification on the sandbox's cgroup. This is done on
// sandbox create since the sandbox process will be created here.
pid := process.Pid()
if pid > 0 {
cg, err := cgroups.Load(cgroups.V1, cgroups.PidPath(pid))
if err != nil {
return nil, fmt.Errorf("loading cgroup for %d: %w", pid, err)
}
if err := s.oomPoller.add(s.id, cg); err != nil {
return nil, fmt.Errorf("add cg to OOM monitor: %w", err)
}
}
// Success
cu.Release()
s.task = process
return &taskAPI.CreateTaskResponse{
Pid: uint32(process.Pid()),
}, nil
}
// Start starts a process.
func (s *service) Start(ctx context.Context, r *taskAPI.StartRequest) (*taskAPI.StartResponse, error) {
log.L.Debugf("Start, id: %s, execID: %s", r.ID, r.ExecID)
p, err := s.getProcess(r.ExecID)
if err != nil {
return nil, err
}
if err := p.Start(ctx); err != nil {
return nil, err
}
// TODO: Set the cgroup and oom notifications on restore.
// https://github.com/google/gvisor-containerd-shim/issues/58
return &taskAPI.StartResponse{
Pid: uint32(p.Pid()),
}, nil
}
// Delete deletes the initial process and container.
func (s *service) Delete(ctx context.Context, r *taskAPI.DeleteRequest) (*taskAPI.DeleteResponse, error) {
log.L.Debugf("Delete, id: %s, execID: %s", r.ID, r.ExecID)
p, err := s.getProcess(r.ExecID)
if err != nil {
return nil, err
}
if err := p.Delete(ctx); err != nil {
return nil, err
}
if len(r.ExecID) != 0 {
s.mu.Lock()
delete(s.processes, r.ExecID)
s.mu.Unlock()
} else if s.platform != nil {
s.platform.Close()
}
return &taskAPI.DeleteResponse{
ExitStatus: uint32(p.ExitStatus()),
ExitedAt: p.ExitedAt(),
Pid: uint32(p.Pid()),
}, nil
}
// Exec spawns an additional process inside the container.
func (s *service) Exec(ctx context.Context, r *taskAPI.ExecProcessRequest) (*types.Empty, error) {
log.L.Debugf("Exec, id: %s, execID: %s", r.ID, r.ExecID)
s.mu.Lock()
p := s.processes[r.ExecID]
s.mu.Unlock()
if p != nil {
return nil, utils.ErrToGRPCf(errdefs.ErrAlreadyExists, "id %s", r.ExecID)
}
if s.task == nil {
return nil, utils.ErrToGRPCf(errdefs.ErrFailedPrecondition, "container must be created")
}
process, err := s.task.Exec(ctx, s.bundle, &proc.ExecConfig{
ID: r.ExecID,
Terminal: r.Terminal,
Stdin: r.Stdin,
Stdout: r.Stdout,
Stderr: r.Stderr,
Spec: r.Spec,
})
if err != nil {
return nil, utils.ErrToGRPC(err)
}
s.mu.Lock()
s.processes[r.ExecID] = process
s.mu.Unlock()
return empty, nil
}
// ResizePty resizes the terminal of a process.
func (s *service) ResizePty(ctx context.Context, r *taskAPI.ResizePtyRequest) (*types.Empty, error) {
log.L.Debugf("ResizePty, id: %s, execID: %s, dimension: %dx%d", r.ID, r.ExecID, r.Height, r.Width)
p, err := s.getProcess(r.ExecID)
if err != nil {
return nil, err
}
ws := console.WinSize{
Width: uint16(r.Width),
Height: uint16(r.Height),
}
if err := p.Resize(ws); err != nil {
return nil, utils.ErrToGRPC(err)
}
return empty, nil
}
// State returns runtime state information for a process.
func (s *service) State(ctx context.Context, r *taskAPI.StateRequest) (*taskAPI.StateResponse, error) {
log.L.Debugf("State, id: %s, execID: %s", r.ID, r.ExecID)
p, err := s.getProcess(r.ExecID)
if err != nil {
log.L.Debugf("State failed to find process: %v", err)
return nil, err
}
st, err := p.Status(ctx)
if err != nil {
log.L.Debugf("State failed: %v", err)
return nil, err
}
status := task.StatusUnknown
switch st {
case "created":
status = task.StatusCreated
case "running":
status = task.StatusRunning
case "stopped":
status = task.StatusStopped
}
sio := p.Stdio()
res := &taskAPI.StateResponse{
ID: p.ID(),
Bundle: s.bundle,
Pid: uint32(p.Pid()),
Status: status,
Stdin: sio.Stdin,
Stdout: sio.Stdout,
Stderr: sio.Stderr,
Terminal: sio.Terminal,
ExitStatus: uint32(p.ExitStatus()),
ExitedAt: p.ExitedAt(),
}
log.L.Debugf("State succeeded, response: %+v", res)
return res, nil
}
// Pause the container.
func (s *service) Pause(ctx context.Context, r *taskAPI.PauseRequest) (*types.Empty, error) {
log.L.Debugf("Pause, id: %s", r.ID)
if s.task == nil {
log.L.Debugf("Pause error, id: %s: container not created", r.ID)
return nil, utils.ErrToGRPCf(errdefs.ErrFailedPrecondition, "container must be created")
}
err := s.task.Runtime().Pause(ctx, r.ID)
if err != nil {
return nil, err
}
return empty, nil
}
// Resume the container.
func (s *service) Resume(ctx context.Context, r *taskAPI.ResumeRequest) (*types.Empty, error) {
log.L.Debugf("Resume, id: %s", r.ID)
if s.task == nil {
log.L.Debugf("Resume error, id: %s: container not created", r.ID)
return nil, utils.ErrToGRPCf(errdefs.ErrFailedPrecondition, "container must be created")
}
err := s.task.Runtime().Resume(ctx, r.ID)
if err != nil {
return nil, err
}
return empty, nil
}
// Kill a process with the provided signal.
func (s *service) Kill(ctx context.Context, r *taskAPI.KillRequest) (*types.Empty, error) {
log.L.Debugf("Kill, id: %s, execID: %s, signal: %d, all: %t", r.ID, r.ExecID, r.Signal, r.All)
p, err := s.getProcess(r.ExecID)
if err != nil {
return nil, err
}
if err := p.Kill(ctx, r.Signal, r.All); err != nil {
log.L.Debugf("Kill failed: %v", err)
return nil, utils.ErrToGRPC(err)
}
log.L.Debugf("Kill succeeded")
return empty, nil
}
// Pids returns all pids inside the container.
func (s *service) Pids(ctx context.Context, r *taskAPI.PidsRequest) (*taskAPI.PidsResponse, error) {
log.L.Debugf("Pids, id: %s", r.ID)
pids, err := s.getContainerPids(ctx, r.ID)
if err != nil {
return nil, utils.ErrToGRPC(err)
}
var processes []*task.ProcessInfo
for _, pid := range pids {
pInfo := task.ProcessInfo{
Pid: pid,
}
for _, p := range s.processes {
if p.Pid() == int(pid) {
d := &runctypes.ProcessDetails{
ExecID: p.ID(),
}
a, err := typeurl.MarshalAny(d)
if err != nil {
return nil, fmt.Errorf("failed to marshal process %d info: %w", pid, err)
}
pInfo.Info = a
break
}
}
processes = append(processes, &pInfo)
}
return &taskAPI.PidsResponse{
Processes: processes,
}, nil
}
// CloseIO closes the I/O context of a process.
func (s *service) CloseIO(ctx context.Context, r *taskAPI.CloseIORequest) (*types.Empty, error) {
log.L.Debugf("CloseIO, id: %s, execID: %s, stdin: %t", r.ID, r.ExecID, r.Stdin)
p, err := s.getProcess(r.ExecID)
if err != nil {
return nil, err
}
if stdin := p.Stdin(); stdin != nil {
if err := stdin.Close(); err != nil {
return nil, fmt.Errorf("close stdin: %w", err)
}
}
return empty, nil
}
// Checkpoint checkpoints the container.
func (s *service) Checkpoint(ctx context.Context, r *taskAPI.CheckpointTaskRequest) (*types.Empty, error) {
log.L.Debugf("Checkpoint, id: %s", r.ID)
return empty, utils.ErrToGRPC(errdefs.ErrNotImplemented)
}
// Connect returns shim information such as the shim's pid.
func (s *service) Connect(ctx context.Context, r *taskAPI.ConnectRequest) (*taskAPI.ConnectResponse, error) {
log.L.Debugf("Connect, id: %s", r.ID)
var pid int
if s.task != nil {
pid = s.task.Pid()
}
return &taskAPI.ConnectResponse{
ShimPid: uint32(os.Getpid()),
TaskPid: uint32(pid),
}, nil
}
func (s *service) Shutdown(ctx context.Context, r *taskAPI.ShutdownRequest) (*types.Empty, error) {
log.L.Debugf("Shutdown, id: %s", r.ID)
s.cancel()
if s.shimAddress != "" {
_ = shim.RemoveSocket(s.shimAddress)
}
os.Exit(0)
panic("Should not get here")
}
func (s *service) Stats(ctx context.Context, r *taskAPI.StatsRequest) (*taskAPI.StatsResponse, error) {
log.L.Debugf("Stats, id: %s", r.ID)
if s.task == nil {
log.L.Debugf("Stats error, id: %s: container not created", r.ID)
return nil, utils.ErrToGRPCf(errdefs.ErrFailedPrecondition, "container must be created")
}
stats, err := s.task.Stats(ctx, s.id)
if err != nil {
log.L.Debugf("Stats error, id: %s: %v", r.ID, err)
return nil, err
}
// gvisor currently (as of 2020-03-03) only returns the total memory
// usage and current PID value[0]. However, we copy the common fields here
// so that future updates will propagate correct information. We're
// using the cgroups.Metrics structure so we're returning the same type
// as runc.
//
// [0]: https://github.com/google/gvisor/blob/277a0d5a1fbe8272d4729c01ee4c6e374d047ebc/runsc/boot/events.go#L61-L81
metrics := &cgroupsstats.Metrics{
CPU: &cgroupsstats.CPUStat{
Usage: &cgroupsstats.CPUUsage{
Total: stats.Cpu.Usage.Total,
Kernel: stats.Cpu.Usage.Kernel,
User: stats.Cpu.Usage.User,
PerCPU: stats.Cpu.Usage.Percpu,
},
Throttling: &cgroupsstats.Throttle{
Periods: stats.Cpu.Throttling.Periods,
ThrottledPeriods: stats.Cpu.Throttling.ThrottledPeriods,
ThrottledTime: stats.Cpu.Throttling.ThrottledTime,
},
},
Memory: &cgroupsstats.MemoryStat{
Cache: stats.Memory.Cache,
Usage: &cgroupsstats.MemoryEntry{
Limit: stats.Memory.Usage.Limit,
Usage: stats.Memory.Usage.Usage,
Max: stats.Memory.Usage.Max,
Failcnt: stats.Memory.Usage.Failcnt,
},
Swap: &cgroupsstats.MemoryEntry{
Limit: stats.Memory.Swap.Limit,
Usage: stats.Memory.Swap.Usage,
Max: stats.Memory.Swap.Max,
Failcnt: stats.Memory.Swap.Failcnt,
},
Kernel: &cgroupsstats.MemoryEntry{
Limit: stats.Memory.Kernel.Limit,
Usage: stats.Memory.Kernel.Usage,
Max: stats.Memory.Kernel.Max,
Failcnt: stats.Memory.Kernel.Failcnt,
},
KernelTCP: &cgroupsstats.MemoryEntry{
Limit: stats.Memory.KernelTCP.Limit,
Usage: stats.Memory.KernelTCP.Usage,
Max: stats.Memory.KernelTCP.Max,
Failcnt: stats.Memory.KernelTCP.Failcnt,
},
},
Pids: &cgroupsstats.PidsStat{
Current: stats.Pids.Current,
Limit: stats.Pids.Limit,
},
}
data, err := typeurl.MarshalAny(metrics)
if err != nil {
log.L.Debugf("Stats error, id: %s: %v", r.ID, err)
return nil, err
}
log.L.Debugf("Stats success, id: %s: %+v", r.ID, data)
return &taskAPI.StatsResponse{
Stats: data,
}, nil
}
// Update updates a running container.
func (s *service) Update(ctx context.Context, r *taskAPI.UpdateTaskRequest) (*types.Empty, error) {
return empty, utils.ErrToGRPC(errdefs.ErrNotImplemented)
}
// Wait waits for a process to exit.
func (s *service) Wait(ctx context.Context, r *taskAPI.WaitRequest) (*taskAPI.WaitResponse, error) {
log.L.Debugf("Wait, id: %s, execID: %s", r.ID, r.ExecID)
p, err := s.getProcess(r.ExecID)
if err != nil {
log.L.Debugf("Wait failed to find process: %v", err)
return nil, err
}
p.Wait()
res := &taskAPI.WaitResponse{
ExitStatus: uint32(p.ExitStatus()),
ExitedAt: p.ExitedAt(),
}
log.L.Debugf("Wait succeeded, response: %+v", res)
return res, nil
}
func (s *service) processExits(ctx context.Context) {
for e := range s.ec {
s.checkProcesses(ctx, e)
}
}
func (s *service) checkProcesses(ctx context.Context, e proc.Exit) {
// TODO(random-liu): Add `shouldKillAll` logic if container pid
// namespace is supported.
for _, p := range s.allProcesses() {
if p.ID() == e.ID {
if ip, ok := p.(*proc.Init); ok {
// Ensure all children are killed.
log.L.Debugf("Container init process exited, killing all container processes")
ip.KillAll(ctx)
}
p.SetExited(e.Status)
s.events <- &events.TaskExit{
ContainerID: s.id,
ID: p.ID(),
Pid: uint32(p.Pid()),
ExitStatus: uint32(e.Status),
ExitedAt: p.ExitedAt(),
}
return
}
}
}
func (s *service) allProcesses() (o []process.Process) {
s.mu.Lock()
defer s.mu.Unlock()
for _, p := range s.processes {
o = append(o, p)
}
if s.task != nil {
o = append(o, s.task)
}
return o
}
func (s *service) getContainerPids(ctx context.Context, id string) ([]uint32, error) {
s.mu.Lock()
p := s.task
s.mu.Unlock()
if p == nil {
return nil, fmt.Errorf("container must be created: %w", errdefs.ErrFailedPrecondition)
}
ps, err := p.Runtime().Ps(ctx, id)
if err != nil {
return nil, err
}
pids := make([]uint32, 0, len(ps))
for _, pid := range ps {
pids = append(pids, uint32(pid))
}
return pids, nil
}
func (s *service) forward(ctx context.Context, publisher shim.Publisher) {
for e := range s.events {
err := publisher.Publish(ctx, getTopic(e), e)
if err != nil {
// Should not happen.
panic(fmt.Errorf("post event: %w", err))
}
}
}
func (s *service) getProcess(execID string) (process.Process, error) {
s.mu.Lock()
defer s.mu.Unlock()
if execID == "" {
if s.task == nil {
return nil, utils.ErrToGRPCf(errdefs.ErrFailedPrecondition, "container must be created")
}
return s.task, nil
}
p := s.processes[execID]
if p == nil {
return nil, utils.ErrToGRPCf(errdefs.ErrNotFound, "process does not exist %s", execID)
}
return p, nil
}
func getTopic(e interface{}) string {
switch e.(type) {
case *events.TaskCreate:
return runtime.TaskCreateEventTopic
case *events.TaskStart:
return runtime.TaskStartEventTopic
case *events.TaskOOM:
return runtime.TaskOOMEventTopic
case *events.TaskExit:
return runtime.TaskExitEventTopic
case *events.TaskDelete:
return runtime.TaskDeleteEventTopic
case *events.TaskExecAdded:
return runtime.TaskExecAddedEventTopic
case *events.TaskExecStarted:
return runtime.TaskExecStartedEventTopic
default:
log.L.Infof("no topic for type %#v", e)
}
return runtime.TaskUnknownTopic
}
func newInit(path, workDir, namespace string, platform stdio.Platform, r *proc.CreateConfig, options *options, rootfs string) (*proc.Init, error) {
spec, err := utils.ReadSpec(r.Bundle)
if err != nil {
return nil, fmt.Errorf("read oci spec: %w", err)
}
updated, err := utils.UpdateVolumeAnnotations(spec)
if err != nil {
return nil, fmt.Errorf("update volume annotations: %w", err)
}
updated = setPodCgroup(spec) || updated
if updated {
if err := utils.WriteSpec(r.Bundle, spec); err != nil {
return nil, err
}
}
runsc.FormatRunscLogPath(r.ID, options.RunscConfig)
runtime := proc.NewRunsc(options.Root, path, namespace, options.BinaryName, options.RunscConfig)
p := proc.New(r.ID, runtime, stdio.Stdio{
Stdin: r.Stdin,
Stdout: r.Stdout,
Stderr: r.Stderr,
Terminal: r.Terminal,
})
p.Bundle = r.Bundle
p.Platform = platform
p.Rootfs = rootfs
p.WorkDir = workDir
p.IoUID = int(options.IoUID)
p.IoGID = int(options.IoGID)
p.Sandbox = specutils.SpecContainerType(spec) == specutils.ContainerTypeSandbox
p.UserLog = utils.UserLogPath(spec)
p.Monitor = reaper.Default
return p, nil
}
// setPodCgroup searches for the pod cgroup path inside the container's cgroup
// path. If found, it's set as an annotation in the spec. This is done so that
// the sandbox joins the pod cgroup. Otherwise, the sandbox would join the pause
// container cgroup. Returns true if the spec was modified. Ex.:
// /kubepods/burstable/pod123/container123 => kubepods/burstable/pod123
//
func setPodCgroup(spec *specs.Spec) bool {
if !utils.IsSandbox(spec) {
return false
}
if spec.Linux == nil || len(spec.Linux.CgroupsPath) == 0 {
return false
}
// Search backwards for the pod cgroup path to make the sandbox use it,
// instead of the pause container's cgroup.
parts := strings.Split(spec.Linux.CgroupsPath, string(filepath.Separator))
for i := len(parts) - 1; i >= 0; i-- {
if strings.HasPrefix(parts[i], "pod") {
var path string
for j := 0; j <= i; j++ {
path = filepath.Join(path, parts[j])
}
// Add back the initial '/' that may have been lost above.
if filepath.IsAbs(spec.Linux.CgroupsPath) {
path = string(filepath.Separator) + path
}
if spec.Linux.CgroupsPath == path {
return false
}
if spec.Annotations == nil {
spec.Annotations = make(map[string]string)
}
spec.Annotations[cgroupParentAnnotation] = path
return true
}
}
return false
}