From bedb6fdd9b5862548d2518fab088f74479eab327 Mon Sep 17 00:00:00 2001 From: Ivo Gosemann Date: Tue, 15 Sep 2026 12:38:53 +0200 Subject: [PATCH] add nxos support for (external)communityset Signed-off-by: Ivo Gosemann --- Tiltfile | 5 + api/core/v1alpha1/communityset_types.go | 109 +++++++ api/core/v1alpha1/extcommunityset_types.go | 96 ++++++ api/core/v1alpha1/zz_generated.deepcopy.go | 229 +++++++++++++ ...itysets.networking.metal.ironcore.dev.yaml | 223 +++++++++++++ ...itysets.networking.metal.ironcore.dev.yaml | 224 +++++++++++++ .../templates/rbac/manager-role.yaml | 6 + cmd/main.go | 24 ++ ...king.metal.ironcore.dev_communitysets.yaml | 219 +++++++++++++ ...g.metal.ironcore.dev_extcommunitysets.yaml | 220 +++++++++++++ config/crd/kustomization.yaml | 2 + config/rbac/role.yaml | 6 + config/samples/kustomization.yaml | 2 + config/samples/v1alpha1_communityset.yaml | 16 + config/samples/v1alpha1_extcommunityset.yaml | 16 + docs/api-reference/index.md | 132 ++++++++ .../core/communityset_controller.go | 302 ++++++++++++++++++ .../core/communityset_controller_test.go | 115 +++++++ .../core/extcommunityset_controller.go | 302 ++++++++++++++++++ .../core/extcommunityset_controller_test.go | 115 +++++++ internal/controller/core/suite_test.go | 52 +++ internal/provider/cisco/nxos/communityset.go | 106 ++++++ .../provider/cisco/nxos/communityset_test.go | 26 ++ .../cisco/nxos/testdata/communityset.json | 21 ++ .../cisco/nxos/testdata/communityset.json.txt | 1 + .../cisco/nxos/testdata/extcommunityset.json | 21 ++ .../nxos/testdata/extcommunityset.json.txt | 1 + internal/provider/provider.go | 26 ++ test/gnmi/gnmi_suite_test.go | 18 ++ .../cisco-nxos-gnmi/communityset.txtar | 71 ++++ .../cisco-nxos-gnmi/extcommunityset.txtar | 71 ++++ 31 files changed, 2777 insertions(+) create mode 100644 api/core/v1alpha1/communityset_types.go create mode 100644 api/core/v1alpha1/extcommunityset_types.go create mode 100644 charts/network-operator/templates/crd/communitysets.networking.metal.ironcore.dev.yaml create mode 100644 charts/network-operator/templates/crd/extcommunitysets.networking.metal.ironcore.dev.yaml create mode 100644 config/crd/bases/networking.metal.ironcore.dev_communitysets.yaml create mode 100644 config/crd/bases/networking.metal.ironcore.dev_extcommunitysets.yaml create mode 100644 config/samples/v1alpha1_communityset.yaml create mode 100644 config/samples/v1alpha1_extcommunityset.yaml create mode 100644 internal/controller/core/communityset_controller.go create mode 100644 internal/controller/core/communityset_controller_test.go create mode 100644 internal/controller/core/extcommunityset_controller.go create mode 100644 internal/controller/core/extcommunityset_controller_test.go create mode 100644 internal/provider/cisco/nxos/communityset.go create mode 100644 internal/provider/cisco/nxos/communityset_test.go create mode 100644 internal/provider/cisco/nxos/testdata/communityset.json create mode 100644 internal/provider/cisco/nxos/testdata/communityset.json.txt create mode 100644 internal/provider/cisco/nxos/testdata/extcommunityset.json create mode 100644 internal/provider/cisco/nxos/testdata/extcommunityset.json.txt create mode 100644 test/gnmi/testdata/cisco-nxos-gnmi/communityset.txtar create mode 100644 test/gnmi/testdata/cisco-nxos-gnmi/extcommunityset.txtar diff --git a/Tiltfile b/Tiltfile index af834f15d..b3d781a3c 100644 --- a/Tiltfile +++ b/Tiltfile @@ -123,6 +123,11 @@ k8s_resource(new_name='bgp', objects=['bgp:bgp'], trigger_mode=TRIGGER_MODE_MANU k8s_resource(new_name='bgp-vrf-cc-admin', objects=['bgp-vrf-cc-admin:bgp'], resource_deps=['vrf-admin'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples']) k8s_resource(new_name='bgp-rdst', objects=['bgp-rdst:bgp'], resource_deps=['bgp-import-policy'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples']) +k8s_yaml('./config/samples/v1alpha1_communityset.yaml') +k8s_resource(new_name='communityset', objects=['communityset:communityset'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples']) +k8s_yaml('./config/samples/v1alpha1_extcommunityset.yaml') +k8s_resource(new_name='extcommunityset', objects=['extcommunityset:extcommunityset'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples']) + k8s_yaml('./config/samples/cisco/nx/v1alpha1_bgpconfig.yaml') k8s_resource(new_name='bgpconfig-adv-pip', objects=['bgpconfig-adv-pip:bgpconfig'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples']) k8s_resource(new_name='bgpconfig-export-gw', objects=['bgpconfig-export-gw:bgpconfig'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples']) diff --git a/api/core/v1alpha1/communityset_types.go b/api/core/v1alpha1/communityset_types.go new file mode 100644 index 000000000..ca01ac519 --- /dev/null +++ b/api/core/v1alpha1/communityset_types.go @@ -0,0 +1,109 @@ +// 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/runtime" +) + +// CommunityMember defines one ordered entry in a community-list with a regex pattern. +type CommunityMember struct { + // Sequence is the order of this entry in the community-list. + // +required + // +kubebuilder:validation:Minimum=1 + Sequence int32 `json:"sequence"` + + // Regex is a POSIX extended regular expression matching BGP community values. + // +required + // +kubebuilder:validation:MinLength=1 + Regex string `json:"regex"` +} + +// CommunitySetSpec defines the desired state of CommunitySet. +type CommunitySetSpec struct { + // DeviceRef is a reference to the Device this object belongs to. The Device object must exist in the same namespace. + // Immutable. + // +required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="DeviceRef is immutable" + DeviceRef LocalObjectReference `json:"deviceRef"` + + // ProviderConfigRef is a reference to a resource holding the provider-specific configuration. + // +optional + ProviderConfigRef *TypedLocalObjectReference `json:"providerConfigRef,omitempty"` + + // Name is the name of the CommunitySet on the device. + // Immutable. + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=32 + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Name is immutable" + Name string `json:"name"` + + // Members is the ordered list of community-list entries. + // +required + // +listType=map + // +listMapKey=sequence + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=100 + Members []CommunityMember `json:"members"` +} + +// CommunitySetStatus defines the observed state of CommunitySet. +type CommunitySetStatus struct { + // Conditions is a list of status conditions describing the state of the CommunitySet. + // +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=communitysets +// +kubebuilder:resource:singular=communityset +// +kubebuilder:printcolumn:name="Community Set",type=string,JSONPath=`.spec.name` +// +kubebuilder:printcolumn:name="Device",type=string,JSONPath=`.spec.deviceRef.name` +// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// CommunitySet is the Schema for the communitysets API. +type CommunitySet struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // +required + Spec CommunitySetSpec `json:"spec,omitempty"` + + // +optional + Status CommunitySetStatus `json:"status,omitzero"` +} + +// GetConditions implements conditions.Getter. +func (c *CommunitySet) GetConditions() []metav1.Condition { + return c.Status.Conditions +} + +// SetConditions implements conditions.Setter. +func (c *CommunitySet) SetConditions(conditions []metav1.Condition) { + c.Status.Conditions = conditions +} + +// +kubebuilder:object:root=true + +// CommunitySetList contains a list of CommunitySet. +type CommunitySetList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitzero"` + Items []CommunitySet `json:"items"` +} + +func init() { + SchemeBuilder.Register(func(s *runtime.Scheme) error { + s.AddKnownTypes(GroupVersion, &CommunitySet{}, &CommunitySetList{}) + return nil + }) +} diff --git a/api/core/v1alpha1/extcommunityset_types.go b/api/core/v1alpha1/extcommunityset_types.go new file mode 100644 index 000000000..15149a070 --- /dev/null +++ b/api/core/v1alpha1/extcommunityset_types.go @@ -0,0 +1,96 @@ +// 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/runtime" +) + +// ExtCommunitySetSpec defines the desired state of ExtCommunitySet. +type ExtCommunitySetSpec struct { + // DeviceRef is a reference to the Device this object belongs to. The Device object must exist in the same namespace. + // Immutable. + // +required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="DeviceRef is immutable" + DeviceRef LocalObjectReference `json:"deviceRef"` + + // ProviderConfigRef is a reference to a resource holding the provider-specific configuration. + // +optional + ProviderConfigRef *TypedLocalObjectReference `json:"providerConfigRef,omitempty"` + + // Name is the name of the ExtCommunitySet on the device. + // Immutable. + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=32 + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Name is immutable" + Name string `json:"name"` + + // Members is the ordered list of extended community-list entries. + // +required + // +listType=map + // +listMapKey=sequence + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=100 + Members []CommunityMember `json:"members"` +} + +// ExtCommunitySetStatus defines the observed state of ExtCommunitySet. +type ExtCommunitySetStatus struct { + // Conditions is a list of status conditions describing the state of the ExtCommunitySet. + // +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=extcommunitysets +// +kubebuilder:resource:singular=extcommunityset +// +kubebuilder:printcolumn:name="Ext Community Set",type=string,JSONPath=`.spec.name` +// +kubebuilder:printcolumn:name="Device",type=string,JSONPath=`.spec.deviceRef.name` +// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// ExtCommunitySet is the Schema for the extcommunitysets API. +type ExtCommunitySet struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // +required + Spec ExtCommunitySetSpec `json:"spec,omitempty"` + + // +optional + Status ExtCommunitySetStatus `json:"status,omitzero"` +} + +// GetConditions implements conditions.Getter. +func (e *ExtCommunitySet) GetConditions() []metav1.Condition { + return e.Status.Conditions +} + +// SetConditions implements conditions.Setter. +func (e *ExtCommunitySet) SetConditions(conditions []metav1.Condition) { + e.Status.Conditions = conditions +} + +// +kubebuilder:object:root=true + +// ExtCommunitySetList contains a list of ExtCommunitySet. +type ExtCommunitySetList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitzero"` + Items []ExtCommunitySet `json:"items"` +} + +func init() { + SchemeBuilder.Register(func(s *runtime.Scheme) error { + s.AddKnownTypes(GroupVersion, &ExtCommunitySet{}, &ExtCommunitySetList{}) + return nil + }) +} diff --git a/api/core/v1alpha1/zz_generated.deepcopy.go b/api/core/v1alpha1/zz_generated.deepcopy.go index 5e0a83e3a..14781e1b9 100644 --- a/api/core/v1alpha1/zz_generated.deepcopy.go +++ b/api/core/v1alpha1/zz_generated.deepcopy.go @@ -1250,6 +1250,128 @@ func (in *CertificateStatus) DeepCopy() *CertificateStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CommunityMember) DeepCopyInto(out *CommunityMember) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommunityMember. +func (in *CommunityMember) DeepCopy() *CommunityMember { + if in == nil { + return nil + } + out := new(CommunityMember) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CommunitySet) DeepCopyInto(out *CommunitySet) { + *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 CommunitySet. +func (in *CommunitySet) DeepCopy() *CommunitySet { + if in == nil { + return nil + } + out := new(CommunitySet) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CommunitySet) 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 *CommunitySetList) DeepCopyInto(out *CommunitySetList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CommunitySet, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommunitySetList. +func (in *CommunitySetList) DeepCopy() *CommunitySetList { + if in == nil { + return nil + } + out := new(CommunitySetList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CommunitySetList) 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 *CommunitySetSpec) DeepCopyInto(out *CommunitySetSpec) { + *out = *in + out.DeviceRef = in.DeviceRef + if in.ProviderConfigRef != nil { + in, out := &in.ProviderConfigRef, &out.ProviderConfigRef + *out = new(TypedLocalObjectReference) + **out = **in + } + if in.Members != nil { + in, out := &in.Members, &out.Members + *out = make([]CommunityMember, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommunitySetSpec. +func (in *CommunitySetSpec) DeepCopy() *CommunitySetSpec { + if in == nil { + return nil + } + out := new(CommunitySetSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CommunitySetStatus) DeepCopyInto(out *CommunitySetStatus) { + *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 CommunitySetStatus. +func (in *CommunitySetStatus) DeepCopy() *CommunitySetStatus { + if in == nil { + return nil + } + out := new(CommunitySetStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConfigBackup) DeepCopyInto(out *ConfigBackup) { *out = *in @@ -2261,6 +2383,113 @@ func (in *EthernetSegmentStatus) DeepCopy() *EthernetSegmentStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtCommunitySet) DeepCopyInto(out *ExtCommunitySet) { + *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 ExtCommunitySet. +func (in *ExtCommunitySet) DeepCopy() *ExtCommunitySet { + if in == nil { + return nil + } + out := new(ExtCommunitySet) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ExtCommunitySet) 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 *ExtCommunitySetList) DeepCopyInto(out *ExtCommunitySetList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ExtCommunitySet, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtCommunitySetList. +func (in *ExtCommunitySetList) DeepCopy() *ExtCommunitySetList { + if in == nil { + return nil + } + out := new(ExtCommunitySetList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ExtCommunitySetList) 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 *ExtCommunitySetSpec) DeepCopyInto(out *ExtCommunitySetSpec) { + *out = *in + out.DeviceRef = in.DeviceRef + if in.ProviderConfigRef != nil { + in, out := &in.ProviderConfigRef, &out.ProviderConfigRef + *out = new(TypedLocalObjectReference) + **out = **in + } + if in.Members != nil { + in, out := &in.Members, &out.Members + *out = make([]CommunityMember, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtCommunitySetSpec. +func (in *ExtCommunitySetSpec) DeepCopy() *ExtCommunitySetSpec { + if in == nil { + return nil + } + out := new(ExtCommunitySetSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtCommunitySetStatus) DeepCopyInto(out *ExtCommunitySetStatus) { + *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 ExtCommunitySetStatus. +func (in *ExtCommunitySetStatus) DeepCopy() *ExtCommunitySetStatus { + if in == nil { + return nil + } + out := new(ExtCommunitySetStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GNMI) DeepCopyInto(out *GNMI) { *out = *in diff --git a/charts/network-operator/templates/crd/communitysets.networking.metal.ironcore.dev.yaml b/charts/network-operator/templates/crd/communitysets.networking.metal.ironcore.dev.yaml new file mode 100644 index 000000000..9d0c6068f --- /dev/null +++ b/charts/network-operator/templates/crd/communitysets.networking.metal.ironcore.dev.yaml @@ -0,0 +1,223 @@ +{{- if .Values.crd.enabled }} +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.22.0 + name: communitysets.networking.metal.ironcore.dev +spec: + group: networking.metal.ironcore.dev + names: + kind: CommunitySet + listKind: CommunitySetList + plural: communitysets + singular: communityset + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.name + name: Community Set + type: string + - jsonPath: .spec.deviceRef.name + name: Device + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: CommunitySet is the Schema for the communitysets 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: CommunitySetSpec defines the desired state of CommunitySet. + properties: + deviceRef: + description: |- + DeviceRef is a reference to the Device this object belongs to. The Device object must exist in the same namespace. + Immutable. + 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 + x-kubernetes-validations: + - message: DeviceRef is immutable + rule: self == oldSelf + members: + description: Members is the ordered list of community-list entries. + items: + description: CommunityMember defines one ordered entry in a community-list + with a regex pattern. + properties: + regex: + description: Regex is a POSIX extended regular expression matching + BGP community values. + minLength: 1 + type: string + sequence: + description: Sequence is the order of this entry in the community-list. + format: int32 + minimum: 1 + type: integer + required: + - regex + - sequence + type: object + maxItems: 100 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - sequence + x-kubernetes-list-type: map + name: + description: |- + Name is the name of the CommunitySet on the device. + Immutable. + maxLength: 32 + minLength: 1 + type: string + x-kubernetes-validations: + - message: Name is immutable + rule: self == oldSelf + providerConfigRef: + description: ProviderConfigRef is a reference to a resource holding + the provider-specific configuration. + properties: + apiVersion: + description: APIVersion is the api group version of the resource + being referenced. + maxLength: 253 + minLength: 1 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/)?([a-z0-9]([-a-z0-9]*[a-z0-9])?)$ + type: string + kind: + description: |- + Kind of the resource being referenced. + Kind must consist of alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name of the resource being referenced. + Name must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - apiVersion + - kind + - name + type: object + x-kubernetes-map-type: atomic + required: + - deviceRef + - members + - name + type: object + status: + description: CommunitySetStatus defines the observed state of CommunitySet. + properties: + conditions: + description: Conditions is a list of status conditions describing + the state of the CommunitySet. + 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/crd/extcommunitysets.networking.metal.ironcore.dev.yaml b/charts/network-operator/templates/crd/extcommunitysets.networking.metal.ironcore.dev.yaml new file mode 100644 index 000000000..6615d2c70 --- /dev/null +++ b/charts/network-operator/templates/crd/extcommunitysets.networking.metal.ironcore.dev.yaml @@ -0,0 +1,224 @@ +{{- if .Values.crd.enabled }} +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.22.0 + name: extcommunitysets.networking.metal.ironcore.dev +spec: + group: networking.metal.ironcore.dev + names: + kind: ExtCommunitySet + listKind: ExtCommunitySetList + plural: extcommunitysets + singular: extcommunityset + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.name + name: Ext Community Set + type: string + - jsonPath: .spec.deviceRef.name + name: Device + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ExtCommunitySet is the Schema for the extcommunitysets 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: ExtCommunitySetSpec defines the desired state of ExtCommunitySet. + properties: + deviceRef: + description: |- + DeviceRef is a reference to the Device this object belongs to. The Device object must exist in the same namespace. + Immutable. + 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 + x-kubernetes-validations: + - message: DeviceRef is immutable + rule: self == oldSelf + members: + description: Members is the ordered list of extended community-list + entries. + items: + description: CommunityMember defines one ordered entry in a community-list + with a regex pattern. + properties: + regex: + description: Regex is a POSIX extended regular expression matching + BGP community values. + minLength: 1 + type: string + sequence: + description: Sequence is the order of this entry in the community-list. + format: int32 + minimum: 1 + type: integer + required: + - regex + - sequence + type: object + maxItems: 100 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - sequence + x-kubernetes-list-type: map + name: + description: |- + Name is the name of the ExtCommunitySet on the device. + Immutable. + maxLength: 32 + minLength: 1 + type: string + x-kubernetes-validations: + - message: Name is immutable + rule: self == oldSelf + providerConfigRef: + description: ProviderConfigRef is a reference to a resource holding + the provider-specific configuration. + properties: + apiVersion: + description: APIVersion is the api group version of the resource + being referenced. + maxLength: 253 + minLength: 1 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/)?([a-z0-9]([-a-z0-9]*[a-z0-9])?)$ + type: string + kind: + description: |- + Kind of the resource being referenced. + Kind must consist of alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name of the resource being referenced. + Name must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - apiVersion + - kind + - name + type: object + x-kubernetes-map-type: atomic + required: + - deviceRef + - members + - name + type: object + status: + description: ExtCommunitySetStatus defines the observed state of ExtCommunitySet. + properties: + conditions: + description: Conditions is a list of status conditions describing + the state of the ExtCommunitySet. + 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/manager-role.yaml b/charts/network-operator/templates/rbac/manager-role.yaml index 79729cf5f..6dfff14d4 100644 --- a/charts/network-operator/templates/rbac/manager-role.yaml +++ b/charts/network-operator/templates/rbac/manager-role.yaml @@ -80,12 +80,14 @@ rules: - bgp - bgppeers - certificates + - communitysets - configbackups - devices - dhcprelays - dns - ethernetsegments - evpninstances + - extcommunitysets - interfaces - isis - lldps @@ -119,11 +121,13 @@ rules: - bgp/finalizers - bgppeers/finalizers - certificates/finalizers + - communitysets/finalizers - devices/finalizers - dhcprelays/finalizers - dns/finalizers - ethernetsegments/finalizers - evpninstances/finalizers + - extcommunitysets/finalizers - interfaces/finalizers - isis/finalizers - lldps/finalizers @@ -150,12 +154,14 @@ rules: - bgp/status - bgppeers/status - certificates/status + - communitysets/status - configbackups/status - devices/status - dhcprelays/status - dns/status - ethernetsegments/status - evpninstances/status + - extcommunitysets/status - interfaces/status - isis/status - lldps/status diff --git a/cmd/main.go b/cmd/main.go index 5b13fcac4..5f5b7a217 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -616,6 +616,30 @@ func main() { //nolint:gocyclo os.Exit(1) } + if err := (&corecontroller.CommunitySetReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorder("communityset-controller"), + WatchFilterValue: watchFilterValue, + Provider: prov, + Locker: locker, + }).SetupWithManager(ctx, mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "CommunitySet") + os.Exit(1) + } + + if err := (&corecontroller.ExtCommunitySetReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorder("extcommunityset-controller"), + WatchFilterValue: watchFilterValue, + Provider: prov, + Locker: locker, + }).SetupWithManager(ctx, mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ExtCommunitySet") + os.Exit(1) + } + if err := (&corecontroller.RoutingPolicyReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), diff --git a/config/crd/bases/networking.metal.ironcore.dev_communitysets.yaml b/config/crd/bases/networking.metal.ironcore.dev_communitysets.yaml new file mode 100644 index 000000000..82e08969c --- /dev/null +++ b/config/crd/bases/networking.metal.ironcore.dev_communitysets.yaml @@ -0,0 +1,219 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.22.0 + name: communitysets.networking.metal.ironcore.dev +spec: + group: networking.metal.ironcore.dev + names: + kind: CommunitySet + listKind: CommunitySetList + plural: communitysets + singular: communityset + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.name + name: Community Set + type: string + - jsonPath: .spec.deviceRef.name + name: Device + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: CommunitySet is the Schema for the communitysets 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: CommunitySetSpec defines the desired state of CommunitySet. + properties: + deviceRef: + description: |- + DeviceRef is a reference to the Device this object belongs to. The Device object must exist in the same namespace. + Immutable. + 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 + x-kubernetes-validations: + - message: DeviceRef is immutable + rule: self == oldSelf + members: + description: Members is the ordered list of community-list entries. + items: + description: CommunityMember defines one ordered entry in a community-list + with a regex pattern. + properties: + regex: + description: Regex is a POSIX extended regular expression matching + BGP community values. + minLength: 1 + type: string + sequence: + description: Sequence is the order of this entry in the community-list. + format: int32 + minimum: 1 + type: integer + required: + - regex + - sequence + type: object + maxItems: 100 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - sequence + x-kubernetes-list-type: map + name: + description: |- + Name is the name of the CommunitySet on the device. + Immutable. + maxLength: 32 + minLength: 1 + type: string + x-kubernetes-validations: + - message: Name is immutable + rule: self == oldSelf + providerConfigRef: + description: ProviderConfigRef is a reference to a resource holding + the provider-specific configuration. + properties: + apiVersion: + description: APIVersion is the api group version of the resource + being referenced. + maxLength: 253 + minLength: 1 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/)?([a-z0-9]([-a-z0-9]*[a-z0-9])?)$ + type: string + kind: + description: |- + Kind of the resource being referenced. + Kind must consist of alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name of the resource being referenced. + Name must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - apiVersion + - kind + - name + type: object + x-kubernetes-map-type: atomic + required: + - deviceRef + - members + - name + type: object + status: + description: CommunitySetStatus defines the observed state of CommunitySet. + properties: + conditions: + description: Conditions is a list of status conditions describing + the state of the CommunitySet. + 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/bases/networking.metal.ironcore.dev_extcommunitysets.yaml b/config/crd/bases/networking.metal.ironcore.dev_extcommunitysets.yaml new file mode 100644 index 000000000..7915f929f --- /dev/null +++ b/config/crd/bases/networking.metal.ironcore.dev_extcommunitysets.yaml @@ -0,0 +1,220 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.22.0 + name: extcommunitysets.networking.metal.ironcore.dev +spec: + group: networking.metal.ironcore.dev + names: + kind: ExtCommunitySet + listKind: ExtCommunitySetList + plural: extcommunitysets + singular: extcommunityset + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.name + name: Ext Community Set + type: string + - jsonPath: .spec.deviceRef.name + name: Device + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ExtCommunitySet is the Schema for the extcommunitysets 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: ExtCommunitySetSpec defines the desired state of ExtCommunitySet. + properties: + deviceRef: + description: |- + DeviceRef is a reference to the Device this object belongs to. The Device object must exist in the same namespace. + Immutable. + 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 + x-kubernetes-validations: + - message: DeviceRef is immutable + rule: self == oldSelf + members: + description: Members is the ordered list of extended community-list + entries. + items: + description: CommunityMember defines one ordered entry in a community-list + with a regex pattern. + properties: + regex: + description: Regex is a POSIX extended regular expression matching + BGP community values. + minLength: 1 + type: string + sequence: + description: Sequence is the order of this entry in the community-list. + format: int32 + minimum: 1 + type: integer + required: + - regex + - sequence + type: object + maxItems: 100 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - sequence + x-kubernetes-list-type: map + name: + description: |- + Name is the name of the ExtCommunitySet on the device. + Immutable. + maxLength: 32 + minLength: 1 + type: string + x-kubernetes-validations: + - message: Name is immutable + rule: self == oldSelf + providerConfigRef: + description: ProviderConfigRef is a reference to a resource holding + the provider-specific configuration. + properties: + apiVersion: + description: APIVersion is the api group version of the resource + being referenced. + maxLength: 253 + minLength: 1 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/)?([a-z0-9]([-a-z0-9]*[a-z0-9])?)$ + type: string + kind: + description: |- + Kind of the resource being referenced. + Kind must consist of alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name of the resource being referenced. + Name must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - apiVersion + - kind + - name + type: object + x-kubernetes-map-type: atomic + required: + - deviceRef + - members + - name + type: object + status: + description: ExtCommunitySetStatus defines the observed state of ExtCommunitySet. + properties: + conditions: + description: Conditions is a list of status conditions describing + the state of the ExtCommunitySet. + 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 0281b3f4e..0d239da8c 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -7,10 +7,12 @@ resources: - bases/networking.metal.ironcore.dev_bgp.yaml - bases/networking.metal.ironcore.dev_bgppeers.yaml - bases/networking.metal.ironcore.dev_certificates.yaml +- bases/networking.metal.ironcore.dev_communitysets.yaml - bases/networking.metal.ironcore.dev_devices.yaml - bases/networking.metal.ironcore.dev_dhcprelays.yaml - bases/networking.metal.ironcore.dev_dns.yaml - bases/networking.metal.ironcore.dev_evpninstances.yaml +- bases/networking.metal.ironcore.dev_extcommunitysets.yaml - bases/networking.metal.ironcore.dev_interfaces.yaml - bases/networking.metal.ironcore.dev_isis.yaml - bases/networking.metal.ironcore.dev_managementaccesses.yaml diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 66cdebb60..227d68d6a 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -74,12 +74,14 @@ rules: - bgp - bgppeers - certificates + - communitysets - configbackups - devices - dhcprelays - dns - ethernetsegments - evpninstances + - extcommunitysets - interfaces - isis - lldps @@ -113,11 +115,13 @@ rules: - bgp/finalizers - bgppeers/finalizers - certificates/finalizers + - communitysets/finalizers - devices/finalizers - dhcprelays/finalizers - dns/finalizers - ethernetsegments/finalizers - evpninstances/finalizers + - extcommunitysets/finalizers - interfaces/finalizers - isis/finalizers - lldps/finalizers @@ -144,12 +148,14 @@ rules: - bgp/status - bgppeers/status - certificates/status + - communitysets/status - configbackups/status - devices/status - dhcprelays/status - dns/status - ethernetsegments/status - evpninstances/status + - extcommunitysets/status - interfaces/status - isis/status - lldps/status diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 4fdb74fa5..3a2c2d1b4 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -36,6 +36,8 @@ resources: - v1alpha1_claim.yaml - v1alpha1_fabric.yaml - v1alpha1_probe.yaml +- v1alpha1_communityset.yaml +- v1alpha1_extcommunityset.yaml - cisco/nx/v1alpha1_bordergateway.yaml - cisco/nx/v1alpha1_managementaccessconfig.yaml - cisco/nx/v1alpha1_nveconfig.yaml diff --git a/config/samples/v1alpha1_communityset.yaml b/config/samples/v1alpha1_communityset.yaml new file mode 100644 index 000000000..2275bc451 --- /dev/null +++ b/config/samples/v1alpha1_communityset.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.metal.ironcore.dev/v1alpha1 +kind: CommunitySet +metadata: + labels: + app.kubernetes.io/name: network-operator + app.kubernetes.io/managed-by: kustomize + name: communityset +spec: + deviceRef: + name: leaf1 + name: WIREAPI + members: + - sequence: 5 + regex: "50000:[0-9][0-9]" + - sequence: 10 + regex: "65001:[0-9]+" diff --git a/config/samples/v1alpha1_extcommunityset.yaml b/config/samples/v1alpha1_extcommunityset.yaml new file mode 100644 index 000000000..543fad3b1 --- /dev/null +++ b/config/samples/v1alpha1_extcommunityset.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.metal.ironcore.dev/v1alpha1 +kind: ExtCommunitySet +metadata: + labels: + app.kubernetes.io/name: network-operator + app.kubernetes.io/managed-by: kustomize + name: extcommunityset +spec: + deviceRef: + name: leaf1 + name: WIREAPI + members: + - sequence: 5 + regex: "65200:[0-9][0-9]" + - sequence: 15 + regex: "65300:[0-9]+" diff --git a/docs/api-reference/index.md b/docs/api-reference/index.md index e7664c675..550f53088 100644 --- a/docs/api-reference/index.md +++ b/docs/api-reference/index.md @@ -331,12 +331,14 @@ Package v1alpha1 contains API Schema definitions for the networking.metal.ironco - [BGPPeer](#bgppeer) - [Banner](#banner) - [Certificate](#certificate) +- [CommunitySet](#communityset) - [ConfigBackup](#configbackup) - [DHCPRelay](#dhcprelay) - [DNS](#dns) - [Device](#device) - [EVPNInstance](#evpninstance) - [EthernetSegment](#ethernetsegment) +- [ExtCommunitySet](#extcommunityset) - [ISIS](#isis) - [Interface](#interface) - [LLDP](#lldp) @@ -1374,6 +1376,78 @@ _Appears in:_ | `MD5` | | +#### CommunityMember + + + +CommunityMember defines one ordered entry in a community-list with a regex pattern. + + + +_Appears in:_ +- [CommunitySetSpec](#communitysetspec) +- [ExtCommunitySetSpec](#extcommunitysetspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `sequence` _integer_ | Sequence is the order of this entry in the community-list. | | Minimum: 1
Required: \{\}
| +| `regex` _string_ | Regex is a POSIX extended regular expression matching BGP community values. | | MinLength: 1
Required: \{\}
| + + +#### CommunitySet + + + +CommunitySet is the Schema for the communitysets API. + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `networking.metal.ironcore.dev/v1alpha1` | | | +| `kind` _string_ | `CommunitySet` | | | +| `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` _[CommunitySetSpec](#communitysetspec)_ | | | Required: \{\}
| +| `status` _[CommunitySetStatus](#communitysetstatus)_ | | | Optional: \{\}
| + + +#### CommunitySetSpec + + + +CommunitySetSpec defines the desired state of CommunitySet. + + + +_Appears in:_ +- [CommunitySet](#communityset) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `deviceRef` _[LocalObjectReference](#localobjectreference)_ | DeviceRef is a reference to the Device this object belongs to. The Device object must exist in the same namespace.
Immutable. | | Required: \{\}
| +| `providerConfigRef` _[TypedLocalObjectReference](#typedlocalobjectreference)_ | ProviderConfigRef is a reference to a resource holding the provider-specific configuration. | | Optional: \{\}
| +| `name` _string_ | Name is the name of the CommunitySet on the device.
Immutable. | | MaxLength: 32
MinLength: 1
Required: \{\}
| +| `members` _[CommunityMember](#communitymember) array_ | Members is the ordered list of community-list entries. | | MaxItems: 100
MinItems: 1
Required: \{\}
| + + +#### CommunitySetStatus + + + +CommunitySetStatus defines the observed state of CommunitySet. + + + +_Appears in:_ +- [CommunitySet](#communityset) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#condition-v1-meta) array_ | Conditions is a list of status conditions describing the state of the CommunitySet. | | Optional: \{\}
| + + #### ConfigBackup @@ -2150,6 +2224,60 @@ _Appears in:_ | `esiType` _[ESIType](#esitype)_ | ESIType is the ESI derivation type parsed from the first byte of ESI. | | Enum: [Arbitrary LACP MST MAC RouterID AS]
Optional: \{\}
| +#### ExtCommunitySet + + + +ExtCommunitySet is the Schema for the extcommunitysets API. + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `networking.metal.ironcore.dev/v1alpha1` | | | +| `kind` _string_ | `ExtCommunitySet` | | | +| `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` _[ExtCommunitySetSpec](#extcommunitysetspec)_ | | | Required: \{\}
| +| `status` _[ExtCommunitySetStatus](#extcommunitysetstatus)_ | | | Optional: \{\}
| + + +#### ExtCommunitySetSpec + + + +ExtCommunitySetSpec defines the desired state of ExtCommunitySet. + + + +_Appears in:_ +- [ExtCommunitySet](#extcommunityset) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `deviceRef` _[LocalObjectReference](#localobjectreference)_ | DeviceRef is a reference to the Device this object belongs to. The Device object must exist in the same namespace.
Immutable. | | Required: \{\}
| +| `providerConfigRef` _[TypedLocalObjectReference](#typedlocalobjectreference)_ | ProviderConfigRef is a reference to a resource holding the provider-specific configuration. | | Optional: \{\}
| +| `name` _string_ | Name is the name of the ExtCommunitySet on the device.
Immutable. | | MaxLength: 32
MinLength: 1
Required: \{\}
| +| `members` _[CommunityMember](#communitymember) array_ | Members is the ordered list of extended community-list entries. | | MaxItems: 100
MinItems: 1
Required: \{\}
| + + +#### ExtCommunitySetStatus + + + +ExtCommunitySetStatus defines the observed state of ExtCommunitySet. + + + +_Appears in:_ +- [ExtCommunitySet](#extcommunityset) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#condition-v1-meta) array_ | Conditions is a list of status conditions describing the state of the ExtCommunitySet. | | Optional: \{\}
| + + #### FECMode _Underlying type:_ _string_ @@ -2651,12 +2779,14 @@ _Appears in:_ - [BannerSpec](#bannerspec) - [BorderGatewaySpec](#bordergatewayspec) - [CertificateSpec](#certificatespec) +- [CommunitySetSpec](#communitysetspec) - [ConfigBackupSpec](#configbackupspec) - [DHCPRelaySpec](#dhcprelayspec) - [DNSSpec](#dnsspec) - [DevicePort](#deviceport) - [EVPNInstanceSpec](#evpninstancespec) - [EthernetSegmentSpec](#ethernetsegmentspec) +- [ExtCommunitySetSpec](#extcommunitysetspec) - [FabricLoopbacksSpec](#fabricloopbacksspec) - [FabricUnderlayAddressingSpec](#fabricunderlayaddressingspec) - [ISISSpec](#isisspec) @@ -4271,11 +4401,13 @@ _Appears in:_ - [CertificateSpec](#certificatespec) - [ClaimSpec](#claimspec) - [ClaimStatus](#claimstatus) +- [CommunitySetSpec](#communitysetspec) - [ConfigBackupSpec](#configbackupspec) - [DHCPRelaySpec](#dhcprelayspec) - [DNSSpec](#dnsspec) - [EVPNInstanceSpec](#evpninstancespec) - [EthernetSegmentSpec](#ethernetsegmentspec) +- [ExtCommunitySetSpec](#extcommunitysetspec) - [IPAddressSpec](#ipaddressspec) - [IPPrefixSpec](#ipprefixspec) - [ISISSpec](#isisspec) diff --git a/internal/controller/core/communityset_controller.go b/internal/controller/core/communityset_controller.go new file mode 100644 index 000000000..6e77dbb6e --- /dev/null +++ b/internal/controller/core/communityset_controller.go @@ -0,0 +1,302 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package core + +import ( + "context" + "errors" + "fmt" + "time" + + "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + 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" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + "github.com/ironcore-dev/network-operator/api/core/v1alpha1" + "github.com/ironcore-dev/network-operator/internal/apistatus" + "github.com/ironcore-dev/network-operator/internal/conditions" + "github.com/ironcore-dev/network-operator/internal/deviceutil" + "github.com/ironcore-dev/network-operator/internal/paused" + "github.com/ironcore-dev/network-operator/internal/provider" + "github.com/ironcore-dev/network-operator/internal/resourcelock" +) + +// CommunitySetReconciler reconciles a CommunitySet object. +type CommunitySetReconciler struct { + client.Client + Scheme *runtime.Scheme + + WatchFilterValue string + Recorder events.EventRecorder + Provider provider.ProviderFunc + Locker *resourcelock.ResourceLocker +} + +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=communitysets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=communitysets/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=communitysets/finalizers,verbs=update +// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch + +func (r *CommunitySetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { + log := ctrl.LoggerFrom(ctx) + log.V(3).Info("Reconciling resource") + + obj := new(v1alpha1.CommunitySet) + if err := r.Get(ctx, req.NamespacedName, obj); err != nil { + if apierrors.IsNotFound(err) { + log.V(3).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 + } + + prov, ok := r.Provider().(provider.CommunitySetProvider) + if !ok { + if meta.SetStatusCondition(&obj.Status.Conditions, metav1.Condition{ + Type: v1alpha1.ReadyCondition, + Status: metav1.ConditionFalse, + Reason: v1alpha1.NotImplementedReason, + Message: "Provider does not implement provider.CommunitySetProvider", + }) { + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + return ctrl.Result{}, nil + } + + device, err := deviceutil.GetDeviceByName(ctx, r, obj.Namespace, obj.Spec.DeviceRef.Name) + if err != nil { + return ctrl.Result{}, err + } + + if isPaused, err := paused.EnsureCondition(ctx, r.Client, device, obj); isPaused || err != nil { + return ctrl.Result{}, err + } + + if err := r.Locker.AcquireLock(ctx, device.Name, "communityset-controller"); err != nil { + if errors.Is(err, resourcelock.ErrLockAlreadyHeld) { + log.V(3).Info("Device is already locked, requeuing reconciliation") + return ctrl.Result{RequeueAfter: Jitter(time.Second), Priority: new(LockWaitPriorityDefault)}, nil + } + log.Error(err, "Failed to acquire device lock") + return ctrl.Result{}, err + } + defer func() { + if err := r.Locker.ReleaseLock(ctx, device.Name, "communityset-controller"); err != nil { + log.Error(err, "Failed to release device lock") + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + }() + + conn, err := deviceutil.GetDeviceConnection(ctx, r, device) + if err != nil { + return ctrl.Result{}, err + } + + var cfg *provider.ProviderConfig + if obj.Spec.ProviderConfigRef != nil { + cfg, err = provider.GetProviderConfig(ctx, r, obj.Namespace, obj.Spec.ProviderConfigRef) + if err != nil { + return ctrl.Result{}, err + } + } + + s := &communitySetScope{ + Device: device, + CommunitySet: obj, + Connection: conn, + ProviderConfig: cfg, + Provider: prov, + } + + if !obj.DeletionTimestamp.IsZero() { + if controllerutil.ContainsFinalizer(obj, v1alpha1.FinalizerName) { + if err := r.finalize(ctx, s); err != nil { + log.Error(err, "Failed to finalize resource") + return ctrl.Result{}, err + } + controllerutil.RemoveFinalizer(obj, v1alpha1.FinalizerName) + if err := r.Update(ctx, obj); err != nil { + log.Error(err, "Failed to remove finalizer from resource") + return ctrl.Result{}, err + } + } + log.V(3).Info("Resource is being deleted, skipping reconciliation") + return ctrl.Result{}, nil + } + + if !controllerutil.ContainsFinalizer(obj, v1alpha1.FinalizerName) { + controllerutil.AddFinalizer(obj, v1alpha1.FinalizerName) + if err := r.Update(ctx, obj); err != nil { + log.Error(err, "Failed to add finalizer to resource") + return ctrl.Result{}, err + } + log.V(1).Info("Added finalizer to resource") + return ctrl.Result{}, nil + } + + orig := obj.DeepCopy() + if conditions.InitializeConditions(obj, v1alpha1.ReadyCondition) { + log.V(1).Info("Initializing status conditions") + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + + defer func() { + if !equality.Semantic.DeepEqual(orig.ObjectMeta, obj.ObjectMeta) { + if err := r.Patch(ctx, obj.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, obj.Status) { + if err := r.Status().Patch(ctx, obj, client.MergeFrom(orig)); err != nil { + log.Error(err, "Failed to update status") + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + } + }() + + if err := r.reconcile(ctx, s); err != nil { + log.Error(err, "Failed to reconcile resource") + return ctrl.Result{}, apistatus.WrapTerminalError(err) + } + + return ctrl.Result{}, nil +} + +func (r *CommunitySetReconciler) SetupWithManager(ctx context.Context, 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) + } + + if err := mgr.GetFieldIndexer().IndexField(ctx, &v1alpha1.CommunitySet{}, v1alpha1.DeviceRefIndexKey, func(obj client.Object) []string { + o := obj.(*v1alpha1.CommunitySet) + return []string{o.Spec.DeviceRef.Name} + }); err != nil { + return err + } + + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.CommunitySet{}). + Named("communityset"). + WithEventFilter(filter). + Watches( + &v1alpha1.Device{}, + handler.EnqueueRequestsFromMapFunc(r.deviceToCommunitySets), + builder.WithPredicates(predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { + return paused.DevicePausedChanged(e.ObjectOld, e.ObjectNew) + }, + GenericFunc: func(e event.GenericEvent) bool { + return false + }, + }), + ). + Complete(r) +} + +type communitySetScope struct { + Device *v1alpha1.Device + CommunitySet *v1alpha1.CommunitySet + Connection *deviceutil.Connection + ProviderConfig *provider.ProviderConfig + Provider provider.CommunitySetProvider +} + +func (r *CommunitySetReconciler) reconcile(ctx context.Context, s *communitySetScope) (reterr error) { + if s.CommunitySet.Labels == nil { + s.CommunitySet.Labels = make(map[string]string) + } + s.CommunitySet.Labels[v1alpha1.DeviceLabel] = s.Device.Name + + if !controllerutil.HasControllerReference(s.CommunitySet) { + if err := controllerutil.SetOwnerReference(s.Device, s.CommunitySet, r.Scheme, controllerutil.WithBlockOwnerDeletion(true)); err != nil { + return err + } + } + + if err := s.Provider.Connect(ctx, s.Connection); err != nil { + return fmt.Errorf("failed to connect to provider: %w", err) + } + defer func() { + if err := s.Provider.Disconnect(ctx, s.Connection); err != nil { + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + }() + + err := s.Provider.EnsureCommunitySet(ctx, &provider.CommunitySetRequest{ + CommunitySet: s.CommunitySet, + ProviderConfig: s.ProviderConfig, + }) + + cond := conditions.FromError(err) + cond.Type = v1alpha1.ReadyCondition + conditions.Set(s.CommunitySet, cond) + + return err +} + +func (r *CommunitySetReconciler) finalize(ctx context.Context, s *communitySetScope) (reterr error) { + if err := s.Provider.Connect(ctx, s.Connection); err != nil { + return fmt.Errorf("failed to connect to provider: %w", err) + } + defer func() { + if err := s.Provider.Disconnect(ctx, s.Connection); err != nil { + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + }() + + return s.Provider.DeleteCommunitySet(ctx, &provider.CommunitySetRequest{ + CommunitySet: s.CommunitySet, + ProviderConfig: s.ProviderConfig, + }) +} + +func (r *CommunitySetReconciler) deviceToCommunitySets(ctx context.Context, obj client.Object) []ctrl.Request { + device, ok := obj.(*v1alpha1.Device) + if !ok { + panic(fmt.Sprintf("Expected a Device but got a %T", obj)) + } + + log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) + + list := new(v1alpha1.CommunitySetList) + if err := r.List( + ctx, list, + client.InNamespace(device.Namespace), + client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, + ); err != nil { + log.Error(err, "Failed to list CommunitySets") + return nil + } + + requests := make([]ctrl.Request, 0, len(list.Items)) + for i := range list.Items { + log.V(2).Info("Enqueuing CommunitySet for reconciliation", "CommunitySet", klog.KObj(&list.Items[i])) + requests = append(requests, ctrl.Request{ + Name: list.Items[i].Name, + Namespace: list.Items[i].Namespace, + }) + } + + return requests +} diff --git a/internal/controller/core/communityset_controller_test.go b/internal/controller/core/communityset_controller_test.go new file mode 100644 index 000000000..0fa25ddf7 --- /dev/null +++ b/internal/controller/core/communityset_controller_test.go @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package core + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/ironcore-dev/network-operator/api/core/v1alpha1" +) + +var _ = Describe("CommunitySet Controller", func() { + Context("When reconciling a resource", func() { + const set = "BGP-COMMUNITY" + var ( + name string + key client.ObjectKey + ) + + BeforeEach(func() { + By("Creating the custom resource for the Kind Device") + device := &v1alpha1.Device{ + GenerateName: "test-communityset-", + Namespace: metav1.NamespaceDefault, + Spec: v1alpha1.DeviceSpec{ + Endpoint: v1alpha1.Endpoint{ + Address: "192.168.10.2:9339", + }, + }, + } + Expect(k8sClient.Create(ctx, device)).To(Succeed()) + name = device.Name + key = client.ObjectKey{Name: name, Namespace: metav1.NamespaceDefault} + + By("Creating the custom resource for the Kind CommunitySet") + resource := &v1alpha1.CommunitySet{ + Name: name, + Namespace: metav1.NamespaceDefault, + Spec: v1alpha1.CommunitySetSpec{ + DeviceRef: v1alpha1.LocalObjectReference{Name: name}, + Name: set, + Members: []v1alpha1.CommunityMember{ + {Sequence: 5, Regex: "65000:[0-9]+"}, + {Sequence: 10, Regex: "65001:[0-9]+"}, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + }) + + AfterEach(func() { + By("Cleaning up the CommunitySet resource") + cs := &v1alpha1.CommunitySet{} + cs.Name = name + cs.Namespace = metav1.NamespaceDefault + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, cs))).To(Succeed()) + + By("Verifying the resource is removed from the provider") + Eventually(func(g Gomega) { + g.Expect(testProvider.CommunitySets.Has(set)).To(BeFalse(), "Provider should not have CommunitySet configured") + }).Should(Succeed()) + + By("Cleaning up the Device resource") + device := &v1alpha1.Device{} + device.Name = name + device.Namespace = metav1.NamespaceDefault + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, device))).To(Succeed()) + }) + + It("Should successfully reconcile the resource", func() { + By("Adding a finalizer to the resource") + Eventually(func(g Gomega) { + resource := &v1alpha1.CommunitySet{} + g.Expect(k8sClient.Get(ctx, key, resource)).To(Succeed()) + g.Expect(controllerutil.ContainsFinalizer(resource, v1alpha1.FinalizerName)).To(BeTrue()) + }).Should(Succeed()) + + By("Adding the device label to the resource") + Eventually(func(g Gomega) { + resource := &v1alpha1.CommunitySet{} + g.Expect(k8sClient.Get(ctx, key, resource)).To(Succeed()) + g.Expect(resource.Labels).To(HaveKeyWithValue(v1alpha1.DeviceLabel, name)) + }).Should(Succeed()) + + By("Adding the device as an owner reference") + Eventually(func(g Gomega) { + resource := &v1alpha1.CommunitySet{} + g.Expect(k8sClient.Get(ctx, key, resource)).To(Succeed()) + g.Expect(resource.OwnerReferences).To(HaveLen(1)) + g.Expect(resource.OwnerReferences[0].Kind).To(Equal("Device")) + g.Expect(resource.OwnerReferences[0].Name).To(Equal(name)) + }).Should(Succeed()) + + By("Updating the resource status") + Eventually(func(g Gomega) { + resource := &v1alpha1.CommunitySet{} + g.Expect(k8sClient.Get(ctx, key, resource)).To(Succeed()) + g.Expect(resource.Status.Conditions).To(HaveLen(2)) + g.Expect(resource.Status.Conditions[0].Type).To(Equal(v1alpha1.ReadyCondition)) + g.Expect(resource.Status.Conditions[0].Status).To(Equal(metav1.ConditionTrue)) + g.Expect(resource.Status.Conditions[1].Type).To(Equal(v1alpha1.PausedCondition)) + g.Expect(resource.Status.Conditions[1].Status).To(Equal(metav1.ConditionFalse)) + }).Should(Succeed()) + + By("Ensuring the resource is created in the provider") + Eventually(func(g Gomega) { + g.Expect(testProvider.CommunitySets.Has(set)).To(BeTrue(), "Provider should have CommunitySet configured") + }).Should(Succeed()) + }) + }) +}) diff --git a/internal/controller/core/extcommunityset_controller.go b/internal/controller/core/extcommunityset_controller.go new file mode 100644 index 000000000..db7a3ddcb --- /dev/null +++ b/internal/controller/core/extcommunityset_controller.go @@ -0,0 +1,302 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package core + +import ( + "context" + "errors" + "fmt" + "time" + + "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + 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" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + "github.com/ironcore-dev/network-operator/api/core/v1alpha1" + "github.com/ironcore-dev/network-operator/internal/apistatus" + "github.com/ironcore-dev/network-operator/internal/conditions" + "github.com/ironcore-dev/network-operator/internal/deviceutil" + "github.com/ironcore-dev/network-operator/internal/paused" + "github.com/ironcore-dev/network-operator/internal/provider" + "github.com/ironcore-dev/network-operator/internal/resourcelock" +) + +// ExtCommunitySetReconciler reconciles an ExtCommunitySet object. +type ExtCommunitySetReconciler struct { + client.Client + Scheme *runtime.Scheme + + WatchFilterValue string + Recorder events.EventRecorder + Provider provider.ProviderFunc + Locker *resourcelock.ResourceLocker +} + +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=extcommunitysets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=extcommunitysets/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=extcommunitysets/finalizers,verbs=update +// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch + +func (r *ExtCommunitySetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { + log := ctrl.LoggerFrom(ctx) + log.V(3).Info("Reconciling resource") + + obj := new(v1alpha1.ExtCommunitySet) + if err := r.Get(ctx, req.NamespacedName, obj); err != nil { + if apierrors.IsNotFound(err) { + log.V(3).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 + } + + prov, ok := r.Provider().(provider.ExtCommunitySetProvider) + if !ok { + if meta.SetStatusCondition(&obj.Status.Conditions, metav1.Condition{ + Type: v1alpha1.ReadyCondition, + Status: metav1.ConditionFalse, + Reason: v1alpha1.NotImplementedReason, + Message: "Provider does not implement provider.ExtCommunitySetProvider", + }) { + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + return ctrl.Result{}, nil + } + + device, err := deviceutil.GetDeviceByName(ctx, r, obj.Namespace, obj.Spec.DeviceRef.Name) + if err != nil { + return ctrl.Result{}, err + } + + if isPaused, err := paused.EnsureCondition(ctx, r.Client, device, obj); isPaused || err != nil { + return ctrl.Result{}, err + } + + if err := r.Locker.AcquireLock(ctx, device.Name, "extcommunityset-controller"); err != nil { + if errors.Is(err, resourcelock.ErrLockAlreadyHeld) { + log.V(3).Info("Device is already locked, requeuing reconciliation") + return ctrl.Result{RequeueAfter: Jitter(time.Second), Priority: new(LockWaitPriorityDefault)}, nil + } + log.Error(err, "Failed to acquire device lock") + return ctrl.Result{}, err + } + defer func() { + if err := r.Locker.ReleaseLock(ctx, device.Name, "extcommunityset-controller"); err != nil { + log.Error(err, "Failed to release device lock") + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + }() + + conn, err := deviceutil.GetDeviceConnection(ctx, r, device) + if err != nil { + return ctrl.Result{}, err + } + + var cfg *provider.ProviderConfig + if obj.Spec.ProviderConfigRef != nil { + cfg, err = provider.GetProviderConfig(ctx, r, obj.Namespace, obj.Spec.ProviderConfigRef) + if err != nil { + return ctrl.Result{}, err + } + } + + s := &extCommunitySetScope{ + Device: device, + ExtCommunitySet: obj, + Connection: conn, + ProviderConfig: cfg, + Provider: prov, + } + + if !obj.DeletionTimestamp.IsZero() { + if controllerutil.ContainsFinalizer(obj, v1alpha1.FinalizerName) { + if err := r.finalizeExt(ctx, s); err != nil { + log.Error(err, "Failed to finalize resource") + return ctrl.Result{}, err + } + controllerutil.RemoveFinalizer(obj, v1alpha1.FinalizerName) + if err := r.Update(ctx, obj); err != nil { + log.Error(err, "Failed to remove finalizer from resource") + return ctrl.Result{}, err + } + } + log.V(3).Info("Resource is being deleted, skipping reconciliation") + return ctrl.Result{}, nil + } + + if !controllerutil.ContainsFinalizer(obj, v1alpha1.FinalizerName) { + controllerutil.AddFinalizer(obj, v1alpha1.FinalizerName) + if err := r.Update(ctx, obj); err != nil { + log.Error(err, "Failed to add finalizer to resource") + return ctrl.Result{}, err + } + log.V(1).Info("Added finalizer to resource") + return ctrl.Result{}, nil + } + + orig := obj.DeepCopy() + if conditions.InitializeConditions(obj, v1alpha1.ReadyCondition) { + log.V(1).Info("Initializing status conditions") + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + + defer func() { + if !equality.Semantic.DeepEqual(orig.ObjectMeta, obj.ObjectMeta) { + if err := r.Patch(ctx, obj.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, obj.Status) { + if err := r.Status().Patch(ctx, obj, client.MergeFrom(orig)); err != nil { + log.Error(err, "Failed to update status") + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + } + }() + + if err := r.reconcileExt(ctx, s); err != nil { + log.Error(err, "Failed to reconcile resource") + return ctrl.Result{}, apistatus.WrapTerminalError(err) + } + + return ctrl.Result{}, nil +} + +func (r *ExtCommunitySetReconciler) SetupWithManager(ctx context.Context, 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) + } + + if err := mgr.GetFieldIndexer().IndexField(ctx, &v1alpha1.ExtCommunitySet{}, v1alpha1.DeviceRefIndexKey, func(obj client.Object) []string { + o := obj.(*v1alpha1.ExtCommunitySet) + return []string{o.Spec.DeviceRef.Name} + }); err != nil { + return err + } + + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.ExtCommunitySet{}). + Named("extcommunityset"). + WithEventFilter(filter). + Watches( + &v1alpha1.Device{}, + handler.EnqueueRequestsFromMapFunc(r.deviceToExtCommunitySets), + builder.WithPredicates(predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { + return paused.DevicePausedChanged(e.ObjectOld, e.ObjectNew) + }, + GenericFunc: func(e event.GenericEvent) bool { + return false + }, + }), + ). + Complete(r) +} + +type extCommunitySetScope struct { + Device *v1alpha1.Device + ExtCommunitySet *v1alpha1.ExtCommunitySet + Connection *deviceutil.Connection + ProviderConfig *provider.ProviderConfig + Provider provider.ExtCommunitySetProvider +} + +func (r *ExtCommunitySetReconciler) reconcileExt(ctx context.Context, s *extCommunitySetScope) (reterr error) { + if s.ExtCommunitySet.Labels == nil { + s.ExtCommunitySet.Labels = make(map[string]string) + } + s.ExtCommunitySet.Labels[v1alpha1.DeviceLabel] = s.Device.Name + + if !controllerutil.HasControllerReference(s.ExtCommunitySet) { + if err := controllerutil.SetOwnerReference(s.Device, s.ExtCommunitySet, r.Scheme, controllerutil.WithBlockOwnerDeletion(true)); err != nil { + return err + } + } + + if err := s.Provider.Connect(ctx, s.Connection); err != nil { + return fmt.Errorf("failed to connect to provider: %w", err) + } + defer func() { + if err := s.Provider.Disconnect(ctx, s.Connection); err != nil { + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + }() + + err := s.Provider.EnsureExtCommunitySet(ctx, &provider.ExtCommunitySetRequest{ + ExtCommunitySet: s.ExtCommunitySet, + ProviderConfig: s.ProviderConfig, + }) + + cond := conditions.FromError(err) + cond.Type = v1alpha1.ReadyCondition + conditions.Set(s.ExtCommunitySet, cond) + + return err +} + +func (r *ExtCommunitySetReconciler) finalizeExt(ctx context.Context, s *extCommunitySetScope) (reterr error) { + if err := s.Provider.Connect(ctx, s.Connection); err != nil { + return fmt.Errorf("failed to connect to provider: %w", err) + } + defer func() { + if err := s.Provider.Disconnect(ctx, s.Connection); err != nil { + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + }() + + return s.Provider.DeleteExtCommunitySet(ctx, &provider.ExtCommunitySetRequest{ + ExtCommunitySet: s.ExtCommunitySet, + ProviderConfig: s.ProviderConfig, + }) +} + +func (r *ExtCommunitySetReconciler) deviceToExtCommunitySets(ctx context.Context, obj client.Object) []ctrl.Request { + device, ok := obj.(*v1alpha1.Device) + if !ok { + panic(fmt.Sprintf("Expected a Device but got a %T", obj)) + } + + log := ctrl.LoggerFrom(ctx, "Device", klog.KObj(device)) + + list := new(v1alpha1.ExtCommunitySetList) + if err := r.List( + ctx, list, + client.InNamespace(device.Namespace), + client.MatchingFields{v1alpha1.DeviceRefIndexKey: device.Name}, + ); err != nil { + log.Error(err, "Failed to list ExtCommunitySets") + return nil + } + + requests := make([]ctrl.Request, 0, len(list.Items)) + for i := range list.Items { + log.V(2).Info("Enqueuing ExtCommunitySet for reconciliation", "ExtCommunitySet", klog.KObj(&list.Items[i])) + requests = append(requests, ctrl.Request{ + Name: list.Items[i].Name, + Namespace: list.Items[i].Namespace, + }) + } + + return requests +} diff --git a/internal/controller/core/extcommunityset_controller_test.go b/internal/controller/core/extcommunityset_controller_test.go new file mode 100644 index 000000000..9a161709b --- /dev/null +++ b/internal/controller/core/extcommunityset_controller_test.go @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package core + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/ironcore-dev/network-operator/api/core/v1alpha1" +) + +var _ = Describe("ExtCommunitySet Controller", func() { + Context("When reconciling a resource", func() { + const set = "BGP-EXT-COMMUNITY" + var ( + name string + key client.ObjectKey + ) + + BeforeEach(func() { + By("Creating the custom resource for the Kind Device") + device := &v1alpha1.Device{ + GenerateName: "test-extcommunityset-", + Namespace: metav1.NamespaceDefault, + Spec: v1alpha1.DeviceSpec{ + Endpoint: v1alpha1.Endpoint{ + Address: "192.168.10.2:9339", + }, + }, + } + Expect(k8sClient.Create(ctx, device)).To(Succeed()) + name = device.Name + key = client.ObjectKey{Name: name, Namespace: metav1.NamespaceDefault} + + By("Creating the custom resource for the Kind ExtCommunitySet") + resource := &v1alpha1.ExtCommunitySet{ + Name: name, + Namespace: metav1.NamespaceDefault, + Spec: v1alpha1.ExtCommunitySetSpec{ + DeviceRef: v1alpha1.LocalObjectReference{Name: name}, + Name: set, + Members: []v1alpha1.CommunityMember{ + {Sequence: 5, Regex: "65000:[0-9]+"}, + {Sequence: 10, Regex: "65001:[0-9]+"}, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + }) + + AfterEach(func() { + By("Cleaning up the ExtCommunitySet resource") + ecs := &v1alpha1.ExtCommunitySet{} + ecs.Name = name + ecs.Namespace = metav1.NamespaceDefault + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, ecs))).To(Succeed()) + + By("Verifying the resource is removed from the provider") + Eventually(func(g Gomega) { + g.Expect(testProvider.ExtCommunitySets.Has(set)).To(BeFalse(), "Provider should not have ExtCommunitySet configured") + }).Should(Succeed()) + + By("Cleaning up the Device resource") + device := &v1alpha1.Device{} + device.Name = name + device.Namespace = metav1.NamespaceDefault + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, device))).To(Succeed()) + }) + + It("Should successfully reconcile the resource", func() { + By("Adding a finalizer to the resource") + Eventually(func(g Gomega) { + resource := &v1alpha1.ExtCommunitySet{} + g.Expect(k8sClient.Get(ctx, key, resource)).To(Succeed()) + g.Expect(controllerutil.ContainsFinalizer(resource, v1alpha1.FinalizerName)).To(BeTrue()) + }).Should(Succeed()) + + By("Adding the device label to the resource") + Eventually(func(g Gomega) { + resource := &v1alpha1.ExtCommunitySet{} + g.Expect(k8sClient.Get(ctx, key, resource)).To(Succeed()) + g.Expect(resource.Labels).To(HaveKeyWithValue(v1alpha1.DeviceLabel, name)) + }).Should(Succeed()) + + By("Adding the device as an owner reference") + Eventually(func(g Gomega) { + resource := &v1alpha1.ExtCommunitySet{} + g.Expect(k8sClient.Get(ctx, key, resource)).To(Succeed()) + g.Expect(resource.OwnerReferences).To(HaveLen(1)) + g.Expect(resource.OwnerReferences[0].Kind).To(Equal("Device")) + g.Expect(resource.OwnerReferences[0].Name).To(Equal(name)) + }).Should(Succeed()) + + By("Updating the resource status") + Eventually(func(g Gomega) { + resource := &v1alpha1.ExtCommunitySet{} + g.Expect(k8sClient.Get(ctx, key, resource)).To(Succeed()) + g.Expect(resource.Status.Conditions).To(HaveLen(2)) + g.Expect(resource.Status.Conditions[0].Type).To(Equal(v1alpha1.ReadyCondition)) + g.Expect(resource.Status.Conditions[0].Status).To(Equal(metav1.ConditionTrue)) + g.Expect(resource.Status.Conditions[1].Type).To(Equal(v1alpha1.PausedCondition)) + g.Expect(resource.Status.Conditions[1].Status).To(Equal(metav1.ConditionFalse)) + }).Should(Succeed()) + + By("Ensuring the resource is created in the provider") + Eventually(func(g Gomega) { + g.Expect(testProvider.ExtCommunitySets.Has(set)).To(BeTrue(), "Provider should have ExtCommunitySet configured") + }).Should(Succeed()) + }) + }) +}) diff --git a/internal/controller/core/suite_test.go b/internal/controller/core/suite_test.go index 15d38f8c5..468d21282 100644 --- a/internal/controller/core/suite_test.go +++ b/internal/controller/core/suite_test.go @@ -318,6 +318,24 @@ var _ = BeforeSuite(func() { }).SetupWithManager(ctx, k8sManager) Expect(err).NotTo(HaveOccurred()) + err = (&CommunitySetReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + Recorder: recorder, + Provider: prov, + Locker: testLocker, + }).SetupWithManager(ctx, k8sManager) + Expect(err).NotTo(HaveOccurred()) + + err = (&ExtCommunitySetReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + Recorder: recorder, + Provider: prov, + Locker: testLocker, + }).SetupWithManager(ctx, k8sManager) + Expect(err).NotTo(HaveOccurred()) + err = (&RoutingPolicyReconciler{ Client: k8sManager.GetClient(), Scheme: k8sManager.GetScheme(), @@ -442,6 +460,8 @@ var ( _ provider.VLANProvider = (*Provider)(nil) _ provider.EVPNInstanceProvider = (*Provider)(nil) _ provider.PrefixSetProvider = (*Provider)(nil) + _ provider.CommunitySetProvider = (*Provider)(nil) + _ provider.ExtCommunitySetProvider = (*Provider)(nil) _ provider.RoutingPolicyProvider = (*Provider)(nil) _ provider.NVEProvider = (*Provider)(nil) _ provider.LLDPProvider = (*Provider)(nil) @@ -479,6 +499,8 @@ type Provider struct { VLANs sets.Set[int16] EVIs sets.Set[int32] PrefixSets sets.Set[string] + CommunitySets sets.Set[string] + ExtCommunitySets sets.Set[string] RoutingPolicies sets.Set[string] NVE *v1alpha1.NetworkVirtualizationEdge LLDP *v1alpha1.LLDP @@ -505,6 +527,8 @@ func NewProvider() *Provider { VLANs: sets.New[int16](), EVIs: sets.New[int32](), PrefixSets: sets.New[string](), + CommunitySets: sets.New[string](), + ExtCommunitySets: sets.New[string](), RoutingPolicies: sets.New[string](), LLDPOperStatus: true, LLDPNeighbors: make(map[string]*provider.LLDPAdjacency), @@ -913,6 +937,34 @@ func (p *Provider) DeletePrefixSet(_ context.Context, req *provider.PrefixSetReq return nil } +func (p *Provider) EnsureCommunitySet(_ context.Context, req *provider.CommunitySetRequest) error { + p.Lock() + defer p.Unlock() + p.CommunitySets.Insert(req.CommunitySet.Spec.Name) + return nil +} + +func (p *Provider) DeleteCommunitySet(_ context.Context, req *provider.CommunitySetRequest) error { + p.Lock() + defer p.Unlock() + p.CommunitySets.Delete(req.CommunitySet.Spec.Name) + return nil +} + +func (p *Provider) EnsureExtCommunitySet(_ context.Context, req *provider.ExtCommunitySetRequest) error { + p.Lock() + defer p.Unlock() + p.ExtCommunitySets.Insert(req.ExtCommunitySet.Spec.Name) + return nil +} + +func (p *Provider) DeleteExtCommunitySet(_ context.Context, req *provider.ExtCommunitySetRequest) error { + p.Lock() + defer p.Unlock() + p.ExtCommunitySets.Delete(req.ExtCommunitySet.Spec.Name) + return nil +} + func (p *Provider) EnsureRoutingPolicy(_ context.Context, req *provider.EnsureRoutingPolicyRequest) error { p.Lock() defer p.Unlock() diff --git a/internal/provider/cisco/nxos/communityset.go b/internal/provider/cisco/nxos/communityset.go new file mode 100644 index 000000000..c301e2473 --- /dev/null +++ b/internal/provider/cisco/nxos/communityset.go @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package nxos + +import ( + "context" + + "github.com/ironcore-dev/network-operator/internal/provider" + "github.com/ironcore-dev/network-operator/internal/transport/gnmiext" +) + +var ( + _ provider.CommunitySetProvider = (*Provider)(nil) + _ provider.ExtCommunitySetProvider = (*Provider)(nil) +) + +// CommunityList represents a named BGP standard community-list on NX-OS. +type CommunityList struct { + Name string `json:"name"` + Mode string `json:"mode"` + EntItems communityEntItems `json:"ent-items"` +} + +func (*CommunityList) IsListItem() {} + +func (c *CommunityList) XPath() string { + return "System/rpm-items/rtregcom-items/Rule-list[name=" + c.Name + "]" +} + +type communityEntItems struct { + EntryList gnmiext.List[int32, *communityEntry] `json:"Entry-list"` +} + +type communityEntry struct { + Order int32 `json:"order"` + Action Action `json:"action"` + Regex string `json:"regex"` +} + +func (e *communityEntry) Key() int32 { return e.Order } + +// ExtCommunityList represents a named BGP extended community-list on NX-OS. +type ExtCommunityList struct { + Name string `json:"name"` + Mode string `json:"mode"` + EntItems extCommunityEntItems `json:"ent-items"` +} + +func (*ExtCommunityList) IsListItem() {} + +func (c *ExtCommunityList) XPath() string { + return "System/rpm-items/rtextcom-items/Rule-list[name=" + c.Name + "]" +} + +type extCommunityEntItems struct { + EntryList gnmiext.List[int32, *extCommunityEntry] `json:"Entry-list"` +} + +type extCommunityEntry struct { + Order int32 `json:"order"` + Action Action `json:"action"` + Regex string `json:"regex"` +} + +func (e *extCommunityEntry) Key() int32 { return e.Order } + +func (p *Provider) EnsureCommunitySet(ctx context.Context, req *provider.CommunitySetRequest) error { + cl := new(CommunityList) + cl.Name = req.CommunitySet.Spec.Name + cl.Mode = "regex" + for _, m := range req.CommunitySet.Spec.Members { + cl.EntItems.EntryList.Set(&communityEntry{ + Order: m.Sequence, + Action: ActionPermit, + Regex: m.Regex, + }) + } + return p.client.Update(ctx, cl) +} + +func (p *Provider) DeleteCommunitySet(ctx context.Context, req *provider.CommunitySetRequest) error { + cl := new(CommunityList) + cl.Name = req.CommunitySet.Spec.Name + return p.client.Delete(ctx, cl) +} + +func (p *Provider) EnsureExtCommunitySet(ctx context.Context, req *provider.ExtCommunitySetRequest) error { + cl := new(ExtCommunityList) + cl.Name = req.ExtCommunitySet.Spec.Name + cl.Mode = "regex" + for _, m := range req.ExtCommunitySet.Spec.Members { + cl.EntItems.EntryList.Set(&extCommunityEntry{ + Order: m.Sequence, + Action: ActionPermit, + Regex: m.Regex, + }) + } + return p.client.Update(ctx, cl) +} + +func (p *Provider) DeleteExtCommunitySet(ctx context.Context, req *provider.ExtCommunitySetRequest) error { + cl := new(ExtCommunityList) + cl.Name = req.ExtCommunitySet.Spec.Name + return p.client.Delete(ctx, cl) +} diff --git a/internal/provider/cisco/nxos/communityset_test.go b/internal/provider/cisco/nxos/communityset_test.go new file mode 100644 index 000000000..3ee1ee748 --- /dev/null +++ b/internal/provider/cisco/nxos/communityset_test.go @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package nxos + +func init() { + cs := &CommunityList{} + cs.Name = "TEST" + cs.Mode = "regex" + cs.EntItems.EntryList.Set(&communityEntry{ + Order: 10, + Action: ActionPermit, + Regex: "50000:[0-9][0-9]", + }) + Register("communityset", cs) + + ecs := &ExtCommunityList{} + ecs.Name = "TEST-EXT" + ecs.Mode = "regex" + ecs.EntItems.EntryList.Set(&extCommunityEntry{ + Order: 15, + Action: ActionPermit, + Regex: "65200:[0-9][0-9]", + }) + Register("extcommunityset", ecs) +} diff --git a/internal/provider/cisco/nxos/testdata/communityset.json b/internal/provider/cisco/nxos/testdata/communityset.json new file mode 100644 index 000000000..7e68fa6c2 --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/communityset.json @@ -0,0 +1,21 @@ +{ + "rpm-items": { + "rtregcom-items": { + "Rule-list": [ + { + "name": "TEST", + "mode": "regex", + "ent-items": { + "Entry-list": [ + { + "order": 10, + "action": "permit", + "regex": "50000:[0-9][0-9]" + } + ] + } + } + ] + } + } +} diff --git a/internal/provider/cisco/nxos/testdata/communityset.json.txt b/internal/provider/cisco/nxos/testdata/communityset.json.txt new file mode 100644 index 000000000..e6585ed6c --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/communityset.json.txt @@ -0,0 +1 @@ +ip community-list expanded TEST seq 10 permit 50000:[0-9][0-9] diff --git a/internal/provider/cisco/nxos/testdata/extcommunityset.json b/internal/provider/cisco/nxos/testdata/extcommunityset.json new file mode 100644 index 000000000..f0684e619 --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/extcommunityset.json @@ -0,0 +1,21 @@ +{ + "rpm-items": { + "rtextcom-items": { + "Rule-list": [ + { + "name": "TEST-EXT", + "mode": "regex", + "ent-items": { + "Entry-list": [ + { + "order": 15, + "action": "permit", + "regex": "65200:[0-9][0-9]" + } + ] + } + } + ] + } + } +} diff --git a/internal/provider/cisco/nxos/testdata/extcommunityset.json.txt b/internal/provider/cisco/nxos/testdata/extcommunityset.json.txt new file mode 100644 index 000000000..ff2c4c425 --- /dev/null +++ b/internal/provider/cisco/nxos/testdata/extcommunityset.json.txt @@ -0,0 +1 @@ +ip extcommunity-list expanded TEST-EXT seq 15 permit 65200:[0-9][0-9] diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 94164922e..bec196641 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -660,6 +660,32 @@ type PrefixSetRequest struct { ProviderConfig *ProviderConfig } +// CommunitySetProvider is the interface for the realization of CommunitySet objects over different providers. +type CommunitySetProvider interface { + Provider + + EnsureCommunitySet(context.Context, *CommunitySetRequest) error + DeleteCommunitySet(context.Context, *CommunitySetRequest) error +} + +type CommunitySetRequest struct { + CommunitySet *v1alpha1.CommunitySet + ProviderConfig *ProviderConfig +} + +// ExtCommunitySetProvider is the interface for the realization of ExtCommunitySet objects over different providers. +type ExtCommunitySetProvider interface { + Provider + + EnsureExtCommunitySet(context.Context, *ExtCommunitySetRequest) error + DeleteExtCommunitySet(context.Context, *ExtCommunitySetRequest) error +} + +type ExtCommunitySetRequest struct { + ExtCommunitySet *v1alpha1.ExtCommunitySet + ProviderConfig *ProviderConfig +} + // RoutingPolicyProvider is the interface for the realization of the RoutingPolicy objects over different providers. type RoutingPolicyProvider interface { Provider diff --git a/test/gnmi/gnmi_suite_test.go b/test/gnmi/gnmi_suite_test.go index b1ba2b06e..08ad6a0a8 100644 --- a/test/gnmi/gnmi_suite_test.go +++ b/test/gnmi/gnmi_suite_test.go @@ -378,6 +378,24 @@ func registerControllers(ctx context.Context, mgr ctrl.Manager, recorder *events }).SetupWithManager(ctx, mgr) Expect(err).NotTo(HaveOccurred()) + err = (&core.CommunitySetReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: recorder, + Provider: providerFn, + Locker: locker, + }).SetupWithManager(ctx, mgr) + Expect(err).NotTo(HaveOccurred()) + + err = (&core.ExtCommunitySetReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: recorder, + Provider: providerFn, + Locker: locker, + }).SetupWithManager(ctx, mgr) + Expect(err).NotTo(HaveOccurred()) + err = (&nxcontroller.SystemReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), diff --git a/test/gnmi/testdata/cisco-nxos-gnmi/communityset.txtar b/test/gnmi/testdata/cisco-nxos-gnmi/communityset.txtar new file mode 100644 index 000000000..cea0d1789 --- /dev/null +++ b/test/gnmi/testdata/cisco-nxos-gnmi/communityset.txtar @@ -0,0 +1,71 @@ +-- communitysets/test-communityset -- +apiVersion: networking.metal.ironcore.dev/v1alpha1 +kind: CommunitySet +metadata: + name: test-communityset + namespace: default +spec: + deviceRef: + name: device + name: BGP-COMMUNITY + members: + - sequence: 5 + regex: "50000:[0-9][0-9]" + - sequence: 10 + regex: "65001:[0-9]+" + +-- state/preload -- +{ + "System": { + "procsys-items": { + "bootTime": "1700000000" + } + } +} + +-- state/expect -- +{ + "System": { + "procsys-items": { + "bootTime": "1700000000" + }, + "rpm-items": { + "rtregcom-items": { + "Rule-list": [ + { + "name": "BGP-COMMUNITY", + "mode": "regex", + "ent-items": { + "Entry-list": [ + { + "order": 5, + "action": "permit", + "regex": "50000:[0-9][0-9]" + }, + { + "order": 10, + "action": "permit", + "regex": "65001:[0-9]+" + } + ] + } + } + ] + } + } + } +} + +-- state/delete -- +{ + "System": { + "procsys-items": { + "bootTime": "1700000000" + }, + "rpm-items": { + "rtregcom-items": { + "Rule-list": [] + } + } + } +} diff --git a/test/gnmi/testdata/cisco-nxos-gnmi/extcommunityset.txtar b/test/gnmi/testdata/cisco-nxos-gnmi/extcommunityset.txtar new file mode 100644 index 000000000..ae22be869 --- /dev/null +++ b/test/gnmi/testdata/cisco-nxos-gnmi/extcommunityset.txtar @@ -0,0 +1,71 @@ +-- extcommunitysets/test-extcommunityset -- +apiVersion: networking.metal.ironcore.dev/v1alpha1 +kind: ExtCommunitySet +metadata: + name: test-extcommunityset + namespace: default +spec: + deviceRef: + name: device + name: BGP-EXT-COMMUNITY + members: + - sequence: 5 + regex: "65200:[0-9][0-9]" + - sequence: 10 + regex: "route-target:65300:[0-9]+" + +-- state/preload -- +{ + "System": { + "procsys-items": { + "bootTime": "1700000000" + } + } +} + +-- state/expect -- +{ + "System": { + "procsys-items": { + "bootTime": "1700000000" + }, + "rpm-items": { + "rtextcom-items": { + "Rule-list": [ + { + "name": "BGP-EXT-COMMUNITY", + "mode": "regex", + "ent-items": { + "Entry-list": [ + { + "order": 5, + "action": "permit", + "regex": "65200:[0-9][0-9]" + }, + { + "order": 10, + "action": "permit", + "regex": "route-target:65300:[0-9]+" + } + ] + } + } + ] + } + } + } +} + +-- state/delete -- +{ + "System": { + "procsys-items": { + "bootTime": "1700000000" + }, + "rpm-items": { + "rtextcom-items": { + "Rule-list": [] + } + } + } +}