// Copyright (c) 2018-2025 Tigera, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package allocateip

import (
	"context"
	"fmt"
	gnet "net"
	"os"
	"reflect"

	api "github.com/projectcalico/api/pkg/apis/projectcalico/v3"
	log "github.com/sirupsen/logrus"

	felixconfig "github.com/projectcalico/calico/felix/config"
	"github.com/projectcalico/calico/libcalico-go/lib/apiconfig"
	"github.com/projectcalico/calico/libcalico-go/lib/apis/internalapi"
	bapi "github.com/projectcalico/calico/libcalico-go/lib/backend/api"
	"github.com/projectcalico/calico/libcalico-go/lib/backend/model"
	"github.com/projectcalico/calico/libcalico-go/lib/backend/syncersv1/tunnelipsyncer"
	client "github.com/projectcalico/calico/libcalico-go/lib/clientv3"
	cerrors "github.com/projectcalico/calico/libcalico-go/lib/errors"
	"github.com/projectcalico/calico/libcalico-go/lib/ipam"
	"github.com/projectcalico/calico/libcalico-go/lib/net"
	"github.com/projectcalico/calico/libcalico-go/lib/options"
	"github.com/projectcalico/calico/node/pkg/calicoclient"
	"github.com/projectcalico/calico/pkg/buildinfo"
	"github.com/projectcalico/calico/typha/pkg/syncclientutils"
	"github.com/projectcalico/calico/typha/pkg/syncproto"
)

// This file contains the main processing and common logic for assigning tunnel addresses,
// used by calico/node to set the host's tunnel address if IPIP or VXLAN is enabled on the pool or
// wireguard is enabled on the node.
//
// It will assign an address if there are any available, and remove any tunnel addresses
// that are configured and should no longer be.

// Run runs the tunnel IP allocator. In oneshot mode it reconciles once and
// returns. In daemon mode it watches for IP pool and node configuration
// changes, reconciling whenever they occur, and blocks until ctx is cancelled.
func Run(ctx context.Context, oneshot bool) error {
	nodename := os.Getenv("NODENAME")
	if nodename == "" {
		return fmt.Errorf("NODENAME environment is not set")
	}

	// Load felix environment configuration. Note that this does not perform the full hierarchical load of felix
	// configuration nor does this watch for changes, so if it is critical that the configuration value used is correct
	// and may be defined outside of an environment variable, do not use this.
	felixEnvConfig := loadFelixEnvConfig()

	// Load the client config from environment.
	cfg, c := calicoclient.CreateClient()

	return run(ctx, nodename, cfg, c, felixEnvConfig, oneshot)
}

func run(
	ctx context.Context, nodename string, cfg *apiconfig.CalicoAPIConfig, c client.Interface,
	felixEnvConfig *felixconfig.Config, oneshot bool,
) error {
	// If configured to use host-local IPAM, there is no need to configure tunnel addresses as they use the
	// first IP of the pod CIDR - this is handled in the k8s backend code in libcalico-go.
	if cfg.Spec.K8sUsePodCIDR {
		log.Debug("Using host-local IPAM, no need to allocate a tunnel IP")
		if !oneshot {
			<-ctx.Done()
		}
		return nil
	}

	if oneshot {
		return reconcileTunnelAddrs(nodename, c, felixEnvConfig)
	}

	// Daemon mode: create a long-running reconciler.
	r := &reconciler{
		nodename:       nodename,
		cfg:            cfg,
		client:         c,
		ch:             make(chan struct{}),
		data:           make(map[string]any),
		felixEnvConfig: felixEnvConfig,
	}

	// Either create a typha syncclient or a local syncer depending on configuration. This calls back into the
	// reconciler to trigger updates when necessary.
	typhaConfig := syncclientutils.ReadTyphaConfig([]string{"FELIX_", "CALICO_"})
	if syncclientutils.MustStartSyncerClientIfTyphaConfigured(
		&typhaConfig, syncproto.SyncerTypeTunnelIPAllocation,
		buildinfo.Version, nodename, fmt.Sprintf("tunnel-ip-allocation %s", buildinfo.Version),
		r,
	) {
		log.Debug("Using typha syncclient")
	} else {
		log.Debug("Using local syncer")
		syncer := tunnelipsyncer.New(c.(backendClientAccessor).Backend(), r, nodename)
		syncer.Start()
	}

	return r.run(ctx)
}

