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 "" +}