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
194 changes: 194 additions & 0 deletions internal/controller/evpn/fabric_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
package evpn

import (
"cmp"
"context"
"fmt"
"net/netip"

"k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
Expand All @@ -14,6 +16,7 @@ import (
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
kerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/tools/events"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
Expand Down Expand Up @@ -53,6 +56,7 @@ type FabricReconciler struct {
// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=interfaces,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
// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch

// Reconcile is part of the main kubernetes reconciliation loop which aims to
Expand Down Expand Up @@ -167,6 +171,13 @@ func (r *FabricReconciler) SetupWithManager(mgr ctrl.Manager) error {
handler.EnqueueRequestsFromMapFunc(r.devicesToFabrics),
builder.WithPredicates(predicate.LabelChangedPredicate{}),
).
// Re-reconcile when an Interface's labels change so that interfaces
// newly matching the spec.underlay.interfaceSelector are enrolled into the fabric.
Watches(
&v1alpha1.Interface{},
handler.EnqueueRequestsFromMapFunc(r.interfacesToFabrics),
builder.WithPredicates(predicate.LabelChangedPredicate{}),
).
WithEventFilter(filter).
Named("evpn-fabric").
Complete(r)
Expand All @@ -181,6 +192,7 @@ func (r *FabricReconciler) reconcile(ctx context.Context, fabric *evpnv1alpha1.F
r.reconcileSystemLoopbacks,
r.reconcileVTEPLoopbacks,
r.reconcileAnycastRPLoopbacks,
r.reconcileUnderlayLinks,
}
for _, phase := range phases {
res, err := phase(ctx, fabric)
Expand Down Expand Up @@ -374,6 +386,155 @@ func (r *FabricReconciler) reconcileLoopbackInterface(ctx context.Context, fabri
return nil
}

// reconcileUnderlayLinks patches pre-existing Interface resources matched by
// spec.underlay.interfaceSelector with MTU 9216 and IPv4 configuration.
// For unnumbered addressing, interfaces borrow the IPv4 address from their device's lo0.
// For numbered addressing, one /31 prefix Claim is allocated per link pair (identified by
// PhysicalInterfaceNeighborLabel); both ends derive their host address from that prefix.
func (r *FabricReconciler) reconcileUnderlayLinks(ctx context.Context, fabric *evpnv1alpha1.Fabric) (ctrl.Result, error) {
intfSelector, err := metav1.LabelSelectorAsSelector(&fabric.Spec.Underlay.InterfaceSelector)
if err != nil {
return ctrl.Result{}, fmt.Errorf("invalid underlay interfaceSelector: %w", err)
}
interfaces := &v1alpha1.InterfaceList{}
if err := r.List(ctx, interfaces, client.InNamespace(fabric.Namespace), client.MatchingLabelsSelector{Selector: intfSelector}); err != nil {
return ctrl.Result{}, fmt.Errorf("listing underlay interfaces: %w", err)
}
deviceSelector, err := metav1.LabelSelectorAsSelector(&fabric.Spec.DeviceSelector)
if err != nil {
return ctrl.Result{}, fmt.Errorf("invalid deviceSelector: %w", err)
}
devices := &v1alpha1.DeviceList{}
if err := r.List(ctx, devices, client.InNamespace(fabric.Namespace), client.MatchingLabelsSelector{Selector: deviceSelector}); err != nil {
return ctrl.Result{}, fmt.Errorf("listing devices: %w", err)
}
deviceSet := sets.New[string]()
for i := range devices.Items {
deviceSet.Insert(devices.Items[i].Name)
}
for i := range interfaces.Items {
intf := &interfaces.Items[i]
if intf.Spec.Type != v1alpha1.InterfaceTypePhysical {
return ctrl.Result{}, fmt.Errorf("interface %s has type %s, expected %s", intf.Name, intf.Spec.Type, v1alpha1.InterfaceTypePhysical)
}
if !deviceSet.Has(intf.Spec.DeviceRef.Name) {
return ctrl.Result{}, fmt.Errorf("interface %s references device %s which is not part of the fabric", intf.Name, intf.Spec.DeviceRef.Name)
}
var err error
switch {
case fabric.Spec.Underlay.Addressing.Unnumbered:
err = r.reconcileUnderlayInterfaceUnnumbered(ctx, fabric, intf)
case fabric.Spec.Underlay.Addressing.IPPrefixPoolRef != nil:
err = r.reconcileUnderlayInterfaceNumbered(ctx, fabric, intf)
}
if err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}

// reconcileUnderlayInterfaceUnnumbered patches the interface to borrow the IPv4 address
// from the device's lo0 interface created by reconcileSystemLoopbacks.
func (r *FabricReconciler) reconcileUnderlayInterfaceUnnumbered(ctx context.Context, fabric *evpnv1alpha1.Fabric, intf *v1alpha1.Interface) error {
orig := intf.DeepCopy()
intf.Spec.MTU = 9216
intf.Spec.IPv4 = &v1alpha1.InterfaceIPv4{
Unnumbered: &v1alpha1.InterfaceIPv4Unnumbered{
InterfaceRef: v1alpha1.LocalObjectReference{
Name: fmt.Sprintf("%s-%s-lo0", fabric.Name, intf.Spec.DeviceRef.Name),
},
},
}
if equality.Semantic.DeepEqual(orig.Spec, intf.Spec) {
return nil
}
// TODO: switch to server-side apply with field ownership once applyconfiguration generation is available
if err := r.Patch(ctx, intf, client.MergeFrom(orig)); err != nil {
return fmt.Errorf("patching underlay interface %s: %w", intf.Name, err)
}
return nil
}

// reconcileUnderlayInterfaceNumbered allocates a /31 prefix per link pair via a Claim and
// assigns a host address to this interface. The peer is identified by PhysicalInterfaceNeighborLabel;
// if absent the interface is skipped (link not yet complete). The claim name is derived from
// the two interface names sorted alphabetically so both ends resolve to the same Claim.
// The lexicographically first interface receives addr 0 of the /31; the second receives addr 1.
func (r *FabricReconciler) reconcileUnderlayInterfaceNumbered(ctx context.Context, fabric *evpnv1alpha1.Fabric, intf *v1alpha1.Interface) error {
peerName, ok := intf.Labels[v1alpha1.PhysicalInterfaceNeighborLabel]
if !ok {
ctrl.LoggerFrom(ctx).V(1).Info("Skipping interface without neighbor label", "interface", intf.Name)
return nil
}

// Stable claim name: sort the two interface names so both ends agree.
a, b := intf.Name, peerName
claimName := fmt.Sprintf("%s-%s-%s-p2p", fabric.Name, min(a, b), max(a, b))

claim, err := r.reconcileUnderlayPrefixClaim(ctx, fabric, claimName)
if err != nil {
return err
}

cond := conditions.Get(claim, poolv1alpha1.AllocatedCondition)
if cond == nil || cond.Status != metav1.ConditionTrue || claim.Status.Value == "" {
return nil
}

prefix, err := v1alpha1.ParsePrefix(claim.Status.Value)
if err != nil {
return reconcile.TerminalError(fmt.Errorf("parsing allocated prefix %q for claim %s: %w", claim.Status.Value, claimName, err))
}

// Assign addr 0 to the lex-first interface, addr 1 to the second.
addr := prefix.Addr()
if cmp.Compare(intf.Name, peerName) > 0 {
addr = addr.Next()
}

orig := intf.DeepCopy()
intf.Spec.MTU = 9216
intf.Spec.IPv4 = &v1alpha1.InterfaceIPv4{
Addresses: []v1alpha1.IPPrefix{{Prefix: netip.PrefixFrom(addr, prefix.Bits())}},
}
if equality.Semantic.DeepEqual(orig.Spec, intf.Spec) {
return nil
}
// TODO: switch to server-side apply with field ownership once applyconfiguration generation is available
if err := r.Patch(ctx, intf, client.MergeFrom(orig)); err != nil {
return fmt.Errorf("patching underlay interface %s: %w", intf.Name, err)
}
return nil
}

// reconcileUnderlayPrefixClaim ensures a Claim against an IPPrefixPool exists for the given link.
func (r *FabricReconciler) reconcileUnderlayPrefixClaim(ctx context.Context, fabric *evpnv1alpha1.Fabric, claimName string) (*poolv1alpha1.Claim, error) {
claim := &poolv1alpha1.Claim{
ObjectMeta: metav1.ObjectMeta{
Name: claimName,
Namespace: fabric.Namespace,
},
}
res, err := controllerutil.CreateOrPatch(ctx, r.Client, claim, func() error {
claim.Spec = poolv1alpha1.ClaimSpec{
PoolRef: v1alpha1.TypedLocalObjectReference{
APIVersion: poolv1alpha1.GroupVersion.String(),
Kind: "IPPrefixPool",
Name: fabric.Spec.Underlay.Addressing.IPPrefixPoolRef.Name,
},
}
return controllerutil.SetControllerReference(fabric, claim, r.Scheme)
})
if err != nil {
return nil, fmt.Errorf("reconciling prefix claim %s: %w", claimName, err)
}
if res == controllerutil.OperationResultCreated {
r.Recorder.Eventf(fabric, nil, "Normal", "ClaimCreated", "Reconcile", "Created underlay prefix claim %s", claimName)
}
return claim, 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 Expand Up @@ -406,3 +567,36 @@ func (r *FabricReconciler) devicesToFabrics(ctx context.Context, obj client.Obje
}
return requests
}

// interfacesToFabrics is a [handler.MapFunc] that enqueues all Fabrics whose
// spec.underlay.interfaceSelector matches the labels of the changed Interface.
func (r *FabricReconciler) interfacesToFabrics(ctx context.Context, obj client.Object) []ctrl.Request {
intf, ok := obj.(*v1alpha1.Interface)
if !ok {
panic(fmt.Sprintf("Expected an Interface but got a %T", obj))
}

log := ctrl.LoggerFrom(ctx)

fabricList := &evpnv1alpha1.FabricList{}
if err := r.List(ctx, fabricList, client.InNamespace(intf.Namespace)); err != nil {
log.Error(err, "Failed to list Fabrics")
return nil
}

var requests []ctrl.Request
for _, fabric := range fabricList.Items {
selector, err := metav1.LabelSelectorAsSelector(&fabric.Spec.Underlay.InterfaceSelector)
if err != nil {
log.Error(err, "Failed to parse underlay interfaceSelector", "fabric", fabric.Name)
continue
}
if selector.Matches(labels.Set(intf.Labels)) {
log.V(2).Info("Enqueuing Fabric for reconciliation", "fabric", fabric.Name)
requests = append(requests, ctrl.Request{
NamespacedName: client.ObjectKeyFromObject(&fabric),
})
}
}
return requests
}
Loading