diff --git a/PROJECT b/PROJECT
index e437c0ca9..2e0c395a2 100644
--- a/PROJECT
+++ b/PROJECT
@@ -352,4 +352,13 @@ resources:
kind: IPPrefix
path: github.com/ironcore-dev/network-operator/api/pool/v1alpha1
version: v1alpha1
+- api:
+ crdVersion: v1
+ namespaced: true
+ controller: true
+ domain: networking.metal.ironcore.dev
+ group: evpn
+ kind: Fabric
+ path: github.com/ironcore-dev/network-operator/api/evpn/v1alpha1
+ version: v1alpha1
version: "3"
diff --git a/Tiltfile b/Tiltfile
index 3c341c52c..7cc17e605 100644
--- a/Tiltfile
+++ b/Tiltfile
@@ -153,6 +153,9 @@ k8s_resource(new_name='claim-index', objects=['claim-index:claim'], resource_dep
k8s_resource(new_name='claim-ipaddress', objects=['claim-ipaddress:claim'], resource_deps=['ipaddresspool'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
k8s_resource(new_name='claim-prefix', objects=['claim-prefix:claim'], resource_deps=['ipprefixpool'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
+k8s_yaml('./config/samples/v1alpha1_fabric.yaml')
+k8s_resource(new_name='fabric', objects=['fabric:fabric', 'loopback-pool:ipaddresspool', 'underlay-p2p-pool:ipprefixpool'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
+
print('🚀 network-operator development environment')
print('👉 Edit the code inside the api/, cmd/, or internal/ directories')
print('👉 Tilt will automatically rebuild and redeploy when changes are detected')
diff --git a/api/evpn/v1alpha1/doc.go b/api/evpn/v1alpha1/doc.go
new file mode 100644
index 000000000..28f9e6f33
--- /dev/null
+++ b/api/evpn/v1alpha1/doc.go
@@ -0,0 +1,8 @@
+// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
+// SPDX-License-Identifier: Apache-2.0
+
+// Package v1alpha1 contains API Schema definitions for the evpn.networking.metal.ironcore.dev v1alpha1 API group.
+// +kubebuilder:validation:Required
+// +kubebuilder:object:generate=true
+// +groupName=evpn.networking.metal.ironcore.dev
+package v1alpha1
diff --git a/api/evpn/v1alpha1/fabric_types.go b/api/evpn/v1alpha1/fabric_types.go
new file mode 100644
index 000000000..a554bf97d
--- /dev/null
+++ b/api/evpn/v1alpha1/fabric_types.go
@@ -0,0 +1,296 @@
+// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package v1alpha1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/util/intstr"
+
+ corev1alpha1 "github.com/ironcore-dev/network-operator/api/core/v1alpha1"
+)
+
+// FabricSpec defines the desired state of Fabric.
+type FabricSpec struct {
+ // deviceSelector identifies which devices are members of this fabric.
+ // All devices whose labels match this selector will be enrolled.
+ // +required
+ DeviceSelector metav1.LabelSelector `json:"deviceSelector"`
+
+ // loopbacks configures IP address allocation for loopback interfaces on
+ // fabric devices.
+ // +required
+ Loopbacks FabricLoopbacksSpec `json:"loopbacks"`
+
+ // underlay defines the underlay routing configuration for the fabric.
+ // +required
+ Underlay FabricUnderlaySpec `json:"underlay"`
+
+ // overlay defines the overlay control-plane configuration for the fabric.
+ // +required
+ Overlay FabricOverlaySpec `json:"overlay"`
+
+ // bum defines how BUM traffic is forwarded across the fabric.
+ // +required
+ BUM FabricBUMSpec `json:"bum"`
+
+ // vtep identifies the VTEP devices and configures their anycast gateway.
+ // +required
+ VTEP FabricVTEPSpec `json:"vtep"`
+}
+
+// FabricLoopbacksSpec configures IP address allocation for loopback interfaces.
+type FabricLoopbacksSpec struct {
+ // ipAddressPoolRef references the IPAddressPool from which loopback addresses
+ // are allocated for devices in the fabric.
+ // +required
+ IPAddressPoolRef corev1alpha1.LocalObjectReference `json:"ipAddressPoolRef"`
+}
+
+// FabricUnderlaySpec defines the underlay network configuration.
+type FabricUnderlaySpec struct {
+ // protocol is the routing protocol used to build IP reachability across the
+ // fabric underlay.
+ // +required
+ Protocol UnderlayProtocol `json:"protocol"`
+
+ // interfaceSelector identifies which interfaces participate in the underlay.
+ // Interfaces on fabric devices matching these labels will be enrolled in the
+ // underlay routing process.
+ // +required
+ InterfaceSelector metav1.LabelSelector `json:"interfaceSelector"`
+
+ // addressing configures how IP addresses are assigned to underlay interfaces.
+ // +required
+ Addressing FabricUnderlayAddressingSpec `json:"addressing"`
+}
+
+// UnderlayProtocol is the routing protocol used for the underlay network.
+// +kubebuilder:validation:Enum=OSPF;ISIS
+type UnderlayProtocol string
+
+const (
+ // UnderlayProtocolOSPF uses OSPF for underlay routing.
+ UnderlayProtocolOSPF UnderlayProtocol = "OSPF"
+ // UnderlayProtocolISIS uses IS-IS for underlay routing.
+ UnderlayProtocolISIS UnderlayProtocol = "ISIS"
+)
+
+// FabricUnderlayAddressingSpec configures how IP addresses are assigned to
+// underlay point-to-point links.
+// +kubebuilder:validation:XValidation:rule="has(self.ipPrefixPoolRef) != self.unnumbered",message="exactly one of ipPrefixPoolRef or unnumbered must be set"
+type FabricUnderlayAddressingSpec struct {
+ // ipPrefixPoolRef references the IPPrefixPool from which point-to-point
+ // prefixes are allocated for underlay interfaces.
+ // +optional
+ IPPrefixPoolRef *corev1alpha1.LocalObjectReference `json:"ipPrefixPoolRef,omitempty"`
+
+ // unnumbered controls whether underlay interfaces use unnumbered addressing
+ // (borrowing from loopback0) instead of dedicated point-to-point addresses.
+ // +optional
+ // +kubebuilder:default=false
+ Unnumbered bool `json:"unnumbered,omitempty"`
+}
+
+// OverlayProtocol is the control-plane protocol used for the overlay network.
+// +kubebuilder:validation:Enum=IBGP
+type OverlayProtocol string
+
+const (
+ // OverlayProtocolIBGP uses iBGP EVPN for the overlay control plane.
+ OverlayProtocolIBGP OverlayProtocol = "IBGP"
+)
+
+// FabricOverlaySpec defines the overlay control-plane configuration.
+// +kubebuilder:validation:XValidation:rule="self.protocol != 'IBGP' || has(self.ibgp)",message="ibgp must be set when protocol is IBGP"
+type FabricOverlaySpec struct {
+ // protocol is the control-plane protocol used for the EVPN overlay.
+ // +required
+ Protocol OverlayProtocol `json:"protocol"`
+
+ // ibgp configures the iBGP overlay when protocol is IBGP.
+ // +optional
+ IBGP *FabricIBGPSpec `json:"ibgp,omitempty"`
+}
+
+// FabricIBGPSpec configures the iBGP overlay control plane.
+// +kubebuilder:validation:XValidation:rule="size(self.routeReflectors) > 0",message="at least one route reflector group must be specified"
+type FabricIBGPSpec struct {
+ // asNumber is the BGP autonomous system number shared by all devices in the
+ // iBGP fabric. Supports both plain format (1-4294967295) and dotted notation
+ // (1-65535.0-65535) as per RFC 5396.
+ // +required
+ ASNumber intstr.IntOrString `json:"asNumber"`
+
+ // routeReflectors lists the route reflector groups that provide iBGP scalability.
+ // Each group designates a set of reflectors and their client devices.
+ // +required
+ // +listType=map
+ // +listMapKey=name
+ // +kubebuilder:validation:MinItems=1
+ RouteReflectors []RouteReflectorGroup `json:"routeReflectors"`
+}
+
+// RouteReflectorGroup defines a set of BGP route reflectors and their clients.
+type RouteReflectorGroup struct {
+ // name is a unique identifier for this route reflector group within the fabric.
+ // +required
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=63
+ Name string `json:"name"`
+
+ // deviceSelector identifies which devices act as route reflectors in this group.
+ // +required
+ DeviceSelector metav1.LabelSelector `json:"deviceSelector"`
+
+ // clientDeviceSelector identifies the devices that are route reflector clients
+ // for this group.
+ // +required
+ ClientDeviceSelector metav1.LabelSelector `json:"clientDeviceSelector"`
+}
+
+// BUMType is the mechanism used to handle BUM (Broadcast, Unknown unicast,
+// Multicast) traffic in the fabric.
+// +kubebuilder:validation:Enum=Multicast
+type BUMType string
+
+const (
+ // BUMTypeMulticast uses PIM sparse mode for BUM traffic forwarding.
+ BUMTypeMulticast BUMType = "Multicast"
+)
+
+// FabricBUMSpec defines how BUM (Broadcast, Unknown unicast, Multicast) traffic
+// is forwarded across the fabric.
+// +kubebuilder:validation:XValidation:rule="self.type != 'Multicast' || has(self.pim)",message="pim must be set when type is Multicast"
+type FabricBUMSpec struct {
+ // type selects the BUM forwarding mechanism.
+ // +required
+ Type BUMType `json:"type"`
+
+ // pim configures PIM sparse mode when type is Multicast.
+ // +optional
+ PIM *FabricPIMSpec `json:"pim,omitempty"`
+}
+
+// FabricPIMSpec configures PIM sparse mode for BUM traffic.
+type FabricPIMSpec struct {
+ // anycastRendezvousPoints lists the anycast rendezvous point groups used for
+ // PIM sparse mode. Anycast RPs share the same IP address across multiple
+ // devices for redundancy.
+ // +required
+ // +listType=map
+ // +listMapKey=name
+ // +kubebuilder:validation:MinItems=1
+ AnycastRendezvousPoints []AnycastRendezvousPoint `json:"anycastRendezvousPoints"`
+}
+
+// AnycastRendezvousPoint defines an anycast PIM rendezvous point group.
+type AnycastRendezvousPoint struct {
+ // name is a unique identifier for this rendezvous point group within the fabric.
+ // +required
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=63
+ Name string `json:"name"`
+
+ // multicastGroups lists the multicast group address ranges served by this
+ // rendezvous point. Each entry must be a valid IPv4 multicast CIDR prefix.
+ // +required
+ // +kubebuilder:validation:MinItems=1
+ MulticastGroups []corev1alpha1.IPPrefix `json:"multicastGroups"`
+
+ // deviceSelector identifies which devices are configured as rendezvous points
+ // in this group.
+ // +required
+ DeviceSelector metav1.LabelSelector `json:"deviceSelector"`
+
+ // clientDeviceSelector identifies the devices that register with the rendezvous
+ // points in this group.
+ // +required
+ ClientDeviceSelector metav1.LabelSelector `json:"clientDeviceSelector"`
+}
+
+// FabricVTEPSpec identifies which devices act as VXLAN Tunnel Endpoints (VTEPs)
+// and optionally configures their shared anycast gateway.
+type FabricVTEPSpec struct {
+ // deviceSelector identifies which devices are configured as VTEPs.
+ // +required
+ DeviceSelector metav1.LabelSelector `json:"deviceSelector"`
+
+ // anycastGateway configures the anycast gateway shared across all VTEP devices.
+ // +optional
+ AnycastGateway *FabricAnycastGatewaySpec `json:"anycastGateway,omitempty"`
+}
+
+// FabricAnycastGatewaySpec configures the anycast gateway on VTEP devices.
+type FabricAnycastGatewaySpec struct {
+ // virtualMAC is the shared MAC address used by all anycast gateway instances
+ // across the fabric. Must be a valid IEEE 802 MAC address in colon-separated
+ // hexadecimal notation (e.g. f0:0c:c1:5c:00:00).
+ // +required
+ // +kubebuilder:validation:Pattern=`^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$`
+ VirtualMAC string `json:"virtualMAC"`
+}
+
+// FabricStatus defines the observed state of Fabric.
+type FabricStatus struct {
+ // conditions represent the current state of the Fabric resource.
+ // Each condition has a unique type and reflects the status of a specific aspect of the resource.
+ // The status of each condition is one of True, False, or Unknown.
+ // +listType=map
+ // +listMapKey=type
+ // +patchStrategy=merge
+ // +patchMergeKey=type
+ // +optional
+ Conditions []metav1.Condition `json:"conditions,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
+// +kubebuilder:resource:path=fabrics
+// +kubebuilder:resource:singular=fabric
+// +kubebuilder:printcolumn:name="Underlay",type=string,JSONPath=`.spec.underlay.protocol`
+// +kubebuilder:printcolumn:name="Overlay",type=string,JSONPath=`.spec.overlay.protocol`
+// +kubebuilder:printcolumn:name="AS Number",type=string,JSONPath=`.spec.overlay.ibgp.asNumber`
+// +kubebuilder:printcolumn:name="BUM",type=string,JSONPath=`.spec.bum.type`,priority=1
+// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
+// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
+
+// Fabric is the Schema for the fabrics API
+type Fabric struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ // Specification of the desired state of the resource.
+ // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
+ // +required
+ Spec FabricSpec `json:"spec"`
+
+ // Status of the resource. This is set and updated automatically.
+ // Read-only.
+ // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
+ // +optional
+ Status FabricStatus `json:"status,omitzero"`
+}
+
+// GetConditions implements conditions.Getter.
+func (f *Fabric) GetConditions() []metav1.Condition {
+ return f.Status.Conditions
+}
+
+// SetConditions implements conditions.Setter.
+func (f *Fabric) SetConditions(conditions []metav1.Condition) {
+ f.Status.Conditions = conditions
+}
+
+// +kubebuilder:object:root=true
+
+// FabricList contains a list of Fabric
+type FabricList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata,omitzero"`
+ Items []Fabric `json:"items"`
+}
+
+func init() {
+ SchemeBuilder.Register(&Fabric{}, &FabricList{})
+}
diff --git a/api/evpn/v1alpha1/groupversion_info.go b/api/evpn/v1alpha1/groupversion_info.go
new file mode 100644
index 000000000..49bf3b4db
--- /dev/null
+++ b/api/evpn/v1alpha1/groupversion_info.go
@@ -0,0 +1,25 @@
+// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package v1alpha1
+
+import (
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "sigs.k8s.io/controller-runtime/pkg/scheme"
+)
+
+var (
+ // GroupVersion is group version used to register these objects.
+ GroupVersion = schema.GroupVersion{Group: "evpn.networking.metal.ironcore.dev", Version: "v1alpha1"}
+
+ // SchemeBuilder is used to add go types to the GroupVersionKind scheme.
+ SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
+
+ // AddToScheme adds the types in this group-version to the given scheme.
+ AddToScheme = SchemeBuilder.AddToScheme
+)
+
+const (
+ // FinalizerName is the identifier used by evpn controllers to perform cleanup before a resource is deleted.
+ FinalizerName = "evpn.networking.metal.ironcore.dev/finalizer"
+)
diff --git a/api/evpn/v1alpha1/zz_generated.deepcopy.go b/api/evpn/v1alpha1/zz_generated.deepcopy.go
new file mode 100644
index 000000000..768b43418
--- /dev/null
+++ b/api/evpn/v1alpha1/zz_generated.deepcopy.go
@@ -0,0 +1,331 @@
+//go:build !ignore_autogenerated
+
+// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
+// SPDX-License-Identifier: Apache-2.0
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ corev1alpha1 "github.com/ironcore-dev/network-operator/api/core/v1alpha1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1"
+ runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AnycastRendezvousPoint) DeepCopyInto(out *AnycastRendezvousPoint) {
+ *out = *in
+ if in.MulticastGroups != nil {
+ in, out := &in.MulticastGroups, &out.MulticastGroups
+ *out = make([]corev1alpha1.IPPrefix, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ in.DeviceSelector.DeepCopyInto(&out.DeviceSelector)
+ in.ClientDeviceSelector.DeepCopyInto(&out.ClientDeviceSelector)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnycastRendezvousPoint.
+func (in *AnycastRendezvousPoint) DeepCopy() *AnycastRendezvousPoint {
+ if in == nil {
+ return nil
+ }
+ out := new(AnycastRendezvousPoint)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Fabric) DeepCopyInto(out *Fabric) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ in.Spec.DeepCopyInto(&out.Spec)
+ in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Fabric.
+func (in *Fabric) DeepCopy() *Fabric {
+ if in == nil {
+ return nil
+ }
+ out := new(Fabric)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *Fabric) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FabricAnycastGatewaySpec) DeepCopyInto(out *FabricAnycastGatewaySpec) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FabricAnycastGatewaySpec.
+func (in *FabricAnycastGatewaySpec) DeepCopy() *FabricAnycastGatewaySpec {
+ if in == nil {
+ return nil
+ }
+ out := new(FabricAnycastGatewaySpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FabricBUMSpec) DeepCopyInto(out *FabricBUMSpec) {
+ *out = *in
+ if in.PIM != nil {
+ in, out := &in.PIM, &out.PIM
+ *out = new(FabricPIMSpec)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FabricBUMSpec.
+func (in *FabricBUMSpec) DeepCopy() *FabricBUMSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(FabricBUMSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FabricIBGPSpec) DeepCopyInto(out *FabricIBGPSpec) {
+ *out = *in
+ out.ASNumber = in.ASNumber
+ if in.RouteReflectors != nil {
+ in, out := &in.RouteReflectors, &out.RouteReflectors
+ *out = make([]RouteReflectorGroup, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FabricIBGPSpec.
+func (in *FabricIBGPSpec) DeepCopy() *FabricIBGPSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(FabricIBGPSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FabricList) DeepCopyInto(out *FabricList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]Fabric, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FabricList.
+func (in *FabricList) DeepCopy() *FabricList {
+ if in == nil {
+ return nil
+ }
+ out := new(FabricList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *FabricList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FabricLoopbacksSpec) DeepCopyInto(out *FabricLoopbacksSpec) {
+ *out = *in
+ out.IPAddressPoolRef = in.IPAddressPoolRef
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FabricLoopbacksSpec.
+func (in *FabricLoopbacksSpec) DeepCopy() *FabricLoopbacksSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(FabricLoopbacksSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FabricOverlaySpec) DeepCopyInto(out *FabricOverlaySpec) {
+ *out = *in
+ if in.IBGP != nil {
+ in, out := &in.IBGP, &out.IBGP
+ *out = new(FabricIBGPSpec)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FabricOverlaySpec.
+func (in *FabricOverlaySpec) DeepCopy() *FabricOverlaySpec {
+ if in == nil {
+ return nil
+ }
+ out := new(FabricOverlaySpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FabricPIMSpec) DeepCopyInto(out *FabricPIMSpec) {
+ *out = *in
+ if in.AnycastRendezvousPoints != nil {
+ in, out := &in.AnycastRendezvousPoints, &out.AnycastRendezvousPoints
+ *out = make([]AnycastRendezvousPoint, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FabricPIMSpec.
+func (in *FabricPIMSpec) DeepCopy() *FabricPIMSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(FabricPIMSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FabricSpec) DeepCopyInto(out *FabricSpec) {
+ *out = *in
+ in.DeviceSelector.DeepCopyInto(&out.DeviceSelector)
+ out.Loopbacks = in.Loopbacks
+ in.Underlay.DeepCopyInto(&out.Underlay)
+ in.Overlay.DeepCopyInto(&out.Overlay)
+ in.BUM.DeepCopyInto(&out.BUM)
+ in.VTEP.DeepCopyInto(&out.VTEP)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FabricSpec.
+func (in *FabricSpec) DeepCopy() *FabricSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(FabricSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FabricStatus) DeepCopyInto(out *FabricStatus) {
+ *out = *in
+ if in.Conditions != nil {
+ in, out := &in.Conditions, &out.Conditions
+ *out = make([]v1.Condition, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FabricStatus.
+func (in *FabricStatus) DeepCopy() *FabricStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(FabricStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FabricUnderlayAddressingSpec) DeepCopyInto(out *FabricUnderlayAddressingSpec) {
+ *out = *in
+ if in.IPPrefixPoolRef != nil {
+ in, out := &in.IPPrefixPoolRef, &out.IPPrefixPoolRef
+ *out = new(corev1alpha1.LocalObjectReference)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FabricUnderlayAddressingSpec.
+func (in *FabricUnderlayAddressingSpec) DeepCopy() *FabricUnderlayAddressingSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(FabricUnderlayAddressingSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FabricUnderlaySpec) DeepCopyInto(out *FabricUnderlaySpec) {
+ *out = *in
+ in.InterfaceSelector.DeepCopyInto(&out.InterfaceSelector)
+ in.Addressing.DeepCopyInto(&out.Addressing)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FabricUnderlaySpec.
+func (in *FabricUnderlaySpec) DeepCopy() *FabricUnderlaySpec {
+ if in == nil {
+ return nil
+ }
+ out := new(FabricUnderlaySpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FabricVTEPSpec) DeepCopyInto(out *FabricVTEPSpec) {
+ *out = *in
+ in.DeviceSelector.DeepCopyInto(&out.DeviceSelector)
+ if in.AnycastGateway != nil {
+ in, out := &in.AnycastGateway, &out.AnycastGateway
+ *out = new(FabricAnycastGatewaySpec)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FabricVTEPSpec.
+func (in *FabricVTEPSpec) DeepCopy() *FabricVTEPSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(FabricVTEPSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RouteReflectorGroup) DeepCopyInto(out *RouteReflectorGroup) {
+ *out = *in
+ in.DeviceSelector.DeepCopyInto(&out.DeviceSelector)
+ in.ClientDeviceSelector.DeepCopyInto(&out.ClientDeviceSelector)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RouteReflectorGroup.
+func (in *RouteReflectorGroup) DeepCopy() *RouteReflectorGroup {
+ if in == nil {
+ return nil
+ }
+ out := new(RouteReflectorGroup)
+ in.DeepCopyInto(out)
+ return out
+}
diff --git a/charts/network-operator/templates/crd/fabrics.evpn.networking.metal.ironcore.dev.yaml b/charts/network-operator/templates/crd/fabrics.evpn.networking.metal.ironcore.dev.yaml
new file mode 100644
index 000000000..087c78e25
--- /dev/null
+++ b/charts/network-operator/templates/crd/fabrics.evpn.networking.metal.ironcore.dev.yaml
@@ -0,0 +1,695 @@
+{{- if .Values.crd.enable }}
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ {{- if .Values.crd.keep }}
+ "helm.sh/resource-policy": keep
+ {{- end }}
+ controller-gen.kubebuilder.io/version: v0.21.0
+ name: fabrics.evpn.networking.metal.ironcore.dev
+spec:
+ group: evpn.networking.metal.ironcore.dev
+ names:
+ kind: Fabric
+ listKind: FabricList
+ plural: fabrics
+ singular: fabric
+ scope: Namespaced
+ versions:
+ - additionalPrinterColumns:
+ - jsonPath: .spec.underlay.protocol
+ name: Underlay
+ type: string
+ - jsonPath: .spec.overlay.protocol
+ name: Overlay
+ type: string
+ - jsonPath: .spec.overlay.ibgp.asNumber
+ name: AS Number
+ type: string
+ - jsonPath: .spec.bum.type
+ name: BUM
+ priority: 1
+ type: string
+ - jsonPath: .status.conditions[?(@.type=="Ready")].status
+ name: Ready
+ type: string
+ - jsonPath: .metadata.creationTimestamp
+ name: Age
+ type: date
+ name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: Fabric is the Schema for the fabrics API
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: |-
+ Specification of the desired state of the resource.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
+ properties:
+ bum:
+ description: bum defines how BUM traffic is forwarded across the fabric.
+ properties:
+ pim:
+ description: pim configures PIM sparse mode when type is Multicast.
+ properties:
+ anycastRendezvousPoints:
+ description: |-
+ anycastRendezvousPoints lists the anycast rendezvous point groups used for
+ PIM sparse mode. Anycast RPs share the same IP address across multiple
+ devices for redundancy.
+ items:
+ description: AnycastRendezvousPoint defines an anycast PIM
+ rendezvous point group.
+ properties:
+ clientDeviceSelector:
+ description: |-
+ clientDeviceSelector identifies the devices that register with the rendezvous
+ points in this group.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label
+ selector requirements. The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the
+ selector applies to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ deviceSelector:
+ description: |-
+ deviceSelector identifies which devices are configured as rendezvous points
+ in this group.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label
+ selector requirements. The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the
+ selector applies to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ multicastGroups:
+ description: |-
+ multicastGroups lists the multicast group address ranges served by this
+ rendezvous point. Each entry must be a valid IPv4 multicast CIDR prefix.
+ items:
+ format: cidr
+ type: string
+ minItems: 1
+ type: array
+ name:
+ description: name is a unique identifier for this rendezvous
+ point group within the fabric.
+ maxLength: 63
+ minLength: 1
+ type: string
+ required:
+ - clientDeviceSelector
+ - deviceSelector
+ - multicastGroups
+ - name
+ type: object
+ minItems: 1
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ required:
+ - anycastRendezvousPoints
+ type: object
+ type:
+ description: type selects the BUM forwarding mechanism.
+ enum:
+ - Multicast
+ type: string
+ required:
+ - type
+ type: object
+ x-kubernetes-validations:
+ - message: pim must be set when type is Multicast
+ rule: self.type != 'Multicast' || has(self.pim)
+ deviceSelector:
+ description: |-
+ deviceSelector identifies which devices are members of this fabric.
+ All devices whose labels match this selector will be enrolled.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label selector requirements.
+ The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the selector applies
+ to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ loopbacks:
+ description: |-
+ loopbacks configures IP address allocation for loopback interfaces on
+ fabric devices.
+ properties:
+ ipAddressPoolRef:
+ description: |-
+ ipAddressPoolRef references the IPAddressPool from which loopback addresses
+ are allocated for devices in the fabric.
+ properties:
+ name:
+ description: |-
+ Name of the referent.
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ maxLength: 63
+ minLength: 1
+ type: string
+ required:
+ - name
+ type: object
+ x-kubernetes-map-type: atomic
+ required:
+ - ipAddressPoolRef
+ type: object
+ overlay:
+ description: overlay defines the overlay control-plane configuration
+ for the fabric.
+ properties:
+ ibgp:
+ description: ibgp configures the iBGP overlay when protocol is
+ IBGP.
+ properties:
+ asNumber:
+ anyOf:
+ - type: integer
+ - type: string
+ description: |-
+ asNumber is the BGP autonomous system number shared by all devices in the
+ iBGP fabric. Supports both plain format (1-4294967295) and dotted notation
+ (1-65535.0-65535) as per RFC 5396.
+ x-kubernetes-int-or-string: true
+ routeReflectors:
+ description: |-
+ routeReflectors lists the route reflector groups that provide iBGP scalability.
+ Each group designates a set of reflectors and their client devices.
+ items:
+ description: RouteReflectorGroup defines a set of BGP route
+ reflectors and their clients.
+ properties:
+ clientDeviceSelector:
+ description: |-
+ clientDeviceSelector identifies the devices that are route reflector clients
+ for this group.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label
+ selector requirements. The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the
+ selector applies to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ deviceSelector:
+ description: deviceSelector identifies which devices
+ act as route reflectors in this group.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label
+ selector requirements. The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the
+ selector applies to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ name:
+ description: name is a unique identifier for this route
+ reflector group within the fabric.
+ maxLength: 63
+ minLength: 1
+ type: string
+ required:
+ - clientDeviceSelector
+ - deviceSelector
+ - name
+ type: object
+ minItems: 1
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ required:
+ - asNumber
+ - routeReflectors
+ type: object
+ x-kubernetes-validations:
+ - message: at least one route reflector group must be specified
+ rule: size(self.routeReflectors) > 0
+ protocol:
+ description: protocol is the control-plane protocol used for the
+ EVPN overlay.
+ enum:
+ - IBGP
+ type: string
+ required:
+ - protocol
+ type: object
+ x-kubernetes-validations:
+ - message: ibgp must be set when protocol is IBGP
+ rule: self.protocol != 'IBGP' || has(self.ibgp)
+ underlay:
+ description: underlay defines the underlay routing configuration for
+ the fabric.
+ properties:
+ addressing:
+ description: addressing configures how IP addresses are assigned
+ to underlay interfaces.
+ properties:
+ ipPrefixPoolRef:
+ description: |-
+ ipPrefixPoolRef references the IPPrefixPool from which point-to-point
+ prefixes are allocated for underlay interfaces.
+ properties:
+ name:
+ description: |-
+ Name of the referent.
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ maxLength: 63
+ minLength: 1
+ type: string
+ required:
+ - name
+ type: object
+ x-kubernetes-map-type: atomic
+ unnumbered:
+ default: false
+ description: |-
+ unnumbered controls whether underlay interfaces use unnumbered addressing
+ (borrowing from loopback0) instead of dedicated point-to-point addresses.
+ type: boolean
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of ipPrefixPoolRef or unnumbered must be
+ set
+ rule: has(self.ipPrefixPoolRef) != self.unnumbered
+ interfaceSelector:
+ description: |-
+ interfaceSelector identifies which interfaces participate in the underlay.
+ Interfaces on fabric devices matching these labels will be enrolled in the
+ underlay routing process.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label selector
+ requirements. The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the selector
+ applies to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ protocol:
+ description: |-
+ protocol is the routing protocol used to build IP reachability across the
+ fabric underlay.
+ enum:
+ - OSPF
+ - ISIS
+ type: string
+ required:
+ - addressing
+ - interfaceSelector
+ - protocol
+ type: object
+ vtep:
+ description: vtep identifies the VTEP devices and configures their
+ anycast gateway.
+ properties:
+ anycastGateway:
+ description: anycastGateway configures the anycast gateway shared
+ across all VTEP devices.
+ properties:
+ virtualMAC:
+ description: |-
+ virtualMAC is the shared MAC address used by all anycast gateway instances
+ across the fabric. Must be a valid IEEE 802 MAC address in colon-separated
+ hexadecimal notation (e.g. f0:0c:c1:5c:00:00).
+ pattern: ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$
+ type: string
+ required:
+ - virtualMAC
+ type: object
+ deviceSelector:
+ description: deviceSelector identifies which devices are configured
+ as VTEPs.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label selector
+ requirements. The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the selector
+ applies to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ required:
+ - deviceSelector
+ type: object
+ required:
+ - bum
+ - deviceSelector
+ - loopbacks
+ - overlay
+ - underlay
+ - vtep
+ type: object
+ status:
+ description: |-
+ Status of the resource. This is set and updated automatically.
+ Read-only.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
+ properties:
+ conditions:
+ description: |-
+ conditions represent the current state of the Fabric resource.
+ Each condition has a unique type and reflects the status of a specific aspect of the resource.
+ The status of each condition is one of True, False, or Unknown.
+ items:
+ description: Condition contains details for one aspect of the current
+ state of this API Resource.
+ properties:
+ lastTransitionTime:
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False, Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: type of condition in CamelCase or in foo.example.com/CamelCase.
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ type: array
+ x-kubernetes-list-map-keys:
+ - type
+ x-kubernetes-list-type: map
+ type: object
+ required:
+ - spec
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
+{{- end }}
diff --git a/charts/network-operator/templates/rbac/evpn-fabric-admin-role.yaml b/charts/network-operator/templates/rbac/evpn-fabric-admin-role.yaml
new file mode 100644
index 000000000..df148f6e4
--- /dev/null
+++ b/charts/network-operator/templates/rbac/evpn-fabric-admin-role.yaml
@@ -0,0 +1,31 @@
+{{- if .Values.rbac.helpers.enable }}
+apiVersion: rbac.authorization.k8s.io/v1
+{{- if .Values.rbac.namespaced }}
+kind: Role
+{{- else }}
+kind: ClusterRole
+{{- end }}
+metadata:
+{{- if .Values.rbac.namespaced }}
+ namespace: {{ .Release.Namespace }}
+{{- end }}
+ labels:
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
+ app.kubernetes.io/name: {{ include "network-operator.name" . }}
+ helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ name: {{ include "network-operator.resourceName" (dict "suffix" "evpn-fabric-admin-role" "context" $) }}
+rules:
+- apiGroups:
+ - evpn.networking.metal.ironcore.dev
+ resources:
+ - fabrics
+ verbs:
+ - '*'
+- apiGroups:
+ - evpn.networking.metal.ironcore.dev
+ resources:
+ - fabrics/status
+ verbs:
+ - get
+{{- end }}
diff --git a/charts/network-operator/templates/rbac/evpn-fabric-editor-role.yaml b/charts/network-operator/templates/rbac/evpn-fabric-editor-role.yaml
new file mode 100644
index 000000000..575a5edcd
--- /dev/null
+++ b/charts/network-operator/templates/rbac/evpn-fabric-editor-role.yaml
@@ -0,0 +1,37 @@
+{{- if .Values.rbac.helpers.enable }}
+apiVersion: rbac.authorization.k8s.io/v1
+{{- if .Values.rbac.namespaced }}
+kind: Role
+{{- else }}
+kind: ClusterRole
+{{- end }}
+metadata:
+{{- if .Values.rbac.namespaced }}
+ namespace: {{ .Release.Namespace }}
+{{- end }}
+ labels:
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
+ app.kubernetes.io/name: {{ include "network-operator.name" . }}
+ helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ name: {{ include "network-operator.resourceName" (dict "suffix" "evpn-fabric-editor-role" "context" $) }}
+rules:
+- apiGroups:
+ - evpn.networking.metal.ironcore.dev
+ resources:
+ - fabrics
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - evpn.networking.metal.ironcore.dev
+ resources:
+ - fabrics/status
+ verbs:
+ - get
+{{- end }}
diff --git a/charts/network-operator/templates/rbac/evpn-fabric-viewer-role.yaml b/charts/network-operator/templates/rbac/evpn-fabric-viewer-role.yaml
new file mode 100644
index 000000000..339486816
--- /dev/null
+++ b/charts/network-operator/templates/rbac/evpn-fabric-viewer-role.yaml
@@ -0,0 +1,33 @@
+{{- if .Values.rbac.helpers.enable }}
+apiVersion: rbac.authorization.k8s.io/v1
+{{- if .Values.rbac.namespaced }}
+kind: Role
+{{- else }}
+kind: ClusterRole
+{{- end }}
+metadata:
+{{- if .Values.rbac.namespaced }}
+ namespace: {{ .Release.Namespace }}
+{{- end }}
+ labels:
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
+ app.kubernetes.io/name: {{ include "network-operator.name" . }}
+ helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ name: {{ include "network-operator.resourceName" (dict "suffix" "evpn-fabric-viewer-role" "context" $) }}
+rules:
+- apiGroups:
+ - evpn.networking.metal.ironcore.dev
+ resources:
+ - fabrics
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - evpn.networking.metal.ironcore.dev
+ resources:
+ - fabrics/status
+ verbs:
+ - get
+{{- end }}
diff --git a/charts/network-operator/templates/rbac/manager-role.yaml b/charts/network-operator/templates/rbac/manager-role.yaml
index 35f34b3af..889750870 100644
--- a/charts/network-operator/templates/rbac/manager-role.yaml
+++ b/charts/network-operator/templates/rbac/manager-role.yaml
@@ -45,6 +45,32 @@ rules:
verbs:
- create
- patch
+- apiGroups:
+ - evpn.networking.metal.ironcore.dev
+ resources:
+ - fabrics
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - evpn.networking.metal.ironcore.dev
+ resources:
+ - fabrics/finalizers
+ verbs:
+ - update
+- apiGroups:
+ - evpn.networking.metal.ironcore.dev
+ resources:
+ - fabrics/status
+ verbs:
+ - get
+ - patch
+ - update
- apiGroups:
- networking.metal.ironcore.dev
resources:
diff --git a/cmd/main.go b/cmd/main.go
index 2e2ac3cf7..3507ba7f3 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -44,9 +44,11 @@ import (
nxv1alpha1 "github.com/ironcore-dev/network-operator/api/cisco/nx/v1alpha1"
"github.com/ironcore-dev/network-operator/api/core/v1alpha1"
+ evpnv1alpha1 "github.com/ironcore-dev/network-operator/api/evpn/v1alpha1"
poolv1alpha1 "github.com/ironcore-dev/network-operator/api/pool/v1alpha1"
nxcontroller "github.com/ironcore-dev/network-operator/internal/controller/cisco/nx"
corecontroller "github.com/ironcore-dev/network-operator/internal/controller/core"
+ evpncontroller "github.com/ironcore-dev/network-operator/internal/controller/evpn"
poolcontroller "github.com/ironcore-dev/network-operator/internal/controller/pool"
"github.com/ironcore-dev/network-operator/internal/provider"
"github.com/ironcore-dev/network-operator/internal/provisioning"
@@ -67,6 +69,7 @@ func init() {
utilruntime.Must(v1alpha1.AddToScheme(scheme))
utilruntime.Must(nxv1alpha1.AddToScheme(scheme))
utilruntime.Must(poolv1alpha1.AddToScheme(scheme))
+ utilruntime.Must(evpnv1alpha1.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}
@@ -762,6 +765,14 @@ func main() { //nolint:gocyclo
os.Exit(1)
}
+ if err := (&evpncontroller.FabricReconciler{
+ Client: mgr.GetClient(),
+ Scheme: mgr.GetScheme(),
+ Recorder: mgr.GetEventRecorder("fabric-controller"),
+ }).SetupWithManager(mgr); err != nil {
+ setupLog.Error(err, "Failed to create controller", "controller", "Fabric")
+ os.Exit(1)
+ }
// +kubebuilder:scaffold:builder
if metricsCertWatcher != nil {
diff --git a/config/crd/bases/evpn.networking.metal.ironcore.dev_fabrics.yaml b/config/crd/bases/evpn.networking.metal.ironcore.dev_fabrics.yaml
new file mode 100644
index 000000000..e5899388f
--- /dev/null
+++ b/config/crd/bases/evpn.networking.metal.ironcore.dev_fabrics.yaml
@@ -0,0 +1,691 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.21.0
+ name: fabrics.evpn.networking.metal.ironcore.dev
+spec:
+ group: evpn.networking.metal.ironcore.dev
+ names:
+ kind: Fabric
+ listKind: FabricList
+ plural: fabrics
+ singular: fabric
+ scope: Namespaced
+ versions:
+ - additionalPrinterColumns:
+ - jsonPath: .spec.underlay.protocol
+ name: Underlay
+ type: string
+ - jsonPath: .spec.overlay.protocol
+ name: Overlay
+ type: string
+ - jsonPath: .spec.overlay.ibgp.asNumber
+ name: AS Number
+ type: string
+ - jsonPath: .spec.bum.type
+ name: BUM
+ priority: 1
+ type: string
+ - jsonPath: .status.conditions[?(@.type=="Ready")].status
+ name: Ready
+ type: string
+ - jsonPath: .metadata.creationTimestamp
+ name: Age
+ type: date
+ name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: Fabric is the Schema for the fabrics API
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: |-
+ Specification of the desired state of the resource.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
+ properties:
+ bum:
+ description: bum defines how BUM traffic is forwarded across the fabric.
+ properties:
+ pim:
+ description: pim configures PIM sparse mode when type is Multicast.
+ properties:
+ anycastRendezvousPoints:
+ description: |-
+ anycastRendezvousPoints lists the anycast rendezvous point groups used for
+ PIM sparse mode. Anycast RPs share the same IP address across multiple
+ devices for redundancy.
+ items:
+ description: AnycastRendezvousPoint defines an anycast PIM
+ rendezvous point group.
+ properties:
+ clientDeviceSelector:
+ description: |-
+ clientDeviceSelector identifies the devices that register with the rendezvous
+ points in this group.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label
+ selector requirements. The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the
+ selector applies to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ deviceSelector:
+ description: |-
+ deviceSelector identifies which devices are configured as rendezvous points
+ in this group.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label
+ selector requirements. The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the
+ selector applies to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ multicastGroups:
+ description: |-
+ multicastGroups lists the multicast group address ranges served by this
+ rendezvous point. Each entry must be a valid IPv4 multicast CIDR prefix.
+ items:
+ format: cidr
+ type: string
+ minItems: 1
+ type: array
+ name:
+ description: name is a unique identifier for this rendezvous
+ point group within the fabric.
+ maxLength: 63
+ minLength: 1
+ type: string
+ required:
+ - clientDeviceSelector
+ - deviceSelector
+ - multicastGroups
+ - name
+ type: object
+ minItems: 1
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ required:
+ - anycastRendezvousPoints
+ type: object
+ type:
+ description: type selects the BUM forwarding mechanism.
+ enum:
+ - Multicast
+ type: string
+ required:
+ - type
+ type: object
+ x-kubernetes-validations:
+ - message: pim must be set when type is Multicast
+ rule: self.type != 'Multicast' || has(self.pim)
+ deviceSelector:
+ description: |-
+ deviceSelector identifies which devices are members of this fabric.
+ All devices whose labels match this selector will be enrolled.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label selector requirements.
+ The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the selector applies
+ to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ loopbacks:
+ description: |-
+ loopbacks configures IP address allocation for loopback interfaces on
+ fabric devices.
+ properties:
+ ipAddressPoolRef:
+ description: |-
+ ipAddressPoolRef references the IPAddressPool from which loopback addresses
+ are allocated for devices in the fabric.
+ properties:
+ name:
+ description: |-
+ Name of the referent.
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ maxLength: 63
+ minLength: 1
+ type: string
+ required:
+ - name
+ type: object
+ x-kubernetes-map-type: atomic
+ required:
+ - ipAddressPoolRef
+ type: object
+ overlay:
+ description: overlay defines the overlay control-plane configuration
+ for the fabric.
+ properties:
+ ibgp:
+ description: ibgp configures the iBGP overlay when protocol is
+ IBGP.
+ properties:
+ asNumber:
+ anyOf:
+ - type: integer
+ - type: string
+ description: |-
+ asNumber is the BGP autonomous system number shared by all devices in the
+ iBGP fabric. Supports both plain format (1-4294967295) and dotted notation
+ (1-65535.0-65535) as per RFC 5396.
+ x-kubernetes-int-or-string: true
+ routeReflectors:
+ description: |-
+ routeReflectors lists the route reflector groups that provide iBGP scalability.
+ Each group designates a set of reflectors and their client devices.
+ items:
+ description: RouteReflectorGroup defines a set of BGP route
+ reflectors and their clients.
+ properties:
+ clientDeviceSelector:
+ description: |-
+ clientDeviceSelector identifies the devices that are route reflector clients
+ for this group.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label
+ selector requirements. The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the
+ selector applies to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ deviceSelector:
+ description: deviceSelector identifies which devices
+ act as route reflectors in this group.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label
+ selector requirements. The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the
+ selector applies to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ name:
+ description: name is a unique identifier for this route
+ reflector group within the fabric.
+ maxLength: 63
+ minLength: 1
+ type: string
+ required:
+ - clientDeviceSelector
+ - deviceSelector
+ - name
+ type: object
+ minItems: 1
+ type: array
+ x-kubernetes-list-map-keys:
+ - name
+ x-kubernetes-list-type: map
+ required:
+ - asNumber
+ - routeReflectors
+ type: object
+ x-kubernetes-validations:
+ - message: at least one route reflector group must be specified
+ rule: size(self.routeReflectors) > 0
+ protocol:
+ description: protocol is the control-plane protocol used for the
+ EVPN overlay.
+ enum:
+ - IBGP
+ type: string
+ required:
+ - protocol
+ type: object
+ x-kubernetes-validations:
+ - message: ibgp must be set when protocol is IBGP
+ rule: self.protocol != 'IBGP' || has(self.ibgp)
+ underlay:
+ description: underlay defines the underlay routing configuration for
+ the fabric.
+ properties:
+ addressing:
+ description: addressing configures how IP addresses are assigned
+ to underlay interfaces.
+ properties:
+ ipPrefixPoolRef:
+ description: |-
+ ipPrefixPoolRef references the IPPrefixPool from which point-to-point
+ prefixes are allocated for underlay interfaces.
+ properties:
+ name:
+ description: |-
+ Name of the referent.
+ More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ maxLength: 63
+ minLength: 1
+ type: string
+ required:
+ - name
+ type: object
+ x-kubernetes-map-type: atomic
+ unnumbered:
+ default: false
+ description: |-
+ unnumbered controls whether underlay interfaces use unnumbered addressing
+ (borrowing from loopback0) instead of dedicated point-to-point addresses.
+ type: boolean
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of ipPrefixPoolRef or unnumbered must be
+ set
+ rule: has(self.ipPrefixPoolRef) != self.unnumbered
+ interfaceSelector:
+ description: |-
+ interfaceSelector identifies which interfaces participate in the underlay.
+ Interfaces on fabric devices matching these labels will be enrolled in the
+ underlay routing process.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label selector
+ requirements. The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the selector
+ applies to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ protocol:
+ description: |-
+ protocol is the routing protocol used to build IP reachability across the
+ fabric underlay.
+ enum:
+ - OSPF
+ - ISIS
+ type: string
+ required:
+ - addressing
+ - interfaceSelector
+ - protocol
+ type: object
+ vtep:
+ description: vtep identifies the VTEP devices and configures their
+ anycast gateway.
+ properties:
+ anycastGateway:
+ description: anycastGateway configures the anycast gateway shared
+ across all VTEP devices.
+ properties:
+ virtualMAC:
+ description: |-
+ virtualMAC is the shared MAC address used by all anycast gateway instances
+ across the fabric. Must be a valid IEEE 802 MAC address in colon-separated
+ hexadecimal notation (e.g. f0:0c:c1:5c:00:00).
+ pattern: ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$
+ type: string
+ required:
+ - virtualMAC
+ type: object
+ deviceSelector:
+ description: deviceSelector identifies which devices are configured
+ as VTEPs.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label selector
+ requirements. The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the selector
+ applies to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ required:
+ - deviceSelector
+ type: object
+ required:
+ - bum
+ - deviceSelector
+ - loopbacks
+ - overlay
+ - underlay
+ - vtep
+ type: object
+ status:
+ description: |-
+ Status of the resource. This is set and updated automatically.
+ Read-only.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
+ properties:
+ conditions:
+ description: |-
+ conditions represent the current state of the Fabric resource.
+ Each condition has a unique type and reflects the status of a specific aspect of the resource.
+ The status of each condition is one of True, False, or Unknown.
+ items:
+ description: Condition contains details for one aspect of the current
+ state of this API Resource.
+ properties:
+ lastTransitionTime:
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False, Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: type of condition in CamelCase or in foo.example.com/CamelCase.
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ type: array
+ x-kubernetes-list-map-keys:
+ - type
+ x-kubernetes-list-type: map
+ type: object
+ required:
+ - spec
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index 85f58776b..230dc25ae 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -33,6 +33,7 @@ resources:
- bases/pool.networking.metal.ironcore.dev_indices.yaml
- bases/pool.networking.metal.ironcore.dev_ipaddresses.yaml
- bases/pool.networking.metal.ironcore.dev_ipprefixes.yaml
+- bases/evpn.networking.metal.ironcore.dev_fabrics.yaml
- bases/nx.cisco.networking.metal.ironcore.dev_bordergateways.yaml
- bases/nx.cisco.networking.metal.ironcore.dev_managementaccessconfigs.yaml
- bases/nx.cisco.networking.metal.ironcore.dev_networkvirtualizationedgeconfigs.yaml
diff --git a/config/rbac/evpn_fabric_admin_role.yaml b/config/rbac/evpn_fabric_admin_role.yaml
new file mode 100644
index 000000000..3740e4f61
--- /dev/null
+++ b/config/rbac/evpn_fabric_admin_role.yaml
@@ -0,0 +1,27 @@
+# This rule is not used by the project network-operator itself.
+# It is provided to allow the cluster admin to help manage permissions for users.
+#
+# Grants full permissions ('*') over evpn.networking.metal.ironcore.dev.
+# This role is intended for users authorized to modify roles and bindings within the cluster,
+# enabling them to delegate specific permissions to other users or groups as needed.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: network-operator
+ app.kubernetes.io/managed-by: kustomize
+ name: evpn-fabric-admin-role
+rules:
+- apiGroups:
+ - evpn.networking.metal.ironcore.dev
+ resources:
+ - fabrics
+ verbs:
+ - '*'
+- apiGroups:
+ - evpn.networking.metal.ironcore.dev
+ resources:
+ - fabrics/status
+ verbs:
+ - get
diff --git a/config/rbac/evpn_fabric_editor_role.yaml b/config/rbac/evpn_fabric_editor_role.yaml
new file mode 100644
index 000000000..79b5c4f95
--- /dev/null
+++ b/config/rbac/evpn_fabric_editor_role.yaml
@@ -0,0 +1,33 @@
+# This rule is not used by the project network-operator itself.
+# It is provided to allow the cluster admin to help manage permissions for users.
+#
+# Grants permissions to create, update, and delete resources within the evpn.networking.metal.ironcore.dev.
+# This role is intended for users who need to manage these resources
+# but should not control RBAC or manage permissions for others.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: network-operator
+ app.kubernetes.io/managed-by: kustomize
+ name: evpn-fabric-editor-role
+rules:
+- apiGroups:
+ - evpn.networking.metal.ironcore.dev
+ resources:
+ - fabrics
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - evpn.networking.metal.ironcore.dev
+ resources:
+ - fabrics/status
+ verbs:
+ - get
diff --git a/config/rbac/evpn_fabric_viewer_role.yaml b/config/rbac/evpn_fabric_viewer_role.yaml
new file mode 100644
index 000000000..3822d6b39
--- /dev/null
+++ b/config/rbac/evpn_fabric_viewer_role.yaml
@@ -0,0 +1,29 @@
+# This rule is not used by the project network-operator itself.
+# It is provided to allow the cluster admin to help manage permissions for users.
+#
+# Grants read-only access to evpn.networking.metal.ironcore.dev resources.
+# This role is intended for users who need visibility into these resources
+# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: network-operator
+ app.kubernetes.io/managed-by: kustomize
+ name: evpn-fabric-viewer-role
+rules:
+- apiGroups:
+ - evpn.networking.metal.ironcore.dev
+ resources:
+ - fabrics
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - evpn.networking.metal.ironcore.dev
+ resources:
+ - fabrics/status
+ verbs:
+ - get
diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml
index c3155fa85..1522b5963 100644
--- a/config/rbac/kustomization.yaml
+++ b/config/rbac/kustomization.yaml
@@ -137,3 +137,6 @@ resources:
- index_admin_role.yaml
- index_editor_role.yaml
- index_viewer_role.yaml
+- evpn_fabric_admin_role.yaml
+- evpn_fabric_editor_role.yaml
+- evpn_fabric_viewer_role.yaml
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index a8101bcd5..18553d50e 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -39,6 +39,32 @@ rules:
verbs:
- create
- patch
+- apiGroups:
+ - evpn.networking.metal.ironcore.dev
+ resources:
+ - fabrics
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - evpn.networking.metal.ironcore.dev
+ resources:
+ - fabrics/finalizers
+ verbs:
+ - update
+- apiGroups:
+ - evpn.networking.metal.ironcore.dev
+ resources:
+ - fabrics/status
+ verbs:
+ - get
+ - patch
+ - update
- apiGroups:
- networking.metal.ironcore.dev
resources:
diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml
index 6ad7cde59..5beb035b6 100644
--- a/config/samples/kustomization.yaml
+++ b/config/samples/kustomization.yaml
@@ -31,6 +31,7 @@ resources:
- v1alpha1_ipaddress.yaml
- v1alpha1_ipprefix.yaml
- v1alpha1_claim.yaml
+- v1alpha1_fabric.yaml
- cisco/nx/v1alpha1_bordergateway.yaml
- cisco/nx/v1alpha1_managementaccessconfig.yaml
- cisco/nx/v1alpha1_nveconfig.yaml
diff --git a/config/samples/v1alpha1_fabric.yaml b/config/samples/v1alpha1_fabric.yaml
new file mode 100644
index 000000000..b6b718919
--- /dev/null
+++ b/config/samples/v1alpha1_fabric.yaml
@@ -0,0 +1,82 @@
+apiVersion: evpn.networking.metal.ironcore.dev/v1alpha1
+kind: Fabric
+metadata:
+ labels:
+ app.kubernetes.io/name: network-operator
+ app.kubernetes.io/managed-by: kustomize
+ name: fabric
+spec:
+ # Selects all devices in the qa-de-1a zone as fabric members.
+ deviceSelector:
+ matchLabels:
+ topology.kubernetes.io/zone: qa-de-1a
+ # Loopback addresses are allocated from the referenced IPAddressPool.
+ loopbacks:
+ ipAddressPoolRef:
+ name: loopback-pool
+ # OSPF underlay using numbered /31 point-to-point links.
+ # Interfaces with role "fabric" in the qa-de-1a zone participate.
+ underlay:
+ protocol: OSPF # or ISIS
+ interfaceSelector:
+ matchLabels:
+ networking.metal.ironcore.dev/role: fabric
+ addressing:
+ ipPrefixPoolRef:
+ name: underlay-p2p-pool
+ # iBGP EVPN overlay with spine devices as route reflectors for leaf clients.
+ overlay:
+ protocol: IBGP
+ ibgp:
+ asNumber: 65000
+ routeReflectors:
+ - name: spine-rrs
+ deviceSelector:
+ matchLabels:
+ networking.metal.ironcore.dev/role: evpn-spine
+ clientDeviceSelector:
+ matchLabels:
+ networking.metal.ironcore.dev/role: evpn-leaf
+ # PIM sparse mode for BUM traffic with anycast rendezvous points on spines.
+ bum:
+ type: Multicast
+ pim:
+ anycastRendezvousPoints:
+ - name: spine-rp
+ multicastGroups: [ 224.0.0.0/4 ]
+ deviceSelector:
+ matchLabels:
+ networking.metal.ironcore.dev/role: evpn-spine
+ clientDeviceSelector:
+ matchLabels:
+ networking.metal.ironcore.dev/role: evpn-leaf
+ # Leaf devices act as VTEPs with a shared anycast gateway MAC.
+ vtep:
+ deviceSelector:
+ matchLabels:
+ networking.metal.ironcore.dev/role: evpn-leaf
+ anycastGateway:
+ virtualMAC: f0:0c:c1:5c:00:00
+---
+apiVersion: pool.networking.metal.ironcore.dev/v1alpha1
+kind: IPAddressPool
+metadata:
+ labels:
+ app.kubernetes.io/name: network-operator
+ app.kubernetes.io/managed-by: kustomize
+ name: loopback-pool
+spec:
+ prefixes:
+ - 10.0.0.0/16
+---
+apiVersion: pool.networking.metal.ironcore.dev/v1alpha1
+kind: IPPrefixPool
+metadata:
+ labels:
+ app.kubernetes.io/name: network-operator
+ app.kubernetes.io/managed-by: kustomize
+ name: underlay-p2p-pool
+spec:
+ prefixes:
+ - prefix: 10.1.0.0/16
+ prefixLength: 31
diff --git a/docs/api-reference/index.md b/docs/api-reference/index.md
index 606a6456c..a852c2de4 100644
--- a/docs/api-reference/index.md
+++ b/docs/api-reference/index.md
@@ -1,6 +1,7 @@
# API Reference
## Packages
+- [evpn.networking.metal.ironcore.dev/v1alpha1](#evpnnetworkingmetalironcoredevv1alpha1)
- [networking.metal.ironcore.dev/v1alpha1](#networking-metal-ironcore-dev-v1alpha1)
- [nx.cisco.networking.metal.ironcore.dev/v1alpha1](#nx-cisco-networking-metal-ironcore-dev-v1alpha1)
- [pool.networking.metal.ironcore.dev/v1alpha1](#poolnetworkingmetalironcoredevv1alpha1)
@@ -8,6 +9,316 @@
- [xr.cisco.networking.metal.ironcore.dev/v1alpha1](#xr-cisco-networking-metal-ironcore-dev-v1alpha1)
+## evpn.networking.metal.ironcore.dev/v1alpha1
+
+Package v1alpha1 contains API Schema definitions for the evpn.networking.metal.ironcore.dev v1alpha1 API group.
+
+### Resource Types
+- [Fabric](#fabric)
+
+
+
+#### AnycastRendezvousPoint
+
+
+
+AnycastRendezvousPoint defines an anycast PIM rendezvous point group.
+
+
+
+_Appears in:_
+- [FabricPIMSpec](#fabricpimspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `name` _string_ | name is a unique identifier for this rendezvous point group within the fabric. | | MaxLength: 63
MinLength: 1
Required: \{\}
|
+| `multicastGroups` _[IPPrefix](#ipprefix) array_ | multicastGroups lists the multicast group address ranges served by this
rendezvous point. Each entry must be a valid IPv4 multicast CIDR prefix. | | Format: cidr
MinItems: 1
Type: string
Required: \{\}
|
+| `deviceSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#labelselector-v1-meta)_ | deviceSelector identifies which devices are configured as rendezvous points
in this group. | | Required: \{\}
|
+| `clientDeviceSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#labelselector-v1-meta)_ | clientDeviceSelector identifies the devices that register with the rendezvous
points in this group. | | Required: \{\}
|
+
+
+#### BUMType
+
+_Underlying type:_ _string_
+
+BUMType is the mechanism used to handle BUM (Broadcast, Unknown unicast,
+Multicast) traffic in the fabric.
+
+_Validation:_
+- Enum: [Multicast]
+
+_Appears in:_
+- [FabricBUMSpec](#fabricbumspec)
+
+| Field | Description |
+| --- | --- |
+| `Multicast` | BUMTypeMulticast uses PIM sparse mode for BUM traffic forwarding.
|
+
+
+#### Fabric
+
+
+
+Fabric is the Schema for the fabrics API
+
+
+
+
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `apiVersion` _string_ | `evpn.networking.metal.ironcore.dev/v1alpha1` | | |
+| `kind` _string_ | `Fabric` | | |
+| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
+| `spec` _[FabricSpec](#fabricspec)_ | Specification of the desired state of the resource.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status | | Required: \{\}
|
+| `status` _[FabricStatus](#fabricstatus)_ | Status of the resource. This is set and updated automatically.
Read-only.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status | | Optional: \{\}
|
+
+
+#### FabricAnycastGatewaySpec
+
+
+
+FabricAnycastGatewaySpec configures the anycast gateway on VTEP devices.
+
+
+
+_Appears in:_
+- [FabricVTEPSpec](#fabricvtepspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `virtualMAC` _string_ | virtualMAC is the shared MAC address used by all anycast gateway instances
across the fabric. Must be a valid IEEE 802 MAC address in colon-separated
hexadecimal notation (e.g. f0:0c:c1:5c:00:00). | | Pattern: `^([0-9a-fA-F]\{2\}:)\{5\}[0-9a-fA-F]\{2\}$`
Required: \{\}
|
+
+
+#### FabricBUMSpec
+
+
+
+FabricBUMSpec defines how BUM (Broadcast, Unknown unicast, Multicast) traffic
+is forwarded across the fabric.
+
+
+
+_Appears in:_
+- [FabricSpec](#fabricspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `type` _[BUMType](#bumtype)_ | type selects the BUM forwarding mechanism. | | Enum: [Multicast]
Required: \{\}
|
+| `pim` _[FabricPIMSpec](#fabricpimspec)_ | pim configures PIM sparse mode when type is Multicast. | | Optional: \{\}
|
+
+
+#### FabricIBGPSpec
+
+
+
+FabricIBGPSpec configures the iBGP overlay control plane.
+
+
+
+_Appears in:_
+- [FabricOverlaySpec](#fabricoverlayspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `asNumber` _[IntOrString](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#intorstring-intstr-util)_ | asNumber is the BGP autonomous system number shared by all devices in the
iBGP fabric. Supports both plain format (1-4294967295) and dotted notation
(1-65535.0-65535) as per RFC 5396. | | Required: \{\}
|
+| `routeReflectors` _[RouteReflectorGroup](#routereflectorgroup) array_ | routeReflectors lists the route reflector groups that provide iBGP scalability.
Each group designates a set of reflectors and their client devices. | | MinItems: 1
Required: \{\}
|
+
+
+#### FabricLoopbacksSpec
+
+
+
+FabricLoopbacksSpec configures IP address allocation for loopback interfaces.
+
+
+
+_Appears in:_
+- [FabricSpec](#fabricspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `ipAddressPoolRef` _[LocalObjectReference](#localobjectreference)_ | ipAddressPoolRef references the IPAddressPool from which loopback addresses
are allocated for devices in the fabric. | | Required: \{\}
|
+
+
+#### FabricOverlaySpec
+
+
+
+FabricOverlaySpec defines the overlay control-plane configuration.
+
+
+
+_Appears in:_
+- [FabricSpec](#fabricspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `protocol` _[OverlayProtocol](#overlayprotocol)_ | protocol is the control-plane protocol used for the EVPN overlay. | | Enum: [IBGP]
Required: \{\}
|
+| `ibgp` _[FabricIBGPSpec](#fabricibgpspec)_ | ibgp configures the iBGP overlay when protocol is IBGP. | | Optional: \{\}
|
+
+
+#### FabricPIMSpec
+
+
+
+FabricPIMSpec configures PIM sparse mode for BUM traffic.
+
+
+
+_Appears in:_
+- [FabricBUMSpec](#fabricbumspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `anycastRendezvousPoints` _[AnycastRendezvousPoint](#anycastrendezvouspoint) array_ | anycastRendezvousPoints lists the anycast rendezvous point groups used for
PIM sparse mode. Anycast RPs share the same IP address across multiple
devices for redundancy. | | MinItems: 1
Required: \{\}
|
+
+
+#### FabricSpec
+
+
+
+FabricSpec defines the desired state of Fabric.
+
+
+
+_Appears in:_
+- [Fabric](#fabric)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `deviceSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#labelselector-v1-meta)_ | deviceSelector identifies which devices are members of this fabric.
All devices whose labels match this selector will be enrolled. | | Required: \{\}
|
+| `loopbacks` _[FabricLoopbacksSpec](#fabricloopbacksspec)_ | loopbacks configures IP address allocation for loopback interfaces on
fabric devices. | | Required: \{\}
|
+| `underlay` _[FabricUnderlaySpec](#fabricunderlayspec)_ | underlay defines the underlay routing configuration for the fabric. | | Required: \{\}
|
+| `overlay` _[FabricOverlaySpec](#fabricoverlayspec)_ | overlay defines the overlay control-plane configuration for the fabric. | | Required: \{\}
|
+| `bum` _[FabricBUMSpec](#fabricbumspec)_ | bum defines how BUM traffic is forwarded across the fabric. | | Required: \{\}
|
+| `vtep` _[FabricVTEPSpec](#fabricvtepspec)_ | vtep identifies the VTEP devices and configures their anycast gateway. | | Required: \{\}
|
+
+
+#### FabricStatus
+
+
+
+FabricStatus defines the observed state of Fabric.
+
+
+
+_Appears in:_
+- [Fabric](#fabric)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#condition-v1-meta) array_ | conditions represent the current state of the Fabric resource.
Each condition has a unique type and reflects the status of a specific aspect of the resource.
The status of each condition is one of True, False, or Unknown. | | Optional: \{\}
|
+
+
+#### FabricUnderlayAddressingSpec
+
+
+
+FabricUnderlayAddressingSpec configures how IP addresses are assigned to
+underlay point-to-point links.
+
+
+
+_Appears in:_
+- [FabricUnderlaySpec](#fabricunderlayspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `ipPrefixPoolRef` _[LocalObjectReference](#localobjectreference)_ | ipPrefixPoolRef references the IPPrefixPool from which point-to-point
prefixes are allocated for underlay interfaces. | | Optional: \{\}
|
+| `unnumbered` _boolean_ | unnumbered controls whether underlay interfaces use unnumbered addressing
(borrowing from loopback0) instead of dedicated point-to-point addresses. | false | Optional: \{\}
|
+
+
+#### FabricUnderlaySpec
+
+
+
+FabricUnderlaySpec defines the underlay network configuration.
+
+
+
+_Appears in:_
+- [FabricSpec](#fabricspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `protocol` _[UnderlayProtocol](#underlayprotocol)_ | protocol is the routing protocol used to build IP reachability across the
fabric underlay. | | Enum: [OSPF ISIS]
Required: \{\}
|
+| `interfaceSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#labelselector-v1-meta)_ | interfaceSelector identifies which interfaces participate in the underlay.
Interfaces on fabric devices matching these labels will be enrolled in the
underlay routing process. | | Required: \{\}
|
+| `addressing` _[FabricUnderlayAddressingSpec](#fabricunderlayaddressingspec)_ | addressing configures how IP addresses are assigned to underlay interfaces. | | Required: \{\}
|
+
+
+#### FabricVTEPSpec
+
+
+
+FabricVTEPSpec identifies which devices act as VXLAN Tunnel Endpoints (VTEPs)
+and optionally configures their shared anycast gateway.
+
+
+
+_Appears in:_
+- [FabricSpec](#fabricspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `deviceSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#labelselector-v1-meta)_ | deviceSelector identifies which devices are configured as VTEPs. | | Required: \{\}
|
+| `anycastGateway` _[FabricAnycastGatewaySpec](#fabricanycastgatewayspec)_ | anycastGateway configures the anycast gateway shared across all VTEP devices. | | Optional: \{\}
|
+
+
+#### OverlayProtocol
+
+_Underlying type:_ _string_
+
+OverlayProtocol is the control-plane protocol used for the overlay network.
+
+_Validation:_
+- Enum: [IBGP]
+
+_Appears in:_
+- [FabricOverlaySpec](#fabricoverlayspec)
+
+| Field | Description |
+| --- | --- |
+| `IBGP` | OverlayProtocolIBGP uses iBGP EVPN for the overlay control plane.
|
+
+
+#### RouteReflectorGroup
+
+
+
+RouteReflectorGroup defines a set of BGP route reflectors and their clients.
+
+
+
+_Appears in:_
+- [FabricIBGPSpec](#fabricibgpspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `name` _string_ | name is a unique identifier for this route reflector group within the fabric. | | MaxLength: 63
MinLength: 1
Required: \{\}
|
+| `deviceSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#labelselector-v1-meta)_ | deviceSelector identifies which devices act as route reflectors in this group. | | Required: \{\}
|
+| `clientDeviceSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#labelselector-v1-meta)_ | clientDeviceSelector identifies the devices that are route reflector clients
for this group. | | Required: \{\}
|
+
+
+#### UnderlayProtocol
+
+_Underlying type:_ _string_
+
+UnderlayProtocol is the routing protocol used for the underlay network.
+
+_Validation:_
+- Enum: [OSPF ISIS]
+
+_Appears in:_
+- [FabricUnderlaySpec](#fabricunderlayspec)
+
+| Field | Description |
+| --- | --- |
+| `OSPF` | UnderlayProtocolOSPF uses OSPF for underlay routing.
|
+| `ISIS` | UnderlayProtocolISIS uses IS-IS for underlay routing.
|
+
+
+
## networking.metal.ironcore.dev/v1alpha1
SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
@@ -1346,6 +1657,7 @@ _Validation:_
_Appears in:_
- [ACLEntry](#aclentry)
+- [AnycastRendezvousPoint](#anycastrendezvouspoint)
- [IPAddressPoolSpec](#ipaddresspoolspec)
- [IPPrefixPoolPrefix](#ipprefixpoolprefix)
- [IPPrefixSpec](#ipprefixspec)
@@ -1706,6 +2018,8 @@ _Appears in:_
- [DNSSpec](#dnsspec)
- [DevicePort](#deviceport)
- [EVPNInstanceSpec](#evpninstancespec)
+- [FabricLoopbacksSpec](#fabricloopbacksspec)
+- [FabricUnderlayAddressingSpec](#fabricunderlayaddressingspec)
- [ISISSpec](#isisspec)
- [InterconnectInterfaceReference](#interconnectinterfacereference)
- [InterfaceIPv4Unnumbered](#interfaceipv4unnumbered)
diff --git a/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/leaf1.yaml b/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/leaf1.yaml
index 7eac47f5f..dc240fe69 100644
--- a/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/leaf1.yaml
+++ b/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/leaf1.yaml
@@ -2,6 +2,7 @@ apiVersion: networking.metal.ironcore.dev/v1alpha1
kind: Device
metadata:
labels:
+ topology.kubernetes.io/zone: qa-de-1a
networking.metal.ironcore.dev/role: evpn-leaf
name: leaf1
spec:
diff --git a/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/leaf2.yaml b/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/leaf2.yaml
index 432ad8a47..0a090ec65 100644
--- a/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/leaf2.yaml
+++ b/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/leaf2.yaml
@@ -2,6 +2,7 @@ apiVersion: networking.metal.ironcore.dev/v1alpha1
kind: Device
metadata:
labels:
+ topology.kubernetes.io/zone: qa-de-1a
networking.metal.ironcore.dev/role: evpn-leaf
name: leaf2
spec:
diff --git a/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/leaf3.yaml b/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/leaf3.yaml
index b426aebc2..0c671f3dc 100644
--- a/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/leaf3.yaml
+++ b/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/leaf3.yaml
@@ -2,6 +2,7 @@ apiVersion: networking.metal.ironcore.dev/v1alpha1
kind: Device
metadata:
labels:
+ topology.kubernetes.io/zone: qa-de-1a
networking.metal.ironcore.dev/role: evpn-leaf
name: leaf3
spec:
diff --git a/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/spine1.yaml b/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/spine1.yaml
index 1d602b447..c4bebdde8 100644
--- a/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/spine1.yaml
+++ b/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/spine1.yaml
@@ -2,6 +2,7 @@ apiVersion: networking.metal.ironcore.dev/v1alpha1
kind: Device
metadata:
labels:
+ topology.kubernetes.io/zone: qa-de-1a
networking.metal.ironcore.dev/role: evpn-spine
name: spine1
spec:
diff --git a/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/spine2.yaml b/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/spine2.yaml
index 759277155..cd32124a5 100644
--- a/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/spine2.yaml
+++ b/examples/cisco-n9k-evpn-vxlan/kubernetes/01-devices/spine2.yaml
@@ -2,6 +2,7 @@ apiVersion: networking.metal.ironcore.dev/v1alpha1
kind: Device
metadata:
labels:
+ topology.kubernetes.io/zone: qa-de-1a
networking.metal.ironcore.dev/role: evpn-spine
name: spine2
spec:
diff --git a/internal/controller/evpn/fabric_controller.go b/internal/controller/evpn/fabric_controller.go
new file mode 100644
index 000000000..159018ddd
--- /dev/null
+++ b/internal/controller/evpn/fabric_controller.go
@@ -0,0 +1,171 @@
+// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package evpn
+
+import (
+ "context"
+ "fmt"
+
+ "k8s.io/apimachinery/pkg/api/equality"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ kerrors "k8s.io/apimachinery/pkg/util/errors"
+ "k8s.io/client-go/tools/events"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+ "sigs.k8s.io/controller-runtime/pkg/predicate"
+
+ "github.com/ironcore-dev/network-operator/api/core/v1alpha1"
+ evpnv1alpha1 "github.com/ironcore-dev/network-operator/api/evpn/v1alpha1"
+ "github.com/ironcore-dev/network-operator/internal/conditions"
+ "github.com/ironcore-dev/network-operator/internal/provider"
+)
+
+// FabricReconciler reconciles a Fabric object
+type FabricReconciler struct {
+ client.Client
+ Scheme *runtime.Scheme
+
+ // WatchFilterValue is the label value used to filter events prior to reconciliation.
+ WatchFilterValue string
+
+ // Recorder is used to record events for the controller.
+ // More info: https://book.kubebuilder.io/reference/raising-events
+ Recorder events.EventRecorder
+
+ // Provider is the driver that will be used to create & delete the interface.
+ Provider provider.ProviderFunc
+}
+
+// +kubebuilder:rbac:groups=evpn.networking.metal.ironcore.dev,resources=fabrics,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=evpn.networking.metal.ironcore.dev,resources=fabrics/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups=evpn.networking.metal.ironcore.dev,resources=fabrics/finalizers,verbs=update
+// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
+
+// Reconcile is part of the main kubernetes reconciliation loop which aims to
+// move the current state of the cluster closer to the desired state.
+//
+// For more details, check Reconcile and its Result here:
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/reconcile
+func (r *FabricReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) {
+ log := ctrl.LoggerFrom(ctx)
+ log.Info("Reconciling resource")
+
+ fabric := new(evpnv1alpha1.Fabric)
+ if err := r.Get(ctx, req.NamespacedName, fabric); err != nil {
+ if apierrors.IsNotFound(err) {
+ log.Info("Resource not found. Ignoring since object must be deleted")
+ return ctrl.Result{}, nil
+ }
+ log.Error(err, "Failed to get resource")
+ return ctrl.Result{}, err
+ }
+
+ if !fabric.DeletionTimestamp.IsZero() {
+ if controllerutil.ContainsFinalizer(fabric, evpnv1alpha1.FinalizerName) {
+ if err := r.finalize(ctx, fabric); err != nil {
+ log.Error(err, "Failed to finalize resource")
+ return ctrl.Result{}, err
+ }
+ controllerutil.RemoveFinalizer(fabric, evpnv1alpha1.FinalizerName)
+ if err := r.Update(ctx, fabric); err != nil {
+ log.Error(err, "Failed to remove finalizer from resource")
+ return ctrl.Result{}, err
+ }
+ }
+ log.Info("Resource is being deleted, skipping reconciliation")
+ return ctrl.Result{}, nil
+ }
+
+ // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers
+ if !controllerutil.ContainsFinalizer(fabric, evpnv1alpha1.FinalizerName) {
+ controllerutil.AddFinalizer(fabric, evpnv1alpha1.FinalizerName)
+ if err := r.Update(ctx, fabric); err != nil {
+ log.Error(err, "Failed to add finalizer to resource")
+ return ctrl.Result{}, err
+ }
+ log.Info("Added finalizer to resource")
+ return ctrl.Result{}, nil
+ }
+
+ orig := fabric.DeepCopy()
+ if conditions.InitializeConditions(fabric, v1alpha1.ReadyCondition) {
+ log.V(1).Info("Initializing status conditions")
+ return ctrl.Result{}, r.Status().Update(ctx, fabric)
+ }
+
+ defer func() {
+ if !equality.Semantic.DeepEqual(orig.ObjectMeta, fabric.ObjectMeta) {
+ // Pass obj.DeepCopy() to avoid Patch() modifying obj and interfering with status update below
+ if err := r.Patch(ctx, fabric.DeepCopy(), client.MergeFrom(orig)); err != nil {
+ log.Error(err, "Failed to update resource metadata")
+ reterr = kerrors.NewAggregate([]error{reterr, err})
+ }
+ }
+ if !equality.Semantic.DeepEqual(orig.Status, fabric.Status) {
+ if err := r.Status().Patch(ctx, fabric, client.MergeFrom(orig)); err != nil {
+ log.Error(err, "Failed to update status")
+ reterr = kerrors.NewAggregate([]error{reterr, err})
+ }
+ }
+ }()
+
+ res, err := r.reconcile(ctx, fabric)
+ if err != nil {
+ log.Error(err, "Failed to reconcile resource")
+ return ctrl.Result{}, err
+ }
+
+ return res, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *FabricReconciler) SetupWithManager(mgr ctrl.Manager) error {
+ labelSelector := metav1.LabelSelector{}
+ if r.WatchFilterValue != "" {
+ labelSelector.MatchLabels = map[string]string{v1alpha1.WatchLabel: r.WatchFilterValue}
+ }
+
+ filter, err := predicate.LabelSelectorPredicate(labelSelector)
+ if err != nil {
+ return fmt.Errorf("failed to create label selector predicate: %w", err)
+ }
+
+ return ctrl.NewControllerManagedBy(mgr).
+ For(&evpnv1alpha1.Fabric{}).
+ WithEventFilter(filter).
+ Named("evpn-fabric").
+ Complete(r)
+}
+
+// ReconcileFunc defines a function type for reconciliation phases.
+// Each phase should return a non-zero Result or an error if it wants to stop the reconciliation loop.
+type ReconcileFunc func(context.Context, *evpnv1alpha1.Fabric) (ctrl.Result, error)
+
+func (r *FabricReconciler) reconcile(ctx context.Context, fabric *evpnv1alpha1.Fabric) (ctrl.Result, error) {
+ phases := []ReconcileFunc{
+ // r.reconcileNodes,
+ }
+ for _, phase := range phases {
+ res, err := phase(ctx, fabric)
+ if err != nil || !res.IsZero() {
+ return res, err
+ }
+ }
+ conditions.Set(fabric, metav1.Condition{
+ Type: v1alpha1.ReadyCondition,
+ Status: metav1.ConditionTrue,
+ Reason: v1alpha1.ReadyReason,
+ Message: "Resource is ready",
+ })
+ return ctrl.Result{}, nil
+}
+
+func (r *FabricReconciler) finalize(ctx context.Context, fabric *evpnv1alpha1.Fabric) error {
+ _ = ctx
+ _ = fabric
+ return nil
+}
diff --git a/internal/controller/evpn/fabric_controller_test.go b/internal/controller/evpn/fabric_controller_test.go
new file mode 100644
index 000000000..7b06b12c7
--- /dev/null
+++ b/internal/controller/evpn/fabric_controller_test.go
@@ -0,0 +1,41 @@
+// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package evpn
+
+import (
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ evpnv1alpha1 "github.com/ironcore-dev/network-operator/api/evpn/v1alpha1"
+)
+
+var _ = Describe("Fabric Controller", func() {
+ var fabric *evpnv1alpha1.Fabric
+
+ BeforeEach(func() {
+ fabric = &evpnv1alpha1.Fabric{
+ ObjectMeta: metav1.ObjectMeta{
+ GenerateName: "fabric-",
+ Namespace: metav1.NamespaceDefault,
+ },
+ Spec: evpnv1alpha1.FabricSpec{},
+ }
+ Expect(k8sClient.Create(ctx, fabric)).To(Succeed())
+ })
+
+ AfterEach(func() {
+ Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, fabric))).To(Succeed())
+ })
+
+ It("Should successfully reconcile a Fabric", func() {
+ By("Updating the status")
+ Eventually(func(g Gomega) {
+ current := &evpnv1alpha1.Fabric{}
+ g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(fabric), current)).To(Succeed())
+ }).Should(Succeed())
+ })
+})
diff --git a/internal/controller/evpn/suite_test.go b/internal/controller/evpn/suite_test.go
new file mode 100644
index 000000000..35a43c3d7
--- /dev/null
+++ b/internal/controller/evpn/suite_test.go
@@ -0,0 +1,141 @@
+// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package evpn
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/kubernetes/scheme"
+ "k8s.io/client-go/tools/events"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/envtest"
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+ "sigs.k8s.io/controller-runtime/pkg/log/zap"
+
+ evpnv1alpha1 "github.com/ironcore-dev/network-operator/api/evpn/v1alpha1"
+ // +kubebuilder:scaffold:imports
+)
+
+// These tests use Ginkgo (BDD-style Go testing framework). Refer to
+// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
+
+var (
+ ctx context.Context
+ cancel context.CancelFunc
+ testEnv *envtest.Environment
+ k8sClient client.Client
+ k8sManager ctrl.Manager
+)
+
+func TestControllers(t *testing.T) {
+ RegisterFailHandler(Fail)
+
+ RunSpecs(t, "Controller Suite")
+}
+
+var _ = BeforeSuite(func() {
+ logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
+
+ SetDefaultEventuallyTimeout(time.Minute)
+ SetDefaultEventuallyPollingInterval(time.Second)
+
+ ctx, cancel = context.WithCancel(ctrl.SetupSignalHandler())
+
+ var err error
+ err = evpnv1alpha1.AddToScheme(scheme.Scheme)
+ Expect(err).NotTo(HaveOccurred())
+
+ // +kubebuilder:scaffold:scheme
+
+ By("bootstrapping test environment")
+ testEnv = &envtest.Environment{
+ CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
+ ErrorIfCRDPathMissing: true,
+ }
+
+ // Retrieve the first found binary directory to allow running tests from IDEs
+ if getFirstFoundEnvTestBinaryDir() != "" {
+ testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
+ }
+
+ cfg, err := testEnv.Start()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cfg).NotTo(BeNil())
+
+ k8sManager, err = ctrl.NewManager(cfg, ctrl.Options{
+ Scheme: scheme.Scheme,
+ Logger: GinkgoLogr,
+ })
+ Expect(err).NotTo(HaveOccurred())
+
+ recorder := events.NewFakeRecorder(100)
+ go func() {
+ defer GinkgoRecover()
+ for event := range recorder.Events {
+ GinkgoLogr.Info("Event", "event", event)
+ }
+ }()
+
+ k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
+ Expect(err).NotTo(HaveOccurred())
+ Expect(k8sClient).NotTo(BeNil())
+
+ err = (&FabricReconciler{
+ Client: k8sManager.GetClient(),
+ Scheme: k8sManager.GetScheme(),
+ Recorder: recorder,
+ }).SetupWithManager(k8sManager)
+ Expect(err).NotTo(HaveOccurred())
+
+ go func() {
+ defer GinkgoRecover()
+ err := k8sManager.Start(ctx)
+ Expect(err).NotTo(HaveOccurred(), "failed to run manager")
+ }()
+
+ Eventually(func() error {
+ var namespace corev1.Namespace
+ return k8sClient.Get(context.Background(), client.ObjectKey{Name: metav1.NamespaceDefault}, &namespace)
+ }).Should(Succeed())
+})
+
+var _ = AfterSuite(func() {
+ By("tearing down the test environment")
+ cancel()
+ err := testEnv.Stop()
+ Expect(err).NotTo(HaveOccurred())
+})
+
+// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
+// ENVTEST-based tests depend on specific binaries, usually located in paths set by
+// controller-runtime. When running tests directly (e.g., via an IDE) without using
+// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
+//
+// This function streamlines the process by finding the required binaries, similar to
+// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
+// properly set up, run 'make setup-envtest' beforehand.
+func getFirstFoundEnvTestBinaryDir() string {
+ basePath := filepath.Join("..", "..", "..", "bin", "k8s")
+ entries, err := os.ReadDir(basePath)
+ if err != nil {
+ logf.Log.Error(err, "Failed to read directory", "path", basePath)
+ return ""
+ }
+ for _, entry := range entries {
+ if entry.IsDir() {
+ return filepath.Join(basePath, entry.Name())
+ }
+ }
+ return ""
+}