Skip to content

Commit 5cb8f03

Browse files
committed
Add support for dual stack / IPv6 for node IPs. Allow defining internal IPs as CIDR ranges.
1 parent f6fe781 commit 5cb8f03

File tree

2 files changed

+409
-48
lines changed

2 files changed

+409
-48
lines changed

pkg/cloud-controller-manager/instance.go

Lines changed: 124 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7-
"net"
7+
"net/netip"
88
"slices"
9+
"strings"
910
"sync"
1011

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

73-
meta.NodeAddresses = getNodeAddresses(node, vmi)
74+
meta.NodeAddresses, err = getNodeAddresses(node, vmi)
75+
if err != nil {
76+
return nil, err
77+
}
7478

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

8690
// getNodeAddresses return nodeAddresses only when the value of annotation `alpha.kubernetes.io/provided-node-ip` is not empty
87-
func getNodeAddresses(node *v1.Node, vmi *kubevirtv1.VirtualMachineInstance) []v1.NodeAddress {
88-
providedNodeIP, ok := node.Annotations[api.AnnotationAlphaProvidedIPAddr]
89-
if !ok {
90-
return nil
91-
}
92-
93-
aiIPs, err := getAdditionalInternalIPs(node)
91+
func getNodeAddresses(node *v1.Node, vmi *kubevirtv1.VirtualMachineInstance) ([]v1.NodeAddress, error) {
92+
internalIPRanges, err := getInternalIPRanges(node)
9493
if err != nil {
95-
// if additional IPs are not correctly marked, only log an error, do not return this error
96-
logrus.WithFields(logrus.Fields{
97-
"namespace": node.Namespace,
98-
"name": node.Name,
99-
}).Debugf("%s, skip it", err.Error())
94+
return nil, err
10095
}
10196

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

101+
// Build a list of network names (names of NICs) on the VM.
102+
networkNames := make([]string, 0, len(vmi.Spec.Networks))
104103
for _, network := range vmi.Spec.Networks {
105-
for _, networkInterface := range vmi.Status.Interfaces {
106-
if network.Name == networkInterface.Name {
107-
if ip := net.ParseIP(networkInterface.IP); ip != nil && ip.To4() != nil {
108-
nodeAddr := v1.NodeAddress{
109-
Address: networkInterface.IP,
110-
}
111-
if networkInterface.IP == providedNodeIP || (aiIPs != nil && slices.Contains(aiIPs, networkInterface.IP)) {
112-
nodeAddr.Type = v1.NodeInternalIP
113-
} else {
114-
nodeAddr.Type = v1.NodeExternalIP
115-
}
116-
nodeAddresses = append(nodeAddresses, nodeAddr)
104+
networkNames = append(networkNames, network.Name)
105+
}
106+
107+
// Find all IP addresses of the VM
108+
for _, networkInterface := range vmi.Status.Interfaces {
109+
// The interface list might contain interfaces that do not belong to any NIC of the VM. Filter them out.
110+
if !slices.Contains(networkNames, networkInterface.Name) {
111+
// Ignore interface since it does not belong to one of the NICs.
112+
continue
113+
}
114+
115+
for _, ipStr := range networkInterface.IPs {
116+
ip, err := netip.ParseAddr(ipStr)
117+
if err != nil {
118+
// Failed to parse IP, skip it
119+
logrus.WithFields(logrus.Fields{
120+
"namespace": node.Namespace,
121+
"name": node.Name,
122+
}).Warnf("Unable to parse IP %s, skip it: %s", ipStr, err.Error())
123+
continue
124+
}
125+
126+
// Determine if the IP should be listed as an internal or external IP.
127+
ipType := v1.NodeExternalIP
128+
for _, internalPrefix := range internalIPRanges {
129+
if internalPrefix.Contains(ip) {
130+
// IP is an internal IP, no need to check further.
131+
ipType = v1.NodeInternalIP
132+
break
117133
}
118134
}
135+
136+
nodeAddresses = append(nodeAddresses, v1.NodeAddress{
137+
Type: ipType,
138+
Address: ip.String(),
139+
})
119140
}
120141
}
142+
121143
nodeAddresses = append(nodeAddresses, v1.NodeAddress{
122144
Type: v1.NodeHostName,
123145
Address: node.Name,
124146
})
125147

126-
return nodeAddresses
148+
return nodeAddresses, nil
149+
}
150+
151+
func getInternalIPRanges(node *v1.Node) ([]netip.Prefix, error) {
152+
internalIPRanges := make([]netip.Prefix, 0, 1) // Most of the time we would only have 1 internal range defined, the provided node IP
153+
154+
// Kubelet sets this node annotation if the --node-ip flag is set and an external cloud provider is used
155+
providedNodeIP, ok := node.Annotations[api.AnnotationAlphaProvidedIPAddr]
156+
if !ok {
157+
// Annotation is not set, this could be because we are running in a dual stack setup.
158+
// Assume all IPs are internal IPs.
159+
internalIPRanges = append(internalIPRanges, netip.MustParsePrefix("0.0.0.0/0"))
160+
internalIPRanges = append(internalIPRanges, netip.MustParsePrefix("::/0"))
161+
return internalIPRanges, nil
162+
}
163+
164+
// We got an IP from kubelet, parse it and convert it to a prefix containing only this IP
165+
nodeIPRange, err := ipStringToPrefix(providedNodeIP)
166+
if err != nil {
167+
return nil, fmt.Errorf("annotation \"%s\" is invalid: %w", api.AnnotationAlphaProvidedIPAddr, err)
168+
}
169+
internalIPRanges = append(internalIPRanges, nodeIPRange)
170+
171+
// Support marking extra IPs as internal
172+
extraInternalIPs, err := getAdditionalInternalIPs(node)
173+
if err != nil {
174+
// Unable to parse extra provided internal IP ranges, ignore them.
175+
logrus.WithFields(logrus.Fields{
176+
"namespace": node.Namespace,
177+
"name": node.Name,
178+
}).Warnf("%s, skip it", err.Error())
179+
180+
// Return list without extra user defined IP ranges.
181+
return internalIPRanges, nil
182+
}
183+
184+
for _, extraInternalIP := range extraInternalIPs {
185+
extraRange, err := ipStringToPrefix(extraInternalIP)
186+
if err != nil {
187+
// IP (range) malformed, skip it.
188+
logrus.WithFields(logrus.Fields{
189+
"namespace": node.Namespace,
190+
"name": node.Name,
191+
}).Warnf("Unable to parse IP %s, skip it: %s", extraInternalIP, err.Error())
192+
continue
193+
}
194+
internalIPRanges = append(internalIPRanges, extraRange)
195+
}
196+
197+
return internalIPRanges, nil
198+
}
199+
200+
// ipStringToPrefix converts an IP / CIDR range to a netip.Prefix. It supports IPv4 and IPv6 addresses.
201+
// If a plain IP address is given, it returns a Prefix that only contains this IP.
202+
// If a CIDR range is given, it returns a Prefix that contains the whole range.
203+
func ipStringToPrefix(str string) (netip.Prefix, error) {
204+
if strings.Contains(str, "/") {
205+
// CIDR notation
206+
return netip.ParsePrefix(str)
207+
}
208+
209+
// Plain IP address
210+
addr, err := netip.ParseAddr(str)
211+
if err != nil {
212+
return netip.Prefix{}, fmt.Errorf("failed to parse IP address \"%s\": %w", str, err)
213+
}
214+
215+
// For a single IPv4 address, the prefix length is 32; for IPv6, it's 128.
216+
prefixLen := 32
217+
if addr.Is6() {
218+
prefixLen = 128
219+
}
220+
221+
// Create a prefix with the single address in it.
222+
return addr.Prefix(prefixLen)
127223
}
128224

129225
// User may want to mark some IPs of the node also as internal

0 commit comments

Comments
 (0)