Skip to content

Commit

Permalink
Add support for dual stack / IPv6 for node IPs. Allow defining intern…
Browse files Browse the repository at this point in the history
…al IPs as CIDR ranges.
  • Loading branch information
marnixbouhuis committed Dec 30, 2024
1 parent f6fe781 commit 3a2856b
Show file tree
Hide file tree
Showing 2 changed files with 409 additions and 48 deletions.
152 changes: 124 additions & 28 deletions pkg/cloud-controller-manager/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"net/netip"
"slices"
"strings"
"sync"

ctlkubevirtv1 "github.com/harvester/harvester/pkg/generated/controllers/kubevirt.io/v1"
Expand Down Expand Up @@ -70,7 +71,10 @@ func (i *instanceManager) InstanceMetadata(ctx context.Context, node *v1.Node) (
meta.Zone = zone
}

meta.NodeAddresses = getNodeAddresses(node, vmi)
meta.NodeAddresses, err = getNodeAddresses(node, vmi)
if err != nil {
return nil, err
}

return meta, nil
}
Expand All @@ -84,46 +88,138 @@ func (i *instanceManager) getVM(node *v1.Node) (*kubevirtv1.VirtualMachine, erro
}

// getNodeAddresses return nodeAddresses only when the value of annotation `alpha.kubernetes.io/provided-node-ip` is not empty
func getNodeAddresses(node *v1.Node, vmi *kubevirtv1.VirtualMachineInstance) []v1.NodeAddress {
providedNodeIP, ok := node.Annotations[api.AnnotationAlphaProvidedIPAddr]
if !ok {
return nil
}

aiIPs, err := getAdditionalInternalIPs(node)
func getNodeAddresses(node *v1.Node, vmi *kubevirtv1.VirtualMachineInstance) ([]v1.NodeAddress, error) {
internalIPRanges, err := getInternalIPRanges(node)
if err != nil {
// if additional IPs are not correctly marked, only log an error, do not return this error
logrus.WithFields(logrus.Fields{
"namespace": node.Namespace,
"name": node.Name,
}).Debugf("%s, skip it", err.Error())
return nil, err
}

nodeAddresses := make([]v1.NodeAddress, 0, len(vmi.Spec.Networks)+1)
// Optimistically assume that for every interface have one IP. Add one for the hostname address that we add later.
// Since the amount of IP addresses is probably very limited this should be fine.
nodeAddresses := make([]v1.NodeAddress, 0, len(vmi.Status.Interfaces)+1)

// Build a list of network names (names of NICs) on the VM.
networkNames := make([]string, 0, len(vmi.Spec.Networks))
for _, network := range vmi.Spec.Networks {
for _, networkInterface := range vmi.Status.Interfaces {
if network.Name == networkInterface.Name {
if ip := net.ParseIP(networkInterface.IP); ip != nil && ip.To4() != nil {
nodeAddr := v1.NodeAddress{
Address: networkInterface.IP,
}
if networkInterface.IP == providedNodeIP || (aiIPs != nil && slices.Contains(aiIPs, networkInterface.IP)) {
nodeAddr.Type = v1.NodeInternalIP
} else {
nodeAddr.Type = v1.NodeExternalIP
}
nodeAddresses = append(nodeAddresses, nodeAddr)
networkNames = append(networkNames, network.Name)
}

// Find all IP addresses of the VM
for _, networkInterface := range vmi.Status.Interfaces {
// The interface list might contain interfaces that do not belong to any NIC of the VM. Filter them out.
if !slices.Contains(networkNames, networkInterface.Name) {
// Ignore interface since it does not belong to one of the NICs.
continue
}

for _, ipStr := range networkInterface.IPs {
ip, err := netip.ParseAddr(ipStr)
if err != nil {
// Failed to parse IP, skip it
logrus.WithFields(logrus.Fields{
"namespace": node.Namespace,
"name": node.Name,
}).Warnf("Unable to parse IP %s, skip it: %s", ipStr, err.Error())
continue
}

// Determine if the IP should be listed as an internal or external IP.
ipType := v1.NodeExternalIP
for _, internalPrefix := range internalIPRanges {
if internalPrefix.Contains(ip) {
// IP is an internal IP, no need to check further.
ipType = v1.NodeInternalIP
break
}
}

nodeAddresses = append(nodeAddresses, v1.NodeAddress{
Type: ipType,
Address: ip.String(),
})
}
}

nodeAddresses = append(nodeAddresses, v1.NodeAddress{
Type: v1.NodeHostName,
Address: node.Name,
})

return nodeAddresses
return nodeAddresses, nil
}

func getInternalIPRanges(node *v1.Node) ([]netip.Prefix, error) {
var internalIPRanges []netip.Prefix

// Kubelet sets this node annotation if the --node-ip flag is set and an external cloud provider is used
providedNodeIP, ok := node.Annotations[api.AnnotationAlphaProvidedIPAddr]
if !ok {
// Annotation is not set, this could be because we are running in a dual stack setup.
// Assume all IPs are internal IPs.
internalIPRanges = append(internalIPRanges, netip.MustParsePrefix("0.0.0.0/0"))
internalIPRanges = append(internalIPRanges, netip.MustParsePrefix("::/0"))
return internalIPRanges, nil
}

// We got an IP from kubelet, parse it and convert it to a prefix containing only this IP
nodeIPRange, err := ipStringToPrefix(providedNodeIP)
if err != nil {
return nil, fmt.Errorf("annotation \"%s\" is invalid: %w", api.AnnotationAlphaProvidedIPAddr, err)
}
internalIPRanges = append(internalIPRanges, nodeIPRange)

// Support marking extra IPs as internal
extraInternalIPs, err := getAdditionalInternalIPs(node)
if err != nil {
// Unable to parse extra provided internal IP ranges, ignore them.
logrus.WithFields(logrus.Fields{
"namespace": node.Namespace,
"name": node.Name,
}).Warnf("%s, skip it", err.Error())

// Return list without extra user defined IP ranges.
return internalIPRanges, nil
}

for _, extraInternalIP := range extraInternalIPs {
extraRange, err := ipStringToPrefix(extraInternalIP)
if err != nil {
// IP (range) malformed, skip it.
logrus.WithFields(logrus.Fields{
"namespace": node.Namespace,
"name": node.Name,
}).Warnf("Unable to parse IP %s, skip it: %s", extraInternalIP, err.Error())
continue
}
internalIPRanges = append(internalIPRanges, extraRange)
}

return internalIPRanges, nil
}

// ipStringToPrefix converts an IP / CIDR range to a netip.Prefix. It supports IPv4 and IPv6 addresses.
// If a plain IP address is given, it returns a Prefix that only contains this IP.
// If a CIDR range is given, it returns a Prefix that contains the whole range.
func ipStringToPrefix(str string) (netip.Prefix, error) {
if strings.Contains(str, "/") {
// CIDR notation
return netip.ParsePrefix(str)
}

// Plain IP address
addr, err := netip.ParseAddr(str)
if err != nil {
return netip.Prefix{}, fmt.Errorf("failed to parse IP address \"%s\": %w", str, err)
}

// For a single IPv4 address, the prefix length is 32; for IPv6, it's 128.
prefixLen := 32
if addr.Is6() {
prefixLen = 128
}

// Create a prefix with the single address in it.
return addr.Prefix(prefixLen)
}

// User may want to mark some IPs of the node also as internal
Expand Down
Loading

0 comments on commit 3a2856b

Please sign in to comment.