github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/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  			return nil, nil, err
    63  		}
    64  		defer socket.Close()
    65  		f, err := socket.File()
    66  		if err != nil {
    67  			return nil, nil, errors.Wrapf(err, "failed to get fd for socket %s", address)
    68  		}
    69  		defer f.Close()
    70  
    71  		stdoutCopy := ioutil.Discard
    72  		stderrCopy := ioutil.Discard
    73  		stdoutLog, err := v1.OpenShimStdoutLog(ctx, config.WorkDir)
    74  		if err != nil {
    75  			return nil, nil, errors.Wrapf(err, "failed to create stdout log")
    76  		}
    77  
    78  		stderrLog, err := v1.OpenShimStderrLog(ctx, config.WorkDir)
    79  		if err != nil {
    80  			return nil, nil, errors.Wrapf(err, "failed to create stderr log")
    81  		}
    82  		if debug {
    83  			stdoutCopy = os.Stdout
    84  			stderrCopy = os.Stderr
    85  		}
    86  
    87  		go io.Copy(stdoutCopy, stdoutLog)
    88  		go io.Copy(stderrCopy, stderrLog)
    89  
    90  		cmd, err := newCommand(binary, daemonAddress, debug, config, f, stdoutLog, stderrLog)
    91  		if err != nil {
    92  			return nil, nil, err
    93  		}
    94  		if err := cmd.Start(); err != nil {
    95  			return nil, nil, errors.Wrapf(err, "failed to start shim")
    96  		}
    97  		defer func() {
    98  			if err != nil {
    99  				cmd.Process.Kill()
   100  			}
   101  		}()
   102  		go func() {
   103  			cmd.Wait()
   104  			exitHandler()
   105  			if stdoutLog != nil {
   106  				stdoutLog.Close()
   107  			}
   108  			if stderrLog != nil {
   109  				stderrLog.Close()
   110  			}
   111  		}()
   112  		log.G(ctx).WithFields(logrus.Fields{
   113  			"pid":     cmd.Process.Pid,
   114  			"address": address,
   115  			"debug":   debug,
   116  		}).Infof("shim %s started", binary)
   117  
   118  		if err := writeFile(filepath.Join(config.Path, "address"), address); err != nil {
   119  			return nil, nil, err
   120  		}
   121  		if err := writeFile(filepath.Join(config.Path, "shim.pid"), strconv.Itoa(cmd.Process.Pid)); err != nil {
   122  			return nil, nil, err
   123  		}
   124  		// set shim in cgroup if it is provided
   125  		if cgroup != "" {
   126  			if err := setCgroup(cgroup, cmd); err != nil {
   127  				return nil, nil, err
   128  			}
   129  			log.G(ctx).WithFields(logrus.Fields{
   130  				"pid":     cmd.Process.Pid,
   131  				"address": address,
   132  			}).Infof("shim placed in cgroup %s", cgroup)
   133  		}
   134  		if err = setupOOMScore(cmd.Process.Pid); err != nil {
   135  			return nil, nil, err
   136  		}
   137  		c, clo, err := WithConnect(address, func() {})(ctx, config)
   138  		if err != nil {
   139  			return nil, nil, errors.Wrap(err, "failed to connect")
   140  		}
   141  		return c, clo, nil
   142  	}
   143  }
   144  
   145  // setupOOMScore gets containerd's oom score and adds +1 to it
   146  // to ensure a shim has a lower* score than the daemons
   147  func setupOOMScore(shimPid int) error {
   148  	pid := os.Getpid()
   149  	score, err := sys.GetOOMScoreAdj(pid)
   150  	if err != nil {
   151  		return errors.Wrap(err, "get daemon OOM score")
   152  	}
   153  	shimScore := score + 1
   154  	if err := sys.SetOOMScore(shimPid, shimScore); err != nil {
   155  		return errors.Wrap(err, "set shim OOM score")
   156  	}
   157  	return nil
   158  }
   159  
   160  func newCommand(binary, daemonAddress string, debug bool, config shim.Config, socket *os.File, stdout, stderr io.Writer) (*exec.Cmd, error) {
   161  	selfExe, err := os.Executable()
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  	args := []string{
   166  		"-namespace", config.Namespace,
   167  		"-workdir", config.WorkDir,
   168  		"-address", daemonAddress,
   169  		"-containerd-binary", selfExe,
   170  	}
   171  
   172  	if config.Criu != "" {
   173  		args = append(args, "-criu-path", config.Criu)
   174  	}
   175  	if config.RuntimeRoot != "" {
   176  		args = append(args, "-runtime-root", config.RuntimeRoot)
   177  	}
   178  	if config.SystemdCgroup {
   179  		args = append(args, "-systemd-cgroup")
   180  	}
   181  	if debug {
   182  		args = append(args, "-debug")
   183  	}
   184  
   185  	cmd := exec.Command(binary, args...)
   186  	cmd.Dir = config.Path
   187  	// make sure the shim can be re-parented to system init
   188  	// and is cloned in a new mount namespace because the overlay/filesystems
   189  	// will be mounted by the shim
   190  	cmd.SysProcAttr = getSysProcAttr()
   191  	cmd.ExtraFiles = append(cmd.ExtraFiles, socket)
   192  	cmd.Env = append(os.Environ(), "GOMAXPROCS=2")
   193  	cmd.Stdout = stdout
   194  	cmd.Stderr = stderr
   195  	return cmd, nil
   196  }
   197  
   198  // writeFile writes a address file atomically
   199  func writeFile(path, address string) error {
   200  	path, err := filepath.Abs(path)
   201  	if err != nil {
   202  		return err
   203  	}
   204  	tempPath := filepath.Join(filepath.Dir(path), fmt.Sprintf(".%s", filepath.Base(path)))
   205  	f, err := os.OpenFile(tempPath, os.O_RDWR|os.O_CREATE|os.O_EXCL|os.O_SYNC, 0666)
   206  	if err != nil {
   207  		return err
   208  	}
   209  	_, err = f.WriteString(address)
   210  	f.Close()
   211  	if err != nil {
   212  		return err
   213  	}
   214  	return os.Rename(tempPath, path)
   215  }
   216  
   217  func newSocket(address string) (*net.UnixListener, error) {
   218  	if len(address) > 106 {
   219  		return nil, errors.Errorf("%q: unix socket path too long (> 106)", address)
   220  	}
   221  	l, err := net.Listen("unix", "\x00"+address)
   222  	if err != nil {
   223  		return nil, errors.Wrapf(err, "failed to listen to abstract unix socket %q", address)
   224  	}
   225  
   226  	return l.(*net.UnixListener), nil
   227  }
   228  
   229  func connect(address string, d func(string, time.Duration) (net.Conn, error)) (net.Conn, error) {
   230  	return d(address, 100*time.Second)
   231  }
   232  
   233  func annonDialer(address string, timeout time.Duration) (net.Conn, error) {
   234  	address = strings.TrimPrefix(address, "unix://")
   235  	return dialer.Dialer("\x00"+address, timeout)
   236  }
   237  
   238  // WithConnect connects to an existing shim
   239  func WithConnect(address string, onClose func()) Opt {
   240  	return func(ctx context.Context, config shim.Config) (shimapi.ShimService, io.Closer, error) {
   241  		conn, err := connect(address, annonDialer)
   242  		if err != nil {
   243  			return nil, nil, err
   244  		}
   245  		client := ttrpc.NewClient(conn, ttrpc.WithOnClose(onClose))
   246  		return shimapi.NewShimClient(client), conn, nil
   247  	}
   248  }
   249  
   250  // WithLocal uses an in process shim
   251  func WithLocal(publisher events.Publisher) func(context.Context, shim.Config) (shimapi.ShimService, io.Closer, error) {
   252  	return func(ctx context.Context, config shim.Config) (shimapi.ShimService, io.Closer, error) {
   253  		service, err := shim.NewService(config, publisher)
   254  		if err != nil {
   255  			return nil, nil, err
   256  		}
   257  		return shim.NewLocal(service), nil, nil
   258  	}
   259  }
   260  
   261  // New returns a new shim client
   262  func New(ctx context.Context, config shim.Config, opt Opt) (*Client, error) {
   263  	s, c, err := opt(ctx, config)
   264  	if err != nil {
   265  		return nil, err
   266  	}
   267  	return &Client{
   268  		ShimService: s,
   269  		c:           c,
   270  		exitCh:      make(chan struct{}),
   271  	}, nil
   272  }
   273  
   274  // Client is a shim client containing the connection to a shim
   275  type Client struct {
   276  	shimapi.ShimService
   277  
   278  	c        io.Closer
   279  	exitCh   chan struct{}
   280  	exitOnce sync.Once
   281  }
   282  
   283  // IsAlive returns true if the shim can be contacted.
   284  // NOTE: a negative answer doesn't mean that the process is gone.
   285  func (c *Client) IsAlive(ctx context.Context) (bool, error) {
   286  	_, err := c.ShimInfo(ctx, empty)
   287  	if err != nil {
   288  		// TODO(stevvooe): There are some error conditions that need to be
   289  		// handle with unix sockets existence to give the right answer here.
   290  		return false, err
   291  	}
   292  	return true, nil
   293  }
   294  
   295  // StopShim signals the shim to exit and wait for the process to disappear
   296  func (c *Client) StopShim(ctx context.Context) error {
   297  	return c.signalShim(ctx, unix.SIGTERM)
   298  }
   299  
   300  // KillShim kills the shim forcefully and wait for the process to disappear
   301  func (c *Client) KillShim(ctx context.Context) error {
   302  	return c.signalShim(ctx, unix.SIGKILL)
   303  }
   304  
   305  // Close the client connection
   306  func (c *Client) Close() error {
   307  	if c.c == nil {
   308  		return nil
   309  	}
   310  	return c.c.Close()
   311  }
   312  
   313  func (c *Client) signalShim(ctx context.Context, sig syscall.Signal) error {
   314  	info, err := c.ShimInfo(ctx, empty)
   315  	if err != nil {
   316  		return err
   317  	}
   318  	pid := int(info.ShimPid)
   319  	// make sure we don't kill ourselves if we are running a local shim
   320  	if os.Getpid() == pid {
   321  		return nil
   322  	}
   323  	if err := unix.Kill(pid, sig); err != nil && err != unix.ESRCH {
   324  		return err
   325  	}
   326  	// wait for shim to die after being signaled
   327  	select {
   328  	case <-ctx.Done():
   329  		return ctx.Err()
   330  	case <-c.waitForExit(ctx, pid):
   331  		return nil
   332  	}
   333  }
   334  
   335  func (c *Client) waitForExit(ctx context.Context, pid int) <-chan struct{} {
   336  	go c.exitOnce.Do(func() {
   337  		defer close(c.exitCh)
   338  
   339  		ticker := time.NewTicker(10 * time.Millisecond)
   340  		defer ticker.Stop()
   341  
   342  		for {
   343  			// use kill(pid, 0) here because the shim could have been reparented
   344  			// and we are no longer able to waitpid(pid, ...) on the shim
   345  			if err := unix.Kill(pid, 0); err == unix.ESRCH {
   346  				return
   347  			}
   348  
   349  			select {
   350  			case <-ticker.C:
   351  			case <-ctx.Done():
   352  				log.G(ctx).WithField("pid", pid).Warn("timed out while waiting for shim to exit")
   353  				return
   354  			}
   355  		}
   356  	})
   357  	return c.exitCh
   358  }