github.com/rish1988/moby@v25.0.2+incompatible/libcontainerd/supervisor/remote_daemon.go (about)

     1  package supervisor // import "github.com/docker/docker/libcontainerd/supervisor"
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"os/exec"
     7  	"path/filepath"
     8  	"runtime"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/containerd/containerd"
    13  	"github.com/containerd/containerd/defaults"
    14  	"github.com/containerd/containerd/services/server/config"
    15  	"github.com/containerd/containerd/sys"
    16  	"github.com/containerd/log"
    17  	"github.com/docker/docker/pkg/pidfile"
    18  	"github.com/docker/docker/pkg/process"
    19  	"github.com/docker/docker/pkg/system"
    20  	"github.com/pelletier/go-toml"
    21  	"github.com/pkg/errors"
    22  	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    23  	"google.golang.org/grpc"
    24  	"google.golang.org/grpc/credentials/insecure"
    25  )
    26  
    27  const (
    28  	maxConnectionRetryCount = 3
    29  	healthCheckTimeout      = 3 * time.Second
    30  	shutdownTimeout         = 15 * time.Second
    31  	startupTimeout          = 15 * time.Second
    32  	configFile              = "containerd.toml"
    33  	binaryName              = "containerd"
    34  	pidFile                 = "containerd.pid"
    35  )
    36  
    37  type remote struct {
    38  	config.Config
    39  
    40  	// configFile is the location where the generated containerd configuration
    41  	// file is saved.
    42  	configFile string
    43  
    44  	daemonPid int
    45  	pidFile   string
    46  	logger    *log.Entry
    47  
    48  	daemonWaitCh  chan struct{}
    49  	daemonStartCh chan error
    50  	daemonStopCh  chan struct{}
    51  
    52  	stateDir string
    53  
    54  	// oomScore adjusts the OOM score for the containerd process.
    55  	oomScore int
    56  
    57  	// logLevel overrides the containerd logging-level through the --log-level
    58  	// command-line option.
    59  	logLevel string
    60  }
    61  
    62  // Daemon represents a running containerd daemon
    63  type Daemon interface {
    64  	WaitTimeout(time.Duration) error
    65  	Address() string
    66  }
    67  
    68  // DaemonOpt allows to configure parameters of container daemons
    69  type DaemonOpt func(c *remote) error
    70  
    71  // Start starts a containerd daemon and monitors it
    72  func Start(ctx context.Context, rootDir, stateDir string, opts ...DaemonOpt) (Daemon, error) {
    73  	r := &remote{
    74  		stateDir: stateDir,
    75  		Config: config.Config{
    76  			Version: 2,
    77  			Root:    filepath.Join(rootDir, "daemon"),
    78  			State:   filepath.Join(stateDir, "daemon"),
    79  		},
    80  		configFile:    filepath.Join(stateDir, configFile),
    81  		daemonPid:     -1,
    82  		pidFile:       filepath.Join(stateDir, pidFile),
    83  		logger:        log.G(ctx).WithField("module", "libcontainerd"),
    84  		daemonStartCh: make(chan error, 1),
    85  		daemonStopCh:  make(chan struct{}),
    86  	}
    87  
    88  	for _, opt := range opts {
    89  		if err := opt(r); err != nil {
    90  			return nil, err
    91  		}
    92  	}
    93  	r.setDefaults()
    94  
    95  	if err := system.MkdirAll(stateDir, 0o700); err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	go r.monitorDaemon(ctx)
   100  
   101  	timeout := time.NewTimer(startupTimeout)
   102  	defer timeout.Stop()
   103  
   104  	select {
   105  	case <-timeout.C:
   106  		return nil, errors.New("timeout waiting for containerd to start")
   107  	case err := <-r.daemonStartCh:
   108  		if err != nil {
   109  			return nil, err
   110  		}
   111  	}
   112  
   113  	return r, nil
   114  }
   115  
   116  func (r *remote) WaitTimeout(d time.Duration) error {
   117  	timeout := time.NewTimer(d)
   118  	defer timeout.Stop()
   119  
   120  	select {
   121  	case <-timeout.C:
   122  		return errors.New("timeout waiting for containerd to stop")
   123  	case <-r.daemonStopCh:
   124  	}
   125  
   126  	return nil
   127  }
   128  
   129  func (r *remote) Address() string {
   130  	return r.GRPC.Address
   131  }
   132  
   133  func (r *remote) getContainerdConfig() (string, error) {
   134  	f, err := os.OpenFile(r.configFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
   135  	if err != nil {
   136  		return "", errors.Wrapf(err, "failed to open containerd config file (%s)", r.configFile)
   137  	}
   138  	defer f.Close()
   139  
   140  	if err := toml.NewEncoder(f).Encode(r); err != nil {
   141  		return "", errors.Wrapf(err, "failed to write containerd config file (%s)", r.configFile)
   142  	}
   143  	return r.configFile, nil
   144  }
   145  
   146  func (r *remote) startContainerd() error {
   147  	pid, err := pidfile.Read(r.pidFile)
   148  	if err != nil && !errors.Is(err, os.ErrNotExist) {
   149  		return err
   150  	}
   151  
   152  	if pid > 0 {
   153  		r.daemonPid = pid
   154  		r.logger.WithField("pid", pid).Infof("%s is still running", binaryName)
   155  		return nil
   156  	}
   157  
   158  	cfgFile, err := r.getContainerdConfig()
   159  	if err != nil {
   160  		return err
   161  	}
   162  	args := []string{"--config", cfgFile}
   163  
   164  	if r.logLevel != "" {
   165  		args = append(args, "--log-level", r.logLevel)
   166  	}
   167  
   168  	cmd := exec.Command(binaryName, args...)
   169  	// redirect containerd logs to docker logs
   170  	cmd.Stdout = os.Stdout
   171  	cmd.Stderr = os.Stderr
   172  	cmd.SysProcAttr = containerdSysProcAttr()
   173  	// clear the NOTIFY_SOCKET from the env when starting containerd
   174  	cmd.Env = nil
   175  	for _, e := range os.Environ() {
   176  		if !strings.HasPrefix(e, "NOTIFY_SOCKET") {
   177  			cmd.Env = append(cmd.Env, e)
   178  		}
   179  	}
   180  
   181  	startedCh := make(chan error)
   182  	go func() {
   183  		// On Linux, when cmd.SysProcAttr.Pdeathsig is set,
   184  		// the signal is sent to the subprocess when the creating thread
   185  		// terminates. The runtime terminates a thread if a goroutine
   186  		// exits while locked to it. Prevent the containerd process
   187  		// from getting killed prematurely by ensuring that the thread
   188  		// used to start it remains alive until it or the daemon process
   189  		// exits. See https://go.dev/issue/27505 for more details.
   190  		runtime.LockOSThread()
   191  		defer runtime.UnlockOSThread()
   192  		err := cmd.Start()
   193  		startedCh <- err
   194  		if err != nil {
   195  			return
   196  		}
   197  
   198  		r.daemonWaitCh = make(chan struct{})
   199  		// Reap our child when needed
   200  		if err := cmd.Wait(); err != nil {
   201  			r.logger.WithError(err).Errorf("containerd did not exit successfully")
   202  		}
   203  		close(r.daemonWaitCh)
   204  	}()
   205  	if err := <-startedCh; err != nil {
   206  		return err
   207  	}
   208  
   209  	r.daemonPid = cmd.Process.Pid
   210  
   211  	if err := r.adjustOOMScore(); err != nil {
   212  		r.logger.WithError(err).Warn("failed to adjust OOM score")
   213  	}
   214  
   215  	if err := pidfile.Write(r.pidFile, r.daemonPid); err != nil {
   216  		_ = process.Kill(r.daemonPid)
   217  		return errors.Wrap(err, "libcontainerd: failed to save daemon pid to disk")
   218  	}
   219  
   220  	r.logger.WithField("pid", r.daemonPid).WithField("address", r.Address()).Infof("started new %s process", binaryName)
   221  
   222  	return nil
   223  }
   224  
   225  func (r *remote) adjustOOMScore() error {
   226  	if r.oomScore == 0 || r.daemonPid <= 1 {
   227  		// no score configured, or daemonPid contains an invalid PID (we don't
   228  		// expect containerd to be running as PID 1 :)).
   229  		return nil
   230  	}
   231  	if err := sys.SetOOMScore(r.daemonPid, r.oomScore); err != nil {
   232  		return errors.Wrap(err, "failed to adjust OOM score for containerd process")
   233  	}
   234  	return nil
   235  }
   236  
   237  func (r *remote) monitorDaemon(ctx context.Context) {
   238  	var (
   239  		transientFailureCount = 0
   240  		client                *containerd.Client
   241  		err                   error
   242  		delay                 time.Duration
   243  		timer                 = time.NewTimer(0)
   244  		started               bool
   245  	)
   246  
   247  	defer func() {
   248  		if r.daemonPid != -1 {
   249  			r.stopDaemon()
   250  		}
   251  
   252  		// cleanup some files
   253  		_ = os.Remove(r.pidFile)
   254  
   255  		r.platformCleanup()
   256  
   257  		close(r.daemonStopCh)
   258  		timer.Stop()
   259  	}()
   260  
   261  	// ensure no races on sending to timer.C even though there is a 0 duration.
   262  	if !timer.Stop() {
   263  		<-timer.C
   264  	}
   265  
   266  	for {
   267  		timer.Reset(delay)
   268  
   269  		select {
   270  		case <-ctx.Done():
   271  			r.logger.Info("stopping healthcheck following graceful shutdown")
   272  			if client != nil {
   273  				client.Close()
   274  			}
   275  			return
   276  		case <-timer.C:
   277  		}
   278  
   279  		if r.daemonPid == -1 {
   280  			if r.daemonWaitCh != nil {
   281  				select {
   282  				case <-ctx.Done():
   283  					r.logger.Info("stopping containerd startup following graceful shutdown")
   284  					return
   285  				case <-r.daemonWaitCh:
   286  				}
   287  			}
   288  
   289  			os.RemoveAll(r.GRPC.Address)
   290  			if err := r.startContainerd(); err != nil {
   291  				if !started {
   292  					r.daemonStartCh <- err
   293  					return
   294  				}
   295  				r.logger.WithError(err).Error("failed restarting containerd")
   296  				delay = 50 * time.Millisecond
   297  				continue
   298  			}
   299  
   300  			client, err = containerd.New(
   301  				r.GRPC.Address,
   302  				containerd.WithTimeout(60*time.Second),
   303  				containerd.WithDialOpts([]grpc.DialOption{
   304  					grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
   305  					grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()),
   306  					grpc.WithTransportCredentials(insecure.NewCredentials()),
   307  					grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(defaults.DefaultMaxRecvMsgSize)),
   308  					grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(defaults.DefaultMaxSendMsgSize)),
   309  				}),
   310  			)
   311  			if err != nil {
   312  				r.logger.WithError(err).Error("failed connecting to containerd")
   313  				delay = 100 * time.Millisecond
   314  				continue
   315  			}
   316  			r.logger.WithField("address", r.GRPC.Address).Debug("created containerd monitoring client")
   317  		}
   318  
   319  		if client != nil {
   320  			tctx, cancel := context.WithTimeout(ctx, healthCheckTimeout)
   321  			_, err := client.IsServing(tctx)
   322  			cancel()
   323  			if err == nil {
   324  				if !started {
   325  					close(r.daemonStartCh)
   326  					started = true
   327  				}
   328  
   329  				transientFailureCount = 0
   330  
   331  				select {
   332  				case <-r.daemonWaitCh:
   333  				case <-ctx.Done():
   334  				}
   335  
   336  				// Set a small delay in case there is a recurring failure (or bug in this code)
   337  				// to ensure we don't end up in a super tight loop.
   338  				delay = 500 * time.Millisecond
   339  				continue
   340  			}
   341  
   342  			r.logger.WithError(err).WithField("binary", binaryName).Debug("daemon is not responding")
   343  
   344  			transientFailureCount++
   345  			if transientFailureCount < maxConnectionRetryCount || process.Alive(r.daemonPid) {
   346  				delay = time.Duration(transientFailureCount) * 200 * time.Millisecond
   347  				continue
   348  			}
   349  			client.Close()
   350  			client = nil
   351  		}
   352  
   353  		if process.Alive(r.daemonPid) {
   354  			r.logger.WithField("pid", r.daemonPid).Info("killing and restarting containerd")
   355  			r.killDaemon()
   356  		}
   357  
   358  		r.daemonPid = -1
   359  		delay = 0
   360  		transientFailureCount = 0
   361  	}
   362  }