mirror of
https://github.com/moby/buildkit.git
synced 2026-08-09 17:18:11 +00:00
Keep exec network modes limited to sandbox, host, and none, and pass proxy network configuration separately through solve and executor runtime state. Proxy execs now use bridge-style egress by default, host egress only for host network mode with entitlement, and no proxy for none mode. Add integration coverage for bridge, host, and none proxy behavior across OCI and containerd workers. Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
823 lines
22 KiB
Go
823 lines
22 KiB
Go
//go:build linux
|
|
|
|
package runcexecutor
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"slices"
|
|
"strconv"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/moby/buildkit/util/bklog"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/trace"
|
|
|
|
"github.com/containerd/containerd/v2/core/mount"
|
|
containerdoci "github.com/containerd/containerd/v2/pkg/oci"
|
|
"github.com/containerd/continuity/fs"
|
|
runc "github.com/containerd/go-runc"
|
|
"github.com/moby/buildkit/executor"
|
|
"github.com/moby/buildkit/executor/oci"
|
|
"github.com/moby/buildkit/executor/resources"
|
|
resourcestypes "github.com/moby/buildkit/executor/resources/types"
|
|
gatewayapi "github.com/moby/buildkit/frontend/gateway/pb"
|
|
"github.com/moby/buildkit/identity"
|
|
"github.com/moby/buildkit/solver/llbsolver/cdidevices"
|
|
"github.com/moby/buildkit/solver/pb"
|
|
"github.com/moby/buildkit/util/network"
|
|
rootlessspecconv "github.com/moby/buildkit/util/rootless/specconv"
|
|
"github.com/moby/buildkit/util/stack"
|
|
"github.com/moby/sys/user"
|
|
"github.com/opencontainers/runtime-spec/specs-go"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type Opt struct {
|
|
// root directory
|
|
Root string
|
|
CommandCandidates []string
|
|
// without root privileges (has nothing to do with Opt.Root directory)
|
|
Rootless bool
|
|
// DefaultCgroupParent is the cgroup-parent name for executor
|
|
DefaultCgroupParent string
|
|
// ProcessMode
|
|
ProcessMode oci.ProcessMode
|
|
IdentityMapping *user.IdentityMapping
|
|
// runc run --no-pivot (unrecommended)
|
|
NoPivot bool
|
|
DNS *oci.DNSConfig
|
|
OOMScoreAdj *int
|
|
ApparmorProfile string
|
|
SELinux bool
|
|
TracingSocket string
|
|
ResourceMonitor *resources.Monitor
|
|
CDIManager *cdidevices.Manager
|
|
ProxyProvider network.ProxyProvider
|
|
}
|
|
|
|
var defaultCommandCandidates = []string{"buildkit-runc", "runc"}
|
|
|
|
type runcExecutor struct {
|
|
runc *runc.Runc
|
|
root string
|
|
cgroupParent string
|
|
rootless bool
|
|
networkProviders map[pb.NetMode]network.Provider
|
|
proxyProvider network.ProxyProvider
|
|
processMode oci.ProcessMode
|
|
idmap *user.IdentityMapping
|
|
noPivot bool
|
|
dns *oci.DNSConfig
|
|
oomScoreAdj *int
|
|
running map[string]chan error
|
|
mu sync.Mutex
|
|
apparmorProfile string
|
|
selinux bool
|
|
tracingSocket string
|
|
resmon *resources.Monitor
|
|
cdiManager *cdidevices.Manager
|
|
}
|
|
|
|
func New(opt Opt, networkProviders map[pb.NetMode]network.Provider) (executor.Executor, error) {
|
|
cmds := opt.CommandCandidates
|
|
if cmds == nil {
|
|
cmds = defaultCommandCandidates
|
|
}
|
|
|
|
var cmd string
|
|
var found bool
|
|
for _, cmd = range cmds {
|
|
if _, err := exec.LookPath(cmd); err == nil {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return nil, errors.Errorf("failed to find %s binary", cmd)
|
|
}
|
|
|
|
root := opt.Root
|
|
|
|
if err := os.MkdirAll(root, 0o711); err != nil {
|
|
return nil, errors.Wrapf(err, "failed to create %s", root)
|
|
}
|
|
|
|
root, err := filepath.Abs(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
root, err = filepath.EvalSymlinks(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// clean up old hosts/resolv.conf file. ignore errors
|
|
os.RemoveAll(filepath.Join(root, "hosts"))
|
|
os.RemoveAll(filepath.Join(root, "resolv.conf"))
|
|
|
|
runtime := &runc.Runc{
|
|
Command: cmd,
|
|
Log: filepath.Join(root, "runc-log.json"),
|
|
LogFormat: runc.JSON,
|
|
Setpgid: true,
|
|
// we don't execute runc with --rootless=(true|false) explicitly,
|
|
// so as to support non-runc runtimes
|
|
}
|
|
|
|
updateRuncFieldsForHostOS(runtime)
|
|
|
|
w := &runcExecutor{
|
|
runc: runtime,
|
|
root: root,
|
|
cgroupParent: opt.DefaultCgroupParent,
|
|
rootless: opt.Rootless,
|
|
networkProviders: networkProviders,
|
|
proxyProvider: opt.ProxyProvider,
|
|
processMode: opt.ProcessMode,
|
|
idmap: opt.IdentityMapping,
|
|
noPivot: opt.NoPivot,
|
|
dns: opt.DNS,
|
|
oomScoreAdj: opt.OOMScoreAdj,
|
|
running: make(map[string]chan error),
|
|
apparmorProfile: opt.ApparmorProfile,
|
|
selinux: opt.SELinux,
|
|
tracingSocket: opt.TracingSocket,
|
|
resmon: opt.ResourceMonitor,
|
|
cdiManager: opt.CDIManager,
|
|
}
|
|
return w, nil
|
|
}
|
|
|
|
func (w *runcExecutor) Run(ctx context.Context, id string, root executor.Mount, mounts []executor.Mount, process executor.ProcessInfo, started chan<- struct{}) (rec resourcestypes.Recorder, err error) {
|
|
if id == "" {
|
|
id = identity.NewID()
|
|
}
|
|
if err := executor.ValidContainerID(id); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
startedOnce := sync.Once{}
|
|
done := make(chan error, 1)
|
|
w.mu.Lock()
|
|
w.running[id] = done
|
|
w.mu.Unlock()
|
|
defer func() {
|
|
w.mu.Lock()
|
|
delete(w.running, id)
|
|
w.mu.Unlock()
|
|
done <- err
|
|
close(done)
|
|
if started != nil {
|
|
startedOnce.Do(func() {
|
|
close(started)
|
|
})
|
|
}
|
|
}()
|
|
|
|
meta := process.Meta
|
|
if meta.NetMode == pb.NetMode_HOST {
|
|
bklog.G(ctx).Info("enabling HostNetworking")
|
|
}
|
|
|
|
proxyConfig := meta.Proxy
|
|
var provider network.Provider
|
|
if proxyConfig == nil {
|
|
var ok bool
|
|
provider, ok = w.networkProviders[meta.NetMode]
|
|
if !ok {
|
|
return nil, errors.Errorf("unknown network mode %s", meta.NetMode)
|
|
}
|
|
} else if w.proxyProvider == nil {
|
|
return nil, errors.New("proxy network provider is not available")
|
|
} else {
|
|
proxyConfig = &network.ProxyConfig{
|
|
Policy: proxyConfig.Policy,
|
|
Capture: proxyConfig.Capture,
|
|
EgressMode: meta.NetMode,
|
|
}
|
|
}
|
|
var namespace network.Namespace
|
|
if proxyConfig != nil {
|
|
namespace, err = w.proxyProvider.NewProxy(ctx, proxyConfig)
|
|
} else {
|
|
namespace, err = provider.New(ctx, meta.Hostname, network.NamespaceOptions{})
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if proxyNS, ok := namespace.(network.ProxyNamespace); ok {
|
|
meta.Env = append(meta.Env, proxyNS.ProxyEnv()...)
|
|
}
|
|
doReleaseNetwork := true
|
|
defer func() {
|
|
if doReleaseNetwork {
|
|
namespace.Close()
|
|
}
|
|
}()
|
|
|
|
stateDirRoot, err := os.OpenRoot(w.root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer stateDirRoot.Close()
|
|
|
|
resolvConfName, err := oci.GetResolvConf(ctx, stateDirRoot, w.idmap, w.dns, meta.NetMode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resolvConf := filepath.Join(w.root, resolvConfName)
|
|
|
|
hostsName, clean, err := oci.GetHostsFile(ctx, stateDirRoot, meta.ExtraHosts, w.idmap, meta.Hostname)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hostsFile := filepath.Join(w.root, hostsName)
|
|
if clean != nil {
|
|
defer clean()
|
|
}
|
|
|
|
mountable, err := root.Src.Mount(ctx, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rootMount, release, err := mountable.Mount()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if release != nil {
|
|
defer release()
|
|
}
|
|
|
|
bundle := filepath.Join(w.root, id)
|
|
|
|
if err := os.Mkdir(bundle, 0o711); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
defer os.RemoveAll(bundle)
|
|
|
|
var rootUID, rootGID int
|
|
if w.idmap != nil {
|
|
rootUID, rootGID = w.idmap.RootPair()
|
|
}
|
|
|
|
rootFSPath := filepath.Join(bundle, "rootfs")
|
|
if err := user.MkdirAllAndChown(rootFSPath, 0o700, rootUID, rootGID); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
if err := mount.All(rootMount, rootFSPath); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
defer mount.Unmount(rootFSPath, 0)
|
|
|
|
defer executor.MountStubsCleaner(context.WithoutCancel(ctx), rootFSPath, mounts, meta.RemoveMountStubsRecursive)()
|
|
if proxyNS, ok := namespace.(network.ProxyNamespace); ok {
|
|
cleanProxyCA, err := executor.InjectProxyCA(rootFSPath, proxyNS.ProxyCACert())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer cleanProxyCA()
|
|
}
|
|
|
|
uid, gid, sgids, err := oci.GetUser(rootFSPath, meta.User)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
f, err := os.Create(filepath.Join(bundle, "config.json"))
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
defer f.Close()
|
|
|
|
opts := []containerdoci.SpecOpts{oci.WithUIDGID(uid, gid, sgids)}
|
|
|
|
if meta.ReadonlyRootFS {
|
|
opts = append(opts, containerdoci.WithRootFSReadonly())
|
|
}
|
|
|
|
rootUID, rootGID = int(uid), int(gid)
|
|
if w.idmap != nil {
|
|
rootUID, rootGID, err = w.idmap.ToHost(rootUID, rootGID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
spec, cleanup, err := oci.GenerateSpec(ctx, meta, mounts, id, resolvConf, hostsFile, namespace, w.cgroupParent, w.processMode, w.idmap, w.apparmorProfile, w.selinux, w.tracingSocket, w.cdiManager, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer cleanup()
|
|
|
|
spec.Root.Path = rootFSPath
|
|
if root.Readonly {
|
|
spec.Root.Readonly = true
|
|
}
|
|
|
|
newp, err := fs.RootPath(rootFSPath, meta.Cwd)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "working dir %s points to invalid target", newp)
|
|
}
|
|
if _, err := os.Stat(newp); err != nil {
|
|
if err := user.MkdirAllAndChown(newp, 0o755, rootUID, rootGID); err != nil {
|
|
return nil, errors.Wrapf(err, "failed to create working directory %s", newp)
|
|
}
|
|
}
|
|
|
|
spec.Process.Terminal = meta.Tty
|
|
spec.Process.OOMScoreAdj = w.oomScoreAdj
|
|
if w.rootless {
|
|
if err := rootlessspecconv.ToRootless(spec); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if err := json.NewEncoder(f).Encode(spec); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
bklog.G(ctx).Debugf("> creating %s %v", id, meta.Args)
|
|
|
|
cgroupPath := spec.Linux.CgroupsPath
|
|
if cgroupPath != "" {
|
|
rec, err = w.resmon.RecordNamespace(cgroupPath, resources.RecordOpt{
|
|
NetworkSampler: namespace,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
trace.SpanFromContext(ctx).AddEvent("Container created")
|
|
err = w.run(ctx, id, bundle, process, func() {
|
|
startedOnce.Do(func() {
|
|
trace.SpanFromContext(ctx).AddEvent("Container started")
|
|
if started != nil {
|
|
close(started)
|
|
}
|
|
if rec != nil {
|
|
rec.Start()
|
|
}
|
|
})
|
|
}, true)
|
|
|
|
releaseContainer := func(ctx context.Context) error {
|
|
err := w.runc.Delete(ctx, id, &runc.DeleteOpts{})
|
|
err1 := namespace.Close()
|
|
if err == nil {
|
|
err = err1
|
|
}
|
|
return err
|
|
}
|
|
doReleaseNetwork = false
|
|
|
|
err = exitError(ctx, cgroupPath, err, process.Meta.ValidExitCodes)
|
|
if err != nil {
|
|
if rec != nil {
|
|
rec.Close()
|
|
}
|
|
releaseContainer(context.TODO())
|
|
return nil, err
|
|
}
|
|
|
|
if rec == nil {
|
|
return nil, releaseContainer(context.TODO())
|
|
}
|
|
|
|
return rec, rec.CloseAsync(releaseContainer)
|
|
}
|
|
|
|
func exitError(ctx context.Context, cgroupPath string, err error, validExitCodes []int) error {
|
|
exitErr := &gatewayapi.ExitError{ExitCode: uint32(gatewayapi.UnknownExitStatus), Err: err}
|
|
|
|
if err == nil {
|
|
exitErr.ExitCode = 0
|
|
} else {
|
|
var runcExitError *runc.ExitError
|
|
if errors.As(err, &runcExitError) {
|
|
exitErr = &gatewayapi.ExitError{ExitCode: uint32(runcExitError.Status)}
|
|
}
|
|
|
|
detectOOM(ctx, cgroupPath, exitErr)
|
|
}
|
|
|
|
trace.SpanFromContext(ctx).AddEvent(
|
|
"Container exited",
|
|
trace.WithAttributes(attribute.Int("exit.code", int(exitErr.ExitCode))),
|
|
)
|
|
|
|
if validExitCodes == nil {
|
|
// no exit codes specified, so only 0 is allowed
|
|
if exitErr.ExitCode == 0 {
|
|
return nil
|
|
}
|
|
} else {
|
|
// exit code in allowed list, so exit cleanly
|
|
if slices.Contains(validExitCodes, int(exitErr.ExitCode)) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
exitErr.Err = errors.Wrap(context.Cause(ctx), exitErr.Error())
|
|
return exitErr
|
|
default:
|
|
return stack.Enable(exitErr)
|
|
}
|
|
}
|
|
|
|
func (w *runcExecutor) Exec(ctx context.Context, id string, process executor.ProcessInfo) (err error) {
|
|
// first verify the container is running, if we get an error assume the container
|
|
// is in the process of being created and check again every 100ms or until
|
|
// context is canceled.
|
|
var state *runc.Container
|
|
for {
|
|
w.mu.Lock()
|
|
done, ok := w.running[id]
|
|
w.mu.Unlock()
|
|
if !ok {
|
|
return errors.Errorf("container %s not found", id)
|
|
}
|
|
|
|
state, _ = w.runc.State(ctx, id)
|
|
if state != nil && state.Status == "running" {
|
|
break
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return context.Cause(ctx)
|
|
case err, ok := <-done:
|
|
if !ok || err == nil {
|
|
return errors.Errorf("container %s has stopped", id)
|
|
}
|
|
return errors.Wrapf(err, "container %s has exited with error", id)
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
}
|
|
|
|
// load default process spec (for Env, Cwd etc) from bundle
|
|
f, err := os.Open(filepath.Join(state.Bundle, "config.json"))
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
defer f.Close()
|
|
|
|
spec := &specs.Spec{}
|
|
dec := json.NewDecoder(f)
|
|
if err := dec.Decode(spec); err != nil {
|
|
return err
|
|
}
|
|
if _, err := dec.Token(); !errors.Is(err, io.EOF) {
|
|
return errors.Errorf("unexpected data after JSON spec object")
|
|
}
|
|
|
|
if process.Meta.User != "" {
|
|
uid, gid, sgids, err := oci.GetUser(state.Rootfs, process.Meta.User)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
spec.Process.User = specs.User{
|
|
UID: uid,
|
|
GID: gid,
|
|
AdditionalGids: sgids,
|
|
}
|
|
}
|
|
|
|
spec.Process.Terminal = process.Meta.Tty
|
|
spec.Process.Args = process.Meta.Args
|
|
if process.Meta.Cwd != "" {
|
|
spec.Process.Cwd = process.Meta.Cwd
|
|
}
|
|
|
|
if len(process.Meta.Env) > 0 {
|
|
spec.Process.Env = process.Meta.Env
|
|
}
|
|
|
|
err = w.exec(ctx, id, spec.Process, process, nil)
|
|
return exitError(ctx, "", err, process.Meta.ValidExitCodes)
|
|
}
|
|
|
|
type forwardIO struct {
|
|
stdin io.ReadCloser
|
|
stdout, stderr io.WriteCloser
|
|
}
|
|
|
|
func (s *forwardIO) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (s *forwardIO) Set(cmd *exec.Cmd) {
|
|
cmd.Stdin = s.stdin
|
|
cmd.Stdout = s.stdout
|
|
cmd.Stderr = s.stderr
|
|
}
|
|
|
|
func (s *forwardIO) Stdin() io.WriteCloser {
|
|
return nil
|
|
}
|
|
|
|
func (s *forwardIO) Stdout() io.ReadCloser {
|
|
return nil
|
|
}
|
|
|
|
func (s *forwardIO) Stderr() io.ReadCloser {
|
|
return nil
|
|
}
|
|
|
|
// newRunProcKiller returns an abstraction for sending SIGKILL to the
|
|
// process inside the container initiated from `runc run`.
|
|
func newRunProcKiller(runC *runc.Runc, id string) procKiller {
|
|
return procKiller{runC: runC, id: id}
|
|
}
|
|
|
|
// newExecProcKiller returns an abstraction for sending SIGKILL to the
|
|
// process inside the container initiated from `runc exec`.
|
|
func newExecProcKiller(runC *runc.Runc, id string) (procKiller, error) {
|
|
// for `runc exec` we need to create a pidfile and read it later to kill
|
|
// the process
|
|
tdir, err := os.MkdirTemp("", "runc")
|
|
if err != nil {
|
|
return procKiller{}, errors.Wrap(err, "failed to create directory for runc pidfile")
|
|
}
|
|
|
|
return procKiller{
|
|
runC: runC,
|
|
id: id,
|
|
pidfile: filepath.Join(tdir, "pidfile"),
|
|
cleanup: func() {
|
|
os.RemoveAll(tdir)
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
type procKiller struct {
|
|
runC *runc.Runc
|
|
id string
|
|
pidfile string
|
|
cleanup func()
|
|
}
|
|
|
|
// Cleanup will delete any tmp files created for the pidfile allocation
|
|
// if this killer was for a `runc exec` process.
|
|
func (k procKiller) Cleanup() {
|
|
if k.cleanup != nil {
|
|
k.cleanup()
|
|
}
|
|
}
|
|
|
|
// Kill will send SIGKILL to the process running inside the container.
|
|
// If the process was created by `runc run` then we will use `runc kill`,
|
|
// otherwise for `runc exec` we will read the pid from a pidfile and then
|
|
// send the signal directly that process.
|
|
func (k procKiller) Kill(ctx context.Context) (err error) {
|
|
bklog.G(ctx).Debugf("sending sigkill to process in container %s", k.id)
|
|
defer func() {
|
|
if err != nil {
|
|
bklog.G(ctx).Errorf("failed to kill process in container id %s: %+v", k.id, err)
|
|
}
|
|
}()
|
|
|
|
// this timeout is generally a no-op, the Kill ctx should already have a
|
|
// shorter timeout but here as a fail-safe for future refactoring.
|
|
ctx, cancel := context.WithCancelCause(ctx)
|
|
ctx, _ = context.WithTimeoutCause(ctx, 10*time.Second, errors.WithStack(context.DeadlineExceeded)) //nolint:govet
|
|
defer func() { cancel(errors.WithStack(context.Canceled)) }()
|
|
|
|
if k.pidfile == "" {
|
|
// for `runc run` process we use `runc kill` to terminate the process
|
|
return k.runC.Kill(ctx, k.id, int(syscall.SIGKILL), nil)
|
|
}
|
|
|
|
// `runc exec` will write the pidfile a few milliseconds after we
|
|
// get the runc pid via the startedCh, so we might need to retry until
|
|
// it appears in the edge case where we want to kill a process
|
|
// immediately after it was created.
|
|
var pidData []byte
|
|
for {
|
|
pidData, err = os.ReadFile(k.pidfile)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return errors.New("context cancelled before runc wrote pidfile")
|
|
case <-time.After(10 * time.Millisecond):
|
|
continue
|
|
}
|
|
}
|
|
return errors.Wrap(err, "failed to read pidfile from runc")
|
|
}
|
|
break
|
|
}
|
|
pid, err := strconv.Atoi(string(pidData))
|
|
if err != nil {
|
|
return errors.Wrap(err, "read invalid pid from pidfile")
|
|
}
|
|
process, err := os.FindProcess(pid)
|
|
if err != nil {
|
|
// error only possible on non-unix hosts
|
|
return errors.Wrapf(err, "failed to find process for pid %d from pidfile", pid)
|
|
}
|
|
defer process.Release()
|
|
return process.Signal(syscall.SIGKILL)
|
|
}
|
|
|
|
// procHandle is to track the process so we can send signals to it
|
|
// and handle graceful shutdown.
|
|
type procHandle struct {
|
|
// this is for the runc process (not the process in-container)
|
|
monitorProcess *os.Process
|
|
ready chan struct{}
|
|
ended chan struct{}
|
|
shutdown func(error)
|
|
// this only used when the request context is canceled and we need
|
|
// to kill the in-container process.
|
|
killer procKiller
|
|
}
|
|
|
|
// runcProcessHandle will create a procHandle that will be monitored, where
|
|
// on ctx.Done the in-container process will receive a SIGKILL. The returned
|
|
// context should be used for the go-runc.(Run|Exec) invocations. The returned
|
|
// context will only be canceled in the case where the request context is
|
|
// canceled and we are unable to send the SIGKILL to the in-container process.
|
|
// The goal is to allow for runc to gracefully shutdown when the request context
|
|
// is cancelled.
|
|
func runcProcessHandle(ctx context.Context, killer procKiller) (*procHandle, context.Context) {
|
|
runcCtx, cancel := context.WithCancelCause(context.Background())
|
|
p := &procHandle{
|
|
ready: make(chan struct{}),
|
|
ended: make(chan struct{}),
|
|
shutdown: cancel,
|
|
killer: killer,
|
|
}
|
|
// preserve the logger on the context used for the runc process handling
|
|
runcCtx = bklog.WithLogger(runcCtx, bklog.G(ctx))
|
|
|
|
go func() {
|
|
// Wait for pid
|
|
select {
|
|
case <-p.ended:
|
|
return // nothing to kill
|
|
case <-p.ready:
|
|
select {
|
|
case <-p.ended:
|
|
return
|
|
default:
|
|
}
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
if err := doKillProc(ctx, p); err != nil {
|
|
cancel(err)
|
|
}
|
|
case <-p.ended:
|
|
}
|
|
}()
|
|
|
|
return p, runcCtx
|
|
}
|
|
|
|
// Release will free resources with a procHandle.
|
|
func (p *procHandle) Release() {
|
|
close(p.ended)
|
|
if p.monitorProcess != nil {
|
|
p.monitorProcess.Release()
|
|
}
|
|
}
|
|
|
|
// Shutdown should be called after the runc process has exited. This will allow
|
|
// the signal handling and tty resize loops to exit, terminating the
|
|
// goroutines.
|
|
func (p *procHandle) Shutdown() {
|
|
if p.shutdown != nil {
|
|
p.shutdown(errors.WithStack(context.Canceled))
|
|
}
|
|
}
|
|
|
|
// WaitForReady will wait until we have received the runc pid via the go-runc
|
|
// Started channel, or until the request context is canceled. This should
|
|
// return without errors before attempting to send signals to the runc process.
|
|
func (p *procHandle) WaitForReady(ctx context.Context) error {
|
|
select {
|
|
case <-ctx.Done():
|
|
return context.Cause(ctx)
|
|
case <-p.ready:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WaitForStart will record the runc pid reported by go-runc via the channel.
|
|
// We wait for up to 10s for the runc pid to be reported. If the started
|
|
// callback is non-nil it will be called after receiving the pid.
|
|
func (p *procHandle) WaitForStart(ctx context.Context, startedCh <-chan int, started func()) error {
|
|
ctx, cancel := context.WithCancelCause(ctx)
|
|
ctx, _ = context.WithTimeoutCause(ctx, 10*time.Second, errors.WithStack(context.DeadlineExceeded)) //nolint:govet
|
|
defer func() { cancel(errors.WithStack(context.Canceled)) }()
|
|
select {
|
|
case <-ctx.Done():
|
|
return errors.New("go-runc started message never received")
|
|
case runcPid, ok := <-startedCh:
|
|
if !ok {
|
|
return errors.New("go-runc failed to send pid")
|
|
}
|
|
if started != nil {
|
|
started()
|
|
}
|
|
var err error
|
|
p.monitorProcess, err = os.FindProcess(runcPid)
|
|
if err != nil {
|
|
// error only possible on non-unix hosts
|
|
return errors.Wrapf(err, "failed to find runc process %d", runcPid)
|
|
}
|
|
close(p.ready)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// handleSignals will wait until the procHandle is ready then will
|
|
// send each signal received on the channel to the runc process (not directly
|
|
// to the in-container process)
|
|
func handleSignals(ctx context.Context, runcProcess *procHandle, signals <-chan syscall.Signal) error {
|
|
if signals == nil {
|
|
return nil
|
|
}
|
|
err := runcProcess.WaitForReady(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil
|
|
case sig := <-signals:
|
|
if sig == syscall.SIGKILL {
|
|
// never send SIGKILL directly to runc, it needs to go to the
|
|
// process in-container
|
|
if err := runcProcess.killer.Kill(ctx); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
if err := runcProcess.monitorProcess.Signal(sig); err != nil {
|
|
bklog.G(ctx).Errorf("failed to signal %s to process: %s", sig, err)
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const killWaitDelay = 50 * time.Millisecond
|
|
|
|
func doKillProc(ctx context.Context, p *procHandle) error {
|
|
// Attempt to kill the process normally.
|
|
for {
|
|
didSucceed := true
|
|
|
|
killCtx, timeout := context.WithTimeoutCause(context.WithoutCancel(ctx), 7*time.Second, errors.WithStack(context.DeadlineExceeded))
|
|
if err := p.killer.Kill(killCtx); err != nil {
|
|
if contextErr := context.Cause(ctx); contextErr != nil {
|
|
timeout()
|
|
return contextErr
|
|
}
|
|
didSucceed = false
|
|
}
|
|
timeout()
|
|
|
|
if didSucceed {
|
|
// The kill was successful so exit this for loop.
|
|
break
|
|
}
|
|
|
|
// The kill did not succeed and the context wasn't canceled.
|
|
// Either wait for the ended signal or try again in 50 milliseconds.
|
|
select {
|
|
case <-p.ended:
|
|
return nil
|
|
case <-time.After(killWaitDelay):
|
|
}
|
|
}
|
|
|
|
// Wait for the process to end. Track how long it takes.
|
|
start := time.Now()
|
|
select {
|
|
case <-p.ended:
|
|
return nil
|
|
case <-time.After(killWaitDelay):
|
|
bklog.G(ctx).Warnf("container id %s is taking a long time to exit after kill", p.killer.id)
|
|
}
|
|
|
|
// We have warned about the slow exit. Just wait for it to exit instead of spamming the logs.
|
|
<-p.ended
|
|
bklog.G(ctx).Warnf("container id %s took %s to exit", p.killer.id, time.Since(start))
|
|
return nil
|
|
}
|