func (r reconciler) run(ctx context.Context) error {
	for {
		select {
		case <-r.ch:
			if err := reconcileTunnelAddrs(r.nodename, r.client, r.felixEnvConfig); err != nil {
				return fmt.Errorf("failed to reconcile tunnel address: %w", err)
			}
		case <-ctx.Done():
			return nil
		}
	}
}

// reconciler watches IPPool and Node configuration and triggers a reconciliation of the Tunnel IP addresses whenever
// it spots a configuration change that may impact IP selection.
type reconciler struct {
	nodename       string
	cfg            *apiconfig.CalicoAPIConfig
	client         client.Interface
	ch             chan struct{}
	data           map[string]any
	felixEnvConfig *felixconfig.Config
	inSync         bool
}

// OnStatusUpdated handles the syncer status callback method.
func (r *reconciler) OnStatusUpdated(status bapi.SyncStatus) {
	if status == bapi.InSync {
		// We are in-sync, trigger an initial scan/update of the IP addresses.
		r.inSync = true
		r.ch <- struct{}{}
	}
}

// OnUpdates handles the syncer resource updates.
func (r *reconciler) OnUpdates(updates []bapi.Update) {
	var updated bool

	for _, u := range updates {
		key := u.Key.String()

		// The presence (or absence) of u.Value determines if this is a delete.
		// Even if UpdateType is "New" or "Updated", Value may be nil if validation failed in the syncer.
		if u.Value == nil {
			log.WithField("key", key).Debug("Received delete or invalid update (Value is nil)")

			// If we had cached data for this key, remove it and trigger reconciliation.
			if _, ok := r.data[key]; ok {
				updated = true
				log.WithField("key", key).Debug("Deleted cached entry - trigger reconciliation")
			}
			delete(r.data, key)
			continue
		}

		// Handle non-nil value updates (Add or Update)
		var data any
		switch v := u.Value.(type) {
		case *model.IPPool:
			log.Debugf("Updated IPPool resource: %s", key)
			data = v

		case *internalapi.Node:
			// Only process our own node
			if v.Name != r.nodename {
				continue
			}
			log.Debugf("Updated Node resource: %s", key)

			// Track both IPv4 and IPv6 WireGuard public keys
			data = wireguardData{
				publicKey:   v.Status.WireguardPublicKey,
				publicKeyV6: v.Status.WireguardPublicKeyV6,
			}

		default:
			// We got an update for an unexpected resource type. Rather than ignore, just treat as updated so that
			// we reconcile the addresses.
			log.Warningf("Unexpected resource update: %s", key)
			updated = true
			continue
		}

		if existing, ok := r.data[key]; !ok || !reflect.DeepEqual(existing, data) {
			// Entry is new or associated data is modified. In either case update the data and flag as updated.
			log.WithFields(log.Fields{
				"key":      key,
				"existing": existing,
				"new":      data,
			}).Debug("Stored data has changed — trigger reconciliation")
			r.data[key] = data
			updated = true
		}
	}

	if updated && r.inSync {
		// We have updated data. Trigger a reconciliation, but don't block if there is already an update pending.
		select {
		case r.ch <- struct{}{}:
		default:
		}
	}
}

