Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 35 additions & 8 deletions pkg/controllers/networkinfo/networkinfo_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package networkinfo
import (
"context"
"fmt"
"net"
"strings"
"time"

stderrors "github.com/vmware/vsphere-automation-sdk-go/lib/vapi/std/errors"
Expand Down Expand Up @@ -319,7 +321,8 @@ func (r *NetworkInfoReconciler) Reconcile(ctx context.Context, req ctrl.Request)
}
}

snatIP, aviSubnetPath, aviSECIDR, nsxLBSNATIP, lbIP := "", "", "", "", ""
snatIP, aviSubnetPath, lbIP := "", "", ""
var lbBackendIPs []string
var networkStack v1alpha1.NetworkStackType
networkStack, err = r.Service.GetNetworkStackFromNC(nc)
if err != nil {
Expand Down Expand Up @@ -390,7 +393,8 @@ func (r *NetworkInfoReconciler) Reconcile(ctx context.Context, req ctrl.Request)
// nsx bug, if set LoadBalancerVpcEndpoint.Enabled to false, when read this VPC back,
// LoadBalancerVpcEndpoint.Enabled will become a nil pointer.
if lbProvider == vpc.AVILB && createdVpc.LoadBalancerVpcEndpoint != nil && createdVpc.LoadBalancerVpcEndpoint.Enabled != nil && *createdVpc.LoadBalancerVpcEndpoint.Enabled {
aviSubnetPath, aviSECIDR, err = r.Service.GetAVISubnetInfo(*createdVpc)
var aviCIDRs []string
aviSubnetPath, aviCIDRs, err = r.Service.GetAVISubnetInfo(*createdVpc)
if err != nil {
log.Error(err, "Failed to read AVI LB Subnet path and CIDR", "VPC", createdVpc.Id)
state := &v1alpha1.VPCState{
Expand All @@ -403,10 +407,12 @@ func (r *NetworkInfoReconciler) Reconcile(ctx context.Context, req ctrl.Request)
setNSNetworkReadyCondition(ctx, r.Client, req.Namespace, nsMsgVPCAviSubnetError.getNSNetworkCondition(err))
return common.ResultRequeueAfter10sec, err
}
lbIP = aviSECIDR
lbIP = primaryLBIP(aviCIDRs)
lbBackendIPs = aviCIDRs
} else if lbProvider == vpc.NSXLB && len(nsxLBSPath) > 0 && len(vpcConnectivityProfilePath) > 0 {
// Only check SNat IP when LB capability is ready and vpcConnectivityProfile exists.
nsxLBSNATIP, err = r.getNSXLBSNATIP(nc, createdVpc, vpcConnectivityProfilePath, gatewayConnectionReady, serviceClusterReady, networkStack == v1alpha1.VLANBackedVPC)
var nsxLBSNATIPs []string
nsxLBSNATIPs, err = r.getNSXLBSNATIP(nc, createdVpc, vpcConnectivityProfilePath, gatewayConnectionReady, serviceClusterReady, networkStack == v1alpha1.VLANBackedVPC)
if err != nil {
log.Error(err, "Failed to read NSX LB SNAT IP", "VPC", createdVpc.Id)
state := &v1alpha1.VPCState{
Expand All @@ -419,13 +425,15 @@ func (r *NetworkInfoReconciler) Reconcile(ctx context.Context, req ctrl.Request)
setNSNetworkReadyCondition(ctx, r.Client, req.Namespace, nsMsgVPCNSXLBSNATIPError.getNSNetworkCondition(err))
return common.ResultRequeueAfter10sec, err
}
lbIP = nsxLBSNATIP
lbIP = primaryLBIP(nsxLBSNATIPs)
lbBackendIPs = nsxLBSNATIPs
}

state := &v1alpha1.VPCState{
Name: *createdVpc.DisplayName,
DefaultSNATIP: snatIP,
LoadBalancerIPAddresses: lbIP,
LoadBalancerBackendIPs: lbBackendIPs,
PrivateIPs: privateIPs,
NetworkStack: networkStack,
}
Expand All @@ -443,15 +451,15 @@ func (r *NetworkInfoReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return common.ResultNormal, nil
}

func (r *NetworkInfoReconciler) getNSXLBSNATIP(nc *v1alpha1.VPCNetworkConfiguration, createdVpc *model.Vpc, vpcConnectivityProfilePath string, gatewayConnectionReady, serviceClusterReady bool, tepLess bool) (string, error) {
func (r *NetworkInfoReconciler) getNSXLBSNATIP(nc *v1alpha1.VPCNetworkConfiguration, createdVpc *model.Vpc, vpcConnectivityProfilePath string, gatewayConnectionReady, serviceClusterReady bool, tepLess bool) ([]string, error) {
checkGatewayConnection := gatewayConnectionReady
checkServiceCluster := serviceClusterReady
// Precreated VPC uses different connectivity profile from system VPC
// Need to check the profile separately
if vpc.IsPreCreatedVPC(nc) {
connectionStatus, err := r.Service.ValidateConnectionStatus(nc, vpcConnectivityProfilePath)
if err != nil {
return "", err
return nil, err
}
checkGatewayConnection = connectionStatus.GatewayConnectionReady
checkServiceCluster = connectionStatus.ServiceClusterReady
Expand All @@ -463,7 +471,26 @@ func (r *NetworkInfoReconciler) getNSXLBSNATIP(nc *v1alpha1.VPCNetworkConfigurat
// DTGW is used for NSX LB
return r.Service.GetNSXLBSNATIP(*createdVpc, "service-interface", tepLess)
}
return "", nil
return nil, nil
}

// primaryLBIP selects the canonical single IP/CIDR for LoadBalancerIPAddresses following
// the Kubernetes dual-stack convention: prefer IPv4 for dual-stack, fall back to the first
// entry for IPv6-only, and return "" for an empty slice.
func primaryLBIP(ips []string) string {
for _, ip := range ips {
raw := ip
if idx := strings.Index(ip, "/"); idx != -1 {
raw = ip[:idx]
}
if net.ParseIP(raw) != nil && net.ParseIP(raw).To4() != nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Can we store the result of calling net.ParseIP(raw) in a local variable instead of calling it repeatedly here?

return ip
}
}
if len(ips) > 0 {
return ips[0]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The IPv6-only fallback uses ips[0] without checking if it is a valid IP address. If the list contains invalid data, this could potentially write an invalid string into LoadBalancerIPAddresses?

}
return ""
}

func (r *NetworkInfoReconciler) setupWithManager(mgr ctrl.Manager) error {
Expand Down
115 changes: 87 additions & 28 deletions pkg/controllers/networkinfo/networkinfo_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,8 @@ func TestNetworkInfoReconciler_Reconcile(t *testing.T) {
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNetworkStackFromNC", func(_ *vpc.VPCService, _ *v1alpha1.VPCNetworkConfiguration) (v1alpha1.NetworkStackType, error) {
return v1alpha1.FullStackVPC, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) (string, error) {
return "100.64.0.3", nil
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) ([]string, error) {
return []string{"100.64.0.3"}, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetDefaultNSXLBSPathByVPC", func(_ *vpc.VPCService, _ string) string {
return "lbs-path"
Expand Down Expand Up @@ -289,8 +289,8 @@ func TestNetworkInfoReconciler_Reconcile(t *testing.T) {
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetLBProvider", func(_ *vpc.VPCService) (vpc.LBProvider, error) {
return vpc.NSXLB, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) (string, error) {
return "100.64.0.3", nil
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) ([]string, error) {
return []string{"100.64.0.3"}, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetDefaultNSXLBSPathByVPC", func(_ *vpc.VPCService, _ string) string {
return "lbs-path"
Expand Down Expand Up @@ -362,8 +362,8 @@ func TestNetworkInfoReconciler_Reconcile(t *testing.T) {
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetLBProvider", func(_ *vpc.VPCService) (vpc.LBProvider, error) {
return vpc.NSXLB, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) (string, error) {
return "100.64.0.3", nil
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) ([]string, error) {
return []string{"100.64.0.3"}, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetDefaultNSXLBSPathByVPC", func(_ *vpc.VPCService, _ string) string {
return "lbs-path"
Expand Down Expand Up @@ -469,8 +469,8 @@ func TestNetworkInfoReconciler_Reconcile(t *testing.T) {
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetLBProvider", func(_ *vpc.VPCService) (vpc.LBProvider, error) {
return vpc.NSXLB, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) (string, error) {
return "100.64.0.3", nil
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) ([]string, error) {
return []string{"100.64.0.3"}, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "CreateOrUpdateVPC", func(_ *vpc.VPCService, ctx context.Context, _ *v1alpha1.NetworkInfo, _ *v1alpha1.VPCNetworkConfiguration, _ vpc.LBProvider) (*model.Vpc, error) {
return &model.Vpc{
Expand Down Expand Up @@ -500,8 +500,8 @@ func TestNetworkInfoReconciler_Reconcile(t *testing.T) {
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetLBProvider", func(_ *vpc.VPCService) (vpc.LBProvider, error) {
return vpc.NSXLB, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) (string, error) {
return "100.64.0.3", nil
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) ([]string, error) {
return []string{"100.64.0.3"}, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetDefaultSNATIP", func(_ *vpc.VPCService, _ model.Vpc) (string, error) {
return "snat-ip", nil
Expand Down Expand Up @@ -561,8 +561,8 @@ func TestNetworkInfoReconciler_Reconcile(t *testing.T) {
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetLBProvider", func(_ *vpc.VPCService) (vpc.LBProvider, error) {
return vpc.NSXLB, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) (string, error) {
return "100.64.0.3", nil
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) ([]string, error) {
return []string{"100.64.0.3"}, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "CreateOrUpdateVPC", func(_ *vpc.VPCService, ctx context.Context, _ *v1alpha1.NetworkInfo, _ *v1alpha1.VPCNetworkConfiguration, _ vpc.LBProvider) (*model.Vpc, error) {
return &model.Vpc{
Expand Down Expand Up @@ -643,8 +643,8 @@ func TestNetworkInfoReconciler_Reconcile(t *testing.T) {
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetLBProvider", func(_ *vpc.VPCService) (vpc.LBProvider, error) {
return vpc.NSXLB, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) (string, error) {
return "100.64.0.3", nil
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) ([]string, error) {
return []string{"100.64.0.3"}, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "CreateOrUpdateVPC", func(_ *vpc.VPCService, ctx context.Context, _ *v1alpha1.NetworkInfo, _ *v1alpha1.VPCNetworkConfiguration, _ vpc.LBProvider) (*model.Vpc, error) {
return &model.Vpc{
Expand Down Expand Up @@ -729,8 +729,8 @@ func TestNetworkInfoReconciler_Reconcile(t *testing.T) {
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetLBProvider", func(_ *vpc.VPCService) (vpc.LBProvider, error) {
return vpc.NSXLB, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) (string, error) {
return "100.64.0.3", nil
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) ([]string, error) {
return []string{"100.64.0.3"}, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "CreateOrUpdateVPC", func(_ *vpc.VPCService, ctx context.Context, _ *v1alpha1.NetworkInfo, _ *v1alpha1.VPCNetworkConfiguration, _ vpc.LBProvider) (*model.Vpc, error) {
return &model.Vpc{
Expand All @@ -757,8 +757,8 @@ func TestNetworkInfoReconciler_Reconcile(t *testing.T) {
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetDefaultNSXLBSPathByVPC", func(_ *vpc.VPCService, _ string) string {
return "lbs-path"
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) (string, error) {
return "100.64.0.3", nil
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) ([]string, error) {
return []string{"100.64.0.3"}, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetDefaultSNATIP", func(_ *vpc.VPCService, _ model.Vpc) (string, error) {
return "snat-ip", nil
Expand Down Expand Up @@ -816,8 +816,8 @@ func TestNetworkInfoReconciler_Reconcile(t *testing.T) {
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetLBProvider", func(_ *vpc.VPCService) (vpc.LBProvider, error) {
return vpc.NSXLB, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) (string, error) {
return "100.64.0.3", nil
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) ([]string, error) {
return []string{"100.64.0.3"}, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "CreateOrUpdateVPC", func(_ *vpc.VPCService, ctx context.Context, _ *v1alpha1.NetworkInfo, _ *v1alpha1.VPCNetworkConfiguration, _ vpc.LBProvider) (*model.Vpc, error) {
return &model.Vpc{
Expand Down Expand Up @@ -987,9 +987,9 @@ func TestNetworkInfoReconciler_Reconcile(t *testing.T) {
}, nil},
Times: 2,
}})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, interfaceID string) (string, error) {
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, interfaceID string) ([]string, error) {
assert.Equal(t, "gateway-interface", interfaceID)
return "100.64.0.3", nil
return []string{"100.64.0.3"}, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNetworkStackFromNC", func(_ *vpc.VPCService, _ *v1alpha1.VPCNetworkConfiguration) (v1alpha1.NetworkStackType, error) {
return v1alpha1.FullStackVPC, nil
Expand Down Expand Up @@ -1111,8 +1111,8 @@ func TestNetworkInfoReconciler_Reconcile(t *testing.T) {
}, nil
})
// GetAVISubnetInfo should be called even without connectivity profile
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetAVISubnetInfo", func(_ *vpc.VPCService, _ model.Vpc) (string, string, error) {
return "/orgs/default/projects/project-quality/vpcs/fake-vpc/subnets/avi-subnet", "100.64.0.0/24", nil
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetAVISubnetInfo", func(_ *vpc.VPCService, _ model.Vpc) (string, []string, error) {
return "/orgs/default/projects/project-quality/vpcs/fake-vpc/subnets/avi-subnet", []string{"100.64.0.0/24"}, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNetworkStackFromNC", func(_ *vpc.VPCService, _ *v1alpha1.VPCNetworkConfiguration) (v1alpha1.NetworkStackType, error) {
return v1alpha1.FullStackVPC, nil
Expand Down Expand Up @@ -1183,9 +1183,9 @@ func TestNetworkInfoReconciler_Reconcile(t *testing.T) {
return "", nil
})
// This should NOT be called when vpcConnectivityProfilePath is empty
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) (string, error) {
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) ([]string, error) {
assert.FailNow(t, "GetNSXLBSNATIP should not be called when vpcConnectivityProfilePath is empty")
return "", nil
return nil, nil
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNetworkStackFromNC", func(_ *vpc.VPCService, _ *v1alpha1.VPCNetworkConfiguration) (v1alpha1.NetworkStackType, error) {
return v1alpha1.FullStackVPC, nil
Expand Down Expand Up @@ -1275,8 +1275,8 @@ func TestNetworkInfoReconciler_Reconcile(t *testing.T) {
assert.FailNow(t, "should set VPCNetworkConfiguration status with AutoSnatEnabled=false")
}
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) (string, error) {
return "", fmt.Errorf("tier1 uplink port IP not found")
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNSXLBSNATIP", func(_ *vpc.VPCService, _ model.Vpc, _ string) ([]string, error) {
return nil, fmt.Errorf("tier1 uplink port IP not found")
})
patches.ApplyMethod(reflect.TypeOf(r.Service), "GetNetworkStackFromNC", func(_ *vpc.VPCService, _ *v1alpha1.VPCNetworkConfiguration) (v1alpha1.NetworkStackType, error) {
return v1alpha1.FullStackVPC, nil
Expand Down Expand Up @@ -2298,3 +2298,62 @@ func (m *MockManager) Add(runnable manager.Runnable) error {
func (m *MockManager) Start(context.Context) error {
return nil
}

func TestPrimaryLBIP(t *testing.T) {
tests := []struct {
name string
ips []string
want string
}{
{
name: "nil slice returns empty string",
ips: nil,
want: "",
},
{
name: "empty slice returns empty string",
ips: []string{},
want: "",
},
{
name: "single IPv4 bare IP",
ips: []string{"100.64.0.1"},
want: "100.64.0.1",
},
{
name: "single IPv6 bare IP falls back to first",
ips: []string{"2001:db8::1"},
want: "2001:db8::1",
},
{
name: "dual-stack: IPv4 preferred (bare IPs)",
ips: []string{"100.64.0.1", "2001:db8::1"},
want: "100.64.0.1",
},
{
name: "dual-stack: IPv4 preferred even when IPv6 is first",
ips: []string{"2001:db8::1", "100.64.0.1"},
want: "100.64.0.1",
},
{
name: "single IPv4 CIDR (AVI format)",
ips: []string{"100.64.0.0/24"},
want: "100.64.0.0/24",
},
{
name: "single IPv6 CIDR falls back to first",
ips: []string{"2001:db8::/64"},
want: "2001:db8::/64",
},
{
name: "dual-stack CIDR: IPv4 preferred",
ips: []string{"100.64.0.0/24", "2001:db8::/64"},
want: "100.64.0.0/24",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, primaryLBIP(tt.ips))
})
}
}
2 changes: 2 additions & 0 deletions pkg/controllers/networkinfo/networkinfo_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ func setNetworkInfoVPCStatus(client client.Client, ctx context.Context, obj clie
}
slices.Sort(existingVPC.PrivateIPs)
slices.Sort(createdVPC.PrivateIPs)
slices.Sort(existingVPC.LoadBalancerBackendIPs)
slices.Sort(createdVPC.LoadBalancerBackendIPs)
if reflect.DeepEqual(*existingVPC, *createdVPC) {
return
}
Expand Down
42 changes: 42 additions & 0 deletions pkg/controllers/networkinfo/networkinfo_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,48 @@ func TestGetNSNetworkCondition(t *testing.T) {
require.True(t, nsConditionEquals(vpcNotReadyCondition, *nsMsgVPCCreateUpdateError.getNSNetworkCondition(msgErr)))
}

// TestSetNetworkInfoVPCStatusIdempotency verifies that reordered LoadBalancerBackendIPs do
// not trigger a spurious CR update because setNetworkInfoVPCStatus sorts both slices before
// reflect.DeepEqual.
func TestSetNetworkInfoVPCStatusIdempotency(t *testing.T) {
ctx := context.TODO()
scheme := clientgoscheme.Scheme
v1alpha1.AddToScheme(scheme)

// Build a NetworkInfo CR that already has a VPC state with LoadBalancerBackendIPs.
existingNetworkInfo := &v1alpha1.NetworkInfo{
ObjectMeta: metav1.ObjectMeta{Name: "ni", Namespace: "default"},
VPCs: []v1alpha1.VPCState{
{
Name: "vpc1",
LoadBalancerIPAddresses: "100.64.0.1",
LoadBalancerBackendIPs: []string{"100.64.0.1", "2001:db8::1"},
},
},
}
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(existingNetworkInfo).Build()

// The reconcile produces the same state but with IPs in reverse order.
reorderedVPC := &v1alpha1.VPCState{
Name: "vpc1",
LoadBalancerIPAddresses: "100.64.0.1",
LoadBalancerBackendIPs: []string{"2001:db8::1", "100.64.0.1"},
}

// setNetworkInfoVPCStatus must NOT call fakeClient.Update because the states are equal
// after sorting. We capture any update by reading the CR back; ResourceVersion should
// be unchanged (fake client does not increment it on no-op, but the Update itself should
// not be called when the sorted slices are equal).
originalRV := existingNetworkInfo.ResourceVersion

setNetworkInfoVPCStatus(fakeClient, ctx, existingNetworkInfo, metav1.Time{}, reorderedVPC)

// Reload the CR and confirm no actual write occurred.
refreshed := &v1alpha1.NetworkInfo{}
require.NoError(t, fakeClient.Get(ctx, apitypes.NamespacedName{Name: "ni", Namespace: "default"}, refreshed))
assert.Equal(t, originalRV, refreshed.ResourceVersion, "CR should not be updated when only IP order differs")
}

func TestHasPodOrVMDefaultSubnets(t *testing.T) {
subnets := []v1alpha1.SharedSubnet{
{
Expand Down
Loading
Loading