github.com/moby/docker@v26.1.3+incompatible/daemon/cluster/noderunner.go (about)

     1  package cluster // import "github.com/docker/docker/daemon/cluster"
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"path/filepath"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/containerd/log"
    12  	types "github.com/docker/docker/api/types/swarm"
    13  	"github.com/docker/docker/daemon/cluster/convert"
    14  	"github.com/docker/docker/daemon/cluster/executor/container"
    15  	lncluster "github.com/docker/docker/libnetwork/cluster"
    16  	"github.com/docker/docker/libnetwork/cnmallocator"
    17  	swarmapi "github.com/moby/swarmkit/v2/api"
    18  	"github.com/moby/swarmkit/v2/manager/allocator/networkallocator"
    19  	swarmnode "github.com/moby/swarmkit/v2/node"
    20  	"github.com/pkg/errors"
    21  	"google.golang.org/grpc"
    22  	"google.golang.org/grpc/codes"
    23  	"google.golang.org/grpc/status"
    24  )
    25  
    26  // nodeRunner implements a manager for continuously running swarmkit node, restarting them with backoff delays if needed.
    27  type nodeRunner struct {
    28  	nodeState
    29  	mu             sync.RWMutex
    30  	done           chan struct{} // closed when swarmNode exits
    31  	ready          chan struct{} // closed when swarmNode becomes active
    32  	reconnectDelay time.Duration
    33  	config         nodeStartConfig
    34  
    35  	repeatedRun     bool
    36  	cancelReconnect func()
    37  	stopping        bool
    38  	cluster         *Cluster // only for accessing config helpers, never call any methods. TODO: change to config struct
    39  }
    40  
    41  // nodeStartConfig holds configuration needed to start a new node. Exported
    42  // fields of this structure are saved to disk in json. Unexported fields
    43  // contain data that shouldn't be persisted between daemon reloads.
    44  type nodeStartConfig struct {
    45  	// LocalAddr is this machine's local IP or hostname, if specified.
    46  	LocalAddr string
    47  	// RemoteAddr is the address that was given to "swarm join". It is used
    48  	// to find LocalAddr if necessary.
    49  	RemoteAddr string
    50  	// ListenAddr is the address we bind to, including a port.
    51  	ListenAddr string
    52  	// AdvertiseAddr is the address other nodes should connect to,
    53  	// including a port.
    54  	AdvertiseAddr string
    55  	// DataPathAddr is the address that has to be used for the data path
    56  	DataPathAddr string
    57  	// DefaultAddressPool contains list of subnets
    58  	DefaultAddressPool []string
    59  	// SubnetSize contains subnet size of DefaultAddressPool
    60  	SubnetSize uint32
    61  	// DataPathPort contains Data path port (VXLAN UDP port) number that is used for data traffic.
    62  	DataPathPort uint32
    63  	// JoinInProgress is set to true if a join operation has started, but
    64  	// not completed yet.
    65  	JoinInProgress bool
    66  
    67  	joinAddr        string
    68  	forceNewCluster bool
    69  	joinToken       string
    70  	lockKey         []byte
    71  	autolock        bool
    72  	availability    types.NodeAvailability
    73  }
    74  
    75  func (n *nodeRunner) Ready() chan error {
    76  	c := make(chan error, 1)
    77  	n.mu.RLock()
    78  	ready, done := n.ready, n.done
    79  	n.mu.RUnlock()
    80  	go func() {
    81  		select {
    82  		case <-ready:
    83  		case <-done:
    84  		}
    85  		select {
    86  		case <-ready:
    87  		default:
    88  			n.mu.RLock()
    89  			c <- n.err
    90  			n.mu.RUnlock()
    91  		}
    92  		close(c)
    93  	}()
    94  	return c
    95  }
    96  
    97  func (n *nodeRunner) Start(conf nodeStartConfig) error {
    98  	n.mu.Lock()
    99  	defer n.mu.Unlock()
   100  
   101  	n.reconnectDelay = initialReconnectDelay
   102  
   103  	return n.start(conf)
   104  }
   105  
   106  func (n *nodeRunner) start(conf nodeStartConfig) error {
   107  	var control string
   108  	if isWindows {
   109  		control = `\\.\pipe\` + controlSocket
   110  	} else {
   111  		control = filepath.Join(n.cluster.runtimeRoot, controlSocket)
   112  	}
   113  
   114  	joinAddr := conf.joinAddr
   115  	if joinAddr == "" && conf.JoinInProgress {
   116  		// We must have been restarted while trying to join a cluster.
   117  		// Continue trying to join instead of forming our own cluster.
   118  		joinAddr = conf.RemoteAddr
   119  	}
   120  
   121  	// Hostname is not set here. Instead, it is obtained from
   122  	// the node description that is reported periodically
   123  	swarmnodeConfig := swarmnode.Config{
   124  		ForceNewCluster:    conf.forceNewCluster,
   125  		ListenControlAPI:   control,
   126  		ListenRemoteAPI:    conf.ListenAddr,
   127  		AdvertiseRemoteAPI: conf.AdvertiseAddr,
   128  		NetworkConfig: &networkallocator.Config{
   129  			DefaultAddrPool: conf.DefaultAddressPool,
   130  			SubnetSize:      conf.SubnetSize,
   131  			VXLANUDPPort:    conf.DataPathPort,
   132  		},
   133  		JoinAddr:  joinAddr,
   134  		StateDir:  n.cluster.root,
   135  		JoinToken: conf.joinToken,
   136  		Executor: container.NewExecutor(
   137  			n.cluster.config.Backend,
   138  			n.cluster.config.PluginBackend,
   139  			n.cluster.config.ImageBackend,
   140  			n.cluster.config.VolumeBackend,
   141  		),
   142  		HeartbeatTick: n.cluster.config.RaftHeartbeatTick,
   143  		// Recommended value in etcd/raft is 10 x (HeartbeatTick).
   144  		// Lower values were seen to have caused instability because of
   145  		// frequent leader elections when running on flakey networks.
   146  		ElectionTick:     n.cluster.config.RaftElectionTick,
   147  		UnlockKey:        conf.lockKey,
   148  		AutoLockManagers: conf.autolock,
   149  		PluginGetter:     convert.SwarmPluginGetter(n.cluster.config.Backend.PluginGetter()),
   150  		NetworkProvider:  cnmallocator.NewProvider(n.cluster.config.Backend.PluginGetter()),
   151  	}
   152  	if conf.availability != "" {
   153  		avail, ok := swarmapi.NodeSpec_Availability_value[strings.ToUpper(string(conf.availability))]
   154  		if !ok {
   155  			return fmt.Errorf("invalid Availability: %q", conf.availability)
   156  		}
   157  		swarmnodeConfig.Availability = swarmapi.NodeSpec_Availability(avail)
   158  	}
   159  	node, err := swarmnode.New(&swarmnodeConfig)
   160  	if err != nil {
   161  		return err
   162  	}
   163  	if err := node.Start(context.Background()); err != nil {
   164  		return err
   165  	}
   166  
   167  	n.done = make(chan struct{})
   168  	n.ready = make(chan struct{})
   169  	n.swarmNode = node
   170  	if conf.joinAddr != "" {
   171  		conf.JoinInProgress = true
   172  	}
   173  	n.config = conf
   174  	savePersistentState(n.cluster.root, conf)
   175  
   176  	ctx, cancel := context.WithCancel(context.Background())
   177  
   178  	go func() {
   179  		n.handleNodeExit(node)
   180  		cancel()
   181  	}()
   182  
   183  	go n.handleReadyEvent(ctx, node, n.ready)
   184  	go n.handleControlSocketChange(ctx, node)
   185  
   186  	return nil
   187  }
   188  
   189  func (n *nodeRunner) handleControlSocketChange(ctx context.Context, node *swarmnode.Node) {
   190  	for conn := range node.ListenControlSocket(ctx) {
   191  		n.mu.Lock()
   192  		if n.grpcConn != conn {
   193  			if conn == nil {
   194  				n.controlClient = nil
   195  				n.logsClient = nil
   196  			} else {
   197  				n.controlClient = swarmapi.NewControlClient(conn)
   198  				n.logsClient = swarmapi.NewLogsClient(conn)
   199  				// push store changes to daemon
   200  				go n.watchClusterEvents(ctx, conn)
   201  			}
   202  		}
   203  		n.grpcConn = conn
   204  		n.mu.Unlock()
   205  		n.cluster.SendClusterEvent(lncluster.EventSocketChange)
   206  	}
   207  }
   208  
   209  func (n *nodeRunner) watchClusterEvents(ctx context.Context, conn *grpc.ClientConn) {
   210  	client := swarmapi.NewWatchClient(conn)
   211  	watch, err := client.Watch(ctx, &swarmapi.WatchRequest{
   212  		Entries: []*swarmapi.WatchRequest_WatchEntry{
   213  			{
   214  				Kind:   "node",
   215  				Action: swarmapi.WatchActionKindCreate | swarmapi.WatchActionKindUpdate | swarmapi.WatchActionKindRemove,
   216  			},
   217  			{
   218  				Kind:   "service",
   219  				Action: swarmapi.WatchActionKindCreate | swarmapi.WatchActionKindUpdate | swarmapi.WatchActionKindRemove,
   220  			},
   221  			{
   222  				Kind:   "network",
   223  				Action: swarmapi.WatchActionKindCreate | swarmapi.WatchActionKindUpdate | swarmapi.WatchActionKindRemove,
   224  			},
   225  			{
   226  				Kind:   "secret",
   227  				Action: swarmapi.WatchActionKindCreate | swarmapi.WatchActionKindUpdate | swarmapi.WatchActionKindRemove,
   228  			},
   229  			{
   230  				Kind:   "config",
   231  				Action: swarmapi.WatchActionKindCreate | swarmapi.WatchActionKindUpdate | swarmapi.WatchActionKindRemove,
   232  			},
   233  		},
   234  		IncludeOldObject: true,
   235  	})
   236  	if err != nil {
   237  		log.G(ctx).WithError(err).Error("failed to watch cluster store")
   238  		return
   239  	}
   240  	for {
   241  		msg, err := watch.Recv()
   242  		if err != nil {
   243  			// store watch is broken
   244  			errStatus, ok := status.FromError(err)
   245  			if !ok || errStatus.Code() != codes.Canceled {
   246  				log.G(ctx).WithError(err).Error("failed to receive changes from store watch API")
   247  			}
   248  			return
   249  		}
   250  		select {
   251  		case <-ctx.Done():
   252  			return
   253  		case n.cluster.watchStream <- msg:
   254  		}
   255  	}
   256  }
   257  
   258  func (n *nodeRunner) handleReadyEvent(ctx context.Context, node *swarmnode.Node, ready chan struct{}) {
   259  	select {
   260  	case <-node.Ready():
   261  		n.mu.Lock()
   262  		n.err = nil
   263  		if n.config.JoinInProgress {
   264  			n.config.JoinInProgress = false
   265  			savePersistentState(n.cluster.root, n.config)
   266  		}
   267  		n.mu.Unlock()
   268  		close(ready)
   269  	case <-ctx.Done():
   270  	}
   271  	n.cluster.SendClusterEvent(lncluster.EventNodeReady)
   272  }
   273  
   274  func (n *nodeRunner) handleNodeExit(node *swarmnode.Node) {
   275  	err := detectLockedError(node.Err(context.Background()))
   276  	if err != nil {
   277  		log.G(context.TODO()).Errorf("cluster exited with error: %v", err)
   278  	}
   279  	n.mu.Lock()
   280  	n.swarmNode = nil
   281  	n.err = err
   282  	close(n.done)
   283  	select {
   284  	case <-n.ready:
   285  		n.enableReconnectWatcher()
   286  	default:
   287  		if n.repeatedRun {
   288  			n.enableReconnectWatcher()
   289  		}
   290  	}
   291  	n.repeatedRun = true
   292  	n.mu.Unlock()
   293  }
   294  
   295  // Stop stops the current swarm node if it is running.
   296  func (n *nodeRunner) Stop() error {
   297  	n.mu.Lock()
   298  	if n.cancelReconnect != nil { // between restarts
   299  		n.cancelReconnect()
   300  		n.cancelReconnect = nil
   301  	}
   302  	if n.swarmNode == nil {
   303  		// even though the swarm node is nil we still may need
   304  		// to send a node leave event to perform any cleanup required.
   305  		if n.cluster != nil {
   306  			n.cluster.SendClusterEvent(lncluster.EventNodeLeave)
   307  		}
   308  		n.mu.Unlock()
   309  		return nil
   310  	}
   311  	n.stopping = true
   312  	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
   313  	defer cancel()
   314  	n.mu.Unlock()
   315  	if err := n.swarmNode.Stop(ctx); err != nil && !strings.Contains(err.Error(), "context canceled") {
   316  		return err
   317  	}
   318  	n.cluster.SendClusterEvent(lncluster.EventNodeLeave)
   319  	<-n.done
   320  	return nil
   321  }
   322  
   323  func (n *nodeRunner) State() nodeState {
   324  	if n == nil {
   325  		return nodeState{status: types.LocalNodeStateInactive}
   326  	}
   327  	n.mu.RLock()
   328  	defer n.mu.RUnlock()
   329  
   330  	ns := n.nodeState
   331  
   332  	if ns.err != nil || n.cancelReconnect != nil {
   333  		if errors.Is(ns.err, errSwarmLocked) {
   334  			ns.status = types.LocalNodeStateLocked
   335  		} else {
   336  			ns.status = types.LocalNodeStateError
   337  		}
   338  	} else {
   339  		select {
   340  		case <-n.ready:
   341  			ns.status = types.LocalNodeStateActive
   342  		default:
   343  			ns.status = types.LocalNodeStatePending
   344  		}
   345  	}
   346  
   347  	return ns
   348  }
   349  
   350  func (n *nodeRunner) enableReconnectWatcher() {
   351  	if n.stopping {
   352  		return
   353  	}
   354  	n.reconnectDelay *= 2
   355  	if n.reconnectDelay > maxReconnectDelay {
   356  		n.reconnectDelay = maxReconnectDelay
   357  	}
   358  	log.G(context.TODO()).Warnf("Restarting swarm in %.2f seconds", n.reconnectDelay.Seconds())
   359  	delayCtx, cancel := context.WithTimeout(context.Background(), n.reconnectDelay)
   360  	n.cancelReconnect = cancel
   361  
   362  	go func() {
   363  		<-delayCtx.Done()
   364  		if delayCtx.Err() != context.DeadlineExceeded {
   365  			return
   366  		}
   367  		n.mu.Lock()
   368  		defer n.mu.Unlock()
   369  		if n.stopping {
   370  			return
   371  		}
   372  
   373  		if err := n.start(n.config); err != nil {
   374  			n.err = err
   375  		}
   376  	}()
   377  }
   378  
   379  // nodeState represents information about the current state of the cluster and
   380  // provides access to the grpc clients.
   381  type nodeState struct {
   382  	swarmNode       *swarmnode.Node
   383  	grpcConn        *grpc.ClientConn
   384  	controlClient   swarmapi.ControlClient
   385  	logsClient      swarmapi.LogsClient
   386  	status          types.LocalNodeState
   387  	actualLocalAddr string
   388  	err             error
   389  }
   390  
   391  // IsActiveManager returns true if node is a manager ready to accept control requests. It is safe to access the client properties if this returns true.
   392  func (ns nodeState) IsActiveManager() bool {
   393  	return ns.controlClient != nil
   394  }
   395  
   396  // IsManager returns true if node is a manager.
   397  func (ns nodeState) IsManager() bool {
   398  	return ns.swarmNode != nil && ns.swarmNode.Manager() != nil
   399  }
   400  
   401  // NodeID returns node's ID or empty string if node is inactive.
   402  func (ns nodeState) NodeID() string {
   403  	if ns.swarmNode != nil {
   404  		return ns.swarmNode.NodeID()
   405  	}
   406  	return ""
   407  }