// reconcileTunnelAddrs performs a single shot update of the tunnel IP allocations.
func reconcileTunnelAddrs(
	nodename string, c client.Interface, felixEnvConfig *felixconfig.Config,
) (retErr error) {
	ctx := context.Background()
	// Get node resource for given nodename.
	node, err := c.Nodes().Get(ctx, nodename, options.GetOptions{})
	if err != nil {
		return fmt.Errorf("failed to fetch node resource '%s': %w", nodename, err)
	}

	// Get list of ip pools
	ipPoolList, err := c.IPPools().List(ctx, options.ListOptions{})
	if err != nil {
		return fmt.Errorf("unable to query IP pool configuration: %w", err)
	}

	originalNode := node.DeepCopy()
	assignIP := make(map[string]string)

	defer func() {
		if retErr != nil {
			for attrType, ip := range assignIP {
				if ip != "" {
					logCtx := getLogger(attrType)
					handle, _ := generateHandleAndAttributes(nodename, attrType)
					if err = c.IPAM().ReleaseByHandle(ctx, handle); err != nil {
						logCtx.WithError(err).WithField("IP", ip).Error("Error releasing IP address on failure")
					}
				}
			}
		}
	}()

	for _, attrType := range []string{ipam.AttributeTypeWireguard, ipam.AttributeTypeWireguardV6, ipam.AttributeTypeIPIP, ipam.AttributeTypeVXLAN, ipam.AttributeTypeVXLANV6} {
		// Query the enabled pools and either configure the tunnel address, or remove it.
		if cidrs := determineEnabledPoolCIDRs(*node, *ipPoolList, felixEnvConfig, attrType); len(cidrs) > 0 {
			ip, err := ensureHostTunnelAddress(ctx, c, node, cidrs, attrType)
			if err != nil {
				return err
			}
			if ip != "" {
				assignIP[attrType] = ip
			}
		} else {
			if err = removeHostTunnelAddr(ctx, c, node, attrType); err != nil {
				return err
			}
		}
	}

	if !reflect.DeepEqual(originalNode, node) {
		// Update the node object.
		_, err = c.Nodes().Update(ctx, node, options.SetOptions{})
		if err != nil {
			log.WithError(err).WithField("node", node.Name).Error("Failed to update node")
			return err
		}
	}

	return nil
}

