diff --git a/.golangci.yml b/.golangci.yml index 15f09f43..15489c5a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -31,6 +31,7 @@ linters: - ginkgolinter - gocheckcompilerdirectives - gochecksumtype + - gocognit - goconst - goprintffuncname - gosec @@ -134,6 +135,7 @@ linters: - copyloopvar - dupl - errcheck + - gocognit - gocyclo - gosec - maintidx diff --git a/cloud/linode/loadbalancers.go b/cloud/linode/loadbalancers.go index 3a1fc50e..05dfb251 100644 --- a/cloud/linode/loadbalancers.go +++ b/cloud/linode/loadbalancers.go @@ -326,47 +326,170 @@ func (l *loadbalancers) createIPChangeWarningEvent(ctx context.Context, service } } -func (l *loadbalancers) updateNodeBalancer( - ctx context.Context, - clusterName string, - service *v1.Service, - nodes []*v1.Node, - nb *linodego.NodeBalancer, -) (err error) { - if len(nodes) == 0 { - return fmt.Errorf("%w: service %s", errNoNodesAvailable, getServiceNn(service)) +// warnIfIPv4AnnotationChanged logs and emits a k8s event if the service's requested IPv4 +// annotation differs from the NodeBalancer's actual IP, since the IP cannot be changed +// after creation. +func (l *loadbalancers) warnIfIPv4AnnotationChanged(ctx context.Context, service *v1.Service, nb *linodego.NodeBalancer) { + ipv4, ok := service.GetAnnotations()[annotations.AnnLinodeLoadBalancerReservedIPv4] + if !ok || ipv4 == *nb.IPv4 { + return } - // Check for IPv4 annotation change - if ipv4, ok := service.GetAnnotations()[annotations.AnnLinodeLoadBalancerReservedIPv4]; ok && ipv4 != *nb.IPv4 { - // Log the error in the CCM's logfile - klog.Warningf("IPv4 annotation has changed for service (%s) from %s to %s, but NodeBalancer (%d) IP cannot be updated after creation", - getServiceNn(service), *nb.IPv4, ipv4, nb.ID) + // Log the error in the CCM's logfile + klog.Warningf("IPv4 annotation has changed for service (%s) from %s to %s, but NodeBalancer (%d) IP cannot be updated after creation", + getServiceNn(service), *nb.IPv4, ipv4, nb.ID) - // Issue a k8s cluster event warning - l.createIPChangeWarningEvent(ctx, service, nb, ipv4) - } + // Issue a k8s cluster event warning + l.createIPChangeWarningEvent(ctx, service, nb, ipv4) +} +// updateNodeBalancerThrottleAndTags updates the NodeBalancer's client connection throttle and +// tags if they differ from the desired state, returning the (possibly refreshed) NodeBalancer. +func (l *loadbalancers) updateNodeBalancerThrottleAndTags(ctx context.Context, clusterName string, service *v1.Service, nb *linodego.NodeBalancer) (*linodego.NodeBalancer, []string, error) { connThrottle := getConnectionThrottle(service) if connThrottle != nb.ClientConnThrottle { update := nb.GetUpdateOptions() update.ClientConnThrottle = &connThrottle - nb, err = l.client.UpdateNodeBalancer(ctx, nb.ID, update) + updated, err := l.client.UpdateNodeBalancer(ctx, nb.ID, update) if err != nil { sentry.CaptureError(ctx, err) - return err + return nb, nil, err } + nb = updated } tags := l.GetLoadBalancerTags(ctx, clusterName, service) if !reflect.DeepEqual(nb.Tags, tags) { update := nb.GetUpdateOptions() update.Tags = tags - nb, err = l.client.UpdateNodeBalancer(ctx, nb.ID, update) + updated, err := l.client.UpdateNodeBalancer(ctx, nb.ID, update) if err != nil { sentry.CaptureError(ctx, err) - return err + return nb, tags, err + } + nb = updated + } + + return nb, tags, nil +} + +// findNodeBalancerConfigForPort returns a pointer to the existing NodeBalancer config matching +// the given port, or nil if none exists. +func findNodeBalancerConfigForPort(nbCfgs []linodego.NodeBalancerConfig, port int) *linodego.NodeBalancerConfig { + for i := range nbCfgs { + if nbCfgs[i].Port == port { + return &nbCfgs[i] + } + } + return nil +} + +// getOldNodeBalancerNodeIDs looks up the existing nodes attached to a NodeBalancer config, and +// returns a map of node address to node ID so that IDs can be reused on rebuild. +func (l *loadbalancers) getOldNodeBalancerNodeIDs(ctx context.Context, nb *linodego.NodeBalancer, currentNBCfg *linodego.NodeBalancerConfig) map[string]int { + oldNBNodeIDs := make(map[string]int) + if currentNBCfg == nil { + return oldNBNodeIDs + } + + // Obtain list of current NB nodes and convert it to map of node IDs + currentNBNodes, err := l.client.ListNodeBalancerNodes(ctx, nb.ID, currentNBCfg.ID, nil) + if err != nil { + // This error can be ignored, because if we fail to get nodes we can anyway rebuild the config from scratch, + // it would just cause the NB to reload config even if the node list did not change, so we prefer to send IDs when it is possible. + klog.Warningf("Unable to list existing nodebalancer nodes for NB %d config %d, error: %s", nb.ID, currentNBCfg.ID, err) + } + for _, node := range currentNBNodes { + oldNBNodeIDs[node.Address] = node.ID + } + klog.Infof("Nodebalancer %d had nodes %v", nb.ID, oldNBNodeIDs) + + return oldNBNodeIDs +} + +// updateNodeBalancerConfigForPort creates or rebuilds the NodeBalancer config for a single +// Service port, attaching the given nodes as backends. +func (l *loadbalancers) updateNodeBalancerConfigForPort( + ctx context.Context, + service *v1.Service, + nodes []*v1.Node, + nb *linodego.NodeBalancer, + nbCfgs []linodego.NodeBalancerConfig, + port v1.ServicePort, +) error { + // Construct a new config for this port + newNBCfg, err := l.buildNodeBalancerConfig(ctx, service, port) + if err != nil { + sentry.CaptureError(ctx, err) + return err + } + + // Look for an existing config for this port + currentNBCfg := findNodeBalancerConfigForPort(nbCfgs, int(port.Port)) + if currentNBCfg == nil { + klog.Infof("No preexisting nodebalancer for port %v found.", port.Port) + } + oldNBNodeIDs := l.getOldNodeBalancerNodeIDs(ctx, nb, currentNBCfg) + + useIPv6Backends := resolveIPv6NodeBalancerBackendState(service) + // Add all of the Nodes to the config + subnetID, err := l.getBackendSubnetID(ctx, service, useIPv6Backends) + if err != nil { + sentry.CaptureError(ctx, err) + return fmt.Errorf("Error getting subnet ID for service %s: %w", service.Name, err) + } + newNBNodes, err := l.buildNodeBalancerConfigNodes(service, nodes, port.NodePort, subnetID, useIPv6Backends, newNBCfg.Protocol, oldNBNodeIDs) + if err != nil { + sentry.CaptureError(ctx, err) + return fmt.Errorf("[port %d] error building NodeBalancer backend node configs: %w", int(port.Port), err) + } + + // If there's no existing config, create it + var rebuildOpts linodego.NodeBalancerConfigRebuildOptions + if currentNBCfg == nil { + createOpts := newNBCfg.GetCreateOptions() + + currentNBCfg, err = l.client.CreateNodeBalancerConfig(ctx, nb.ID, createOpts) + if err != nil { + sentry.CaptureError(ctx, err) + return fmt.Errorf("[port %d] error creating NodeBalancer config: %w", int(port.Port), err) } + rebuildOpts = currentNBCfg.GetRebuildOptions() + + // SSLCert and SSLKey return from the API, so copy the + // value that we sent in create for the rebuild + rebuildOpts.SSLCert = newNBCfg.SSLCert + rebuildOpts.SSLKey = newNBCfg.SSLKey + } else { + rebuildOpts = newNBCfg.GetRebuildOptions() + } + + rebuildOpts.Nodes = newNBNodes + + if _, err = l.client.RebuildNodeBalancerConfig(ctx, nb.ID, currentNBCfg.ID, rebuildOpts); err != nil { + sentry.CaptureError(ctx, err) + return fmt.Errorf("[port %d] error rebuilding NodeBalancer config: %w", int(port.Port), err) + } + + return nil +} + +func (l *loadbalancers) updateNodeBalancer( + ctx context.Context, + clusterName string, + service *v1.Service, + nodes []*v1.Node, + nb *linodego.NodeBalancer, +) (err error) { + if len(nodes) == 0 { + return fmt.Errorf("%w: service %s", errNoNodesAvailable, getServiceNn(service)) + } + + l.warnIfIPv4AnnotationChanged(ctx, service, nb) + + nb, tags, err := l.updateNodeBalancerThrottleAndTags(ctx, clusterName, service, nb) + if err != nil { + return err } fwClient := services.LinodeClient{Client: l.client} @@ -390,79 +513,9 @@ func (l *loadbalancers) updateNodeBalancer( // Add or overwrite configs for each of the Service's ports for _, port := range service.Spec.Ports { - // Construct a new config for this port - newNBCfg, err := l.buildNodeBalancerConfig(ctx, service, port) - if err != nil { - sentry.CaptureError(ctx, err) + if err := l.updateNodeBalancerConfigForPort(ctx, service, nodes, nb, nbCfgs, port); err != nil { return err } - - // Look for an existing config for this port - var currentNBCfg *linodego.NodeBalancerConfig - for i := range nbCfgs { - nbc := nbCfgs[i] - if nbc.Port == int(port.Port) { - currentNBCfg = &nbc - break - } - } - oldNBNodeIDs := make(map[string]int) - var currentNBNodes []linodego.NodeBalancerNode - if currentNBCfg != nil { - // Obtain list of current NB nodes and convert it to map of node IDs - currentNBNodes, err = l.client.ListNodeBalancerNodes(ctx, nb.ID, currentNBCfg.ID, nil) - if err != nil { - // This error can be ignored, because if we fail to get nodes we can anyway rebuild the config from scratch, - // it would just cause the NB to reload config even if the node list did not change, so we prefer to send IDs when it is possible. - klog.Warningf("Unable to list existing nodebalancer nodes for NB %d config %d, error: %s", nb.ID, newNBCfg.ID, err) - } - for _, node := range currentNBNodes { - oldNBNodeIDs[node.Address] = node.ID - } - klog.Infof("Nodebalancer %d had nodes %v", nb.ID, oldNBNodeIDs) - } else { - klog.Infof("No preexisting nodebalancer for port %v found.", port.Port) - } - - useIPv6Backends := resolveIPv6NodeBalancerBackendState(service) - // Add all of the Nodes to the config - subnetID, err := l.getBackendSubnetID(ctx, service, useIPv6Backends) - if err != nil { - sentry.CaptureError(ctx, err) - return fmt.Errorf("Error getting subnet ID for service %s: %w", service.Name, err) - } - newNBNodes, err := l.buildNodeBalancerConfigNodes(service, nodes, port.NodePort, subnetID, useIPv6Backends, newNBCfg.Protocol, oldNBNodeIDs) - if err != nil { - sentry.CaptureError(ctx, err) - return fmt.Errorf("[port %d] error building NodeBalancer backend node configs: %w", int(port.Port), err) - } - - // If there's no existing config, create it - var rebuildOpts linodego.NodeBalancerConfigRebuildOptions - if currentNBCfg == nil { - createOpts := newNBCfg.GetCreateOptions() - - currentNBCfg, err = l.client.CreateNodeBalancerConfig(ctx, nb.ID, createOpts) - if err != nil { - sentry.CaptureError(ctx, err) - return fmt.Errorf("[port %d] error creating NodeBalancer config: %w", int(port.Port), err) - } - rebuildOpts = currentNBCfg.GetRebuildOptions() - - // SSLCert and SSLKey return from the API, so copy the - // value that we sent in create for the rebuild - rebuildOpts.SSLCert = newNBCfg.SSLCert - rebuildOpts.SSLKey = newNBCfg.SSLKey - } else { - rebuildOpts = newNBCfg.GetRebuildOptions() - } - - rebuildOpts.Nodes = newNBNodes - - if _, err = l.client.RebuildNodeBalancerConfig(ctx, nb.ID, currentNBCfg.ID, rebuildOpts); err != nil { - sentry.CaptureError(ctx, err) - return fmt.Errorf("[port %d] error rebuilding NodeBalancer config: %w", int(port.Port), err) - } } return nil diff --git a/cloud/linode/services/instances.go b/cloud/linode/services/instances.go index ae36816a..3a0bcf01 100644 --- a/cloud/linode/services/instances.go +++ b/cloud/linode/services/instances.go @@ -78,66 +78,100 @@ func (nc *nodeCache) getInstanceAddresses(instance linodego.Instance, vpcips []s return ips } -// refreshInstances conditionally loads all instances from the Linode API and caches them. -// It does not refresh if the last update happened less than `nodeCache.ttl` ago. -func (nc *nodeCache) refreshInstances(ctx context.Context, client linodeClient.Client) error { - nc.Lock() - defer nc.Unlock() - - if time.Since(nc.lastUpdate) < nc.ttl { - return nil - } - - filter := linodego.Filter{} - if options.Options.LinodeTagFilter != "" { - filter.AddField(linodego.Contains, "tags", options.Options.LinodeTagFilter) - } - filterJSON, err := filter.MarshalJSON() +// addVPCIPv4Addresses looks up the IPv4 addresses for a VPC and appends them to vpcNodes, keyed +// by Linode instance ID. +func addVPCIPv4Addresses(ctx context.Context, client linodeClient.Client, vpcName string, vpcNodes map[int][]string) error { + resp, err := GetVPCIPAddresses(ctx, client, vpcName) if err != nil { - return fmt.Errorf("failed to marshal filter: %w", err) + return fmt.Errorf("failed updating instances cache for VPC %s: %w", vpcName, err) + } + for _, vpcip := range resp { + if vpcip.Address == nil { + continue + } + vpcNodes[vpcip.LinodeID] = append(vpcNodes[vpcip.LinodeID], *vpcip.Address) } + return nil +} - instances, err := client.ListInstances(ctx, &linodego.ListOptions{PageSize: linodeClient.MaxPageSize, Filter: string(filterJSON)}) +// addVPCIPv6Addresses looks up the IPv6 addresses for a VPC and appends them to vpcNodes, also +// recording the address type (internal/external) for each address in vpcIPv6AddrTypes. +func addVPCIPv6Addresses(ctx context.Context, client linodeClient.Client, vpcName string, vpcNodes map[int][]string, vpcIPv6AddrTypes map[string]v1.NodeAddressType) error { + resp, err := GetVPCIPv6Addresses(ctx, client, vpcName) if err != nil { - return err + return fmt.Errorf("failed updating instances cache for VPC %s: %w", vpcName, err) + } + for _, vpcip := range resp { + if len(vpcip.IPv6Addresses) == 0 { + continue + } + vpcIPv6AddrType := v1.NodeInternalIP + if vpcip.IPv6IsPublic != nil && *vpcip.IPv6IsPublic { + vpcIPv6AddrType = v1.NodeExternalIP + } + for _, ipv6 := range vpcip.IPv6Addresses { + vpcNodes[vpcip.LinodeID] = append(vpcNodes[vpcip.LinodeID], ipv6.SLAACAddress) + vpcIPv6AddrTypes[ipv6.SLAACAddress] = vpcIPv6AddrType + } } + return nil +} - // If running within VPC, find instances and store their ips +// getVPCNodeIPs returns, for every configured VPC, a map of Linode instance ID to VPC IP +// addresses (IPv4 and IPv6), along with the address type of each VPC IPv6 address. +func getVPCNodeIPs(ctx context.Context, client linodeClient.Client) (map[int][]string, map[string]v1.NodeAddressType, error) { vpcNodes := map[int][]string{} vpcIPv6AddrTypes := map[string]v1.NodeAddressType{} + for _, name := range options.Options.VPCNames { vpcName := strings.TrimSpace(name) if vpcName == "" { continue } - resp, err := GetVPCIPAddresses(ctx, client, vpcName) - if err != nil { - return fmt.Errorf("failed updating instances cache for VPC %s: %w", vpcName, err) + if err := addVPCIPv4Addresses(ctx, client, vpcName, vpcNodes); err != nil { + return nil, nil, err } - for _, vpcip := range resp { - if vpcip.Address == nil { - continue - } - vpcNodes[vpcip.LinodeID] = append(vpcNodes[vpcip.LinodeID], *vpcip.Address) + if err := addVPCIPv6Addresses(ctx, client, vpcName, vpcNodes, vpcIPv6AddrTypes); err != nil { + return nil, nil, err } + } - resp, err = GetVPCIPv6Addresses(ctx, client, vpcName) - if err != nil { - return fmt.Errorf("failed updating instances cache for VPC %s: %w", vpcName, err) - } - for _, vpcip := range resp { - if len(vpcip.IPv6Addresses) == 0 { - continue - } - vpcIPv6AddrType := v1.NodeInternalIP - if vpcip.IPv6IsPublic != nil && *vpcip.IPv6IsPublic { - vpcIPv6AddrType = v1.NodeExternalIP - } - for _, ipv6 := range vpcip.IPv6Addresses { - vpcNodes[vpcip.LinodeID] = append(vpcNodes[vpcip.LinodeID], ipv6.SLAACAddress) - vpcIPv6AddrTypes[ipv6.SLAACAddress] = vpcIPv6AddrType - } - } + return vpcNodes, vpcIPv6AddrTypes, nil +} + +// listFilteredInstances lists all Linode instances, applying the configured tag filter if set. +func listFilteredInstances(ctx context.Context, client linodeClient.Client) ([]linodego.Instance, error) { + filter := linodego.Filter{} + if options.Options.LinodeTagFilter != "" { + filter.AddField(linodego.Contains, "tags", options.Options.LinodeTagFilter) + } + filterJSON, err := filter.MarshalJSON() + if err != nil { + return nil, fmt.Errorf("failed to marshal filter: %w", err) + } + + return client.ListInstances(ctx, &linodego.ListOptions{PageSize: linodeClient.MaxPageSize, Filter: string(filterJSON)}) +} + +// refreshInstances conditionally loads all instances from the Linode API and caches them. +// It does not refresh if the last update happened less than `nodeCache.ttl` ago. +func (nc *nodeCache) refreshInstances(ctx context.Context, client linodeClient.Client) error { + nc.Lock() + defer nc.Unlock() + + if time.Since(nc.lastUpdate) < nc.ttl { + return nil + } + + instances, err := listFilteredInstances(ctx, client) + if err != nil { + return err + } + + // If running within VPC, find instances and store their ips + vpcNodes, vpcIPv6AddrTypes, err := getVPCNodeIPs(ctx, client) + if err != nil { + return err } newNodes := make(map[int]linodeInstance, len(instances)) diff --git a/cloud/nodeipam/ipam/cloud_allocator.go b/cloud/nodeipam/ipam/cloud_allocator.go index 15138806..e2494ef3 100644 --- a/cloud/nodeipam/ipam/cloud_allocator.go +++ b/cloud/nodeipam/ipam/cloud_allocator.go @@ -82,6 +82,89 @@ const ( var _ CIDRAllocator = &cloudAllocator{} +// reserveFinalIPv4BlockIfNeeded checks whether the final block in the cluster CIDR must be +// reserved (because it collides with the VPC subnet's reserved last IP), and if so occupies it +// in the given cidrSet. +func reserveFinalIPv4BlockIfNeeded(ctx context.Context, linodeClient linode.Client, clusterCIDR *net.IPNet, cidrSet *cidrset.CidrSet) error { + reserveFinalIPv4Block, err := shouldReserveFinalIPv4Block(ctx, linodeClient, clusterCIDR) + if err != nil { + return err + } + if !reserveFinalIPv4Block { + return nil + } + + // Reserve the last block in the cluster range by occupying its last IP. + lastIP, err := lastIPForCIDR(clusterCIDR) + if err != nil { + return err + } + return cidrSet.Occupy(&net.IPNet{IP: lastIP.To4(), Mask: net.CIDRMask(32, 32)}) +} + +// occupyExistingNodeCIDRs marks the CIDRs of any pre-existing nodes as occupied in the +// allocator's CIDR maps, so that they are not handed out again. +func (ca *cloudAllocator) occupyExistingNodeCIDRs(ctx context.Context, logger klog.Logger, nodeList *v1.NodeList) error { + if nodeList == nil { + return nil + } + + for _, node := range nodeList.Items { + if len(node.Spec.PodCIDRs) == 0 { + logger.V(4).Info("Node has no CIDR, ignoring", "node", klog.KObj(&node)) + continue + } + logger.V(4).Info("Node has CIDR, occupying it in CIDR map", "node", klog.KObj(&node), "podCIDR", node.Spec.PodCIDR) + if err := ca.occupyCIDRs(ctx, &node); err != nil { + // This will happen if: + // 1. We find garbage in the podCIDRs field. Retrying is useless. + // 2. CIDR out of range: This means a node CIDR has changed. + // This error will keep crashing controller-manager. + return err + } + } + return nil +} + +// nodeEventHandlerFuncs builds the informer event handler that queues nodes for CIDR +// allocation/release on add, update, and delete events. +func (ca *cloudAllocator) nodeEventHandlerFuncs(logger klog.Logger) cache.ResourceEventHandlerFuncs { + return cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + key, err := cache.MetaNamespaceKeyFunc(obj) + if err == nil { + ca.queue.Add(key) + } + }, + UpdateFunc: func(oldObj, newObj interface{}) { + key, err := cache.MetaNamespaceKeyFunc(newObj) + if err == nil { + ca.queue.Add(key) + } + }, + DeleteFunc: func(obj interface{}) { + // The informer cache no longer has the object, and since Node doesn't have a finalizer, + // we don't see the Update with DeletionTimestamp != 0. + node, ok := obj.(*v1.Node) + if !ok { + tombstone, ok := obj.(cache.DeletedFinalStateUnknown) + if !ok { + utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", obj)) + return + } + node, ok = tombstone.Obj.(*v1.Node) + if !ok { + utilruntime.HandleError(fmt.Errorf("unexpected object types: %v", obj)) + return + } + } + if err := ca.ReleaseCIDR(logger, node); err != nil { + utilruntime.HandleError(fmt.Errorf("error while processing CIDR Release: %w", err)) + } + }, + } +} + // NewLinodeCIDRAllocator returns a CIDRAllocator to allocate CIDRs for node // Caller must ensure subNetMaskSize is not less than cluster CIDR mask size. // Caller must always pass in a list of existing nodes so the new allocator. @@ -106,20 +189,9 @@ func NewLinodeCIDRAllocator(ctx context.Context, linodeClient linode.Client, cli // Using Linode API, check if we need to reserve the final block in the cluster CIDR. // Reserve when cluster CIDR last IP is the same as the VPC subnet last IP. // We cannot reserve that block since the last IP is a reserved IP for VPC functionality. - reserveFinalIPv4Block, err := shouldReserveFinalIPv4Block(ctx, linodeClient, allocatorParams.ClusterCIDRs[0]) - if err != nil { + if err := reserveFinalIPv4BlockIfNeeded(ctx, linodeClient, allocatorParams.ClusterCIDRs[0], cidrSet); err != nil { return nil, err } - if reserveFinalIPv4Block { - // Reserve the last block in the cluster range by occupying its last IP. - lastIP, err := lastIPForCIDR(allocatorParams.ClusterCIDRs[0]) - if err != nil { - return nil, err - } - if err := cidrSet.Occupy(&net.IPNet{IP: lastIP.To4(), Mask: net.CIDRMask(32, 32)}); err != nil { - return nil, err - } - } ca := &cloudAllocator{ client: client, @@ -147,57 +219,11 @@ func NewLinodeCIDRAllocator(ctx context.Context, linodeClient linode.Client, cli logger.Info("No Secondary Service CIDR provided. Skipping filtering out secondary service addresses") } - if nodeList != nil { - for _, node := range nodeList.Items { - if len(node.Spec.PodCIDRs) == 0 { - logger.V(4).Info("Node has no CIDR, ignoring", "node", klog.KObj(&node)) - continue - } - logger.V(4).Info("Node has CIDR, occupying it in CIDR map", "node", klog.KObj(&node), "podCIDR", node.Spec.PodCIDR) - if err := ca.occupyCIDRs(ctx, &node); err != nil { - // This will happen if: - // 1. We find garbage in the podCIDRs field. Retrying is useless. - // 2. CIDR out of range: This means a node CIDR has changed. - // This error will keep crashing controller-manager. - return nil, err - } - } + if err := ca.occupyExistingNodeCIDRs(ctx, logger, nodeList); err != nil { + return nil, err } - if _, err := nodeInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - key, err := cache.MetaNamespaceKeyFunc(obj) - if err == nil { - ca.queue.Add(key) - } - }, - UpdateFunc: func(oldObj, newObj interface{}) { - key, err := cache.MetaNamespaceKeyFunc(newObj) - if err == nil { - ca.queue.Add(key) - } - }, - DeleteFunc: func(obj interface{}) { - // The informer cache no longer has the object, and since Node doesn't have a finalizer, - // we don't see the Update with DeletionTimestamp != 0. - node, ok := obj.(*v1.Node) - if !ok { - tombstone, ok := obj.(cache.DeletedFinalStateUnknown) - if !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", obj)) - return - } - node, ok = tombstone.Obj.(*v1.Node) - if !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object types: %v", obj)) - return - } - } - if err := ca.ReleaseCIDR(logger, node); err != nil { - utilruntime.HandleError(fmt.Errorf("error while processing CIDR Release: %w", err)) - } - }, - }); err != nil { + if _, err := nodeInformer.Informer().AddEventHandler(ca.nodeEventHandlerFuncs(logger)); err != nil { logger.Error(err, "Failed to add event handler to node informer") return nil, err } @@ -449,71 +475,69 @@ func getIPv6PodCIDR(ip net.IP, desiredMask int) (*net.IPNet, bool) { // It then creates a new net.IPNet with the IPv6 address and mask size defined // by nodeCIDRMaskSizeIPv6. The function returns an error if it fails to retrieve // the instance configuration or parse the IPv6 range. -func (c *cloudAllocator) allocateIPv6CIDR(ctx context.Context, node *v1.Node) (*net.IPNet, error) { - logger := klog.FromContext(ctx) - +// parseLinodeIDFromProviderID extracts the numeric Linode instance ID from a node's ProviderID +// (expected format "linode://"). +func parseLinodeIDFromProviderID(node *v1.Node) (int, error) { if node.Spec.ProviderID == "" { - return nil, fmt.Errorf("node %s has no ProviderID set, cannot calculate ipv6 range for it", node.Name) + return 0, fmt.Errorf("node %s has no ProviderID set, cannot calculate ipv6 range for it", node.Name) } - // Extract the Linode ID from the ProviderID if !strings.HasPrefix(node.Spec.ProviderID, providerIDPrefix) { - return nil, fmt.Errorf("node %s has invalid ProviderID %s, expected prefix '%s'", node.Name, node.Spec.ProviderID, providerIDPrefix) + return 0, fmt.Errorf("node %s has invalid ProviderID %s, expected prefix '%s'", node.Name, node.Spec.ProviderID, providerIDPrefix) } - // Parse the Linode ID from the ProviderID id, err := strconv.Atoi(strings.TrimPrefix(node.Spec.ProviderID, providerIDPrefix)) if err != nil { - return nil, fmt.Errorf("failed to parse Linode ID from ProviderID %s: %w", node.Spec.ProviderID, err) + return 0, fmt.Errorf("failed to parse Linode ID from ProviderID %s: %w", node.Spec.ProviderID, err) } + return id, nil +} - // fetch the instance so we can determine which interface generation to use - instance, err := c.linodeClient.GetInstance(ctx, id) - if err != nil { - return nil, fmt.Errorf("failed get linode with id %d: %w", id, err) - } - ipv6Range := "" - if instance.InterfaceGeneration == linodego.GenerationLinode { - ifaces, listErr := c.linodeClient.ListInterfaces(ctx, id, &linodego.ListOptions{}) - if listErr != nil || len(ifaces) == 0 { - return nil, fmt.Errorf("failed to list interfaces: %w", listErr) - } - for _, iface := range ifaces { - if iface.VPC != nil { - ipv6Range = getIPv6RangeFromLinodeInterface(iface) - if ipv6Range != "" { - break - } +// ipv6RangeFromLinodeInterfaces finds the VPC IPv6 range among a Linode instance's (new +// generation) interfaces. +func (c *cloudAllocator) ipv6RangeFromLinodeInterfaces(ctx context.Context, id int) (string, error) { + ifaces, listErr := c.linodeClient.ListInterfaces(ctx, id, &linodego.ListOptions{}) + if listErr != nil || len(ifaces) == 0 { + return "", fmt.Errorf("failed to list interfaces: %w", listErr) + } + for _, iface := range ifaces { + if iface.VPC != nil { + if ipv6Range := getIPv6RangeFromLinodeInterface(iface); ipv6Range != "" { + return ipv6Range, nil } } + } + return "", fmt.Errorf("failed to find ipv6 range in Linode interfaces: %v", ifaces) +} - if ipv6Range == "" { - return nil, fmt.Errorf("failed to find ipv6 range in Linode interfaces: %v", ifaces) - } - } else { - // Retrieve the instance configuration for the Linode ID - configs, listErr := c.linodeClient.ListInstanceConfigs(ctx, id, &linodego.ListOptions{}) - if listErr != nil || len(configs) == 0 { - return nil, fmt.Errorf("failed to list instance configs: %w", listErr) - } +// ipv6RangeFromInstanceConfig finds the VPC IPv6 range among a Linode instance's (legacy +// generation) config interfaces. +func (c *cloudAllocator) ipv6RangeFromInstanceConfig(ctx context.Context, id int) (string, error) { + configs, listErr := c.linodeClient.ListInstanceConfigs(ctx, id, &linodego.ListOptions{}) + if listErr != nil || len(configs) == 0 { + return "", fmt.Errorf("failed to list instance configs: %w", listErr) + } - for _, iface := range configs[0].Interfaces { - if iface.Purpose == linodego.InterfacePurposeVPC { - ipv6Range = getIPv6RangeFromInterface(iface) - if ipv6Range != "" { - break - } + for _, iface := range configs[0].Interfaces { + if iface.Purpose == linodego.InterfacePurposeVPC { + if ipv6Range := getIPv6RangeFromInterface(iface); ipv6Range != "" { + return ipv6Range, nil } } - - if ipv6Range == "" { - return nil, fmt.Errorf("failed to find ipv6 range in instance config: %v", configs[0]) - } } + return "", fmt.Errorf("failed to find ipv6 range in instance config: %v", configs[0]) +} - ip, base, err := net.ParseCIDR(ipv6Range) - if err != nil { - return nil, fmt.Errorf("failed parsing ipv6 range %s: %w", ipv6Range, err) +// getIPv6RangeForInstance returns the VPC IPv6 range configured for a Linode instance, using the +// appropriate lookup method (Linode interfaces vs instance configs) based on interface generation. +func (c *cloudAllocator) getIPv6RangeForInstance(ctx context.Context, id int, instance *linodego.Instance) (string, error) { + if instance.InterfaceGeneration == linodego.GenerationLinode { + return c.ipv6RangeFromLinodeInterfaces(ctx, id) } + return c.ipv6RangeFromInstanceConfig(ctx, id) +} +// buildIPv6PodCIDR derives the pod CIDR for a node from its VPC IPv6 base range, preferring the +// stable mnemonic subprefix and falling back to masking the start of the range. +func (c *cloudAllocator) buildIPv6PodCIDR(logger klog.Logger, ip net.IP, base *net.IPNet) (*net.IPNet, error) { // get pod cidr using stable mnemonic subprefix :0:c::/112 if podCIDR, ok := getIPv6PodCIDR(ip, c.nodeCIDRMaskSizeIPv6); ok { logger.V(4).Info("Using stable IPv6 PodCIDR subprefix :0:c::/112", "ip", ip, "podCIDR", podCIDR) @@ -531,6 +555,33 @@ func (c *cloudAllocator) allocateIPv6CIDR(ctx context.Context, node *v1.Node) (* return fallbackPodCIDR, nil } +func (c *cloudAllocator) allocateIPv6CIDR(ctx context.Context, node *v1.Node) (*net.IPNet, error) { + logger := klog.FromContext(ctx) + + id, err := parseLinodeIDFromProviderID(node) + if err != nil { + return nil, err + } + + // fetch the instance so we can determine which interface generation to use + instance, err := c.linodeClient.GetInstance(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed get linode with id %d: %w", id, err) + } + + ipv6Range, err := c.getIPv6RangeForInstance(ctx, id, instance) + if err != nil { + return nil, err + } + + ip, base, err := net.ParseCIDR(ipv6Range) + if err != nil { + return nil, fmt.Errorf("failed parsing ipv6 range %s: %w", ipv6Range, err) + } + + return c.buildIPv6PodCIDR(logger, ip, base) +} + // WARNING: If you're adding any return calls or defer any more work from this // function you have to make sure to update nodesInProcessing properly with the // disposition of the node when the work is done.