diff --git a/internal/controller/networkcontext_controller.go b/internal/controller/networkcontext_controller.go index 949a05e..bddb95f 100644 --- a/internal/controller/networkcontext_controller.go +++ b/internal/controller/networkcontext_controller.go @@ -118,16 +118,30 @@ func (r *NetworkContextReconciler) networksForContext( if subnet.Spec.NetworkContext.Name != networkContext.Name { continue } - if subnet.Status.StartAddress == nil || subnet.Status.PrefixLength == nil { + start, prefixLength, ok := subnetRange(&subnet) + if !ok { continue } networks = append(networks, cloudv1alpha1.Network( - fmt.Sprintf("%s/%d", *subnet.Status.StartAddress, *subnet.Status.PrefixLength))) + fmt.Sprintf("%s/%d", start, prefixLength))) } slices.Sort(networks) return networks, nil } +// subnetRange reads a subnet's allocated range, preferring status and falling +// back to spec. A location's copy arrives by propagation, which carries spec +// and never status. +func subnetRange(subnet *networkingv1alpha.Subnet) (string, int32, bool) { + if subnet.Status.StartAddress != nil && subnet.Status.PrefixLength != nil { + return *subnet.Status.StartAddress, *subnet.Status.PrefixLength, true + } + if subnet.Spec.StartAddress != "" && subnet.Spec.PrefixLength != 0 { + return subnet.Spec.StartAddress, subnet.Spec.PrefixLength, true + } + return "", 0, false +} + // allocateVPCIdentifier draws a random 48-bit identifier not already in use. // A single leader-elected controller is the only writer, so a list plus a // collision check serializes correctly. diff --git a/internal/controller/networkcontext_controller_test.go b/internal/controller/networkcontext_controller_test.go new file mode 100644 index 0000000..5734222 --- /dev/null +++ b/internal/controller/networkcontext_controller_test.go @@ -0,0 +1,52 @@ +/* +Copyright © 2026 Datum Technology, Inc. All rights reserved. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package controller + +import ( + "testing" + + networkingv1alpha "go.datum.net/network-services-operator/api/v1alpha" +) + +// A location's Subnet arrives by propagation, which carries spec and never +// status, so the allocated range has to be readable from either. +func TestSubnetRangePrefersStatusAndFallsBackToSpec(t *testing.T) { + start := "fd00::" + var length int32 = 48 + + withStatus := &networkingv1alpha.Subnet{ + Spec: networkingv1alpha.SubnetSpec{StartAddress: "fd20::", PrefixLength: 64}, + Status: networkingv1alpha.SubnetStatus{StartAddress: &start, PrefixLength: &length}, + } + gotStart, gotLength, ok := subnetRange(withStatus) + if !ok || gotStart != "fd00::" || gotLength != 48 { + t.Fatalf("status should win when present, got %q/%d ok=%v", gotStart, gotLength, ok) + } + + specOnly := &networkingv1alpha.Subnet{ + Spec: networkingv1alpha.SubnetSpec{StartAddress: "fd20::", PrefixLength: 64}, + } + gotStart, gotLength, ok = subnetRange(specOnly) + if !ok || gotStart != "fd20::" || gotLength != 64 { + t.Fatalf("a propagated copy carries spec only, got %q/%d ok=%v", gotStart, gotLength, ok) + } + + if _, _, ok := subnetRange(&networkingv1alpha.Subnet{}); ok { + t.Fatal("an unallocated subnet should yield nothing") + } +}