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
73 changes: 73 additions & 0 deletions internal/controller/evpn/fabric_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type FabricReconciler struct {
// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=bgp,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=bgppeers,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=pim,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=networkvirtualizationedges,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=pool.networking.metal.ironcore.dev,resources=claims,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=pool.networking.metal.ironcore.dev,resources=ipaddresspools,verbs=get;list;watch
// +kubebuilder:rbac:groups=pool.networking.metal.ironcore.dev,resources=ipprefixpools,verbs=get;list;watch
Expand Down Expand Up @@ -175,6 +176,7 @@ func (r *FabricReconciler) SetupWithManager(mgr ctrl.Manager) error {
Owns(&v1alpha1.BGP{}).
Owns(&v1alpha1.BGPPeer{}).
Owns(&v1alpha1.PIM{}).
Owns(&v1alpha1.NetworkVirtualizationEdge{}).
// Re-reconcile when a Device's labels change so that devices newly
// matching a deviceSelector are enrolled into the fabric.
Watches(
Expand Down Expand Up @@ -218,6 +220,7 @@ func (r *FabricReconciler) reconcile(ctx context.Context, fabric *evpnv1alpha1.F
r.reconcileUnderlayIGP,
r.reconcileOverlayBGP,
r.reconcileMulticastPIM,
r.reconcileVTEPNVE,
}
for _, phase := range phases {
res, err := phase(ctx, fabric, state)
Expand Down Expand Up @@ -1139,6 +1142,76 @@ func (r *FabricReconciler) reconcilePIM(ctx context.Context, deviceName string,
return nil
}

// reconcileVTEPNVE creates one NetworkVirtualizationEdge resource per VTEP device.
// The NVE references lo1 (primary VTEP) as source and lo2 (anycast VTEP) as anycast source.
func (r *FabricReconciler) reconcileVTEPNVE(ctx context.Context, fabric *evpnv1alpha1.Fabric, state *ReconcileState) (ctrl.Result, error) {
selector, err := metav1.LabelSelectorAsSelector(&fabric.Spec.VTEP.DeviceSelector)
if err != nil {
return ctrl.Result{}, reconcile.TerminalError(fmt.Errorf("invalid vtep deviceSelector: %w", err))
}

devices := &v1alpha1.DeviceList{}
if err := r.List(ctx, devices, client.InNamespace(fabric.Namespace), client.MatchingLabelsSelector{Selector: selector}); err != nil {
return ctrl.Result{}, fmt.Errorf("listing VTEP devices: %w", err)
}

for i := range devices.Items {
device := &devices.Items[i]

lo1Name := fmt.Sprintf("%s-%s-lo%d", fabric.Name, device.Name, LoopbackVTEP)
lo2Name := fmt.Sprintf("%s-%s-lo%d", fabric.Name, device.Name, LoopbackVTEPAnycast)

// Skip if lo1 is not yet allocated.
loopbacks := state.loopbacks[device.Name]
if !slices.ContainsFunc(loopbacks, func(intf *v1alpha1.Interface) bool { return intf.Name == lo1Name }) {
ctrl.LoggerFrom(ctx).V(1).Info("Skipping NVE reconciliation: lo1 not yet allocated", "device", device.Name)
continue
}

if err := r.reconcileNVE(ctx, device, fabric, lo1Name, lo2Name); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}

// reconcileNVE creates or updates the NetworkVirtualizationEdge resource for a VTEP device.
func (r *FabricReconciler) reconcileNVE(ctx context.Context, device *v1alpha1.Device, fabric *evpnv1alpha1.Fabric, lo1Name, lo2Name string) error {
name := fmt.Sprintf("%s-%s-nve", fabric.Name, device.Name)

nve := &v1alpha1.NetworkVirtualizationEdge{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: fabric.Namespace,
},
}
res, err := controllerutil.CreateOrPatch(ctx, r.Client, nve, func() error {
if nve.Labels == nil {
nve.Labels = make(map[string]string)
}
nve.Labels[evpnv1alpha1.FabricLabel] = fabric.Name
nve.Spec.DeviceRef = v1alpha1.LocalObjectReference{Name: device.Name}
nve.Spec.AdminState = v1alpha1.AdminStateUp
nve.Spec.HostReachability = v1alpha1.HostReachabilityTypeBGP
nve.Spec.SuppressARP = true
nve.Spec.SourceInterfaceRef = v1alpha1.LocalObjectReference{Name: lo1Name}
nve.Spec.AnycastSourceInterfaceRef = &v1alpha1.LocalObjectReference{Name: lo2Name}
if fabric.Spec.VTEP.AnycastGateway != nil {
nve.Spec.AnycastGateway = &v1alpha1.AnycastGateway{
VirtualMAC: fabric.Spec.VTEP.AnycastGateway.VirtualMAC,
}
}
return controllerutil.SetControllerReference(fabric, nve, r.Scheme)
})
if err != nil {
return fmt.Errorf("reconciling NVE %s: %w", name, err)
}
if res == controllerutil.OperationResultCreated {
r.Recorder.Eventf(fabric, nil, "Normal", "NVECreated", "Reconcile", "Created NVE %s", name)
}
return nil
}

// devicesToFabrics is a [handler.MapFunc] that enqueues all Fabrics whose
// spec.deviceSelector matches the labels of the changed Device.
func (r *FabricReconciler) devicesToFabrics(ctx context.Context, obj client.Object) []ctrl.Request {
Expand Down
21 changes: 21 additions & 0 deletions internal/controller/evpn/fabric_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,27 @@ var _ = Describe("Fabric Controller", func() {
}).Should(Succeed())
}

By("Verifying NVE resources are created for VTEP devices (leaves)")
for _, leaf := range []*corev1alpha1.Device{leaf1, leaf2} {
Eventually(func(g Gomega) {
nve := &corev1alpha1.NetworkVirtualizationEdge{}
g.Expect(k8sClient.Get(ctx, client.ObjectKey{Name: fabric.Name + "-" + leaf.Name + "-nve", Namespace: metav1.NamespaceDefault}, nve)).To(Succeed())
g.Expect(nve.Spec.DeviceRef.Name).To(Equal(leaf.Name))
g.Expect(nve.Spec.AdminState).To(Equal(corev1alpha1.AdminStateUp))
g.Expect(nve.Spec.HostReachability).To(Equal(corev1alpha1.HostReachabilityTypeBGP))
g.Expect(nve.Spec.SuppressARP).To(BeTrue())
g.Expect(nve.Spec.SourceInterfaceRef.Name).To(Equal(fabric.Name + "-" + leaf.Name + "-lo1"))
g.Expect(nve.Spec.AnycastSourceInterfaceRef).NotTo(BeNil())
g.Expect(nve.Spec.AnycastSourceInterfaceRef.Name).To(Equal(fabric.Name + "-" + leaf.Name + "-lo2"))

g.Expect(nve.OwnerReferences).To(ContainElement(SatisfyAll(
HaveField("Kind", "Fabric"),
HaveField("Name", fabric.Name),
HaveField("Controller", HaveValue(BeTrue())),
)))
}).Should(Succeed())
}

By("Verifying the Fabric Ready condition is True once all phases are complete")
Eventually(func(g Gomega) {
f := &evpnv1alpha1.Fabric{}
Expand Down
Loading