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