func ensureHostTunnelAddress(ctx context.Context, c client.Interface, node *internalapi.Node, cidrs []net.IPNet, attrType string) (string, error) {
	logCtx := getLogger(attrType)
	logCtx.WithField("node", node.Name).Debug("Ensure tunnel address is set")

	// Get the address and ipam attribute string
	var addr string
	switch attrType {
	case ipam.AttributeTypeVXLAN:
		addr = node.Spec.IPv4VXLANTunnelAddr
	case ipam.AttributeTypeVXLANV6:
		addr = node.Spec.IPv6VXLANTunnelAddr
	case ipam.AttributeTypeIPIP:
		if node.Spec.BGP != nil {
			addr = node.Spec.BGP.IPv4IPIPTunnelAddr
		}
	case ipam.AttributeTypeWireguard:
		if node.Spec.Wireguard != nil {
			addr = node.Spec.Wireguard.InterfaceIPv4Address
		}
	case ipam.AttributeTypeWireguardV6:
		if node.Spec.Wireguard != nil {
			addr = node.Spec.Wireguard.InterfaceIPv6Address
		}
	}

	// Work out if we need to assign a tunnel address.
	// In most cases we should not release current address and should assign new one.
	release := false
	assign := true
	if addr == "" {
		// The tunnel has no IP address assigned, assign one.
		logCtx.Info("Assign a new tunnel address")

		// Defensively release any IP addresses with this handle. This covers a theoretical case
		// where the node object has lost its reference to its IP, but the allocation still exists
		// in IPAM. For example, if the node object was manually edited.
		release = true
	} else {
		// Go ahead checking status of current address.
		ipAddr := gnet.ParseIP(addr)
		if ipAddr == nil {
			logCtx.Errorf("Failed to parse the CIDR '%s'", addr)
			return "", fmt.Errorf("failed to parse the CIDR '%s'", addr)
		}

		// Check if we got correct assignment attributes.
		allocAttr, err := c.IPAM().GetAssignmentAttributes(ctx, net.IP{IP: ipAddr})
		if err == nil {
			attr := allocAttr.ActiveOwnerAttrs
			handle := allocAttr.HandleID
			if attr[ipam.AttributeType] == attrType && attr[ipam.AttributeNode] == node.Name {
				// The tunnel address is still assigned to this node, but is it in the correct pool this time?
				if !isIpInPool(addr, cidrs) {
					// Wrong pool, release this address.
					logCtx.WithField("currentAddr", addr).Info("Current address is not in a valid pool, release it and reassign")
					release = true
				} else {
					// Correct pool, keep this address.
					logCtx.WithField("currentAddr", addr).Info("Current address is still valid, do nothing")
					assign = false
				}
			} else if len(attr) == 0 {
				// No attributes means that this is an old address, assigned by code that didn't use
				// allocation attributes. It might be a pod address, or it might be a node tunnel
				// address. The only way to tell is by the existence of a handle, since workload
				// addresses have always used a handle, whereas tunnel addresses didn't start
				// using handles until the same time as they got allocation attributes.
				if handle != nil {
					// Handle exists, so this address belongs to a workload. We need to assign
					// a new one for the node, but we shouldn't clean up the old address.
					logCtx.WithField("currentAddr", addr).Info("Current address is occupied, assign a new one")
				} else {
					// Handle does not exist. This is just an old tunnel address that comes from
					// a time before we used handles and allocation attributes. Attempt to
					// reassign the same address, but now with metadata. It's possible that someone
					// else takes the address while we do this, in which case we'll just
					// need to assign a new address.
					if err := correctAllocationWithHandle(ctx, c, addr, node.Name, attrType); err != nil {
						if _, ok := err.(cerrors.ErrorResourceAlreadyExists); !ok {
							// Unknown error attempting to allocate the address. Exit.
							logCtx.WithError(err).Error("Error correcting tunnel IP allocation")
							return "", err
						}

						// The address was taken by someone else. We need to assign a new one.
						logCtx.WithError(err).Warn("Failed to correct missing attributes, will assign a new address")
					} else {
						// We corrected the address, we can just return.
						logCtx.Info("Updated tunnel address with allocation attributes")
						return "", nil
					}
				}
			} else {
				// The allocation has attributes, but doesn't belong to us. Assign a new one.
				logCtx.WithField("currentAddr", addr).Info("Current address is occupied, assign a new one")
			}
		} else if _, ok := err.(cerrors.ErrorResourceDoesNotExist); ok {
			// The tunnel address is not assigned, reassign it.
			logCtx.WithField("currentAddr", addr).Info("Current address is not assigned, assign a new one")

			// Defensively release any IP addresses with this handle. This covers a theoretical case
			// where the node object has lost its reference to its correct IP, but the allocation still exists
			// in IPAM. For example, if the node object was manually edited.
			release = true
		} else {
			// Failed to get assignment attributes, datastore connection issues possible.
			return "", fmt.Errorf("failed to get assignment attributes for CIDR '%s': %w", addr, err)
		}
	}

	if release {
		logCtx.WithField("IP", addr).Info("Release any old tunnel addresses")
		handle, _ := generateHandleAndAttributes(node.Name, attrType)
		if err := c.IPAM().ReleaseByHandle(ctx, handle); err != nil {
			if _, ok := err.(cerrors.ErrorResourceDoesNotExist); !ok {
				logCtx.WithError(err).Error("Failed to release old addresses")
				return "", err
			}
			// No existing allocations for this node.
		}
	}

	if assign {
		ip, err := assignHostTunnelAddr(ctx, c, node, cidrs, attrType)
		if err == nil {
			logCtx.WithField("IP", ip).Info("Assign new tunnel address")
		}
		return ip, err
	}

	return "", nil
}

func correctAllocationWithHandle(ctx context.Context, c client.Interface, addr, nodename string, attrType string) error {
	ipAddr := net.ParseIP(addr)
	if ipAddr == nil {
		return fmt.Errorf("failed to parse node tunnel address '%s'", addr)
	}

	// Release the old allocation.
	_, _, err := c.IPAM().ReleaseIPs(ctx, ipam.ReleaseOptions{Address: ipAddr.String()})
	if err != nil {
		// If we fail to release the old allocation, return an error.
		log.WithField("IP", ipAddr.String()).WithError(err).Error("Error releasing address")
		return err
	}

	// Attempt to re-assign the same address, but with a handle this time.
	handle, attrs := generateHandleAndAttributes(nodename, attrType)
	args := ipam.AssignIPArgs{
		IP:       *ipAddr,
		HandleID: &handle,
		Attrs:    attrs,
		Hostname: nodename,
	}

	// If we fail to allocate the same IP, return an error. We'll just
	// have to allocate a new one.
	return c.IPAM().AssignIP(ctx, args)
}

