From 08f4d79c0c860f690a2d4448c0111908c070660d Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Fri, 21 Aug 2026 12:06:08 -0500 Subject: [PATCH] fix: read a location's subnet range from spec when status is absent A location's Subnet arrives by Karmada propagation, which carries spec and never the status subresource, and no cell runs the reconciler that would populate status there. networksForContext read status only, so it found no address space, never created the VPC, and every interface stayed at Prepared=False/AwaitingVPC. Confirmed on us-central-1-staging-lab: the propagated copy carries startAddress fd20:: and prefixLength 64 in spec, with an empty status. Status still wins where it exists, which is the control plane's own copy. Co-Authored-By: Claude Opus 5 (1M context) --- .../controller/networkcontext_controller.go | 18 ++++++- .../networkcontext_controller_test.go | 52 +++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 internal/controller/networkcontext_controller_test.go 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") + } +}