Skip to content

Commit 0c7eff1

Browse files
authored
Merge pull request kubernetes-sigs#1721 from adobley/ipam-condition-bound-ips-count
✨ ipam display count of address claims bound in condition
2 parents 813e3a4 + 01e40b1 commit 0c7eff1

File tree

2 files changed

+367
-134
lines changed

2 files changed

+367
-134
lines changed

pkg/services/govmomi/service.go

Lines changed: 228 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"encoding/base64"
2121
"fmt"
2222
"net/netip"
23+
"strings"
2324

2425
"github.com/pkg/errors"
2526
"github.com/vmware/govmomi/object"
@@ -53,6 +54,18 @@ import (
5354
// VMService provdes API to interact with the VMs using govmomi.
5455
type VMService struct{}
5556

57+
// ipamDeviceConfig aids and holds state for the process of parsing IPAM
58+
// addresses for a given device.
59+
type ipamDeviceConfig struct {
60+
DeviceIndex int
61+
IPAMAddresses []*ipamv1.IPAddress
62+
MACAddress string
63+
NetworkSpecGateway4 string
64+
IPAMConfigGateway4 string
65+
NetworkSpecGateway6 string
66+
IPAMConfigGateway6 string
67+
}
68+
5669
// ReconcileVM makes sure that the VM is in the desired state by:
5770
// 1. Creating the VM if it does not exist, then...
5871
// 2. Updating the VM with the bootstrap data, such as the cloud-init meta and user data, before...
@@ -279,17 +292,8 @@ func (vms *VMService) reconcileNetworkStatus(ctx *virtualMachineContext) error {
279292
func (vms *VMService) reconcileIPAddressClaims(ctx *context.VMContext) (bool, error) {
280293
for devIdx, device := range ctx.VSphereVM.Spec.Network.Devices {
281294
for poolRefIdx, poolRef := range device.AddressesFromPools {
282-
// check if claim exists
283-
ipAddrClaim := &ipamv1.IPAddressClaim{}
284295
ipAddrClaimName := IPAddressClaimName(ctx.VSphereVM.Name, devIdx, poolRefIdx)
285-
ipAddrClaimKey := apitypes.NamespacedName{
286-
Namespace: ctx.VSphereVM.Namespace,
287-
Name: ipAddrClaimName,
288-
}
289-
var err error
290-
if err = ctx.Client.Get(ctx, ipAddrClaimKey, ipAddrClaim); err != nil && !apierrors.IsNotFound(err) {
291-
return false, err
292-
}
296+
_, err := getIPAddrClaim(ctx, ipAddrClaimName)
293297
if err == nil {
294298
ctx.Logger.V(5).Info("IPAddressClaim found", "name", ipAddrClaimName)
295299
}
@@ -335,122 +339,241 @@ func createIPAddressClaim(ctx *context.VMContext, ipAddrClaimName string, poolRe
335339
// expected to contain a valid IP, Prefix and Gateway.
336340
func (vms *VMService) reconcileIPAddresses(ctx *virtualMachineContext) (bool, error) {
337341
ctx.IPAMState = map[string]infrav1.NetworkDeviceSpec{}
338-
for devIdx, device := range ctx.VSphereVM.Spec.Network.Devices {
339-
var ipAddrs []string
340-
var gateway4 string
341-
var gateway6 string
342-
343-
//TODO: Break this up into smaller functions
344-
for poolRefIdx := range device.AddressesFromPools {
345-
// check if claim exists
346-
ipAddrClaim := &ipamv1.IPAddressClaim{}
347-
ipAddrClaimName := IPAddressClaimName(ctx.VSphereVM.Name, devIdx, poolRefIdx)
348-
ipAddrClaimKey := apitypes.NamespacedName{
349-
Namespace: ctx.VSphereVM.Namespace,
350-
Name: ipAddrClaimName,
342+
343+
ipamDeviceConfigs, err := buildIPAMDeviceConfigs(ctx)
344+
if err != nil {
345+
return false, err
346+
}
347+
348+
var errs []error
349+
for _, ipamDeviceConfig := range ipamDeviceConfigs {
350+
var addressWithPrefixes []netip.Prefix
351+
for _, ipamAddress := range ipamDeviceConfig.IPAMAddresses {
352+
addressWithPrefix, err := parseAddressWithPrefix(ipamAddress)
353+
if err != nil {
354+
errs = append(errs, err)
355+
continue
356+
}
357+
358+
if slices.Contains(addressWithPrefixes, addressWithPrefix) {
359+
errs = append(errs,
360+
fmt.Errorf("IPAddress %s/%s is a duplicate of another address: %q",
361+
ipamAddress.Namespace,
362+
ipamAddress.Name,
363+
addressWithPrefix))
364+
continue
351365
}
352-
var err error
353-
ctx.Logger.V(5).Info("fetching IPAddressClaim", "name", ipAddrClaimKey.String())
354-
if err = ctx.Client.Get(ctx, ipAddrClaimKey, ipAddrClaim); err != nil && !apierrors.IsNotFound(err) {
366+
367+
gatewayAddr, err := parseGateway(ipamAddress, addressWithPrefix, ipamDeviceConfig)
368+
if err != nil {
369+
errs = append(errs, err)
370+
continue
371+
}
372+
373+
if gatewayAddr.Is4() {
374+
ipamDeviceConfig.IPAMConfigGateway4 = ipamAddress.Spec.Gateway
375+
} else {
376+
ipamDeviceConfig.IPAMConfigGateway6 = ipamAddress.Spec.Gateway
377+
}
378+
379+
addressWithPrefixes = append(addressWithPrefixes, addressWithPrefix)
380+
}
381+
382+
if len(addressWithPrefixes) > 0 {
383+
ctx.IPAMState[ipamDeviceConfig.MACAddress] = infrav1.NetworkDeviceSpec{
384+
IPAddrs: prefixesAsStrings(addressWithPrefixes),
385+
Gateway4: ipamDeviceConfig.IPAMConfigGateway4,
386+
Gateway6: ipamDeviceConfig.IPAMConfigGateway6,
387+
}
388+
}
389+
}
390+
391+
if len(errs) > 0 {
392+
var msgs []string
393+
for _, err := range errs {
394+
msgs = append(msgs, err.Error())
395+
}
396+
msg := strings.Join(msgs, "\n")
397+
conditions.MarkFalse(ctx.VSphereVM,
398+
infrav1.IPAddressClaimedCondition,
399+
infrav1.IPAddressInvalidReason,
400+
clusterv1.ConditionSeverityError,
401+
msg)
402+
return false, errors.New(msg)
403+
}
404+
405+
if len(ctx.IPAMState) > 0 {
406+
conditions.MarkTrue(ctx.VSphereVM, infrav1.IPAddressClaimedCondition)
407+
}
408+
409+
return true, nil
410+
}
411+
412+
// prefixesAsStrings converts []netip.Prefix to []string.
413+
func prefixesAsStrings(prefixes []netip.Prefix) []string {
414+
prefixSrings := make([]string, 0, len(prefixes))
415+
for _, prefix := range prefixes {
416+
prefixSrings = append(prefixSrings, prefix.String())
417+
}
418+
return prefixSrings
419+
}
420+
421+
// parseAddressWithPrefix converts a *ipamv1.IPAddress to a string, e.g. '10.0.0.1/24'.
422+
func parseAddressWithPrefix(ipamAddress *ipamv1.IPAddress) (netip.Prefix, error) {
423+
addressWithPrefix := fmt.Sprintf("%s/%d", ipamAddress.Spec.Address, ipamAddress.Spec.Prefix)
424+
parsedPrefix, err := netip.ParsePrefix(addressWithPrefix)
425+
if err != nil {
426+
return netip.Prefix{}, fmt.Errorf("IPAddress %s/%s has invalid ip address: %q",
427+
ipamAddress.Namespace,
428+
ipamAddress.Name,
429+
addressWithPrefix,
430+
)
431+
}
432+
433+
return parsedPrefix, nil
434+
}
435+
436+
// parseGateway parses the gateway address on a ipamv1.IPAddress and ensures it
437+
// does not conflict with the gateway addresses parsed from other
438+
// ipamv1.IPAddresses on the current device. Gateway addresses must be the same
439+
// family as the address on the ipamv1.IPAddress. Gateway addresses of one
440+
// family must match the other addresses of the same family.
441+
func parseGateway(ipamAddress *ipamv1.IPAddress, addressWithPrefix netip.Prefix, ipamDeviceConfig ipamDeviceConfig) (netip.Addr, error) {
442+
gatewayAddr, err := netip.ParseAddr(ipamAddress.Spec.Gateway)
443+
if err != nil {
444+
return netip.Addr{}, fmt.Errorf("IPAddress %s/%s has invalid gateway: %q",
445+
ipamAddress.Namespace,
446+
ipamAddress.Name,
447+
ipamAddress.Spec.Gateway,
448+
)
449+
}
450+
451+
if addressWithPrefix.Addr().Is4() != gatewayAddr.Is4() {
452+
return netip.Addr{}, fmt.Errorf("IPAddress %s/%s has mismatched gateway and address IP families",
453+
ipamAddress.Namespace,
454+
ipamAddress.Name,
455+
)
456+
}
457+
458+
if gatewayAddr.Is4() {
459+
if areGatewaysMismatched(ipamDeviceConfig.NetworkSpecGateway4, ipamAddress.Spec.Gateway) {
460+
return netip.Addr{}, fmt.Errorf("the IPv4 Gateway for IPAddress %s does not match the Gateway4 already configured on device (index %d)",
461+
ipamAddress.Name,
462+
ipamDeviceConfig.DeviceIndex,
463+
)
464+
}
465+
if areGatewaysMismatched(ipamDeviceConfig.IPAMConfigGateway4, ipamAddress.Spec.Gateway) {
466+
return netip.Addr{}, fmt.Errorf("the IPv4 IPAddresses assigned to the same device (index %d) do not have the same gateway",
467+
ipamDeviceConfig.DeviceIndex,
468+
)
469+
}
470+
} else {
471+
if areGatewaysMismatched(ipamDeviceConfig.NetworkSpecGateway6, ipamAddress.Spec.Gateway) {
472+
return netip.Addr{}, fmt.Errorf("the IPv6 Gateway for IPAddress %s does not match the Gateway6 already configured on device (index %d)",
473+
ipamAddress.Name,
474+
ipamDeviceConfig.DeviceIndex,
475+
)
476+
}
477+
if areGatewaysMismatched(ipamDeviceConfig.IPAMConfigGateway6, ipamAddress.Spec.Gateway) {
478+
return netip.Addr{}, fmt.Errorf("the IPv6 IPAddresses assigned to the same device (index %d) do not have the same gateway",
479+
ipamDeviceConfig.DeviceIndex,
480+
)
481+
}
482+
}
483+
484+
return gatewayAddr, nil
485+
}
486+
487+
// buildIPAMDeviceConfigs checks that all the IPAddressClaims have been
488+
// satisfied.
489+
// If each IPAddressClaim has an associated IPAddress, a slice of
490+
// ipamDeviceConfig is returned, one for each device with addressesFromPools.
491+
// If any of the IPAddressClaims do not have an associated IPAddress yet,
492+
// a false condition is set and an error is returned, effectively stopping the
493+
// current reconcilliation loop.
494+
func buildIPAMDeviceConfigs(ctx *virtualMachineContext) ([]ipamDeviceConfig, error) {
495+
boundClaims := 0
496+
totalClaims := 0
497+
ipamDeviceConfigs := []ipamDeviceConfig{}
498+
for devIdx, networkSpecDevice := range ctx.VSphereVM.Spec.Network.Devices {
499+
ipamDeviceConfig := ipamDeviceConfig{
500+
IPAMAddresses: []*ipamv1.IPAddress{},
501+
MACAddress: networkSpecDevice.MACAddr,
502+
NetworkSpecGateway4: networkSpecDevice.Gateway4,
503+
NetworkSpecGateway6: networkSpecDevice.Gateway6,
504+
DeviceIndex: devIdx,
505+
}
506+
507+
for poolRefIdx := range networkSpecDevice.AddressesFromPools {
508+
totalClaims++
509+
510+
ipAddrClaimName := IPAddressClaimName(ctx.VSphereVM.Name, ipamDeviceConfig.DeviceIndex, poolRefIdx)
511+
512+
ipAddrClaim, err := getIPAddrClaim(&ctx.VMContext, ipAddrClaimName)
513+
if err != nil {
355514
ctx.Logger.Error(err, "error fetching IPAddressClaim", "name", ipAddrClaimName)
356-
return false, err
515+
if apierrors.IsNotFound(err) {
516+
// it would be odd for this to occur, a findorcreate just happened in a previous step
517+
continue
518+
}
519+
return nil, err
357520
}
358521

522+
ctx.Logger.V(5).Info("fetched IPAddressClaim", "name", ipAddrClaimName, "namespace", ctx.VSphereVM.Namespace)
523+
359524
ipAddrName := ipAddrClaim.Status.AddressRef.Name
360-
ctx.Logger.V(5).Info("fetched IPAddressClaim", "name", ipAddrClaimName, "IPAddressClaim.Status.AddressRef.Name", ipAddrName)
361525
if ipAddrName == "" {
362-
ctx.Logger.V(5).Info("IPAddress name was empty on IPAddressClaim", "name", ipAddrClaimName, "IPAddressClaim.Status.AddressRef.Name", ipAddrName)
363-
msg := "Waiting for IPAddressClaim to have an IPAddress bound"
364-
markIPAddressClaimedConditionWaitingForClaimAddress(ctx.VSphereVM, msg)
365-
return false, errors.New(msg)
526+
ctx.Logger.V(5).Info("IPAddress not yet bound to IPAddressClaim", "name", ipAddrClaimName, "namespace", ctx.VSphereVM.Namespace)
527+
continue
366528
}
367529

368530
ipAddr := &ipamv1.IPAddress{}
369531
ipAddrKey := apitypes.NamespacedName{
370532
Namespace: ctx.VSphereVM.Namespace,
371533
Name: ipAddrName,
372534
}
373-
if err = ctx.Client.Get(ctx, ipAddrKey, ipAddr); err != nil {
374-
return false, err
375-
}
376535

377-
toAdd := fmt.Sprintf("%s/%d", ipAddr.Spec.Address, ipAddr.Spec.Prefix)
378-
parsedPrefix, err := netip.ParsePrefix(toAdd)
379-
if err != nil {
380-
msg := fmt.Sprintf("IPAddress %s/%s has invalid ip address: %q",
381-
ipAddrKey.Namespace,
382-
ipAddrKey.Name,
383-
toAdd,
384-
)
385-
return markIPAddressClaimedConditionInvalidIPWithError(ctx.VSphereVM, msg)
536+
if err := ctx.Client.Get(ctx, ipAddrKey, ipAddr); err != nil {
537+
// because the ref was set on the claim, it is expected this error should not occur
538+
return nil, err
386539
}
387540

388-
if !slices.Contains(ipAddrs, toAdd) {
389-
ipAddrs = append(ipAddrs, toAdd)
390-
391-
gatewayAddr, err := netip.ParseAddr(ipAddr.Spec.Gateway)
392-
if err != nil {
393-
msg := fmt.Sprintf("IPAddress %s/%s has invalid gateway: %q",
394-
ipAddrKey.Namespace,
395-
ipAddrKey.Name,
396-
ipAddr.Spec.Gateway,
397-
)
398-
return markIPAddressClaimedConditionInvalidIPWithError(ctx.VSphereVM, msg)
399-
}
400-
401-
if parsedPrefix.Addr().Is4() != gatewayAddr.Is4() {
402-
msg := fmt.Sprintf("IPAddress %s/%s has mismatched gateway and address IP families",
403-
ipAddrKey.Namespace,
404-
ipAddrKey.Name,
405-
)
406-
return markIPAddressClaimedConditionInvalidIPWithError(ctx.VSphereVM, msg)
407-
}
408-
409-
if gatewayAddr.Is4() {
410-
if device.Gateway4 != "" && device.Gateway4 != ipAddr.Spec.Gateway {
411-
msg := fmt.Sprintf("The IPv4 Gateway for IPAddress %s does not match the Gateway4 already configured on device (index %d)",
412-
ipAddrName,
413-
devIdx,
414-
)
415-
return markIPAddressClaimedConditionInvalidIPWithError(ctx.VSphereVM, msg)
416-
}
417-
if gateway4 != "" && gateway4 != ipAddr.Spec.Gateway {
418-
msg := fmt.Sprintf("The IPv4 IPAddresses assigned to the same device (index %d) do not have the same gateway",
419-
devIdx,
420-
)
421-
return markIPAddressClaimedConditionInvalidIPWithError(ctx.VSphereVM, msg)
422-
}
423-
gateway4 = ipAddr.Spec.Gateway
424-
} else {
425-
if device.Gateway6 != "" && device.Gateway6 != ipAddr.Spec.Gateway {
426-
msg := fmt.Sprintf("The IPv6 Gateway for IPAddress %s does not match the Gateway6 already configured on device (index %d)",
427-
ipAddrName,
428-
devIdx,
429-
)
430-
return markIPAddressClaimedConditionInvalidIPWithError(ctx.VSphereVM, msg)
431-
}
432-
if gateway6 != "" && gateway6 != ipAddr.Spec.Gateway {
433-
msg := fmt.Sprintf("The IPv6 IPAddresses assigned to the same device (index %d) do not have the same gateway",
434-
devIdx,
435-
)
436-
return markIPAddressClaimedConditionInvalidIPWithError(ctx.VSphereVM, msg)
437-
}
438-
gateway6 = ipAddr.Spec.Gateway
439-
}
440-
}
441-
ctx.IPAMState[device.MACAddr] = infrav1.NetworkDeviceSpec{
442-
IPAddrs: ipAddrs,
443-
Gateway4: gateway4,
444-
Gateway6: gateway6,
445-
}
541+
ipamDeviceConfig.IPAMAddresses = append(ipamDeviceConfig.IPAMAddresses, ipAddr)
542+
boundClaims++
446543
}
544+
ipamDeviceConfigs = append(ipamDeviceConfigs, ipamDeviceConfig)
447545
}
448546

449-
if len(ctx.IPAMState) > 0 {
450-
conditions.MarkTrue(ctx.VSphereVM, infrav1.IPAddressClaimedCondition)
547+
if boundClaims < totalClaims {
548+
msg := fmt.Sprintf("Waiting for IPAddressClaim to have an IPAddress bound, %d out of %d bound", boundClaims, totalClaims)
549+
markIPAddressClaimedConditionWaitingForClaimAddress(ctx.VSphereVM, msg)
550+
return nil, errors.New(msg)
451551
}
452552

453-
return true, nil
553+
return ipamDeviceConfigs, nil
554+
}
555+
556+
// areGatewaysMismatched checks that a gateway for a device is equal to an
557+
// IPAddresses gateway. We can assume that IPAddresses will always have
558+
// gateways so we do not need to check for empty string. It is possible to
559+
// configure a device and not a gateway, we don't want to fail in that case.
560+
func areGatewaysMismatched(deviceGateway, ipAddressGateway string) bool {
561+
return deviceGateway != "" && deviceGateway != ipAddressGateway
562+
}
563+
564+
// getIPAddrClaim fetches an IPAddressClaim from the api with the given name.
565+
func getIPAddrClaim(ctx *context.VMContext, ipAddrClaimName string) (*ipamv1.IPAddressClaim, error) {
566+
ipAddrClaim := &ipamv1.IPAddressClaim{}
567+
ipAddrClaimKey := apitypes.NamespacedName{
568+
Namespace: ctx.VSphereVM.Namespace,
569+
Name: ipAddrClaimName,
570+
}
571+
572+
ctx.Logger.V(5).Info("fetching IPAddressClaim", "name", ipAddrClaimKey.String())
573+
if err := ctx.Client.Get(ctx, ipAddrClaimKey, ipAddrClaim); err != nil {
574+
return nil, err
575+
}
576+
return ipAddrClaim, nil
454577
}
455578

456579
func (vms *VMService) reconcileMetadata(ctx *virtualMachineContext) (bool, error) {
@@ -854,15 +977,6 @@ func (vms *VMService) reconcileClusterModuleMembership(ctx *virtualMachineContex
854977
return nil
855978
}
856979

857-
func markIPAddressClaimedConditionInvalidIPWithError(vm *infrav1.VSphereVM, msg string) (bool, error) {
858-
conditions.MarkFalse(vm,
859-
infrav1.IPAddressClaimedCondition,
860-
infrav1.IPAddressInvalidReason,
861-
clusterv1.ConditionSeverityError,
862-
msg)
863-
return false, errors.New(msg)
864-
}
865-
866980
func markIPAddressClaimedConditionWaitingForClaimAddress(vm *infrav1.VSphereVM, msg string) {
867981
conditions.MarkFalse(vm,
868982
infrav1.IPAddressClaimedCondition,

0 commit comments

Comments
 (0)