func generateHandleAndAttributes(nodename string, attrType string) (string, map[string]string) {
	attrs := map[string]string{ipam.AttributeNode: nodename}
	var handle string
	switch attrType {
	case ipam.AttributeTypeVXLAN:
		handle = fmt.Sprintf("vxlan-tunnel-addr-%s", nodename)
	case ipam.AttributeTypeVXLANV6:
		handle = fmt.Sprintf("vxlan-v6-tunnel-addr-%s", nodename)
	case ipam.AttributeTypeIPIP:
		handle = fmt.Sprintf("ipip-tunnel-addr-%s", nodename)
	case ipam.AttributeTypeWireguard:
		handle = fmt.Sprintf("wireguard-tunnel-addr-%s", nodename)
	case ipam.AttributeTypeWireguardV6:
		handle = fmt.Sprintf("wireguard-v6-tunnel-addr-%s", nodename)
	}
	attrs[ipam.AttributeType] = attrType
	return handle, attrs
}

// assignHostTunnelAddr claims an IP address from the first pool
// with some space. Stores the result in the host's config as its tunnel
// address. It will assign a VXLAN address if vxlan is true, otherwise an IPIP address.
func assignHostTunnelAddr(ctx context.Context, c client.Interface, node *internalapi.Node, cidrs []net.IPNet, attrType string) (string, error) {
	// Build attributes and handle for this allocation.
	handle, attrs := generateHandleAndAttributes(node.Name, attrType)
	logCtx := getLogger(attrType)

	if len(cidrs) == 0 {
		return "", fmt.Errorf("unable to assign an address: no IP pools provided")
	}

	args := ipam.AutoAssignArgs{
		HandleID:    &handle,
		Attrs:       attrs,
		Hostname:    node.Name,
		IntendedUse: api.IPPoolAllowedUseTunnel,
	}
	switch cidrs[0].Version() {
	case 4:
		args.Num4 = 1
		args.IPv4Pools = cidrs
	case 6:
		args.Num6 = 1
		args.IPv6Pools = cidrs
	default:
		return "", fmt.Errorf("unable to assign an address: invalid IP version for IP pool %v", cidrs[0])
	}

	v4Assignments, v6Assignments, err := c.IPAM().AutoAssign(ctx, args)
	if err != nil {
		logCtx.WithError(err).Errorf("Unable to autoassign an address")
		return "", err
	}

	ip := ""
	if v4Assignments != nil {
		if err = v4Assignments.PartialFulfillmentError(); err != nil {
			logCtx.WithError(err).Errorf("Unable to autoassign an IPv4 address")
			return "", err
		}
		ip = v4Assignments.IPs[0].IP.String()
	}
	if v6Assignments != nil {
		if err = v6Assignments.PartialFulfillmentError(); err != nil {
			logCtx.WithError(err).Errorf("Unable to autoassign an IPv6 address")
			return "", err
		}
		ip = v6Assignments.IPs[0].IP.String()
	}

	// Update the node object with the assigned address.
	updateNodeWithAddress(node, ip, attrType)

	logCtx.WithField("IP", ip).Info("Assigned tunnel address to node")
	return ip, nil
}

func updateNodeWithAddress(node *internalapi.Node, addr string, attrType string) {
	switch attrType {
	case ipam.AttributeTypeVXLAN:
		node.Spec.IPv4VXLANTunnelAddr = addr
	case ipam.AttributeTypeVXLANV6:
		node.Spec.IPv6VXLANTunnelAddr = addr
	case ipam.AttributeTypeIPIP:
		if node.Spec.BGP == nil {
			node.Spec.BGP = &internalapi.NodeBGPSpec{}
		}
		node.Spec.BGP.IPv4IPIPTunnelAddr = addr
	case ipam.AttributeTypeWireguard:
		if node.Spec.Wireguard == nil {
			node.Spec.Wireguard = &internalapi.NodeWireguardSpec{}
		}
		node.Spec.Wireguard.InterfaceIPv4Address = addr
	case ipam.AttributeTypeWireguardV6:
		if node.Spec.Wireguard == nil {
			node.Spec.Wireguard = &internalapi.NodeWireguardSpec{}
		}
		node.Spec.Wireguard.InterfaceIPv6Address = addr
	}
}

