Files
Jing Chen ff3425ac1f Fix crash and resource-leak bugs in the containerd shim
- runsc/debug.go: the SIGUSR2 stack dumper never exited its loop, so one
signal pinned a core for the life of the shim.
- proc/init_state.go: the start-failure path closed p.io unconditionally,
but Init.Create leaves it nil for terminal containers. The nil deref
killed the shim, and with it the pod.
- proc/utils.go: getLastRuntimeError leaked the log file it opened, on
every failed runsc command.
- utils/volumes.go: configureShm wrote into a nil annotation map, which
IsSandbox permits.
- runsc/epoll.go: the cgroups v1 OOM watcher panicked on the first
publish failure, which is normal under CRI-O. Reuse forward's leniency
check; the panic under containerd is kept.
- runsccmd/runsc.go: cmdOutput returned slices aliasing sync.Pool buffers
that putBuf immediately recycled.
2026-08-26 22:47:36 +00:00

955 lines
26 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 runsc implements Containerd Shim v2 interface.
package runsc
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
taskServer "gvisor.dev/gvisor/pkg/shim/v1/taskserver"
pb "gvisor.dev/gvisor/pkg/shim/v1/taskserver/task_server_go_proto"
cgroups "github.com/containerd/cgroups/v3"
cgroup1 "github.com/containerd/cgroups/v3/cgroup1"
cgroupsv2 "github.com/containerd/cgroups/v3/cgroup2"
"github.com/containerd/containerd/api/events"
task "github.com/containerd/containerd/api/runtime/task/v2"
runctypes "github.com/containerd/containerd/api/types/runc/options"
tasktypes "github.com/containerd/containerd/api/types/task"
"github.com/containerd/containerd/v2/cmd/containerd-shim-runc-v2/process"
"github.com/containerd/containerd/v2/core/runtime"
"github.com/containerd/containerd/v2/pkg/protobuf"
types "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/pkg/stdio"
"github.com/containerd/containerd/v2/pkg/sys/reaper"
"github.com/containerd/errdefs"
errgrpc "github.com/containerd/errdefs/pkg/errgrpc"
"github.com/containerd/log"
typeurl "github.com/containerd/typeurl/v2"
specs "github.com/opencontainers/runtime-spec/specs-go"
"gvisor.dev/gvisor/pkg/shim/v1/extension"
"gvisor.dev/gvisor/pkg/shim/v1/proc"
"gvisor.dev/gvisor/pkg/shim/v1/runsccmd"
"gvisor.dev/gvisor/pkg/shim/v1/utils"
"gvisor.dev/gvisor/runsc/specutils"
)
var (
empty = &types.Empty{}
bufPool = sync.Pool{
New: func() any {
buffer := make([]byte, 32<<10)
return &buffer
},
}
)
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"
cgroupParentAnnotation = "dev.gvisor.spec.cgroup-parent"
)
type oomPoller interface {
io.Closer
// add adds `cg` cgroup to oom poller. `cg` is cgroups.Cgroup in v1 and
// `cgroupsv2.Manager` in v2
add(id string, cg any) error
// run monitors oom event and notifies the shim about them
run(ctx context.Context)
// isOOM synchronously checks if the container was OOM-killed by reading
// the cgroup's memory events. This is used at container exit time to
// ensure the TaskOOM event is published before TaskExit, even if the
// async notification has not yet arrived. This is a one-shot operation:
// the cgroup reference is consumed on the first call, and subsequent
// calls for the same id return false.
isOOM(id string) bool
}
// runscService is the shim implementation of a remote shim over gRPC. It converts
// shim calls into `runsc` commands. 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
// with the required context.
type runscService struct {
mu sync.Mutex
events chan any
// platform handles operations related to the console.
platform stdio.Platform
// 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 oomPoller
// containers maps container id to a container.
containers map[string]*Container
// root is the runsc root directory.
root string
// runtime is runsc runtime configured for sandbox.
runtime *runsccmd.Runsc
shutdown shutdown.Service
}
var _ extension.TaskServiceExt = (*runscService)(nil)
// NewTaskService returns a runsc task service.
func NewTaskService(ctx context.Context, publisher shim.Publisher, sd shutdown.Service) (extension.TaskServiceExt, error) {
var (
ep oomPoller
err error
)
if cgroups.Mode() == cgroups.Unified {
ep, err = newOOMv2Poller(publisher)
} else {
ep, err = newOOMEpoller(publisher)
}
if err != nil {
return nil, err
}
go ep.run(ctx)
s := &runscService{
events: make(chan any, 128),
containers: make(map[string]*Container),
ec: proc.ExitCh,
oomPoller: ep,
shutdown: sd,
root: proc.RunscRoot,
runtime: &runsccmd.Runsc{Root: proc.RunscRoot},
}
go s.processExits(ctx)
runsccmd.Monitor = &runsccmd.LogMonitor{Next: reaper.Default}
if err := s.initPlatform(); err != nil {
return nil, fmt.Errorf("failed to initialized platform behavior: %w", err)
}
go s.forward(ctx, publisher)
sd.RegisterCallback(func(context.Context) error {
close(s.events)
return nil
})
return s, nil
}
// getContainer returns the container by id.
func (s *runscService) getContainer(id string) (*Container, error) {
s.mu.Lock()
defer s.mu.Unlock()
c := s.containers[id]
if c == nil {
return nil, errgrpc.ToGRPCf(errdefs.ErrNotFound, "container not created")
}
return c, nil
}
// Create creates a new initial process and container with the underlying OCI
// runtime.
func (s *runscService) Create(ctx context.Context, r *task.CreateTaskRequest) (*task.CreateTaskResponse, error) {
return s.CreateWithFSRestore(ctx, &extension.CreateWithFSRestoreRequest{
Create: r,
})
}
// CreateWithFSRestore is the same as Create, but it additionally restores the
// container's filesystem from a snapshot.
func (s *runscService) CreateWithFSRestore(ctx context.Context, rfs *extension.CreateWithFSRestoreRequest) (*task.CreateTaskResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
c, err := NewContainer(ctx, s.platform, &ContainerConfig{
ID: rfs.Create.ID,
Bundle: rfs.Create.Bundle,
Rootfs: rfs.Create.Rootfs,
Options: rfs.Create.Options,
Terminal: rfs.Create.Terminal,
Stdin: rfs.Create.Stdin,
Stdout: rfs.Create.Stdout,
Stderr: rfs.Create.Stderr,
FSRestoreImagePath: rfs.Conf.ImagePath,
FSRestoreDirect: rfs.Conf.Direct,
})
if err != nil {
return nil, err
}
s.containers[rfs.Create.ID] = c
p, err := c.Process("")
if err != nil {
return nil, err
}
if initProcess, ok := p.(*proc.Init); ok && initProcess.Sandbox {
s.root = initProcess.Runtime().Root
s.runtime = initProcess.Runtime()
}
// Set up OOM notification on the sandbox's cgroup. This is done on
// sandbox create since the sandbox process will be created here.
pid := p.Pid()
if pid > 0 {
var (
cg any
err error
)
if cgroups.Mode() == cgroups.Unified {
var cgPath string
cgPath, err = cgroupsv2.PidGroupPath(pid)
if err == nil {
cg, err = cgroupsv2.Load(cgPath)
}
} else {
cg, err = cgroup1.Load(cgroup1.PidPath(pid))
}
if err != nil {
return nil, fmt.Errorf("loading cgroup for %d: %w", pid, err)
}
// Register the sandbox cgroup with the OOM poller using this
// container's id
if err := s.oomPoller.add(rfs.Create.ID, cg); err != nil {
return nil, fmt.Errorf("add cg to OOM monitor: %w", err)
}
}
return &task.CreateTaskResponse{
Pid: uint32(pid),
}, nil
}
// Start starts the container.
func (s *runscService) Start(ctx context.Context, r *task.StartRequest) (*task.StartResponse, error) {
c, err := s.getContainer(r.ID)
if err != nil {
return nil, err
}
p, err := c.Start(ctx, r)
if err != nil {
return nil, errgrpc.ToGRPC(err)
}
if len(r.ExecID) == 0 {
s.send(&events.TaskStart{
ContainerID: r.ID,
Pid: uint32(p.Pid()),
})
} else {
s.send(&events.TaskExecStarted{
ContainerID: r.ID,
ExecID: r.ExecID,
Pid: uint32(p.Pid()),
})
}
// TODO: Set the cgroup and oom notifications on restore.
return &task.StartResponse{
Pid: uint32(p.Pid()),
}, nil
}
// Delete deletes the initial process and container.
func (s *runscService) Delete(ctx context.Context, r *task.DeleteRequest) (*task.DeleteResponse, error) {
c, err := s.getContainer(r.ID)
if err != nil {
return nil, err
}
p, err := c.Delete(ctx, r)
if err != nil {
return nil, errgrpc.ToGRPC(err)
}
// ExecID will be empty for init container process.
if len(r.ExecID) == 0 {
s.mu.Lock()
delete(s.containers, r.ID)
hasCont := len(s.containers) > 0
s.mu.Unlock()
if !hasCont && s.platform != nil {
s.platform.Close()
}
}
return &task.DeleteResponse{
ExitStatus: uint32(p.ExitStatus()),
ExitedAt: protobuf.ToTimestamp(p.ExitedAt()),
Pid: uint32(p.Pid()),
}, nil
}
// Exec spawns an additional process inside the container.
func (s *runscService) Exec(ctx context.Context, r *task.ExecProcessRequest) (*types.Empty, error) {
c, err := s.getContainer(r.ID)
if err != nil {
return nil, err
}
// Check whether or not the process already exists in the container.
p, _ := c.Process(r.ExecID)
if p != nil {
return nil, errgrpc.ToGRPCf(errdefs.ErrAlreadyExists, "id %s", r.ExecID)
}
p, err = c.Exec(ctx, r)
if err != nil {
return nil, err
}
s.send(&events.TaskExecAdded{
ContainerID: r.ID,
ExecID: p.ID(),
})
return empty, nil
}
// ResizePty resizes the terminal of a process.
func (s *runscService) ResizePty(ctx context.Context, r *task.ResizePtyRequest) (*types.Empty, error) {
c, err := s.getContainer(r.ID)
if err != nil {
return nil, errgrpc.ToGRPCf(errdefs.ErrFailedPrecondition, "container must be created")
}
if err := c.ResizePty(ctx, r); err != nil {
return nil, errgrpc.ToGRPC(err)
}
return empty, nil
}
// State returns runtime state information for the container.
func (s *runscService) State(ctx context.Context, r *task.StateRequest) (*task.StateResponse, error) {
c, err := s.getContainer(r.ID)
if err != nil {
return nil, err
}
p, err := c.Process(r.ExecID)
if err != nil {
log.L.Debugf("State failed to find process: %v", err)
return nil, errgrpc.ToGRPC(err)
}
st, err := p.Status(ctx)
if err != nil {
log.L.Debugf("State failed: %v", err)
return nil, err
}
status := tasktypes.Status_UNKNOWN
switch st {
case "created":
status = tasktypes.Status_CREATED
case "running":
status = tasktypes.Status_RUNNING
case "stopped":
status = tasktypes.Status_STOPPED
case "pausing":
status = tasktypes.Status_PAUSING
case "paused":
status = tasktypes.Status_PAUSED
}
sio := p.Stdio()
return &task.StateResponse{
ID: p.ID(),
Bundle: c.Bundle,
Pid: uint32(p.Pid()),
Status: status,
Stdin: sio.Stdin,
Stdout: sio.Stdout,
Stderr: sio.Stderr,
Terminal: sio.Terminal,
ExitStatus: uint32(p.ExitStatus()),
ExitedAt: protobuf.ToTimestamp(p.ExitedAt()),
}, nil
}
// Pause the container.
func (s *runscService) Pause(ctx context.Context, r *task.PauseRequest) (*types.Empty, error) {
c, err := s.getContainer(r.ID)
if err != nil {
return nil, err
}
if err := c.Pause(ctx, r); err != nil {
return nil, errgrpc.ToGRPC(err)
}
s.send(&events.TaskPaused{
ContainerID: c.ID,
})
return empty, nil
}
// Resume the container.
func (s *runscService) Resume(ctx context.Context, r *task.ResumeRequest) (*types.Empty, error) {
c, err := s.getContainer(r.ID)
if err != nil {
return nil, err
}
if err := c.Resume(ctx, r); err != nil {
return nil, errgrpc.ToGRPC(err)
}
s.send(&events.TaskResumed{
ContainerID: c.ID,
})
return empty, nil
}
// Kill the container with the provided signal.
func (s *runscService) Kill(ctx context.Context, r *task.KillRequest) (*types.Empty, error) {
c, err := s.getContainer(r.ID)
if err != nil {
return nil, err
}
if err := c.Kill(ctx, r); err != nil {
return nil, errgrpc.ToGRPC(err)
}
return empty, nil
}
// Pids returns all pids inside the container.
func (s *runscService) Pids(ctx context.Context, r *task.PidsRequest) (*task.PidsResponse, error) {
c, err := s.getContainer(r.ID)
if err != nil {
return nil, err
}
pids, err := s.getContainerPids(ctx, c)
if err != nil {
return nil, errgrpc.ToGRPC(err)
}
var processes []*tasktypes.ProcessInfo
for _, pid := range pids {
pInfo := tasktypes.ProcessInfo{
Pid: pid,
}
for _, p := range c.ExecdProcesses() {
if p.Pid() == int(pid) {
d := &runctypes.ProcessDetails{
ExecID: p.ID(),
}
a, err := typeurl.MarshalAnyToProto(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 &task.PidsResponse{
Processes: processes,
}, nil
}
// CloseIO closes the I/O context of the container.
func (s *runscService) CloseIO(ctx context.Context, r *task.CloseIORequest) (*types.Empty, error) {
c, err := s.getContainer(r.ID)
if err != nil {
return nil, err
}
if err := c.CloseIO(ctx, r); err != nil {
return nil, errgrpc.ToGRPC(err)
}
return empty, nil
}
// Checkpoint checkpoints the container.
func (s *runscService) Checkpoint(ctx context.Context, r *task.CheckpointTaskRequest) (*types.Empty, error) {
return empty, errdefs.ErrNotImplemented
}
// Restore restores the container.
func (s *runscService) Restore(ctx context.Context, r *extension.RestoreRequest) (*task.StartResponse, error) {
c, err := s.getContainer(r.Start.ID)
if err != nil {
return nil, err
}
p, err := c.Restore(ctx, r)
if err != nil {
return nil, err
}
return &task.StartResponse{
Pid: uint32(p.Pid()),
}, nil
}
// Connect returns shim information such as the shim's pid.
func (s *runscService) Connect(ctx context.Context, r *task.ConnectRequest) (*task.ConnectResponse, error) {
var pid int
if c, err := s.getContainer(r.ID); err == nil {
pid = c.Pid()
}
return &task.ConnectResponse{
ShimPid: uint32(os.Getpid()),
TaskPid: uint32(pid),
}, nil
}
func (s *runscService) Shutdown(ctx context.Context, r *task.ShutdownRequest) (*types.Empty, error) {
s.mu.Lock()
defer s.mu.Unlock()
if len(s.containers) > 0 {
return nil, fmt.Errorf("shim still servicing %d containers", len(s.containers))
}
return empty, nil
}
func (s *runscService) Stats(ctx context.Context, r *task.StatsRequest) (*task.StatsResponse, error) {
c, err := s.getContainer(r.ID)
if err != nil {
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
return c.Stats(ctx)
}
// Update updates a running container.
func (s *runscService) Update(ctx context.Context, r *task.UpdateTaskRequest) (*types.Empty, error) {
c, err := s.getContainer(r.ID)
if err != nil {
return nil, err
}
if err := c.Update(ctx, r); err != nil {
return nil, errgrpc.ToGRPC(err)
}
return empty, nil
}
// Wait waits for the container to exit.
func (s *runscService) Wait(ctx context.Context, r *task.WaitRequest) (*task.WaitResponse, error) {
c, err := s.getContainer(r.ID)
if err != nil {
return nil, err
}
p, err := c.Process(r.ExecID)
if err != nil {
return nil, errgrpc.ToGRPC(err)
}
p.Wait()
res := &task.WaitResponse{
ExitStatus: uint32(p.ExitStatus()),
ExitedAt: protobuf.ToTimestamp(p.ExitedAt()),
}
log.L.Debugf("Wait succeeded, response: %+v", res)
return res, nil
}
func (s *runscService) processExits(ctx context.Context) {
for e := range s.ec {
s.checkProcesses(ctx, e)
}
}
func (s *runscService) checkProcesses(ctx context.Context, e proc.Exit) {
// Find the container whose process exited.
s.mu.Lock()
var (
containerID string
exitingP process.Process
)
for cid, c := range s.containers {
for _, p := range c.All() {
if p.ID() == e.ID {
containerID = cid
exitingP = p
break
}
}
if exitingP != nil {
break
}
}
s.mu.Unlock()
if exitingP == nil {
return
}
p := exitingP
ip, isInit := p.(*proc.Init)
if isInit {
// Ensure all children are killed.
log.L.Debugf("Container init process exited, killing all container processes")
ip.KillAll(ctx)
}
p.SetExited(e.Status)
// On init process exit, synchronously check the cgroup for OOM kills
// before publishing the exit event. The async OOM notification via
// EventChan (cgroups v2) can lose the race against the container exit
// on some architectures (notably aarch64), causing kubelet to report
// reason=Error instead of reason=OOMKilled. Only check on init exit —
// exec processes share the sandbox cgroup and would produce spurious
// events.
// Use the per-container id for TaskOOM routing.
if isInit && s.oomPoller.isOOM(containerID) {
s.send(&events.TaskOOM{ContainerID: containerID})
}
s.send(&events.TaskExit{
ContainerID: containerID,
ID: p.ID(),
Pid: uint32(p.Pid()),
ExitStatus: uint32(e.Status),
ExitedAt: protobuf.ToTimestamp(p.ExitedAt()),
})
}
func (s *runscService) send(event any) {
s.events <- event
}
func (s *runscService) allProcessesForAllContainers() (o []process.Process) {
s.mu.Lock()
defer s.mu.Unlock()
for _, p := range s.containers {
for _, p := range p.All() {
o = append(o, p)
}
}
return o
}
func (s *runscService) getContainerPids(ctx context.Context, c *Container) ([]uint32, error) {
p, err := c.Process("")
if err != nil {
return nil, err
}
ps, err := p.(*proc.Init).Runtime().Ps(ctx, c.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
}
// publishFailureIsFatal reports whether a failure to publish an event is fatal.
// An empty TTRPC_ADDRESS means no event sink is configured (as under CRI-O), so
// the publisher can never connect and publish errors are expected.
func publishFailureIsFatal() bool {
return os.Getenv("TTRPC_ADDRESS") != ""
}
func (s *runscService) forward(ctx context.Context, publisher shim.Publisher) {
isFatal := publishFailureIsFatal()
for e := range s.events {
if err := publisher.Publish(ctx, getTopic(e), e); err != nil {
if isFatal {
// Should not happen when an event sink is configured.
panic(fmt.Errorf("post event: %w", err))
}
log.L.Warningf("Failed to post event (no containerd event sink): %v", err)
}
}
}
func getTopic(e any) 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
case *events.TaskPaused:
return runtime.TaskPausedEventTopic
case *events.TaskResumed:
return runtime.TaskResumedEventTopic
default:
log.L.Infof("no topic for type %#v", e)
}
return runtime.TaskUnknownTopic
}
func newInit(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
}
}
runtime := proc.NewRunsc(options.Root, r.Bundle, namespace, options.BinaryName, options.RunscConfig, spec)
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
}
// GvisorTaskServer adapters runscService to taskServer.GvisorTaskServiceExt.
type GvisorTaskServer struct {
s *runscService
}
var _ taskServer.GvisorTaskServiceExt = (*GvisorTaskServer)(nil)
// NewGvisorTaskServer creates a new GvisorTaskServer adapter.
func NewGvisorTaskServer(s extension.TaskServiceExt) *GvisorTaskServer {
rs, ok := s.(*runscService)
if !ok {
return nil
}
return &GvisorTaskServer{s: rs}
}
// Checkpoint implements taskServer.GvisorTaskServiceExt.
func (g *GvisorTaskServer) Checkpoint(ctx context.Context, req *pb.CheckpointRequest) (*pb.CheckpointResponse, error) {
c, err := g.s.getContainer(req.GetId())
if err != nil {
return nil, err
}
p, err := c.Process("")
if err != nil {
return nil, err
}
initProc, ok := p.(*proc.Init)
if !ok {
return nil, fmt.Errorf("process is not init process")
}
// Set debug options on the runtime for this execution
runsc := setDebug(initProc.Runtime(), req.GetDebug())
opts := &runsccmd.CheckpointOpts{
ImagePath: req.GetImagePath(),
LeaveRunning: req.GetLeaveRunning(),
Direct: req.GetDirect(),
Compression: req.GetCompression(),
ExcludeCommittedZeroPages: req.GetExcludeCommittedZeroPages(),
SaveRestoreExecArgv: req.GetSaveRestoreExecArgv(),
SaveRestoreExecTimeout: req.GetSaveRestoreExecTimeout(),
CudaCheckpointPath: req.GetCudaCheckpointPath(),
CudaCheckpointSequential: req.GetCudaCheckpointSequential(),
WorkPath: req.GetWorkPath(),
FSPath: req.GetFsPath(),
}
if req.GetFsCheckpoint() {
if err := runsc.FSCheckpoint(ctx, c.ID, opts); err != nil {
return nil, err
}
} else {
if err := runsc.Checkpoint(ctx, c.ID, opts); err != nil {
return nil, err
}
}
return &pb.CheckpointResponse{}, nil
}
// Wait implements taskServer.GvisorTaskServiceExt.
func (g *GvisorTaskServer) Wait(ctx context.Context, req *pb.WaitRequest) (*pb.WaitResponse, error) {
c, err := g.s.getContainer(req.GetId())
if err != nil {
return nil, err
}
p, err := c.Process("")
if err != nil {
return nil, err
}
initProc, ok := p.(*proc.Init)
if !ok {
return nil, fmt.Errorf("process is not init process")
}
// Set debug options on the runtime for this execution
runsc := setDebug(initProc.Runtime(), req.GetDebug())
opts := &runsccmd.WaitOpts{
PID: int(req.GetPid()),
RootPID: int(req.GetRootPid()),
}
switch req.GetWaitType() {
case pb.WaitRequest_CHECKPOINT:
opts.Checkpoint = true
case pb.WaitRequest_RESTORE:
opts.Restore = true
case pb.WaitRequest_FSCHECKPOINT:
opts.FSCheckpoint = true
case pb.WaitRequest_FSRESTORE:
opts.FSRestore = true
}
exitStatus, err := runsc.WaitWithOptions(ctx, c.ID, opts)
if err != nil {
return nil, err
}
return &pb.WaitResponse{ExitStatus: int32(exitStatus)}, nil
}
// State implements taskServer.GvisorTaskServiceExt.
func (g *GvisorTaskServer) State(ctx context.Context, req *pb.StateRequest) (*pb.StateResponse, error) {
if c, err := g.s.getContainer(req.GetId()); err == nil {
if p, err := c.Process(""); err == nil {
if initProc, ok := p.(*proc.Init); ok {
runsc := setDebug(initProc.Runtime(), req.GetDebug())
if _, err := runsc.State(ctx, c.ID); err != nil {
log.L.Debugf("State with debug failed: %v", err)
}
}
}
}
resp, err := g.s.State(ctx, &task.StateRequest{ID: req.GetId()})
if err != nil {
return nil, err
}
stateStr := ""
switch resp.Status {
case tasktypes.Status_CREATED:
stateStr = "created"
case tasktypes.Status_RUNNING:
stateStr = "running"
case tasktypes.Status_STOPPED:
stateStr = "stopped"
case tasktypes.Status_PAUSING:
stateStr = "pausing"
case tasktypes.Status_PAUSED:
stateStr = "paused"
default:
stateStr = "unknown"
}
id := resp.ID
pid := int32(resp.Pid)
return &pb.StateResponse{
Id: id,
State: stateStr,
Pid: pid,
}, nil
}
// Version implements taskServer.GvisorTaskServiceExt.
func (g *GvisorTaskServer) Version(ctx context.Context, req *pb.VersionRequest) (*pb.VersionResponse, error) {
cmd := exec.Command("runsc", "-version")
if g.s.runtime.Command != "" {
cmd = exec.Command(g.s.runtime.Command, "-version")
}
out, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("failed to get runsc version: %w, output: %s", err, string(out))
}
versionStr := strings.TrimSpace(string(out))
return &pb.VersionResponse{Version: versionStr}, nil
}
func setDebug(r *runsccmd.Runsc, d *pb.Debug) *runsccmd.Runsc {
if r == nil {
return nil
}
cp := *r
if d == nil {
cp.Debug = false
cp.DebugLog = ""
cp.DebugLogFD = nil
return &cp
}
cp.Debug = true
cp.DebugLog = d.GetDebugLog()
cp.DebugLogFD = nil
if fd := int(d.GetDebugLogFd()); fd != 0 {
cp.DebugLogFD = &fd
}
return &cp
}