github.com/webmeshproj/webmesh-cni@v0.0.27/internal/controllers/peercontainer_controller.go (about) 1 /* 2 Copyright 2023 Avi Zimmerman <avi.zimmerman@gmail.com>. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package controllers 18 19 import ( 20 "context" 21 "fmt" 22 "net/netip" 23 "sync" 24 "time" 25 26 v1 "github.com/webmeshproj/api/go/v1" 27 "github.com/webmeshproj/storage-provider-k8s/provider" 28 "github.com/webmeshproj/webmesh/pkg/crypto" 29 "github.com/webmeshproj/webmesh/pkg/logging" 30 "github.com/webmeshproj/webmesh/pkg/meshnet" 31 "github.com/webmeshproj/webmesh/pkg/meshnet/endpoints" 32 netutil "github.com/webmeshproj/webmesh/pkg/meshnet/netutil" 33 meshtransport "github.com/webmeshproj/webmesh/pkg/meshnet/transport" 34 meshnode "github.com/webmeshproj/webmesh/pkg/meshnode" 35 meshdns "github.com/webmeshproj/webmesh/pkg/services/meshdns" 36 meshstorage "github.com/webmeshproj/webmesh/pkg/storage" 37 mesherrors "github.com/webmeshproj/webmesh/pkg/storage/errors" 38 meshtypes "github.com/webmeshproj/webmesh/pkg/storage/types" 39 ctrl "sigs.k8s.io/controller-runtime" 40 "sigs.k8s.io/controller-runtime/pkg/client" 41 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 42 "sigs.k8s.io/controller-runtime/pkg/log" 43 44 cniv1 "github.com/webmeshproj/webmesh-cni/api/v1" 45 "github.com/webmeshproj/webmesh-cni/internal/config" 46 "github.com/webmeshproj/webmesh-cni/internal/host" 47 ) 48 49 //+kubebuilder:rbac:groups=cni.webmesh.io,resources=peercontainers,verbs=get;list;watch;create;update;patch;delete 50 //+kubebuilder:rbac:groups=cni.webmesh.io,resources=peercontainers/status,verbs=get;update;patch 51 //+kubebuilder:rbac:groups=cni.webmesh.io,resources=peercontainers/finalizers,verbs=update 52 53 // PeerContainerReconciler reconciles a PeerContainer object. Reconcile 54 // attempts will fail until SetNetworkState is called. 55 type PeerContainerReconciler struct { 56 client.Client 57 config.Config 58 Provider *provider.Provider 59 Host host.Node 60 61 dns *meshdns.Server 62 containerNodes map[client.ObjectKey]meshnode.Node 63 mu sync.Mutex 64 } 65 66 // SetupWithManager sets up the controller with the Manager. 67 func (r *PeerContainerReconciler) SetupWithManager(mgr ctrl.Manager) (err error) { 68 // Create clients for IPAM locking 69 r.containerNodes = make(map[client.ObjectKey]meshnode.Node) 70 return ctrl.NewControllerManagedBy(mgr). 71 For(&cniv1.PeerContainer{}). 72 Complete(r) 73 } 74 75 // SetDNSServer sets the DNS server for the controller. 76 func (r *PeerContainerReconciler) SetDNSServer(dns *meshdns.Server) { 77 r.mu.Lock() 78 defer r.mu.Unlock() 79 r.dns = dns 80 } 81 82 // LookupPrivateKey looks up the private key for the given node ID. 83 func (r *PeerContainerReconciler) LookupPrivateKey(nodeID meshtypes.NodeID) (crypto.PrivateKey, bool) { 84 r.mu.Lock() 85 defer r.mu.Unlock() 86 if nodeID == r.Host.ID() || string(nodeID) == r.Host.Node().Key().ID() { 87 return r.Host.Node().Key(), true 88 } 89 for _, node := range r.containerNodes { 90 if node.ID() == nodeID { 91 return node.Key(), true 92 } 93 } 94 return nil, false 95 } 96 97 // Shutdown shuts down the controller and all running mesh nodes. 98 func (r *PeerContainerReconciler) Shutdown(ctx context.Context) { 99 r.mu.Lock() 100 defer r.mu.Unlock() 101 for id, node := range r.containerNodes { 102 log.FromContext(ctx).V(1).Info("Stopping mesh node for container", "container", id) 103 if err := node.Close(ctx); err != nil { 104 log.FromContext(ctx).Error(err, "Failed to stop mesh node for container") 105 } 106 delete(r.containerNodes, id) 107 } 108 } 109 110 // Reconcile reconciles a PeerContainer. 111 func (r *PeerContainerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 112 r.mu.Lock() 113 defer r.mu.Unlock() 114 log := log.FromContext(ctx) 115 if !r.Host.Started() { 116 log.Info("Controller is not ready yet, requeing reconcile request") 117 return ctrl.Result{ 118 Requeue: true, 119 RequeueAfter: 1 * time.Second, 120 }, nil 121 } 122 if r.Manager.ReconcileTimeout > 0 { 123 log.V(1).Info("Setting reconcile timeout", "timeout", r.Manager.ReconcileTimeout) 124 var cancel context.CancelFunc 125 ctx, cancel = context.WithTimeout(ctx, r.Manager.ReconcileTimeout) 126 defer cancel() 127 } 128 var container cniv1.PeerContainer 129 if err := r.Get(ctx, req.NamespacedName, &container); err != nil { 130 if client.IgnoreNotFound(err) == nil { 131 return ctrl.Result{}, nil 132 } 133 log.Error(err, "Failed to get container") 134 return ctrl.Result{}, err 135 } 136 if container.Spec.NodeName != r.Host.ID().String() { 137 // This container is not for this node, so we don't care about it. 138 log.V(1).Info("Ignoring container for another node") 139 return ctrl.Result{}, nil 140 } 141 // Always ensure the type meta is set 142 container.TypeMeta = cniv1.PeerContainerTypeMeta 143 if container.GetDeletionTimestamp() != nil { 144 // Stop the mesh node for this container. 145 log.Info("Tearing down mesh node for container") 146 return ctrl.Result{}, r.teardownPeerContainer(ctx, req, &container) 147 } 148 // Reconcile the mesh node for this container. 149 log.Info("Reconciling mesh node for container") 150 return ctrl.Result{}, r.reconcilePeerContainer(ctx, req, &container) 151 } 152 153 // NoOpStorageCloser wraps the storage provider with a no-op closer so 154 // that mesh nodes will not close the storage provider. 155 type NoOpStorageCloser struct { 156 meshstorage.Provider 157 } 158 159 // Close is a no-op. 160 func (n *NoOpStorageCloser) Close() error { 161 return nil 162 } 163 164 // reconcilePeerContainer reconciles the given PeerContainer. 165 func (r *PeerContainerReconciler) reconcilePeerContainer(ctx context.Context, req ctrl.Request, container *cniv1.PeerContainer) error { 166 log := log.FromContext(ctx) 167 168 // Make sure the finalizer is present first. 169 if !controllerutil.ContainsFinalizer(container, cniv1.PeerContainerFinalizer) { 170 updated := controllerutil.AddFinalizer(container, cniv1.PeerContainerFinalizer) 171 if updated { 172 log.V(1).Info("Adding finalizer to container") 173 if err := r.Update(ctx, container); err != nil { 174 return fmt.Errorf("failed to add finalizer: %w", err) 175 } 176 return nil 177 } 178 } 179 180 // Check if we have registered the node yet 181 node, ok := r.containerNodes[req.NamespacedName] 182 if !ok { 183 // We need to create the node. 184 nodeID := container.Spec.NodeID 185 log.Info("Webmesh node for container not found, we must need to create it") 186 log.V(1).Info("Creating new webmesh node with container spec", "spec", container.Spec) 187 key, err := crypto.GenerateKey() 188 if err != nil { 189 return fmt.Errorf("failed to generate key: %w", err) 190 } 191 var ipv4addr string 192 if !container.Spec.DisableIPv4 && container.Status.IPv4Address == "" { 193 // If the container does not have an IPv4 address and we are not disabling 194 // IPv4, use the default plugin to allocate one. 195 err = r.Host.IPAM().Locker().Acquire(ctx) 196 if err != nil { 197 return fmt.Errorf("failed to acquire IPAM lock: %w", err) 198 } 199 defer r.Host.IPAM().Locker().Release(ctx) 200 alloc, err := r.Host.IPAM().Allocate(ctx, meshtypes.NodeID(nodeID)) 201 if err != nil { 202 return fmt.Errorf("failed to allocate IPv4 address: %w", err) 203 } 204 ipv4addr = alloc.String() 205 } 206 // Go ahead and register a Peer for this node. 207 encoded, err := key.PublicKey().Encode() 208 if err != nil { 209 return fmt.Errorf("failed to encode public key: %w", err) 210 } 211 peer := meshtypes.MeshNode{ 212 MeshNode: &v1.MeshNode{ 213 Id: nodeID, 214 PublicKey: encoded, 215 ZoneAwarenessID: container.Spec.NodeName, 216 PrivateIPv4: ipv4addr, 217 PrivateIPv6: func() string { 218 if container.Spec.DisableIPv6 { 219 return "" 220 } 221 return netutil.AssignToPrefix(r.Host.Node().Network().NetworkV6(), key.PublicKey()).String() 222 }(), 223 }, 224 } 225 log.Info("Registering peer with meshdb", "peer", peer.MeshNode) 226 if err := r.Provider.MeshDB().Peers().Put(ctx, peer); err != nil { 227 return fmt.Errorf("failed to register peer: %w", err) 228 } 229 // Create the mesh node. 230 r.containerNodes[req.NamespacedName] = NewNode(logging.NewLogger(container.Spec.LogLevel, "json"), meshnode.Config{ 231 Key: key, 232 NodeID: nodeID, 233 ZoneAwarenessID: container.Spec.NodeName, 234 DisableIPv4: container.Spec.DisableIPv4, 235 DisableIPv6: container.Spec.DisableIPv6, 236 }) 237 // Update the status to created. 238 log.Info("Updating container interface status to created") 239 container.Status.InterfaceStatus = cniv1.InterfaceStatusCreated 240 if err := r.updateContainerStatus(ctx, container); err != nil { 241 return fmt.Errorf("failed to update status: %w", err) 242 } 243 return nil 244 } 245 246 log = log.WithValues("nodeID", node.ID()) 247 248 // If the node is not started, start it. 249 if !node.Started() { 250 log.Info("Starting webmesh node for container") 251 rtt := meshtransport.JoinRoundTripperFunc(func(ctx context.Context, _ *v1.JoinRequest) (*v1.JoinResponse, error) { 252 // Retrieve the peer we created earlier 253 peer, err := r.Provider.MeshDB().Peers().Get(ctx, node.ID()) 254 if err != nil { 255 return nil, fmt.Errorf("failed to get registered peer for container: %w", err) 256 } 257 // Compute the current topology for the container. 258 peers, err := meshnet.WireGuardPeersFor(ctx, r.Provider.MeshDB(), node.ID()) 259 if err != nil { 260 return nil, fmt.Errorf("failed to get peers for container: %w", err) 261 } 262 return &v1.JoinResponse{ 263 MeshDomain: r.Host.Node().Domain(), 264 // We always return both networks regardless of IP preferences. 265 NetworkIPv4: r.Host.Node().Network().NetworkV4().String(), 266 NetworkIPv6: r.Host.Node().Network().NetworkV6().String(), 267 // Addresses as allocated above. 268 AddressIPv4: peer.PrivateIPv4, 269 AddressIPv6: peer.PrivateIPv6, 270 Peers: peers, 271 }, nil 272 }) 273 err := node.Connect(ctx, meshnode.ConnectOptions{ 274 StorageProvider: &NoOpStorageCloser{r.Provider}, 275 MaxJoinRetries: 10, 276 JoinRoundTripper: rtt, 277 LeaveRoundTripper: meshtransport.LeaveRoundTripperFunc(func(ctx context.Context, req *v1.LeaveRequest) (*v1.LeaveResponse, error) { 278 // No-op, we clean up in the finalizers 279 return &v1.LeaveResponse{}, nil 280 }), 281 NetworkOptions: meshnet.Options{ 282 NetNs: container.Spec.Netns, 283 InterfaceName: container.Spec.IfName, 284 ForceReplace: true, 285 MTU: container.Spec.MTU, 286 ZoneAwarenessID: container.Spec.NodeName, 287 DisableIPv4: container.Spec.DisableIPv4, 288 DisableIPv6: container.Spec.DisableIPv6, 289 // Maybe by configuration? 290 RecordMetrics: false, 291 RecordMetricsInterval: 0, 292 }, 293 DirectPeers: func() map[meshtypes.NodeID]v1.ConnectProtocol { 294 peers := make(map[meshtypes.NodeID]v1.ConnectProtocol) 295 for _, n := range r.containerNodes { 296 if n.ID() == node.ID() { 297 continue 298 } 299 peers[n.ID()] = v1.ConnectProtocol_CONNECT_NATIVE 300 } 301 return peers 302 }(), 303 PreferIPv6: !container.Spec.DisableIPv6, 304 }) 305 if err != nil { 306 log.Error(err, "Failed to connect meshnode to network") 307 r.setFailedStatus(ctx, container, err) 308 // Create a new node on the next reconcile. 309 delete(r.containerNodes, req.NamespacedName) 310 return fmt.Errorf("failed to connect node: %w", err) 311 } 312 // Update the status to starting. 313 log.Info("Updating container interface status to starting") 314 container.Status.InterfaceStatus = cniv1.InterfaceStatusStarting 315 if err := r.updateContainerStatus(ctx, container); err != nil { 316 return fmt.Errorf("failed to update status: %w", err) 317 } 318 return nil 319 } 320 321 log.Info("Ensuring the container webmesh node is ready") 322 select { 323 case <-node.Ready(): 324 hwaddr, _ := node.Network().WireGuard().HardwareAddr() 325 log.Info("Webmesh node for container is running", 326 "interfaceName", node.Network().WireGuard().Name(), 327 "macAddress", hwaddr.String(), 328 "ipv4Address", validOrNone(node.Network().WireGuard().AddressV4()), 329 "ipv4Address", validOrNone(node.Network().WireGuard().AddressV6()), 330 "networkV4", validOrNone(node.Network().NetworkV4()), 331 "networkV6", validOrNone(node.Network().NetworkV6()), 332 ) 333 updated, err := r.ensureInterfaceReadyStatus(ctx, container, node) 334 if err != nil { 335 log.Error(err, "Failed to update container status") 336 return fmt.Errorf("failed to update container status: %w", err) 337 } 338 if updated { 339 // Return and continue on the next reconcile. 340 return nil 341 } 342 case <-ctx.Done(): 343 // Update the status to failed. 344 log.Error(ctx.Err(), "Timed out waiting for mesh node to start") 345 // Don't delete the node or set it to failed yet, maybe it'll be ready on the next reconcile. 346 return ctx.Err() 347 } 348 349 // Register the node to the storage provider. 350 wireguardPort, err := node.Network().WireGuard().ListenPort() 351 if err != nil { 352 // Something went terribly wrong, we need to recreate the node. 353 defer func() { 354 if err := node.Close(ctx); err != nil { 355 log.Error(err, "Failed to stop mesh node for container") 356 } 357 }() 358 delete(r.containerNodes, req.NamespacedName) 359 r.setFailedStatus(ctx, container, err) 360 return fmt.Errorf("failed to get wireguard port: %w", err) 361 } 362 encoded, err := node.Key().PublicKey().Encode() 363 if err != nil { 364 // Something went terribly wrong, we need to recreate the node. 365 defer func() { 366 if err := node.Close(ctx); err != nil { 367 log.Error(err, "Failed to stop mesh node for container") 368 } 369 }() 370 delete(r.containerNodes, req.NamespacedName) 371 r.setFailedStatus(ctx, container, err) 372 return fmt.Errorf("failed to encode public key: %w", err) 373 } 374 // Detect the current endpoints on the machine. 375 eps, err := endpoints.Detect(ctx, endpoints.DetectOpts{ 376 DetectPrivate: true, // Required for finding endpoints for other containers on the local node. 377 DetectIPv6: !container.Spec.DisableIPv6, 378 AllowRemoteDetection: r.Manager.RemoteEndpointDetection, 379 SkipInterfaces: func() []string { 380 var out []string 381 for _, n := range r.containerNodes { 382 if n.Started() { 383 out = append(out, n.Network().WireGuard().Name()) 384 } 385 } 386 return out 387 }(), 388 }) 389 if err != nil { 390 // Try again on the next reconcile. 391 return fmt.Errorf("failed to detect endpoints: %w", err) 392 } 393 var wgeps []string 394 for _, ep := range eps.AddrPorts(uint16(wireguardPort)) { 395 wgeps = append(wgeps, ep.String()) 396 } 397 // Register the peer's endpoints. 398 log.Info("Registering peer endpoints", 399 "wireguardPort", wireguardPort, 400 "primaryEndpoint", eps.FirstPublicAddr().String(), 401 "wireguardEndpoints", wgeps, 402 ) 403 err = r.Provider.MeshDB().Peers().Put(ctx, meshtypes.MeshNode{ 404 MeshNode: &v1.MeshNode{ 405 Id: node.ID().String(), 406 PublicKey: encoded, 407 WireguardEndpoints: wgeps, 408 ZoneAwarenessID: container.Spec.NodeName, 409 PrivateIPv4: validOrEmpty(node.Network().WireGuard().AddressV4()), 410 PrivateIPv6: validOrEmpty(node.Network().WireGuard().AddressV6()), 411 }, 412 }) 413 if err != nil { 414 // Try again on the next reconcile. 415 log.Error(err, "Failed to register peer") 416 return fmt.Errorf("failed to register peer: %w", err) 417 } 418 // Make sure all MeshEdges are up to date for this node. 419 log.Info("Forcing sync of peers and topology") 420 peers, err := r.Provider.MeshDB().Peers().List( 421 ctx, 422 meshstorage.FilterAgainstNode(node.ID()), 423 meshstorage.FilterByZoneID(container.Spec.NodeName), 424 ) 425 if err != nil { 426 // Try again on the next reconcile. 427 log.Error(err, "Failed to list peers") 428 return fmt.Errorf("failed to list peers: %w", err) 429 } 430 for _, peer := range peers { 431 if err := r.Provider.MeshDB().Peers().PutEdge(ctx, meshtypes.MeshEdge{MeshEdge: &v1.MeshEdge{ 432 Source: node.ID().String(), 433 Target: peer.NodeID().String(), 434 Weight: 50, 435 }}); err != nil { 436 // Try again on the next reconcile. 437 log.Error(err, "Failed to create edge", "targetNode", peer.NodeID()) 438 return fmt.Errorf("failed to create edge: %w", err) 439 } 440 } 441 // Force a sync of the node. 442 err = node.Network().Peers().Sync(ctx) 443 if err != nil { 444 log.Error(err, "Failed to sync peers") 445 // We don't return an error because the peer will eventually sync on its own. 446 } 447 return nil 448 } 449 450 // teardownPeerContainer tears down the given PeerContainer. 451 func (r *PeerContainerReconciler) teardownPeerContainer(ctx context.Context, req ctrl.Request, container *cniv1.PeerContainer) error { 452 log := log.FromContext(ctx) 453 node, ok := r.containerNodes[req.NamespacedName] 454 if !ok { 455 log.Info("Mesh node for container not found, we must have already deleted") 456 } else { 457 if err := node.Close(ctx); err != nil { 458 log.Error(err, "Failed to stop mesh node for container") 459 } 460 delete(r.containerNodes, req.NamespacedName) 461 } 462 // Make sure we've deleted the mesh peer from the database. 463 if err := r.Provider.MeshDB().Peers().Delete(ctx, meshtypes.NodeID(req.Name)); err != nil { 464 if !mesherrors.Is(err, mesherrors.ErrNodeNotFound) { 465 log.Error(err, "Failed to delete peer from meshdb") 466 return fmt.Errorf("failed to delete peer: %w", err) 467 } 468 } 469 if controllerutil.ContainsFinalizer(container, cniv1.PeerContainerFinalizer) { 470 updated := controllerutil.RemoveFinalizer(container, cniv1.PeerContainerFinalizer) 471 if updated { 472 log.Info("Removing finalizer from container") 473 if err := r.Update(ctx, container); err != nil { 474 return fmt.Errorf("failed to remove finalizer: %w", err) 475 } 476 } 477 } 478 return nil 479 } 480 481 func (r *PeerContainerReconciler) ensureInterfaceReadyStatus(ctx context.Context, container *cniv1.PeerContainer, node meshnode.Node) (updated bool, err error) { 482 log := log.FromContext(ctx) 483 // Update the status to running and sets its IP address. 484 var updateStatus bool 485 origStatus := container.Status 486 addrV4 := validOrEmpty(node.Network().WireGuard().AddressV4()) 487 addrV6 := validOrEmpty(node.Network().WireGuard().AddressV6()) 488 netv4 := validOrEmpty(node.Network().NetworkV4()) 489 netv6 := validOrEmpty(node.Network().NetworkV6()) 490 if container.Status.InterfaceStatus != cniv1.InterfaceStatusRunning { 491 // Update the status to running and sets its IP address. 492 container.Status.InterfaceStatus = cniv1.InterfaceStatusRunning 493 updateStatus = true 494 } 495 hwaddr, _ := node.Network().WireGuard().HardwareAddr() 496 if container.Status.MACAddress != hwaddr.String() { 497 container.Status.MACAddress = hwaddr.String() 498 updateStatus = true 499 } 500 if r.dns != nil { 501 // Add ourself as a DNS server for the container. 502 var addr netip.Addr 503 if container.Spec.DisableIPv4 && r.Host.Node().Network().WireGuard().AddressV6().IsValid() { 504 addr = r.Host.Node().Network().WireGuard().AddressV6().Addr() 505 } else if r.Host.Node().Network().WireGuard().AddressV4().IsValid() { 506 // Prefer IPv4 if it's available. 507 addr = r.Host.Node().Network().WireGuard().AddressV4().Addr() 508 } 509 if addr.IsValid() { 510 addrport := netip.AddrPortFrom(addr, uint16(r.dns.ListenPort())) 511 if len(container.Status.DNSServers) == 0 || container.Status.DNSServers[0] != addrport.String() { 512 container.Status.DNSServers = []string{addrport.String()} 513 updateStatus = true 514 } 515 } 516 } 517 if container.Status.IPv4Address != addrV4 { 518 container.Status.IPv4Address = addrV4 519 updateStatus = true 520 } 521 if container.Status.IPv6Address != addrV6 { 522 container.Status.IPv6Address = addrV6 523 updateStatus = true 524 } 525 if container.Status.NetworkV4 != netv4 { 526 container.Status.NetworkV4 = netv4 527 updateStatus = true 528 } 529 if container.Status.NetworkV6 != netv6 { 530 container.Status.NetworkV6 = netv6 531 updateStatus = true 532 } 533 if container.Status.InterfaceName != node.Network().WireGuard().Name() { 534 container.Status.InterfaceName = node.Network().WireGuard().Name() 535 updateStatus = true 536 } 537 if container.Status.Error != "" { 538 container.Status.Error = "" 539 updateStatus = true 540 } 541 if updateStatus { 542 log.Info("Updating container interface status", 543 "newStatus", container.Status, 544 "oldStatus", origStatus, 545 ) 546 return true, r.updateContainerStatus(ctx, container) 547 } 548 return false, nil 549 } 550 551 func (r *PeerContainerReconciler) setFailedStatus(ctx context.Context, container *cniv1.PeerContainer, reason error) { 552 container.Status.InterfaceStatus = cniv1.InterfaceStatusFailed 553 container.Status.Error = reason.Error() 554 err := r.updateContainerStatus(ctx, container) 555 if err != nil { 556 log.FromContext(ctx).Error(err, "Failed to update container status") 557 } 558 } 559 560 func (r *PeerContainerReconciler) updateContainerStatus(ctx context.Context, container *cniv1.PeerContainer) error { 561 container.SetManagedFields(nil) 562 err := r.Status().Patch(ctx, 563 container, 564 client.Apply, 565 client.ForceOwnership, 566 client.FieldOwner(cniv1.FieldOwner), 567 ) 568 if err != nil { 569 return fmt.Errorf("failed to update status: %w", err) 570 } 571 return nil 572 }