// removeHostTunnelAddr removes any existing IP address for this host's
// tunnel device and releases the IP from IPAM.  If no IP is assigned this function
// is a no-op.
func removeHostTunnelAddr(ctx context.Context, c client.Interface, node *internalapi.Node, attrType string) error {
	logCtx := getLogger(attrType)
	logCtx.WithField("node", node.Name).Debug("Remove tunnel addresses")

	// Find out the currently assigned address and remove it from the node.
	var ipAddrStr string
	var ipAddr *net.IP
	switch attrType {
	case ipam.AttributeTypeVXLAN:
		ipAddrStr = node.Spec.IPv4VXLANTunnelAddr
		node.Spec.IPv4VXLANTunnelAddr = ""
	case ipam.AttributeTypeVXLANV6:
		ipAddrStr = node.Spec.IPv6VXLANTunnelAddr
		node.Spec.IPv6VXLANTunnelAddr = ""
	case ipam.AttributeTypeIPIP:
		if node.Spec.BGP != nil {
			ipAddrStr = node.Spec.BGP.IPv4IPIPTunnelAddr
			node.Spec.BGP.IPv4IPIPTunnelAddr = ""

			// If removing the tunnel address causes the BGP spec to be empty, then nil it out.
			// libcalico asserts that if a BGP spec is present, that it not be empty.
			if reflect.DeepEqual(*node.Spec.BGP, internalapi.NodeBGPSpec{}) {
				logCtx.Debug("BGP spec is now empty, setting to nil")
				node.Spec.BGP = nil
			}
		}
	case ipam.AttributeTypeWireguard:
		if node.Spec.Wireguard != nil {
			ipAddrStr = node.Spec.Wireguard.InterfaceIPv4Address
			node.Spec.Wireguard.InterfaceIPv4Address = ""

			if reflect.DeepEqual(*node.Spec.Wireguard, internalapi.NodeWireguardSpec{}) {
				logCtx.Debug("Wireguard spec is now empty, setting to nil")
				node.Spec.Wireguard = nil
			}
		}
	case ipam.AttributeTypeWireguardV6:
		if node.Spec.Wireguard != nil {
			ipAddrStr = node.Spec.Wireguard.InterfaceIPv6Address
			node.Spec.Wireguard.InterfaceIPv6Address = ""

			if reflect.DeepEqual(*node.Spec.Wireguard, internalapi.NodeWireguardSpec{}) {
				logCtx.Debug("Wireguard spec is now empty, setting to nil")
				node.Spec.Wireguard = nil
			}
		}
	}

	if ipAddrStr != "" {
		ipAddr = net.ParseIP(ipAddrStr)
	}

	// Release tunnel IP address(es) for the node.
	handle, _ := generateHandleAndAttributes(node.Name, attrType)
	if err := c.IPAM().ReleaseByHandle(ctx, handle); err != nil {
		if _, ok := err.(cerrors.ErrorResourceDoesNotExist); !ok {
			// Unknown error releasing the address.
			logCtx.WithError(err).WithFields(log.Fields{
				"IP":     ipAddrStr,
				"Handle": handle,
			}).Error("Error releasing address by handle")
			return err
		}

		// Resource does not exist. This can occur in a few scenarios:
		//
		// 1. The IP is genuinely not allocated, and there's nothing for us to do.
		// 2. The IP pre-dates the use of handles in this code, and so the handle doesn't exist.
		// 3. We have gotten into an invalid state where the handle has been deleted but the IP is still allocated.
		//
		// For scenario #1, there is no more work to do.
		// We can determine if we're encountering scenario #2 or #3 by inspecting the allocation's attributes.
		// For scenario #2, we expect no attributes and no handle to be stored with the allocation.
		// For scenario #3, we expect a handle in the attributes and it should match the expected value.
		if ipAddr != nil {
			// There are no addresses with this handle. If there is an IP configured on the node, check to see if it
			// belongs to us. If it has no handle and no attributes, then we can pretty confidently
			// say that it belongs to us rather than a pod and should be cleaned up.
			logCtx.WithField("handle", handle).Info("No IPs with handle, release exact IP")
			allocAttr, err := c.IPAM().GetAssignmentAttributes(ctx, *ipAddr)
			if err != nil {
				if _, ok := err.(cerrors.ErrorResourceDoesNotExist); !ok {
					logCtx.WithError(err).Error("Failed to query attributes")
					return err
				}
				// Scenario #1: The allocation actually doesn't exist, we don't have anything to do.
			} else {
				attr := allocAttr.ActiveOwnerAttrs
				storedHandle := allocAttr.HandleID
				if len(attr) == 0 && storedHandle == nil {
					// Scenario #2: The allocation exists, but has no handle whatsoever.
					// This is an ancient allocation and can be released.
					if _, _, err := c.IPAM().ReleaseIPs(ctx, ipam.ReleaseOptions{Address: ipAddr.String()}); err != nil {
						logCtx.WithError(err).WithField("IP", ipAddr.String()).Error("Error releasing address from IPAM")
						return err
					}
				} else if storedHandle != nil && *storedHandle == handle {
					// Scenario #3: The allocation exists, has a handle, and it matches the one we expect.
					// This means the handle object itself was wrongfully deleted. We can clean it up
					// by releasing the IP directly with both address and handle specified.
					if _, _, err := c.IPAM().ReleaseIPs(ctx, ipam.ReleaseOptions{Address: ipAddr.String(), Handle: handle}); err != nil {
						logCtx.WithError(err).WithField("IP", ipAddr.String()).Error("Error releasing address from IPAM")
						return err
					}
				} else {
					// The final scenario: the IP on the node is allocated, but it is allocated to some other handle.
					// It doesn't belong to us. We can't do anything here but it's worth logging.
					fields := log.Fields{"attributes": attr, "IP": ipAddr.String()}
					logCtx.WithFields(fields).Warnf("IP address has been reused by something else")
				}
			}
		}
	}
	return nil
}

