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 }