github.com/moby/docker@v26.1.3+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  		if err != nil {
   194  			startedCh <- err
   195  			return
   196  		}
   197  		r.daemonWaitCh = make(chan struct{})
   198  		startedCh <- nil
   199  
   200  		// Reap our child when needed
   201  		if err := cmd.Wait(); err != nil {
   202  			r.logger.WithError(err).Errorf("containerd did not exit successfully")
   203  		}
   204  		close(r.daemonWaitCh)
   205  	}()
   206  	if err := <-startedCh; err != nil {
   207  		return err
   208  	}
   209  
   210  	r.daemonPid = cmd.Process.Pid
   211  
   212  	if err := r.adjustOOMScore(); err != nil {
   213  		r.logger.WithError(err).Warn("failed to adjust OOM score")
   214  	}
   215  
   216  	if err := pidfile.Write(r.pidFile, r.daemonPid); err != nil {
   217  		_ = process.Kill(r.daemonPid)
   218  		return errors.Wrap(err, "libcontainerd: failed to save daemon pid to disk")
   219  	}
   220  
   221  	r.logger.WithField("pid", r.daemonPid).WithField("address", r.Address()).Infof("started new %s process", binaryName)
   222  
   223  	return nil
   224  }
   225  
   226  func (r *remote) adjustOOMScore() error {
   227  	if r.oomScore == 0 || r.daemonPid <= 1 {
   228  		// no score configured, or daemonPid contains an invalid PID (we don't
   229  		// expect containerd to be running as PID 1 :)).
   230  		return nil
   231  	}
   232  	if err := sys.SetOOMScore(r.daemonPid, r.oomScore); err != nil {
   233  		return errors.Wrap(err, "failed to adjust OOM score for containerd process")
   234  	}
   235  	return nil
   236  }
   237  
   238  func (r *remote) monitorDaemon(ctx context.Context) {
   239  	var (
   240  		transientFailureCount = 0
   241  		client                *containerd.Client
   242  		err                   error
   243  		delay                 time.Duration
   244  		timer                 = time.NewTimer(0)
   245  		started               bool
   246  	)
   247  
   248  	defer func() {
   249  		if r.daemonPid != -1 {
   250  			r.stopDaemon()
   251  		}
   252  
   253  		// cleanup some files
   254  		_ = os.Remove(r.pidFile)
   255  
   256  		r.platformCleanup()
   257  
   258  		close(r.daemonStopCh)
   259  		timer.Stop()
   260  	}()
   261  
   262  	// ensure no races on sending to timer.C even though there is a 0 duration.
   263  	if !timer.Stop() {
   264  		<-timer.C
   265  	}
   266  
   267  	for {
   268  		timer.Reset(delay)
   269  
   270  		select {
   271  		case <-ctx.Done():
   272  			r.logger.Info("stopping healthcheck following graceful shutdown")
   273  			if client != nil {
   274  				client.Close()
   275  			}
   276  			return
   277  		case <-timer.C:
   278  		}
   279  
   280  		if r.daemonPid == -1 {
   281  			if r.daemonWaitCh != nil {
   282  				select {
   283  				case <-ctx.Done():
   284  					r.logger.Info("stopping containerd startup following graceful shutdown")
   285  					return
   286  				case <-r.daemonWaitCh:
   287  				}
   288  			}
   289  
   290  			os.RemoveAll(r.GRPC.Address)
   291  			if err := r.startContainerd(); err != nil {
   292  				if !started {
   293  					r.daemonStartCh <- err
   294  					return
   295  				}
   296  				r.logger.WithError(err).Error("failed restarting containerd")
   297  				delay = 50 * time.Millisecond
   298  				continue
   299  			}
   300  
   301  			client, err = containerd.New(
   302  				r.GRPC.Address,
   303  				containerd.WithTimeout(60*time.Second),
   304  				containerd.WithDialOpts([]grpc.DialOption{
   305  					grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),   //nolint:staticcheck // TODO(thaJeztah): ignore SA1019 for deprecated options: see https://github.com/moby/moby/issues/47437
   306  					grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()), //nolint:staticcheck // TODO(thaJeztah): ignore SA1019 for deprecated options: see https://github.com/moby/moby/issues/47437
   307  					grpc.WithTransportCredentials(insecure.NewCredentials()),
   308  					grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(defaults.DefaultMaxRecvMsgSize)),
   309  					grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(defaults.DefaultMaxSendMsgSize)),
   310  				}),
   311  			)
   312  			if err != nil {
   313  				r.logger.WithError(err).Error("failed connecting to containerd")
   314  				delay = 100 * time.Millisecond
   315  				continue
   316  			}
   317  			r.logger.WithField("address", r.GRPC.Address).Debug("created containerd monitoring client")
   318  		}
   319  
   320  		if client != nil {
   321  			tctx, cancel := context.WithTimeout(ctx, healthCheckTimeout)
   322  			_, err := client.IsServing(tctx)
   323  			cancel()
   324  			if err == nil {
   325  				if !started {
   326  					close(r.daemonStartCh)
   327  					started = true
   328  				}
   329  
   330  				transientFailureCount = 0
   331  
   332  				select {
   333  				case <-r.daemonWaitCh:
   334  				case <-ctx.Done():
   335  				}
   336  
   337  				// Set a small delay in case there is a recurring failure (or bug in this code)
   338  				// to ensure we don't end up in a super tight loop.
   339  				delay = 500 * time.Millisecond
   340  				continue
   341  			}
   342  
   343  			r.logger.WithError(err).WithField("binary", binaryName).Debug("daemon is not responding")
   344  
   345  			transientFailureCount++
   346  			if transientFailureCount < maxConnectionRetryCount || process.Alive(r.daemonPid) {
   347  				delay = time.Duration(transientFailureCount) * 200 * time.Millisecond
   348  				continue
   349  			}
   350  			client.Close()
   351  			client = nil
   352  		}
   353  
   354  		if process.Alive(r.daemonPid) {
   355  			r.logger.WithField("pid", r.daemonPid).Info("killing and restarting containerd")
   356  			r.killDaemon()
   357  		}
   358  
   359  		r.daemonPid = -1
   360  		delay = 0
   361  		transientFailureCount = 0
   362  	}
   363  }