// determineEnabledPools returns all enabled pools. If vxlan is true, then it will only return VXLAN pools. Otherwise
// it will only return IPIP enabled pools.
func determineEnabledPoolCIDRs(
	node internalapi.Node, ipPoolList api.IPPoolList, felixEnvConfig *felixconfig.Config, attrType string,
) []net.IPNet {
	// For wireguard, an IP is only allocated from a pool if wireguard is actually running (there will be a public
	// key configured on the node), and the cluster is not running in host encryption mode (which is required for
	// managed cloud with non-Calico CNI). When running in host encryption mode, the wireguard dataplane will use the
	// node IP for the device.
	switch attrType {
	case ipam.AttributeTypeWireguard:
		if felixEnvConfig.WireguardHostEncryptionEnabled {
			log.Debug("Wireguard is running in host encryption mode, do not allocate a device IPv4 address")
			return nil
		}
		if node.Status.WireguardPublicKey == "" {
			log.Debugf("Wireguard is not running on node %s, do not allocate a device IPv4 address", node.Name)
			return nil
		}
	case ipam.AttributeTypeWireguardV6:
		if felixEnvConfig.WireguardHostEncryptionEnabled {
			log.Debug("Wireguard is running in host encryption mode, do not allocate a device IPv6 address")
			return nil
		}
		if node.Status.WireguardPublicKeyV6 == "" {
			log.Debugf("Wireguard is not running on node %s, do not allocate a device IPv6 address", node.Name)
			return nil
		}
	}

	var cidrs []net.IPNet
	for _, ipPool := range ipPoolList.Items {
		_, poolCidr, err := net.ParseCIDR(ipPool.Spec.CIDR)
		if err != nil {
			log.WithError(err).Fatalf("Failed to parse CIDR '%s' for IPPool '%s'", ipPool.Spec.CIDR, ipPool.Name)
		}

		// Check if the pool has tunnel IPs in its allowed uses.
		if len(ipPool.Spec.AllowedUses) > 0 {
			allowed := false
			for _, use := range ipPool.Spec.AllowedUses {
				if use == api.IPPoolAllowedUseTunnel {
					allowed = true
				}
			}
			if !allowed {
				log.Debugf("IPPool '%s' is not allowed for tunnel IPs.  Allowed uses: %v", ipPool.Name, ipPool.Spec.AllowedUses)
				continue
			}
		}

		// Check if IP pool selects the node
		if selects, err := ipam.SelectsNode(ipPool, node); err != nil {
			log.WithError(err).Errorf("Failed to compare nodeSelector '%s' for IPPool '%s', skipping", ipPool.Spec.NodeSelector, ipPool.Name)
			continue
		} else if !selects {
			log.Debugf("IPPool '%s' does not select Node '%s'", ipPool.Name, node.Name)
			continue
		}

		// An IP pool is disabled either if explicitly configured as such, or if it is currently being deleted.
		disabled := ipPool.Spec.Disabled || ipPool.GetDeletionTimestamp() != nil

		// Check if desired encap is enabled in the IP pool, the IP pool is not disabled, and it is IPv4 pool since we
		// don't support encap with IPv6.
		switch attrType {
		case ipam.AttributeTypeVXLAN:
			if (ipPool.Spec.VXLANMode == api.VXLANModeAlways || ipPool.Spec.VXLANMode == api.VXLANModeCrossSubnet) && !disabled && poolCidr.Version() == 4 {
				cidrs = append(cidrs, *poolCidr)
			}
		case ipam.AttributeTypeVXLANV6:
			if (ipPool.Spec.VXLANMode == api.VXLANModeAlways || ipPool.Spec.VXLANMode == api.VXLANModeCrossSubnet) && !disabled && poolCidr.Version() == 6 {
				cidrs = append(cidrs, *poolCidr)
			}
		case ipam.AttributeTypeIPIP:
			// Check if IPIP is enabled in the IP pool, the IP pool is not disabled, and it is IPv4 pool since we don't support IPIP with IPv6.
			if (ipPool.Spec.IPIPMode == api.IPIPModeCrossSubnet || ipPool.Spec.IPIPMode == api.IPIPModeAlways) && !disabled && poolCidr.Version() == 4 {
				cidrs = append(cidrs, *poolCidr)
			}
		case ipam.AttributeTypeWireguard:
			// Wireguard does not require a specific encap configuration on the pool.
			if !disabled && poolCidr.Version() == 4 {
				cidrs = append(cidrs, *poolCidr)
			}
		case ipam.AttributeTypeWireguardV6:
			// Wireguard does not require a specific encap configuration on the pool.
			if !disabled && poolCidr.Version() == 6 {
				cidrs = append(cidrs, *poolCidr)
			}
		}
	}
	return cidrs
}

