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": []
+ }
+ }
+ }
+}