Skip to content

Commit b5ed6b5

Browse files
Add vrf association to Interface resource
This change introduces a new field on the `Interface` resource to allow to configure an interface as part of a specified `VRF` (Network Instance) instead of configuring them in the DEFAULT_INSTANCE vrf where they are configured by default. This configuration is only allowed in L3 interfaces.
1 parent c68f3a4 commit b5ed6b5

File tree

17 files changed

+505
-45
lines changed

17 files changed

+505
-45
lines changed

Tiltfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ k8s_resource(new_name='eth1-2', objects=['eth1-2:interface'], trigger_mode=TRIGG
4949
k8s_resource(new_name='eth1-10', objects=['eth1-10:interface'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
5050
k8s_resource(new_name='po10', objects=['po-10:interface'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
5151
k8s_resource(new_name='svi-10', objects=['svi-10:interface'], resource_deps=['vlan-10'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
52+
k8s_resource(new_name='eth1-30', objects=['eth1-30:interface'], resource_deps=['vrf-vpc-keepalive'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
5253

5354
k8s_yaml('./config/samples/v1alpha1_banner.yaml')
5455
k8s_resource(new_name='banner', objects=['banner:banner'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
@@ -83,6 +84,7 @@ k8s_resource(new_name='isis-underlay', objects=['underlay:isis'], resource_deps=
8384
k8s_yaml('./config/samples/v1alpha1_vrf.yaml')
8485
k8s_resource(new_name='vrf-admin', objects=['vrf-cc-admin:vrf'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
8586
k8s_resource(new_name='vrf-001', objects=['vrf-cc-prod-001:vrf'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
87+
k8s_resource(new_name='vrf-vpc-keepalive', objects=['vpc-keepalive:vrf'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
8688

8789
k8s_yaml('./config/samples/v1alpha1_pim.yaml')
8890
k8s_resource(new_name='pim', objects=['pim:pim'], resource_deps=['lo0', 'lo1', 'eth1-1', 'eth1-2'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)

api/core/v1alpha1/groupversion_info.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ const AggregateLabel = "networking.metal.ironcore.dev/aggregate-name"
5050
// the name of the RoutedVLAN interface that provides Layer 3 routing for the VLAN.
5151
const RoutedVLANLabel = "networking.metal.ironcore.dev/routed-vlan-name"
5252

53+
// VRFLabel is a label applied to interfaces to indicate
54+
// the name of the VRF they belong to.
55+
const VRFLabel = "networking.metal.ironcore.dev/vrf-name"
56+
5357
// Condition types that are used across different objects.
5458
const (
5559
// ReadyCondition is the top-level status condition that reports if an object is ready.
@@ -122,4 +126,7 @@ const (
122126

123127
// VLANAlreadyInUseReason indicates that a VLAN is already in use by another routed VLAN interface.
124128
VLANAlreadyInUseReason = "VLANAlreadyInUse"
129+
130+
// VRFNotFoundReason indicates that a referenced VRF was not found.
131+
VRFNotFoundReason = "VRFNotFound"
125132
)

api/core/v1alpha1/interface_types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
// +kubebuilder:validation:XValidation:rule="self.type == 'RoutedVLAN' || !has(self.vlanRef)", message="vlanRef must only be specified on interfaces of type RoutedVLAN"
1919
// +kubebuilder:validation:XValidation:rule="self.type != 'RoutedVLAN' || !has(self.switchport)", message="switchport must not be specified for interfaces of type RoutedVLAN"
2020
// +kubebuilder:validation:XValidation:rule="self.type != 'RoutedVLAN' || !has(self.aggregation)", message="aggregation must not be specified for interfaces of type RoutedVLAN"
21+
// +kubebuilder:validation:XValidation:rule="self.type != 'Aggregate' || !has(self.vrfRef)", message="vrfRef must not be specified for interfaces of type Aggregate"
22+
// +kubebuilder:validation:XValidation:rule="self.type != 'Physical' || !has(self.switchport) || !has(self.vrfRef)", message="vrfRef must not be specified for Physical interfaces with switchport configuration"
2123
type InterfaceSpec struct {
2224
// DeviceName is the name of the Device this object belongs to. The Device object must exist in the same namespace.
2325
// Immutable.
@@ -76,6 +78,13 @@ type InterfaceSpec struct {
7678
// The referenced VLAN must exist in the same namespace.
7779
// +optional
7880
VlanRef *LocalObjectReference `json:"vlanRef,omitempty"`
81+
82+
// VrfRef is a reference to the VRF resource that this interface belongs to.
83+
// If not specified, the interface will be part of the default VRF.
84+
// This is only applicable for Layer 3 interfaces.
85+
// The referenced VRF must exist in the same namespace.
86+
// +optional
87+
VrfRef *LocalObjectReference `json:"vrfRef,omitempty"`
7988
}
8089

8190
// AdminState represents the administrative state of the interface.

api/core/v1alpha1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

charts/network-operator/templates/crd/networking.metal.ironcore.dev_interfaces.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,24 @@ spec:
345345
- name
346346
type: object
347347
x-kubernetes-map-type: atomic
348+
vrfRef:
349+
description: |-
350+
VrfRef is a reference to the VRF resource that this interface belongs to.
351+
If not specified, the interface will be part of the default VRF.
352+
This is only applicable for Layer 3 interfaces.
353+
The referenced VRF must exist in the same namespace.
354+
properties:
355+
name:
356+
description: |-
357+
Name of the referent.
358+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
359+
maxLength: 63
360+
minLength: 1
361+
type: string
362+
required:
363+
- name
364+
type: object
365+
x-kubernetes-map-type: atomic
348366
required:
349367
- adminState
350368
- deviceRef
@@ -373,6 +391,11 @@ spec:
373391
rule: self.type != 'RoutedVLAN' || !has(self.switchport)
374392
- message: aggregation must not be specified for interfaces of type RoutedVLAN
375393
rule: self.type != 'RoutedVLAN' || !has(self.aggregation)
394+
- message: vrfRef must not be specified for interfaces of type Aggregate
395+
rule: self.type != 'Aggregate' || !has(self.vrfRef)
396+
- message: vrfRef must not be specified for Physical interfaces with switchport
397+
configuration
398+
rule: self.type != 'Physical' || !has(self.switchport) || !has(self.vrfRef)
376399
status:
377400
description: |-
378401
Status of the resource. This is set and updated automatically.

config/crd/bases/networking.metal.ironcore.dev_interfaces.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,24 @@ spec:
339339
- name
340340
type: object
341341
x-kubernetes-map-type: atomic
342+
vrfRef:
343+
description: |-
344+
VrfRef is a reference to the VRF resource that this interface belongs to.
345+
If not specified, the interface will be part of the default VRF.
346+
This is only applicable for Layer 3 interfaces.
347+
The referenced VRF must exist in the same namespace.
348+
properties:
349+
name:
350+
description: |-
351+
Name of the referent.
352+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
353+
maxLength: 63
354+
minLength: 1
355+
type: string
356+
required:
357+
- name
358+
type: object
359+
x-kubernetes-map-type: atomic
342360
required:
343361
- adminState
344362
- deviceRef
@@ -367,6 +385,11 @@ spec:
367385
rule: self.type != 'RoutedVLAN' || !has(self.switchport)
368386
- message: aggregation must not be specified for interfaces of type RoutedVLAN
369387
rule: self.type != 'RoutedVLAN' || !has(self.aggregation)
388+
- message: vrfRef must not be specified for interfaces of type Aggregate
389+
rule: self.type != 'Aggregate' || !has(self.vrfRef)
390+
- message: vrfRef must not be specified for Physical interfaces with switchport
391+
configuration
392+
rule: self.type != 'Physical' || !has(self.switchport) || !has(self.vrfRef)
370393
status:
371394
description: |-
372395
Status of the resource. This is set and updated automatically.

config/samples/v1alpha1_interface.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,24 @@ spec:
155155
ipv4:
156156
addresses:
157157
- 192.168.10.254/24
158+
---
159+
apiVersion: networking.metal.ironcore.dev/v1alpha1
160+
kind: Interface
161+
metadata:
162+
labels:
163+
app.kubernetes.io/name: network-operator
164+
app.kubernetes.io/managed-by: kustomize
165+
networking.metal.ironcore.dev/device-name: leaf1
166+
name: eth1-30
167+
spec:
168+
deviceRef:
169+
name: leaf1
170+
name: eth1/30
171+
description: vPC Keepalive
172+
adminState: Up
173+
type: Physical
174+
ipv4:
175+
addresses:
176+
- 10.1.1.1/30
177+
vrfRef:
178+
name: vpc-keepalive

config/samples/v1alpha1_vrf.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,17 @@ spec:
5050
- IPv4
5151
- IPv4EVPN
5252
action: Both
53+
---
54+
apiVersion: networking.metal.ironcore.dev/v1alpha1
55+
kind: VRF
56+
metadata:
57+
labels:
58+
app.kubernetes.io/name: network-operator
59+
app.kubernetes.io/managed-by: kustomize
60+
networking.metal.ironcore.dev/device-name: leaf1
61+
name: vpc-keepalive
62+
spec:
63+
deviceRef:
64+
name: leaf1
65+
name: VPC_KEEPALIVE
66+
description: VRF for vPC Keepalive

hack/provider/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ func main() {
205205
case "create":
206206
switch resource := obj.(type) {
207207
case *v1alpha1.Interface:
208-
err = ip.EnsureInterface(ctx, &provider.InterfaceRequest{
208+
err = ip.EnsureInterface(ctx, &provider.EnsureInterfaceRequest{
209209
Interface: resource,
210210
ProviderConfig: nil,
211211
})

internal/controller/core/interface_controller.go

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ type InterfaceReconciler struct {
5959
// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=interfaces/finalizers,verbs=update
6060
// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=vlans,verbs=get;list;watch
6161
// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=vlans/status,verbs=get;update;patch
62+
// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=vrfs,verbs=get;list;watch
6263
// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
6364

6465
// Reconcile is part of the main kubernetes reconciliation loop which aims to
@@ -189,6 +190,7 @@ var (
189190
interfaceTypeKey = ".spec.type"
190191
interfaceUnnumberedRefKey = ".spec.ipv4.unnumbered.interfaceRef.name"
191192
interfaceVlanRefKey = ".spec.vlanRef.name"
193+
interfaceVrfRefKey = ".spec.vrfRef.name"
192194
)
193195

194196
// SetupWithManager sets up the controller with the Manager.
@@ -234,6 +236,16 @@ func (r *InterfaceReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man
234236
return err
235237
}
236238

239+
if err := mgr.GetFieldIndexer().IndexField(ctx, &v1alpha1.Interface{}, interfaceVrfRefKey, func(obj client.Object) []string {
240+
intf := obj.(*v1alpha1.Interface)
241+
if intf.Spec.VrfRef == nil {
242+
return nil
243+
}
244+
return []string{intf.Spec.VrfRef.Name}
245+
}); err != nil {
246+
return err
247+
}
248+
237249
return ctrl.NewControllerManagedBy(mgr).
238250
For(&v1alpha1.Interface{}).
239251
Named("interface").
@@ -279,6 +291,20 @@ func (r *InterfaceReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man
279291
},
280292
}),
281293
).
294+
// Watches enqueues Interfaces for updates in referenced VRF resources.
295+
// Only triggers on create and delete events since VRF names are immutable.
296+
Watches(
297+
&v1alpha1.VRF{},
298+
handler.EnqueueRequestsFromMapFunc(r.vrfToInterface),
299+
builder.WithPredicates(predicate.Funcs{
300+
UpdateFunc: func(e event.UpdateEvent) bool {
301+
return false
302+
},
303+
GenericFunc: func(e event.GenericEvent) bool {
304+
return false
305+
},
306+
}),
307+
).
282308
Complete(r)
283309
}
284310

@@ -332,6 +358,15 @@ func (r *InterfaceReconciler) reconcile(ctx context.Context, s *scope) (_ ctrl.R
332358
}
333359
}
334360

361+
var vrf *v1alpha1.VRF
362+
if s.Interface.Spec.VrfRef != nil {
363+
var err error
364+
vrf, err = r.reconcileVRF(ctx, s)
365+
if err != nil {
366+
return ctrl.Result{}, err
367+
}
368+
}
369+
335370
var ip provider.IPv4
336371
if s.Interface.Spec.IPv4 != nil {
337372
var err error
@@ -351,13 +386,14 @@ func (r *InterfaceReconciler) reconcile(ctx context.Context, s *scope) (_ ctrl.R
351386
}()
352387

353388
// Ensure the Interface is realized on the provider.
354-
err := s.Provider.EnsureInterface(ctx, &provider.InterfaceRequest{
389+
err := s.Provider.EnsureInterface(ctx, &provider.EnsureInterfaceRequest{
355390
Interface: s.Interface,
356391
ProviderConfig: s.ProviderConfig,
357392
IPv4: ip,
358393
Members: members,
359394
MultiChassisID: multiChassisID,
360395
VLAN: vlan,
396+
VRF: vrf,
361397
})
362398

363399
cond := conditions.FromError(err)
@@ -509,6 +545,50 @@ func (r *InterfaceReconciler) reconcileVLAN(ctx context.Context, s *scope) (*v1a
509545
return vlan, nil
510546
}
511547

548+
// reconcileVRF ensures that the referenced VRF exists and belongs to the same device as the Interface.
549+
// It also adds a label to the Interface indicating which VRF it belongs to. This can be used for lookup purposes.
550+
func (r *InterfaceReconciler) reconcileVRF(ctx context.Context, s *scope) (*v1alpha1.VRF, error) {
551+
key := client.ObjectKey{
552+
Name: s.Interface.Spec.VrfRef.Name,
553+
Namespace: s.Interface.Namespace,
554+
}
555+
556+
vrf := new(v1alpha1.VRF)
557+
if err := r.Get(ctx, key, vrf); err != nil {
558+
if apierrors.IsNotFound(err) {
559+
conditions.Set(s.Interface, metav1.Condition{
560+
Type: v1alpha1.ConfiguredCondition,
561+
Status: metav1.ConditionFalse,
562+
Reason: v1alpha1.VRFNotFoundReason,
563+
Message: fmt.Sprintf("referenced VRF %q not found", key),
564+
})
565+
return nil, reconcile.TerminalError(fmt.Errorf("referenced VRF %q not found", key))
566+
}
567+
return nil, fmt.Errorf("failed to get referenced VRF %q: %w", key, err)
568+
}
569+
570+
if vrf.Spec.DeviceRef.Name != s.Device.Name {
571+
conditions.Set(s.Interface, metav1.Condition{
572+
Type: v1alpha1.ConfiguredCondition,
573+
Status: metav1.ConditionFalse,
574+
Reason: v1alpha1.CrossDeviceReferenceReason,
575+
Message: fmt.Sprintf("referenced VRF %q does not belong to device %q", vrf.Name, s.Device.Name),
576+
})
577+
return nil, reconcile.TerminalError(fmt.Errorf("referenced VRF %q does not belong to device %q", vrf.Name, s.Device.Name))
578+
}
579+
580+
// Add label to interface indicating which VRF it belongs to
581+
if s.Interface.Labels == nil {
582+
s.Interface.Labels = make(map[string]string)
583+
}
584+
585+
if s.Interface.Labels[v1alpha1.VRFLabel] != vrf.Name {
586+
s.Interface.Labels[v1alpha1.VRFLabel] = vrf.Name
587+
}
588+
589+
return vrf, nil
590+
}
591+
512592
// reconcileMemberInterfaces ensures that all member interfaces exist and belong to the same device as the aggregate interface.
513593
// It also updates the member interfaces to reference the aggregate interface by setting their MemberOf status field and [v1alpha1.AggregateLabel] label.
514594
func (r *InterfaceReconciler) reconcileMemberInterfaces(ctx context.Context, s *scope) ([]*v1alpha1.Interface, error) {
@@ -767,3 +847,36 @@ func (r *InterfaceReconciler) vlanToRoutedVLAN(ctx context.Context, obj client.O
767847

768848
return requests
769849
}
850+
851+
// vrfToInterface is a [handler.MapFunc] to be used to enqueue requests for reconciliation
852+
// for Interfaces when their referenced VRF changes.
853+
func (r *InterfaceReconciler) vrfToInterface(ctx context.Context, obj client.Object) []ctrl.Request {
854+
vrf, ok := obj.(*v1alpha1.VRF)
855+
if !ok {
856+
panic(fmt.Sprintf("Expected a VRF but got a %T", obj))
857+
}
858+
859+
log := ctrl.LoggerFrom(ctx, "VRF", klog.KObj(vrf))
860+
861+
interfaces := new(v1alpha1.InterfaceList)
862+
if err := r.List(ctx, interfaces, client.InNamespace(vrf.Namespace), client.MatchingFields{interfaceVrfRefKey: vrf.Name}); err != nil {
863+
log.Error(err, "Failed to list Interfaces")
864+
return nil
865+
}
866+
867+
requests := []ctrl.Request{}
868+
for _, i := range interfaces.Items {
869+
if i.Spec.VrfRef != nil && i.Spec.VrfRef.Name == vrf.Name {
870+
log.Info("Enqueuing Interface for reconciliation", "Interface", klog.KObj(&i))
871+
872+
requests = append(requests, ctrl.Request{
873+
NamespacedName: client.ObjectKey{
874+
Name: i.Name,
875+
Namespace: i.Namespace,
876+
},
877+
})
878+
}
879+
}
880+
881+
return requests
882+
}

0 commit comments

Comments
 (0)