// isIpInPool returns if the IP address is in one of the supplied pools.
func isIpInPool(ipAddrStr string, cidrs []net.IPNet) bool {
	ipAddress := net.ParseIP(ipAddrStr)
	for _, cidr := range cidrs {
		if cidr.Contains(ipAddress.IP) {
			return true
		}
	}
	return false
}

func getLogger(attrType string) *log.Entry {
	switch attrType {
	case ipam.AttributeTypeVXLAN:
		return log.WithField("type", "vxlanTunnelAddress")
	case ipam.AttributeTypeVXLANV6:
		return log.WithField("type", "vxlanV6TunnelAddress")
	case ipam.AttributeTypeIPIP:
		return log.WithField("type", "ipipTunnelAddress")
	case ipam.AttributeTypeWireguard:
		return log.WithField("type", "wireguardTunnelAddress")
	case ipam.AttributeTypeWireguardV6:
		return log.WithField("type", "wireguardV6TunnelAddress")
	}
	return nil
}

// backendClientAccessor is an interface to access the backend client from the main v2 client.
type backendClientAccessor interface {
	Backend() bapi.Client
}

// loadFelixEnvConfig loads the felix configuration from environment. It does not perform a hierarchical load across
// env, file and felixconfigurations resources and as such this is should only be used for configuration that is only
// expected to be specified through environment.
func loadFelixEnvConfig() *felixconfig.Config {
	// Load locally-defined config from the environment variables.
	configParams := felixconfig.New()
	envConfig := felixconfig.LoadConfigFromEnvironment(os.Environ())
	// Parse and merge the local config.
	_, err := configParams.UpdateFrom(envConfig, felixconfig.EnvironmentVariable)
	if err != nil {
		log.WithError(err).Panic("Failed to parse Felix environments")
	}
	return configParams
}

// Home for wireguard public keys in the cache.
type wireguardData struct {
	publicKey   string
	publicKeyV6 string
}
