github.com/containerd/Containerd@v1.4.13/runtime/v1/shim/client/client.go (about)

     1  // +build !windows
     2  
     3  /*
     4     Copyright The containerd Authors.
     5  
     6     Licensed under the Apache License, Version 2.0 (the "License");
     7     you may not use this file except in compliance with the License.
     8     You may obtain a copy of the License at
     9  
    10         http://www.apache.org/licenses/LICENSE-2.0
    11  
    12     Unless required by applicable law or agreed to in writing, software
    13     distributed under the License is distributed on an "AS IS" BASIS,
    14     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15     See the License for the specific language governing permissions and
    16     limitations under the License.
    17  */
    18  
    19  package client
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"io"
    25  	"io/ioutil"
    26  	"net"
    27  	"os"
    28  	"os/exec"
    29  	"path/filepath"
    30  	"strconv"
    31  	"strings"
    32  	"sync"
    33  	"syscall"
    34  	"time"
    35  
    36  	"golang.org/x/sys/unix"
    37  
    38  	"github.com/containerd/ttrpc"
    39  	"github.com/pkg/errors"
    40  	"github.com/sirupsen/logrus"
    41  
    42  	"github.com/containerd/containerd/events"
    43  	"github.com/containerd/containerd/log"
    44  	"github.com/containerd/containerd/pkg/dialer"
    45  	v1 "github.com/containerd/containerd/runtime/v1"
    46  	"github.com/containerd/containerd/runtime/v1/shim"
    47  	shimapi "github.com/containerd/containerd/runtime/v1/shim/v1"
    48  	"github.com/containerd/containerd/sys"
    49  	ptypes "github.com/gogo/protobuf/types"
    50  )
    51  
    52  var empty = &ptypes.Empty{}
    53  
    54  // Opt is an option for a shim client configuration
    55  type Opt func(context.Context, shim.Config) (shimapi.ShimService, io.Closer, error)
    56  
    57  // WithStart executes a new shim process
    58  func WithStart(binary, address, daemonAddress, cgroup string, debug bool, exitHandler func()) Opt {
    59  	return func(ctx context.Context, config shim.Config) (_ shimapi.ShimService, _ io.Closer, err error) {
    60  		socket, err := newSocket(address)
    61  		if err != nil {
    62  			if !eaddrinuse(err) {
    63  				return nil, nil, err
    64  			}
    65  			if err := RemoveSocket(address); err != nil {
    66  				return nil, nil, errors.Wrap(err, "remove already used socket")
    67  			}
    68  			if socket, err = newSocket(address); err != nil {
    69  				return nil, nil, err
    70  			}
    71  		}
    72  
    73  		f, err := socket.File()
    74  		if err != nil {
    75  			return nil, nil, errors.Wrapf(err, "failed to get fd for socket %s", address)
    76  		}
    77  		defer f.Close()
    78  
    79  		stdoutCopy := ioutil.Discard
    80  		stderrCopy := ioutil.Discard
    81  		stdoutLog, err := v1.OpenShimStdoutLog(ctx, config.WorkDir)
    82  		if err != nil {
    83  			return nil, nil, errors.Wrapf(err, "failed to create stdout log")
    84  		}
    85  
    86  		stderrLog, err := v1.OpenShimStderrLog(ctx, config.WorkDir)
    87  		if err != nil {
    88  			return nil, nil, errors.Wrapf(err, "failed to create stderr log")
    89  		}
    90  		if debug {
    91  			stdoutCopy = os.Stdout
    92  			stderrCopy = os.Stderr
    93  		}
    94  
    95  		go io.Copy(stdoutCopy, stdoutLog)
    96  		go io.Copy(stderrCopy, stderrLog)
    97  
    98  		cmd, err := newCommand(binary, daemonAddress, debug, config, f, stdoutLog, stderrLog)
    99  		if err != nil {
   100  			return nil, nil, err
   101  		}
   102  		if err := cmd.Start(); err != nil {
   103  			return nil, nil, errors.Wrapf(err, "failed to start shim")
   104  		}
   105  		defer func() {
   106  			if err != nil {
   107  				cmd.Process.Kill()
   108  			}
   109  		}()
   110  		go func() {
   111  			cmd.Wait()
   112  			exitHandler()
   113  			if stdoutLog != nil {
   114  				stdoutLog.Close()
   115  			}
   116  			if stderrLog != nil {
   117  				stderrLog.Close()
   118  			}
   119  			socket.Close()
   120  			RemoveSocket(address)
   121  		}()
   122  		log.G(ctx).WithFields(logrus.Fields{
   123  			"pid":     cmd.Process.Pid,
   124  			"address": address,
   125  			"debug":   debug,
   126  		}).Infof("shim %s started", binary)
   127  
   128  		if err := writeFile(filepath.Join(config.Path, "address"), address); err != nil {
   129  			return nil, nil, err
   130  		}
   131  		if err := writeFile(filepath.Join(config.Path, "shim.pid"), strconv.Itoa(cmd.Process.Pid)); err != nil {
   132  			return nil, nil, err
   133  		}
   134  		// set shim in cgroup if it is provided
   135  		if cgroup != "" {
   136  			if err := setCgroup(cgroup, cmd); err != nil {
   137  				return nil, nil, err
   138  			}
   139  			log.G(ctx).WithFields(logrus.Fields{
   140  				"pid":     cmd.Process.Pid,
   141  				"address": address,
   142  			}).Infof("shim placed in cgroup %s", cgroup)
   143  		}
   144  		if err = setupOOMScore(cmd.Process.Pid); err != nil {
   145  			return nil, nil, err
   146  		}
   147  		c, clo, err := WithConnect(address, func() {})(ctx, config)
   148  		if err != nil {
   149  			return nil, nil, errors.Wrap(err, "failed to connect")
   150  		}
   151  		return c, clo, nil
   152  	}
   153  }
   154  
   155  func eaddrinuse(err error) bool {
   156  	cause := errors.Cause(err)
   157  	netErr, ok := cause.(*net.OpError)
   158  	if !ok {
   159  		return false
   160  	}
   161  	if netErr.Op != "listen" {
   162  		return false
   163  	}
   164  	syscallErr, ok := netErr.Err.(*os.SyscallError)
   165  	if !ok {
   166  		return false
   167  	}
   168  	errno, ok := syscallErr.Err.(syscall.Errno)
   169  	if !ok {
   170  		return false
   171  	}
   172  	return errno == syscall.EADDRINUSE
   173  }
   174  
   175  // setupOOMScore gets containerd's oom score and adds +1 to it
   176  // to ensure a shim has a lower* score than the daemons
   177  // if not already at the maximum OOM Score
   178  func setupOOMScore(shimPid int) error {
   179  	pid := os.Getpid()
   180  	score, err := sys.GetOOMScoreAdj(pid)
   181  	if err != nil {
   182  		return errors.Wrap(err, "get daemon OOM score")
   183  	}
   184  	shimScore := score + 1
   185  	if shimScore > sys.OOMScoreAdjMax {
   186  		shimScore = sys.OOMScoreAdjMax
   187  	}
   188  	if err := sys.SetOOMScore(shimPid, shimScore); err != nil {
   189  		return errors.Wrap(err, "set shim OOM score")
   190  	}
   191  	return nil
   192  }
   193  
   194  func newCommand(binary, daemonAddress string, debug bool, config shim.Config, socket *os.File, stdout, stderr io.Writer) (*exec.Cmd, error) {
   195  	selfExe, err := os.Executable()
   196  	if err != nil {
   197  		return nil, err
   198  	}
   199  	args := []string{
   200  		"-namespace", config.Namespace,
   201  		"-workdir", config.WorkDir,
   202  		"-address", daemonAddress,
   203  		"-containerd-binary", selfExe,
   204  	}
   205  
   206  	if config.Criu != "" {
   207  		args = append(args, "-criu-path", config.Criu)
   208  	}
   209  	if config.RuntimeRoot != "" {
   210  		args = append(args, "-runtime-root", config.RuntimeRoot)
   211  	}
   212  	if config.SystemdCgroup {
   213  		args = append(args, "-systemd-cgroup")
   214  	}
   215  	if debug {
   216  		args = append(args, "-debug")
   217  	}
   218  
   219  	cmd := exec.Command(binary, args...)
   220  	cmd.Dir = config.Path
   221  	// make sure the shim can be re-parented to system init
   222  	// and is cloned in a new mount namespace because the overlay/filesystems
   223  	// will be mounted by the shim
   224  	cmd.SysProcAttr = getSysProcAttr()
   225  	cmd.ExtraFiles = append(cmd.ExtraFiles, socket)
   226  	cmd.Env = append(os.Environ(), "GOMAXPROCS=2")
   227  	cmd.Stdout = stdout
   228  	cmd.Stderr = stderr
   229  	return cmd, nil
   230  }
   231  
   232  // writeFile writes a address file atomically
   233  func writeFile(path, address string) error {
   234  	path, err := filepath.Abs(path)
   235  	if err != nil {
   236  		return err
   237  	}
   238  	tempPath := filepath.Join(filepath.Dir(path), fmt.Sprintf(".%s", filepath.Base(path)))
   239  	f, err := os.OpenFile(tempPath, os.O_RDWR|os.O_CREATE|os.O_EXCL|os.O_SYNC, 0666)
   240  	if err != nil {
   241  		return err
   242  	}
   243  	_, err = f.WriteString(address)
   244  	f.Close()
   245  	if err != nil {
   246  		return err
   247  	}
   248  	return os.Rename(tempPath, path)
   249  }
   250  
   251  const (
   252  	abstractSocketPrefix = "\x00"
   253  	socketPathLimit      = 106
   254  )
   255  
   256  type socket string
   257  
   258  func (s socket) isAbstract() bool {
   259  	return !strings.HasPrefix(string(s), "unix://")
   260  }
   261  
   262  func (s socket) path() string {
   263  	path := strings.TrimPrefix(string(s), "unix://")
   264  	// if there was no trim performed, we assume an abstract socket
   265  	if len(path) == len(s) {
   266  		path = abstractSocketPrefix + path
   267  	}
   268  	return path
   269  }
   270  
   271  func newSocket(address string) (*net.UnixListener, error) {
   272  	if len(address) > socketPathLimit {
   273  		return nil, errors.Errorf("%q: unix socket path too long (> %d)", address, socketPathLimit)
   274  	}
   275  	var (
   276  		sock = socket(address)
   277  		path = sock.path()
   278  	)
   279  	if !sock.isAbstract() {
   280  		if err := os.MkdirAll(filepath.Dir(path), 0600); err != nil {
   281  			return nil, errors.Wrapf(err, "%s", path)
   282  		}
   283  	}
   284  	l, err := net.Listen("unix", path)
   285  	if err != nil {
   286  		return nil, errors.Wrapf(err, "failed to listen to unix socket %q (abstract: %t)", address, sock.isAbstract())
   287  	}
   288  	if err := os.Chmod(path, 0600); err != nil {
   289  		l.Close()
   290  		return nil, err
   291  	}
   292  
   293  	return l.(*net.UnixListener), nil
   294  }
   295  
   296  // RemoveSocket removes the socket at the specified address if
   297  // it exists on the filesystem
   298  func RemoveSocket(address string) error {
   299  	sock := socket(address)
   300  	if !sock.isAbstract() {
   301  		return os.Remove(sock.path())
   302  	}
   303  	return nil
   304  }
   305  
   306  func connect(address string, d func(string, time.Duration) (net.Conn, error)) (net.Conn, error) {
   307  	return d(address, 100*time.Second)
   308  }
   309  
   310  func anonDialer(address string, timeout time.Duration) (net.Conn, error) {
   311  	return dialer.Dialer(socket(address).path(), timeout)
   312  }
   313  
   314  // WithConnect connects to an existing shim
   315  func WithConnect(address string, onClose func()) Opt {
   316  	return func(ctx context.Context, config shim.Config) (shimapi.ShimService, io.Closer, error) {
   317  		conn, err := connect(address, anonDialer)
   318  		if err != nil {
   319  			return nil, nil, err
   320  		}
   321  		client := ttrpc.NewClient(conn, ttrpc.WithOnClose(onClose))
   322  		return shimapi.NewShimClient(client), conn, nil
   323  	}
   324  }
   325  
   326  // WithLocal uses an in process shim
   327  func WithLocal(publisher events.Publisher) func(context.Context, shim.Config) (shimapi.ShimService, io.Closer, error) {
   328  	return func(ctx context.Context, config shim.Config) (shimapi.ShimService, io.Closer, error) {
   329  		service, err := shim.NewService(config, publisher)
   330  		if err != nil {
   331  			return nil, nil, err
   332  		}
   333  		return shim.NewLocal(service), nil, nil
   334  	}
   335  }
   336  
   337  // New returns a new shim client
   338  func New(ctx context.Context, config shim.Config, opt Opt) (*Client, error) {
   339  	s, c, err := opt(ctx, config)
   340  	if err != nil {
   341  		return nil, err
   342  	}
   343  	return &Client{
   344  		ShimService: s,
   345  		c:           c,
   346  		exitCh:      make(chan struct{}),
   347  	}, nil
   348  }
   349  
   350  // Client is a shim client containing the connection to a shim
   351  type Client struct {
   352  	shimapi.ShimService
   353  
   354  	c        io.Closer
   355  	exitCh   chan struct{}
   356  	exitOnce sync.Once
   357  }
   358  
   359  // IsAlive returns true if the shim can be contacted.
   360  // NOTE: a negative answer doesn't mean that the process is gone.
   361  func (c *Client) IsAlive(ctx context.Context) (bool, error) {
   362  	_, err := c.ShimInfo(ctx, empty)
   363  	if err != nil {
   364  		// TODO(stevvooe): There are some error conditions that need to be
   365  		// handle with unix sockets existence to give the right answer here.
   366  		return false, err
   367  	}
   368  	return true, nil
   369  }
   370  
   371  // StopShim signals the shim to exit and wait for the process to disappear
   372  func (c *Client) StopShim(ctx context.Context) error {
   373  	return c.signalShim(ctx, unix.SIGTERM)
   374  }
   375  
   376  // KillShim kills the shim forcefully and wait for the process to disappear
   377  func (c *Client) KillShim(ctx context.Context) error {
   378  	return c.signalShim(ctx, unix.SIGKILL)
   379  }
   380  
   381  // Close the client connection
   382  func (c *Client) Close() error {
   383  	if c.c == nil {
   384  		return nil
   385  	}
   386  	return c.c.Close()
   387  }
   388  
   389  func (c *Client) signalShim(ctx context.Context, sig syscall.Signal) error {
   390  	info, err := c.ShimInfo(ctx, empty)
   391  	if err != nil {
   392  		return err
   393  	}
   394  	pid := int(info.ShimPid)
   395  	// make sure we don't kill ourselves if we are running a local shim
   396  	if os.Getpid() == pid {
   397  		return nil
   398  	}
   399  	if err := unix.Kill(pid, sig); err != nil && err != unix.ESRCH {
   400  		return err
   401  	}
   402  	// wait for shim to die after being signaled
   403  	select {
   404  	case <-ctx.Done():
   405  		return ctx.Err()
   406  	case <-c.waitForExit(ctx, pid):
   407  		return nil
   408  	}
   409  }
   410  
   411  func (c *Client) waitForExit(ctx context.Context, pid int) <-chan struct{} {
   412  	go c.exitOnce.Do(func() {
   413  		defer close(c.exitCh)
   414  
   415  		ticker := time.NewTicker(10 * time.Millisecond)
   416  		defer ticker.Stop()
   417  
   418  		for {
   419  			// use kill(pid, 0) here because the shim could have been reparented
   420  			// and we are no longer able to waitpid(pid, ...) on the shim
   421  			if err := unix.Kill(pid, 0); err == unix.ESRCH {
   422  				return
   423  			}
   424  
   425  			select {
   426  			case <-ticker.C:
   427  			case <-ctx.Done():
   428  				log.G(ctx).WithField("pid", pid).Warn("timed out while waiting for shim to exit")
   429  				return
   430  			}
   431  		}
   432  	})
   433  	return c.exitCh
   434  }