buildkit/executor/runcexecutor/executor.go

441 lines
9.4 KiB
Go

package runcexecutor
import (
"context"
"encoding/json"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"github.com/containerd/containerd/contrib/seccomp"
"github.com/containerd/containerd/mount"
containerdoci "github.com/containerd/containerd/oci"
"github.com/containerd/continuity/fs"
runc "github.com/containerd/go-runc"
"github.com/moby/buildkit/cache"
"github.com/moby/buildkit/executor"
"github.com/moby/buildkit/executor/oci"
"github.com/moby/buildkit/identity"
"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/system"
runcsystem "github.com/opencontainers/runc/libcontainer/system"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type Opt struct {
// root directory
Root string
CommandCandidates []string
// without root privileges (has nothing to do with Opt.Root directory)
Rootless bool
}
var defaultCommandCandidates = []string{"buildkit-runc", "runc"}
type runcExecutor struct {
runc *runc.Runc
root string
cmd string
rootless bool
networkProvider network.Provider
}
func New(opt Opt, networkProvider 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 := setSubReaper(); err != nil {
return nil, err
}
if err := os.MkdirAll(root, 0700); 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
}
runtime := &runc.Runc{
Command: cmd,
Log: filepath.Join(root, "runc-log.json"),
LogFormat: runc.JSON,
PdeathSignal: syscall.SIGKILL,
Setpgid: true,
// we don't execute runc with --rootless=(true|false) explicitly,
// so as to support non-runc runtimes
}
w := &runcExecutor{
runc: runtime,
root: root,
rootless: opt.Rootless,
networkProvider: networkProvider,
}
return w, nil
}
func (w *runcExecutor) Exec(ctx context.Context, meta executor.Meta, root cache.Mountable, mounts []executor.Mount, stdin io.ReadCloser, stdout, stderr io.WriteCloser) error {
var iface network.Interface
// FIXME: still uses host if no provider configured
if meta.NetMode == pb.NetMode_UNSET {
if w.networkProvider != nil {
var err error
iface, err = w.networkProvider.NewInterface()
if err != nil || iface == nil {
meta.NetMode = pb.NetMode_HOST
}
} else {
meta.NetMode = pb.NetMode_HOST
}
}
if meta.NetMode == pb.NetMode_HOST {
logrus.Info("enabling HostNetworking")
}
defer func() {
if iface != nil {
w.networkProvider.Release(iface)
}
}()
resolvConf, err := oci.GetResolvConf(ctx, w.root)
if err != nil {
return err
}
hostsFile, clean, err := oci.GetHostsFile(ctx, w.root, meta.ExtraHosts)
if err != nil {
return err
}
if clean != nil {
defer clean()
}
mountable, err := root.Mount(ctx, false)
if err != nil {
return err
}
rootMount, err := mountable.Mount()
if err != nil {
return err
}
defer mountable.Release()
id := identity.NewID()
bundle := filepath.Join(w.root, id)
if err := os.Mkdir(bundle, 0700); err != nil {
return err
}
defer os.RemoveAll(bundle)
rootFSPath := filepath.Join(bundle, "rootfs")
if err := os.Mkdir(rootFSPath, 0700); err != nil {
return err
}
if err := mount.All(rootMount, rootFSPath); err != nil {
return err
}
defer mount.Unmount(rootFSPath, 0)
uid, gid, sgids, err := oci.GetUser(ctx, rootFSPath, meta.User)
if err != nil {
return err
}
f, err := os.Create(filepath.Join(bundle, "config.json"))
if err != nil {
return err
}
defer f.Close()
opts := []containerdoci.SpecOpts{oci.WithUIDGID(uid, gid, sgids)}
if system.SeccompSupported() {
opts = append(opts, seccomp.WithDefaultProfile())
}
if meta.ReadonlyRootFS {
opts = append(opts, containerdoci.WithRootFSReadonly())
}
spec, cleanup, err := oci.GenerateSpec(ctx, meta, mounts, id, resolvConf, hostsFile, meta.NetMode == pb.NetMode_HOST, opts...)
if err != nil {
return err
}
defer cleanup()
spec.Root.Path = rootFSPath
if _, ok := root.(cache.ImmutableRef); ok { // TODO: pass in with mount, not ref type
spec.Root.Readonly = true
}
newp, err := fs.RootPath(rootFSPath, meta.Cwd)
if err != nil {
return errors.Wrapf(err, "working dir %s points to invalid target", newp)
}
if err := os.MkdirAll(newp, 0755); err != nil {
return errors.Wrapf(err, "failed to create working directory %s", newp)
}
if err := setOOMScoreAdj(spec); err != nil {
return err
}
if w.rootless {
if err := rootlessspecconv.ToRootless(spec); err != nil {
return err
}
}
if err := json.NewEncoder(f).Encode(spec); err != nil {
return err
}
forwardIO, err := newForwardIO(stdin, stdout, stderr)
if err != nil {
return errors.Wrap(err, "creating new forwarding IO")
}
defer forwardIO.Close()
pidFilePath := filepath.Join(w.root, "runc_pid_"+identity.NewID())
defer os.RemoveAll(pidFilePath)
logrus.Debugf("> creating %s %v", id, meta.Args)
err = w.runc.Create(ctx, id, bundle, &runc.CreateOpts{
PidFile: pidFilePath,
IO: forwardIO,
})
if err != nil {
return err
}
forwardIO.release()
defer func() {
go func() {
if err := w.runc.Delete(context.TODO(), id, &runc.DeleteOpts{}); err != nil {
logrus.Errorf("failed to delete %s: %+v", id, err)
}
}()
}()
dt, err := ioutil.ReadFile(pidFilePath)
if err != nil {
return err
}
pid, err := strconv.Atoi(string(dt))
if err != nil {
return err
}
done := make(chan struct{})
defer close(done)
go func() {
select {
case <-done:
case <-ctx.Done():
syscall.Kill(-pid, syscall.SIGKILL)
}
}()
if iface != nil {
if err := iface.Set(pid); err != nil {
return errors.Wrap(err, "could not set the network")
}
defer func() {
iface.Remove(pid)
}()
}
err = w.runc.Start(ctx, id)
if err != nil {
return err
}
p, err := os.FindProcess(pid)
if err != nil {
return err
}
status := 0
ps, err := p.Wait()
if err != nil {
status = 255
}
if ws, ok := ps.Sys().(syscall.WaitStatus); ok {
status = ws.ExitStatus()
}
if status != 0 {
return errors.Errorf("exit code: %d", status)
}
return nil
}
type forwardIO struct {
stdin, stdout, stderr *os.File
toRelease []io.Closer
toClose []io.Closer
}
func newForwardIO(stdin io.ReadCloser, stdout, stderr io.WriteCloser) (f *forwardIO, err error) {
fio := &forwardIO{}
defer func() {
if err != nil {
fio.Close()
}
}()
if stdin != nil {
fio.stdin, err = fio.readCloserToFile(stdin)
if err != nil {
return nil, err
}
}
if stdout != nil {
fio.stdout, err = fio.writeCloserToFile(stdout)
if err != nil {
return nil, err
}
}
if stderr != nil {
fio.stderr, err = fio.writeCloserToFile(stderr)
if err != nil {
return nil, err
}
}
return fio, nil
}
func (s *forwardIO) Close() error {
s.release()
var err error
for _, cl := range s.toClose {
if err1 := cl.Close(); err == nil {
err = err1
}
}
s.toClose = nil
return err
}
// release releases active FDs if the process doesn't need them any more
func (s *forwardIO) release() {
for _, cl := range s.toRelease {
cl.Close()
}
s.toRelease = nil
}
func (s *forwardIO) Set(cmd *exec.Cmd) {
cmd.Stdin = s.stdin
cmd.Stdout = s.stdout
cmd.Stderr = s.stderr
}
func (s *forwardIO) readCloserToFile(rc io.ReadCloser) (*os.File, error) {
if f, ok := rc.(*os.File); ok {
return f, nil
}
pr, pw, err := os.Pipe()
if err != nil {
return nil, err
}
s.toClose = append(s.toClose, pw)
s.toRelease = append(s.toRelease, pr)
go func() {
_, err := io.Copy(pw, rc)
if err1 := pw.Close(); err == nil {
err = err1
}
_ = err
}()
return pr, nil
}
func (s *forwardIO) writeCloserToFile(wc io.WriteCloser) (*os.File, error) {
if f, ok := wc.(*os.File); ok {
return f, nil
}
pr, pw, err := os.Pipe()
if err != nil {
return nil, err
}
s.toClose = append(s.toClose, pr)
s.toRelease = append(s.toRelease, pw)
go func() {
_, err := io.Copy(wc, pr)
if err1 := pw.Close(); err == nil {
err = err1
}
_ = err
}()
return pw, nil
}
var subReaperOnce sync.Once
var subReaperError error
func setSubReaper() error {
subReaperOnce.Do(func() {
subReaperError = runcsystem.SetSubreaper(1)
})
return subReaperError
}
func (s *forwardIO) Stdin() io.WriteCloser {
return nil
}
func (s *forwardIO) Stdout() io.ReadCloser {
return nil
}
func (s *forwardIO) Stderr() io.ReadCloser {
return nil
}
// setOOMScoreAdj comes from https://github.com/genuinetools/img/blob/2fabe60b7dc4623aa392b515e013bbc69ad510ab/executor/runc/executor.go#L182-L192
func setOOMScoreAdj(spec *specs.Spec) error {
// Set the oom_score_adj of our children containers to that of the current process.
b, err := ioutil.ReadFile("/proc/self/oom_score_adj")
if err != nil {
return errors.Wrap(err, "failed to read /proc/self/oom_score_adj")
}
s := strings.TrimSpace(string(b))
oom, err := strconv.Atoi(s)
if err != nil {
return errors.Wrapf(err, "failed to parse %s as int", s)
}
spec.Process.OOMScoreAdj = &oom
return nil
}