From 4fc9371c0316abfa882a2f56ba2374f118a4378c Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 20 Aug 2026 18:45:00 -0500 Subject: [PATCH] Publish the bound NetworkInterface on Instance status An infrastructure provider needs the interface an instance ended up with. Without a published reference it has to rebuild compute's private claim name to find it, which is a copy of an internal convention in another repo. The claim already carries the reference, so this is a passthrough. Co-Authored-By: Claude Opus 5 (1M context) --- api/v1alpha/instance_types.go | 7 +++ api/v1alpha/zz_generated.deepcopy.go | 5 ++ .../compute.datumapis.com_instances.yaml | 14 ++++++ internal/controller/networkinterfaceclaim.go | 5 ++ .../networkinterfaceclaim_controller_test.go | 49 +++++++++++++++++++ 5 files changed, 80 insertions(+) diff --git a/api/v1alpha/instance_types.go b/api/v1alpha/instance_types.go index 0fecebe5..b24f16ab 100644 --- a/api/v1alpha/instance_types.go +++ b/api/v1alpha/instance_types.go @@ -415,6 +415,13 @@ type InstanceNetworkInterfaceStatus struct { // +kubebuilder:validation:Optional Name string `json:"name,omitempty"` + // The NetworkInterface bound to this entry, in the instance's namespace. An + // infrastructure provider follows it to configure the NIC, so it never has to + // derive the name of the claim that produced it. + // + // +kubebuilder:validation:Optional + NetworkInterfaceRef *networkingv1alpha.LocalNetworkInterfaceRef `json:"networkInterfaceRef,omitempty"` + // The addresses the interface holds inside its network, each with its prefix // length and, once the location has a subnet, its gateway. // diff --git a/api/v1alpha/zz_generated.deepcopy.go b/api/v1alpha/zz_generated.deepcopy.go index c67e77ea..f5002879 100644 --- a/api/v1alpha/zz_generated.deepcopy.go +++ b/api/v1alpha/zz_generated.deepcopy.go @@ -479,6 +479,11 @@ func (in *InstanceNetworkInterfaceNetworkPolicy) DeepCopy() *InstanceNetworkInte // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InstanceNetworkInterfaceStatus) DeepCopyInto(out *InstanceNetworkInterfaceStatus) { *out = *in + if in.NetworkInterfaceRef != nil { + in, out := &in.NetworkInterfaceRef, &out.NetworkInterfaceRef + *out = new(apiv1alpha.LocalNetworkInterfaceRef) + **out = **in + } if in.Addresses != nil { in, out := &in.Addresses, &out.Addresses *out = make([]InstanceNetworkInterfaceAddress, len(*in)) diff --git a/config/base/crd/bases/compute.datumapis.com_instances.yaml b/config/base/crd/bases/compute.datumapis.com_instances.yaml index 5e050e61..521aa7dd 100644 --- a/config/base/crd/bases/compute.datumapis.com_instances.yaml +++ b/config/base/crd/bases/compute.datumapis.com_instances.yaml @@ -1313,6 +1313,20 @@ spec: The name of the interface this entry reports on, matching the name in the instance's spec. type: string + networkInterfaceRef: + description: |- + The NetworkInterface bound to this entry, in the instance's namespace. An + infrastructure provider follows it to configure the NIC, so it never has to + derive the name of the claim that produced it. + properties: + name: + description: name is the network interface name. + maxLength: 253 + minLength: 1 + type: string + required: + - name + type: object type: object type: array suspended: diff --git a/internal/controller/networkinterfaceclaim.go b/internal/controller/networkinterfaceclaim.go index 54f9efa3..3c4078df 100644 --- a/internal/controller/networkinterfaceclaim.go +++ b/internal/controller/networkinterfaceclaim.go @@ -129,6 +129,11 @@ func instanceNetworkInterfaceStatus( return status } + // Published so a provider reads the interface without deriving the claim name. + if ref := claim.Status.NetworkInterfaceRef; ref != nil { + status.NetworkInterfaceRef = ref.DeepCopy() + } + for _, address := range claim.Status.Addresses { status.Addresses = append(status.Addresses, computev1alpha.InstanceNetworkInterfaceAddress{ Family: address.Family, diff --git a/internal/controller/networkinterfaceclaim_controller_test.go b/internal/controller/networkinterfaceclaim_controller_test.go index b27a0f9a..2a2d9137 100644 --- a/internal/controller/networkinterfaceclaim_controller_test.go +++ b/internal/controller/networkinterfaceclaim_controller_test.go @@ -368,3 +368,52 @@ func TestCheckForNetworkCreationFailure_SurfacesClaimRejection(t *testing.T) { require.NoError(t, err) assert.False(t, failed) } + +// TestInstancePublishesTheBoundNetworkInterface pins the reference a provider +// follows. Without it a provider has to rebuild the claim name from compute's +// own convention, which is private and changes without notice. +func TestInstancePublishesTheBoundNetworkInterface(t *testing.T) { + t.Parallel() + + instance := newClaimTestInstance(claimTestDeployment+"-0", + computev1alpha.InstanceNetworkInterface{ + Network: networkingv1alpha.NetworkRef{Name: claimTestNetwork}, + Name: defaultInterfaceName, + }) + + claim := &networkingv1alpha.NetworkInterfaceClaim{ + ObjectMeta: metav1.ObjectMeta{Name: instance.Name + "-eth0", Namespace: claimTestNamespace}, + Status: networkingv1alpha.NetworkInterfaceClaimStatus{ + NetworkInterfaceRef: &networkingv1alpha.LocalNetworkInterfaceRef{Name: "nic-4f2a9c1e"}, + Addresses: []networkingv1alpha.NetworkInterfaceAddress{ + {Family: networkingv1alpha.IPv4Protocol, Address: claimTestAddressCIDR, Primary: true}, + }, + Conditions: []metav1.Condition{ + claimCondition(networkingv1alpha.NetworkInterfaceClaimBound, metav1.ConditionTrue, "Bound"), + claimCondition(networkingv1alpha.NetworkInterfaceClaimAllocated, metav1.ConditionTrue, "Allocated"), + }, + }, + } + + cl := fake.NewClientBuilder(). + WithScheme(newClaimTestScheme()). + WithObjects(instance, claim). + Build() + + r := &InstanceReconciler{NetworkingEnabled: true} + changed, err := r.reconcileNetworkInterfaceStatus(context.Background(), cl, instance) + require.NoError(t, err) + require.True(t, changed) + + require.Len(t, instance.Status.NetworkInterfaces, 1) + published := instance.Status.NetworkInterfaces[0] + require.NotNil(t, published.NetworkInterfaceRef, + "a provider reads the interface through this reference") + assert.Equal(t, "nic-4f2a9c1e", published.NetworkInterfaceRef.Name) + + // An unbound claim publishes no reference rather than an empty one. + unbound := instanceNetworkInterfaceStatus(defaultInterfaceName, + &networkingv1alpha.NetworkInterfaceClaim{}) + assert.Nil(t, unbound.NetworkInterfaceRef) + assert.Nil(t, instanceNetworkInterfaceStatus(defaultInterfaceName, nil).NetworkInterfaceRef) +}