diff --git a/api/core/v1alpha1/groupversion_info.go b/api/core/v1alpha1/groupversion_info.go
index 3b0fdc9af..ea427a3ce 100644
--- a/api/core/v1alpha1/groupversion_info.go
+++ b/api/core/v1alpha1/groupversion_info.go
@@ -270,6 +270,10 @@ const (
const (
// PrefixSetNotFoundReason indicates that a referenced PrefixSet was not found.
PrefixSetNotFoundReason = "PrefixSetNotFound"
+ // CommunitySetNotFoundReason indicates that a referenced CommunitySet was not found.
+ CommunitySetNotFoundReason = "CommunitySetNotFound"
+ // ExtCommunitySetNotFoundReason indicates that a referenced ExtCommunitySet was not found.
+ ExtCommunitySetNotFoundReason = "ExtCommunitySetNotFound"
// SecretNotFoundReason indicates that a referenced Secret was not found.
SecretNotFoundReason = "SecretNotFound"
// RemoteEndpointUnreachableReason indicates that the remote object storage endpoint is not reachable.
diff --git a/api/core/v1alpha1/routingpolicy_types.go b/api/core/v1alpha1/routingpolicy_types.go
index 4e234423e..4895250b7 100644
--- a/api/core/v1alpha1/routingpolicy_types.go
+++ b/api/core/v1alpha1/routingpolicy_types.go
@@ -63,6 +63,14 @@ type PolicyConditions struct {
// MatchPrefixSet matches routes against a PrefixSet resource.
// +optional
MatchPrefixSet *PrefixSetMatchCondition `json:"matchPrefixSet,omitempty"`
+
+ // MatchCommunitySet matches routes against a CommunitySet resource.
+ // +optional
+ MatchCommunitySet *CommunitySetMatchCondition `json:"matchCommunitySet,omitempty"`
+
+ // MatchExtCommunitySet matches routes against an ExtCommunitySet resource.
+ // +optional
+ MatchExtCommunitySet *ExtCommunitySetMatchCondition `json:"matchExtCommunitySet,omitempty"`
}
// PrefixSetMatchCondition defines the condition for matching against a PrefixSet.
@@ -73,6 +81,47 @@ type PrefixSetMatchCondition struct {
PrefixSetRef LocalObjectReference `json:"prefixSetRef"`
}
+// CommunitySetMatchCondition defines the condition for matching against a CommunitySet.
+type CommunitySetMatchCondition struct {
+ // CommunitySetRef references a CommunitySet in the same namespace.
+ // The CommunitySet must exist and belong to the same device.
+ // +required
+ CommunitySetRef LocalObjectReference `json:"communitySetRef"`
+
+ // MatchSetOptions defines how a route's communities are compared against the referenced set.
+ // ANY matches a route carrying at least one member of the set; ALL matches only a route
+ // carrying every member of the set.
+ // +optional
+ // +kubebuilder:default=ANY
+ MatchSetOptions MatchSetOptions `json:"matchSetOptions,omitempty"`
+}
+
+// ExtCommunitySetMatchCondition defines the condition for matching against an ExtCommunitySet.
+type ExtCommunitySetMatchCondition struct {
+ // ExtCommunitySetRef references an ExtCommunitySet in the same namespace.
+ // The ExtCommunitySet must exist and belong to the same device.
+ // +required
+ ExtCommunitySetRef LocalObjectReference `json:"extCommunitySetRef"`
+
+ // MatchSetOptions defines how a route's extended communities are compared against the referenced set.
+ // ANY matches a route carrying at least one member of the set; ALL matches only a route
+ // carrying every member of the set.
+ // +optional
+ // +kubebuilder:default=ANY
+ MatchSetOptions MatchSetOptions `json:"matchSetOptions,omitempty"`
+}
+
+// MatchSetOptions defines how a route's attributes are compared against a referenced set.
+// +kubebuilder:validation:Enum=ANY;ALL
+type MatchSetOptions string
+
+const (
+ // MatchSetOptionsAny matches a route carrying at least one member of the referenced set.
+ MatchSetOptionsAny MatchSetOptions = "ANY"
+ // MatchSetOptionsAll matches only a route carrying every member of the referenced set.
+ MatchSetOptionsAll MatchSetOptions = "ALL"
+)
+
// PolicyActions defines the actions to take when a policy statement matches.
// +kubebuilder:validation:XValidation:rule="self.routeDisposition == 'AcceptRoute' || !has(self.bgpActions)",message="bgpActions cannot be specified when routeDisposition is RejectRoute"
type PolicyActions struct {
diff --git a/api/core/v1alpha1/zz_generated.deepcopy.go b/api/core/v1alpha1/zz_generated.deepcopy.go
index 14781e1b9..a3f0ef4b6 100644
--- a/api/core/v1alpha1/zz_generated.deepcopy.go
+++ b/api/core/v1alpha1/zz_generated.deepcopy.go
@@ -1324,6 +1324,22 @@ func (in *CommunitySetList) DeepCopyObject() runtime.Object {
return nil
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CommunitySetMatchCondition) DeepCopyInto(out *CommunitySetMatchCondition) {
+ *out = *in
+ out.CommunitySetRef = in.CommunitySetRef
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommunitySetMatchCondition.
+func (in *CommunitySetMatchCondition) DeepCopy() *CommunitySetMatchCondition {
+ if in == nil {
+ return nil
+ }
+ out := new(CommunitySetMatchCondition)
+ in.DeepCopyInto(out)
+ return out
+}
+
// 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
@@ -2442,6 +2458,22 @@ func (in *ExtCommunitySetList) DeepCopyObject() runtime.Object {
return nil
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExtCommunitySetMatchCondition) DeepCopyInto(out *ExtCommunitySetMatchCondition) {
+ *out = *in
+ out.ExtCommunitySetRef = in.ExtCommunitySetRef
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtCommunitySetMatchCondition.
+func (in *ExtCommunitySetMatchCondition) DeepCopy() *ExtCommunitySetMatchCondition {
+ if in == nil {
+ return nil
+ }
+ out := new(ExtCommunitySetMatchCondition)
+ in.DeepCopyInto(out)
+ return out
+}
+
// 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
@@ -3925,6 +3957,16 @@ func (in *PolicyConditions) DeepCopyInto(out *PolicyConditions) {
*out = new(PrefixSetMatchCondition)
**out = **in
}
+ if in.MatchCommunitySet != nil {
+ in, out := &in.MatchCommunitySet, &out.MatchCommunitySet
+ *out = new(CommunitySetMatchCondition)
+ **out = **in
+ }
+ if in.MatchExtCommunitySet != nil {
+ in, out := &in.MatchExtCommunitySet, &out.MatchExtCommunitySet
+ *out = new(ExtCommunitySetMatchCondition)
+ **out = **in
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyConditions.
diff --git a/charts/network-operator/templates/crd/routingpolicies.networking.metal.ironcore.dev.yaml b/charts/network-operator/templates/crd/routingpolicies.networking.metal.ironcore.dev.yaml
index 122aaef88..15c214322 100644
--- a/charts/network-operator/templates/crd/routingpolicies.networking.metal.ironcore.dev.yaml
+++ b/charts/network-operator/templates/crd/routingpolicies.networking.metal.ironcore.dev.yaml
@@ -270,6 +270,72 @@ spec:
Conditions define the match criteria for this statement.
If no conditions are specified, the statement matches all routes.
properties:
+ matchCommunitySet:
+ description: MatchCommunitySet matches routes against a
+ CommunitySet resource.
+ properties:
+ communitySetRef:
+ description: |-
+ CommunitySetRef references a CommunitySet in the same namespace.
+ The CommunitySet must exist and belong to the same device.
+ 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
+ matchSetOptions:
+ default: ANY
+ description: |-
+ MatchSetOptions defines how a route's communities are compared against the referenced set.
+ ANY matches a route carrying at least one member of the set; ALL matches only a route
+ carrying every member of the set.
+ enum:
+ - ANY
+ - ALL
+ type: string
+ required:
+ - communitySetRef
+ type: object
+ matchExtCommunitySet:
+ description: MatchExtCommunitySet matches routes against
+ an ExtCommunitySet resource.
+ properties:
+ extCommunitySetRef:
+ description: |-
+ ExtCommunitySetRef references an ExtCommunitySet in the same namespace.
+ The ExtCommunitySet must exist and belong to the same device.
+ 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
+ matchSetOptions:
+ default: ANY
+ description: |-
+ MatchSetOptions defines how a route's extended communities are compared against the referenced set.
+ ANY matches a route carrying at least one member of the set; ALL matches only a route
+ carrying every member of the set.
+ enum:
+ - ANY
+ - ALL
+ type: string
+ required:
+ - extCommunitySetRef
+ type: object
matchPrefixSet:
description: MatchPrefixSet matches routes against a PrefixSet
resource.
diff --git a/config/crd/bases/networking.metal.ironcore.dev_routingpolicies.yaml b/config/crd/bases/networking.metal.ironcore.dev_routingpolicies.yaml
index 27bc8c122..7ebd9f5cd 100644
--- a/config/crd/bases/networking.metal.ironcore.dev_routingpolicies.yaml
+++ b/config/crd/bases/networking.metal.ironcore.dev_routingpolicies.yaml
@@ -267,6 +267,72 @@ spec:
Conditions define the match criteria for this statement.
If no conditions are specified, the statement matches all routes.
properties:
+ matchCommunitySet:
+ description: MatchCommunitySet matches routes against a
+ CommunitySet resource.
+ properties:
+ communitySetRef:
+ description: |-
+ CommunitySetRef references a CommunitySet in the same namespace.
+ The CommunitySet must exist and belong to the same device.
+ 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
+ matchSetOptions:
+ default: ANY
+ description: |-
+ MatchSetOptions defines how a route's communities are compared against the referenced set.
+ ANY matches a route carrying at least one member of the set; ALL matches only a route
+ carrying every member of the set.
+ enum:
+ - ANY
+ - ALL
+ type: string
+ required:
+ - communitySetRef
+ type: object
+ matchExtCommunitySet:
+ description: MatchExtCommunitySet matches routes against
+ an ExtCommunitySet resource.
+ properties:
+ extCommunitySetRef:
+ description: |-
+ ExtCommunitySetRef references an ExtCommunitySet in the same namespace.
+ The ExtCommunitySet must exist and belong to the same device.
+ 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
+ matchSetOptions:
+ default: ANY
+ description: |-
+ MatchSetOptions defines how a route's extended communities are compared against the referenced set.
+ ANY matches a route carrying at least one member of the set; ALL matches only a route
+ carrying every member of the set.
+ enum:
+ - ANY
+ - ALL
+ type: string
+ required:
+ - extCommunitySetRef
+ type: object
matchPrefixSet:
description: MatchPrefixSet matches routes against a PrefixSet
resource.
diff --git a/config/samples/v1alpha1_routingpolicy.yaml b/config/samples/v1alpha1_routingpolicy.yaml
index 174c139d5..47611561f 100644
--- a/config/samples/v1alpha1_routingpolicy.yaml
+++ b/config/samples/v1alpha1_routingpolicy.yaml
@@ -72,6 +72,21 @@ spec:
bgpActions:
setASPath:
asNumber: 65000
+ - sequence: 80
+ conditions:
+ matchCommunitySet:
+ communitySetRef:
+ name: communityset
+ matchSetOptions: ALL
+ actions:
+ routeDisposition: AcceptRoute
+ - sequence: 90
+ conditions:
+ matchExtCommunitySet:
+ extCommunitySetRef:
+ name: extcommunityset
+ actions:
+ routeDisposition: AcceptRoute
- sequence: 100
actions:
routeDisposition: AcceptRoute
@@ -129,3 +144,37 @@ spec:
maskLengthRange:
min: 25
max: 32
+---
+apiVersion: networking.metal.ironcore.dev/v1alpha1
+kind: CommunitySet
+metadata:
+ labels:
+ app.kubernetes.io/name: network-operator
+ app.kubernetes.io/managed-by: kustomize
+ name: communityset-rp
+spec:
+ deviceRef:
+ name: leaf1
+ name: WIREAPI
+ members:
+ - sequence: 5
+ regex: "50000:[0-9][0-9]"
+ - sequence: 10
+ regex: "65001:[0-9]+"
+---
+apiVersion: networking.metal.ironcore.dev/v1alpha1
+kind: ExtCommunitySet
+metadata:
+ labels:
+ app.kubernetes.io/name: network-operator
+ app.kubernetes.io/managed-by: kustomize
+ name: extcommunityset-rp
+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 550f53088..b4767723e 100644
--- a/docs/api-reference/index.md
+++ b/docs/api-reference/index.md
@@ -1413,6 +1413,23 @@ CommunitySet is the Schema for the communitysets API.
| `status` _[CommunitySetStatus](#communitysetstatus)_ | | | Optional: \{\}
|
+#### CommunitySetMatchCondition
+
+
+
+CommunitySetMatchCondition defines the condition for matching against a CommunitySet.
+
+
+
+_Appears in:_
+- [PolicyConditions](#policyconditions)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `communitySetRef` _[LocalObjectReference](#localobjectreference)_ | CommunitySetRef references a CommunitySet in the same namespace.
The CommunitySet must exist and belong to the same device. | | Required: \{\}
|
+| `matchSetOptions` _[MatchSetOptions](#matchsetoptions)_ | MatchSetOptions defines how a route's communities are compared against the referenced set.
ANY matches a route carrying at least one member of the set; ALL matches only a route
carrying every member of the set. | ANY | Enum: [ANY ALL]
Optional: \{\}
|
+
+
#### CommunitySetSpec
@@ -2243,6 +2260,23 @@ ExtCommunitySet is the Schema for the extcommunitysets API.
| `status` _[ExtCommunitySetStatus](#extcommunitysetstatus)_ | | | Optional: \{\}
|
+#### ExtCommunitySetMatchCondition
+
+
+
+ExtCommunitySetMatchCondition defines the condition for matching against an ExtCommunitySet.
+
+
+
+_Appears in:_
+- [PolicyConditions](#policyconditions)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `extCommunitySetRef` _[LocalObjectReference](#localobjectreference)_ | ExtCommunitySetRef references an ExtCommunitySet in the same namespace.
The ExtCommunitySet must exist and belong to the same device. | | Required: \{\}
|
+| `matchSetOptions` _[MatchSetOptions](#matchsetoptions)_ | MatchSetOptions defines how a route's extended communities are compared against the referenced set.
ANY matches a route carrying at least one member of the set; ALL matches only a route
carrying every member of the set. | ANY | Enum: [ANY ALL]
Optional: \{\}
|
+
+
#### ExtCommunitySetSpec
@@ -2779,6 +2813,7 @@ _Appears in:_
- [BannerSpec](#bannerspec)
- [BorderGatewaySpec](#bordergatewayspec)
- [CertificateSpec](#certificatespec)
+- [CommunitySetMatchCondition](#communitysetmatchcondition)
- [CommunitySetSpec](#communitysetspec)
- [ConfigBackupSpec](#configbackupspec)
- [DHCPRelaySpec](#dhcprelayspec)
@@ -2786,6 +2821,7 @@ _Appears in:_
- [DevicePort](#deviceport)
- [EVPNInstanceSpec](#evpninstancespec)
- [EthernetSegmentSpec](#ethernetsegmentspec)
+- [ExtCommunitySetMatchCondition](#extcommunitysetmatchcondition)
- [ExtCommunitySetSpec](#extcommunitysetspec)
- [FabricLoopbacksSpec](#fabricloopbacksspec)
- [FabricUnderlayAddressingSpec](#fabricunderlayaddressingspec)
@@ -2951,6 +2987,25 @@ _Appears in:_
| `max` _integer_ | Maximum mask length. | | Maximum: 128
Minimum: 0
Required: \{\}
|
+#### MatchSetOptions
+
+_Underlying type:_ _string_
+
+MatchSetOptions defines how a route's attributes are compared against a referenced set.
+
+_Validation:_
+- Enum: [ANY ALL]
+
+_Appears in:_
+- [CommunitySetMatchCondition](#communitysetmatchcondition)
+- [ExtCommunitySetMatchCondition](#extcommunitysetmatchcondition)
+
+| Field | Description |
+| --- | --- |
+| `ANY` | MatchSetOptionsAny matches a route carrying at least one member of the referenced set.
|
+| `ALL` | MatchSetOptionsAll matches only a route carrying every member of the referenced set.
|
+
+
#### MultiChassis
@@ -3506,6 +3561,8 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `matchPrefixSet` _[PrefixSetMatchCondition](#prefixsetmatchcondition)_ | MatchPrefixSet matches routes against a PrefixSet resource. | | Optional: \{\}
|
+| `matchCommunitySet` _[CommunitySetMatchCondition](#communitysetmatchcondition)_ | MatchCommunitySet matches routes against a CommunitySet resource. | | Optional: \{\}
|
+| `matchExtCommunitySet` _[ExtCommunitySetMatchCondition](#extcommunitysetmatchcondition)_ | MatchExtCommunitySet matches routes against an ExtCommunitySet resource. | | Optional: \{\}
|
#### PolicyStatement
diff --git a/internal/controller/core/routingpolicy_controller.go b/internal/controller/core/routingpolicy_controller.go
index 4cb9aac4a..5482d0bd6 100644
--- a/internal/controller/core/routingpolicy_controller.go
+++ b/internal/controller/core/routingpolicy_controller.go
@@ -58,6 +58,8 @@ type RoutingPolicyReconciler struct {
// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=routingpolicies,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=routingpolicies/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=routingpolicies/finalizers,verbs=update
+// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=communitysets,verbs=get;list;watch
+// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=extcommunitysets,verbs=get;list;watch
// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
// Reconcile is part of the main kubernetes reconciliation loop which aims to
@@ -201,7 +203,11 @@ func (r *RoutingPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Reques
return ctrl.Result{}, nil
}
-var routingPolicyPrefixSetRefKey = ".spec.statements[].conditions.matchPrefixSet.prefixSetRef.name"
+var (
+ routingPolicyPrefixSetRefKey = ".spec.statements[].conditions.matchPrefixSet.prefixSetRef.name"
+ routingPolicyCommunitySetRefKey = ".spec.statements[].conditions.matchCommunitySet.communitySetRef.name"
+ routingPolicyExtCommunitySetRefKey = ".spec.statements[].conditions.matchExtCommunitySet.extCommunitySetRef.name"
+)
// SetupWithManager sets up the controller with the Manager.
func (r *RoutingPolicyReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
@@ -228,6 +234,32 @@ func (r *RoutingPolicyReconciler) SetupWithManager(ctx context.Context, mgr ctrl
return err
}
+ if err := mgr.GetFieldIndexer().IndexField(ctx, &v1alpha1.RoutingPolicy{}, routingPolicyCommunitySetRefKey, func(obj client.Object) []string {
+ rp := obj.(*v1alpha1.RoutingPolicy)
+ var names []string
+ for _, stmt := range rp.Spec.Statements {
+ if stmt.Conditions != nil && stmt.Conditions.MatchCommunitySet != nil {
+ names = append(names, stmt.Conditions.MatchCommunitySet.CommunitySetRef.Name)
+ }
+ }
+ return names
+ }); err != nil {
+ return err
+ }
+
+ if err := mgr.GetFieldIndexer().IndexField(ctx, &v1alpha1.RoutingPolicy{}, routingPolicyExtCommunitySetRefKey, func(obj client.Object) []string {
+ rp := obj.(*v1alpha1.RoutingPolicy)
+ var names []string
+ for _, stmt := range rp.Spec.Statements {
+ if stmt.Conditions != nil && stmt.Conditions.MatchExtCommunitySet != nil {
+ names = append(names, stmt.Conditions.MatchExtCommunitySet.ExtCommunitySetRef.Name)
+ }
+ }
+ return names
+ }); err != nil {
+ return err
+ }
+
if err := mgr.GetFieldIndexer().IndexField(ctx, &v1alpha1.RoutingPolicy{}, v1alpha1.DeviceRefIndexKey, func(obj client.Object) []string {
o := obj.(*v1alpha1.RoutingPolicy)
return []string{o.Spec.DeviceRef.Name}
@@ -266,6 +298,34 @@ func (r *RoutingPolicyReconciler) SetupWithManager(ctx context.Context, mgr ctrl
},
}),
).
+ // Watches enqueues RoutingPolicies for changes in referenced CommunitySet resources.
+ // Only triggers on create and delete events since CommunitySet names are immutable.
+ Watches(
+ &v1alpha1.CommunitySet{},
+ handler.EnqueueRequestsFromMapFunc(r.communitySetToRoutingPolicy),
+ builder.WithPredicates(predicate.Funcs{
+ UpdateFunc: func(e event.UpdateEvent) bool {
+ return false
+ },
+ GenericFunc: func(e event.GenericEvent) bool {
+ return false
+ },
+ }),
+ ).
+ // Watches enqueues RoutingPolicies for changes in referenced ExtCommunitySet resources.
+ // Only triggers on create and delete events since ExtCommunitySet names are immutable.
+ Watches(
+ &v1alpha1.ExtCommunitySet{},
+ handler.EnqueueRequestsFromMapFunc(r.extCommunitySetToRoutingPolicy),
+ builder.WithPredicates(predicate.Funcs{
+ UpdateFunc: func(e event.UpdateEvent) bool {
+ return false
+ },
+ GenericFunc: func(e event.GenericEvent) bool {
+ return false
+ },
+ }),
+ ).
// Watches enqueues RoutingPolicies for updates in referenced Device resources.
// Triggers on create, delete, and update events when the device's effective pause state changes.
Watches(
@@ -355,6 +415,26 @@ func (r *RoutingPolicyReconciler) reconcileStatements(ctx context.Context, s *ro
PrefixSet: prefixSet,
})
}
+ if stmt.Conditions != nil && stmt.Conditions.MatchCommunitySet != nil {
+ communitySet, err := r.reconcileCommunitySet(ctx, s, stmt.Conditions.MatchCommunitySet)
+ if err != nil {
+ return nil, err
+ }
+ cond = append(cond, provider.MatchCommunitySetCondition{
+ CommunitySet: communitySet,
+ MatchAll: stmt.Conditions.MatchCommunitySet.MatchSetOptions == v1alpha1.MatchSetOptionsAll,
+ })
+ }
+ if stmt.Conditions != nil && stmt.Conditions.MatchExtCommunitySet != nil {
+ extCommunitySet, err := r.reconcileExtCommunitySet(ctx, s, stmt.Conditions.MatchExtCommunitySet)
+ if err != nil {
+ return nil, err
+ }
+ cond = append(cond, provider.MatchExtCommunitySetCondition{
+ ExtCommunitySet: extCommunitySet,
+ MatchAll: stmt.Conditions.MatchExtCommunitySet.MatchSetOptions == v1alpha1.MatchSetOptionsAll,
+ })
+ }
statements = append(statements, provider.PolicyStatement{
Sequence: stmt.Sequence,
@@ -400,6 +480,74 @@ func (r *RoutingPolicyReconciler) reconcilePrefixSet(ctx context.Context, s *rou
return prefixSet, nil
}
+// reconcileCommunitySet ensures that the referenced CommunitySet exists and belongs to the same device as the RoutingPolicy.
+func (r *RoutingPolicyReconciler) reconcileCommunitySet(ctx context.Context, s *routingPolicyScope, c *v1alpha1.CommunitySetMatchCondition) (*v1alpha1.CommunitySet, error) {
+ key := client.ObjectKey{
+ Name: c.CommunitySetRef.Name,
+ Namespace: s.RoutingPolicy.Namespace,
+ }
+
+ communitySet := new(v1alpha1.CommunitySet)
+ if err := r.Get(ctx, key, communitySet); err != nil {
+ if apierrors.IsNotFound(err) {
+ conditions.Set(s.RoutingPolicy, metav1.Condition{
+ Type: v1alpha1.ReadyCondition,
+ Status: metav1.ConditionFalse,
+ Reason: v1alpha1.CommunitySetNotFoundReason,
+ Message: fmt.Sprintf("referenced CommunitySet %q not found", key),
+ })
+ return nil, reconcile.TerminalError(fmt.Errorf("referenced CommunitySet %q not found", key))
+ }
+ return nil, fmt.Errorf("failed to get referenced CommunitySet %q: %w", key, err)
+ }
+
+ if communitySet.Spec.DeviceRef.Name != s.Device.Name {
+ conditions.Set(s.RoutingPolicy, metav1.Condition{
+ Type: v1alpha1.ReadyCondition,
+ Status: metav1.ConditionFalse,
+ Reason: v1alpha1.CrossDeviceReferenceReason,
+ Message: fmt.Sprintf("referenced CommunitySet %q does not belong to device %q", communitySet.Name, s.Device.Name),
+ })
+ return nil, reconcile.TerminalError(fmt.Errorf("referenced CommunitySet %q does not belong to device %q", communitySet.Name, s.Device.Name))
+ }
+
+ return communitySet, nil
+}
+
+// reconcileExtCommunitySet ensures that the referenced ExtCommunitySet exists and belongs to the same device as the RoutingPolicy.
+func (r *RoutingPolicyReconciler) reconcileExtCommunitySet(ctx context.Context, s *routingPolicyScope, c *v1alpha1.ExtCommunitySetMatchCondition) (*v1alpha1.ExtCommunitySet, error) {
+ key := client.ObjectKey{
+ Name: c.ExtCommunitySetRef.Name,
+ Namespace: s.RoutingPolicy.Namespace,
+ }
+
+ extCommunitySet := new(v1alpha1.ExtCommunitySet)
+ if err := r.Get(ctx, key, extCommunitySet); err != nil {
+ if apierrors.IsNotFound(err) {
+ conditions.Set(s.RoutingPolicy, metav1.Condition{
+ Type: v1alpha1.ReadyCondition,
+ Status: metav1.ConditionFalse,
+ Reason: v1alpha1.ExtCommunitySetNotFoundReason,
+ Message: fmt.Sprintf("referenced ExtCommunitySet %q not found", key),
+ })
+ return nil, reconcile.TerminalError(fmt.Errorf("referenced ExtCommunitySet %q not found", key))
+ }
+ return nil, fmt.Errorf("failed to get referenced ExtCommunitySet %q: %w", key, err)
+ }
+
+ if extCommunitySet.Spec.DeviceRef.Name != s.Device.Name {
+ conditions.Set(s.RoutingPolicy, metav1.Condition{
+ Type: v1alpha1.ReadyCondition,
+ Status: metav1.ConditionFalse,
+ Reason: v1alpha1.CrossDeviceReferenceReason,
+ Message: fmt.Sprintf("referenced ExtCommunitySet %q does not belong to device %q", extCommunitySet.Name, s.Device.Name),
+ })
+ return nil, reconcile.TerminalError(fmt.Errorf("referenced ExtCommunitySet %q does not belong to device %q", extCommunitySet.Name, s.Device.Name))
+ }
+
+ return extCommunitySet, nil
+}
+
func (r *RoutingPolicyReconciler) finalize(ctx context.Context, s *routingPolicyScope) (reterr error) {
if err := s.Provider.Connect(ctx, s.Connection); err != nil {
return fmt.Errorf("failed to connect to provider: %w", err)
@@ -448,6 +596,74 @@ func (r *RoutingPolicyReconciler) prefixSetToRoutingPolicy(ctx context.Context,
return requests
}
+// communitySetToRoutingPolicy is a [handler.MapFunc] to be used to enqueue requests for reconciliation
+// for RoutingPolicies when their referenced CommunitySet changes.
+func (r *RoutingPolicyReconciler) communitySetToRoutingPolicy(ctx context.Context, obj client.Object) []ctrl.Request {
+ communitySet, ok := obj.(*v1alpha1.CommunitySet)
+ if !ok {
+ panic(fmt.Sprintf("Expected a CommunitySet but got a %T", obj))
+ }
+
+ log := ctrl.LoggerFrom(ctx, "CommunitySet", klog.KObj(communitySet))
+
+ routingPolicies := new(v1alpha1.RoutingPolicyList)
+ if err := r.List(ctx, routingPolicies, client.InNamespace(communitySet.Namespace), client.MatchingFields{routingPolicyCommunitySetRefKey: communitySet.Spec.Name}); err != nil {
+ log.Error(err, "Failed to list RoutingPolicies")
+ return nil
+ }
+
+ requests := []ctrl.Request{}
+ for i := range routingPolicies.Items {
+ rp := &routingPolicies.Items[i]
+ for _, stmt := range rp.Spec.Statements {
+ if stmt.Conditions != nil && stmt.Conditions.MatchCommunitySet != nil && stmt.Conditions.MatchCommunitySet.CommunitySetRef.Name == communitySet.Spec.Name {
+ log.V(2).Info("Enqueuing RoutingPolicy for reconciliation", "RoutingPolicy", klog.KObj(rp))
+ requests = append(requests, ctrl.Request{
+ Name: rp.Name,
+ Namespace: rp.Namespace,
+ })
+ break
+ }
+ }
+ }
+
+ return requests
+}
+
+// extCommunitySetToRoutingPolicy is a [handler.MapFunc] to be used to enqueue requests for reconciliation
+// for RoutingPolicies when their referenced ExtCommunitySet changes.
+func (r *RoutingPolicyReconciler) extCommunitySetToRoutingPolicy(ctx context.Context, obj client.Object) []ctrl.Request {
+ extCommunitySet, ok := obj.(*v1alpha1.ExtCommunitySet)
+ if !ok {
+ panic(fmt.Sprintf("Expected an ExtCommunitySet but got a %T", obj))
+ }
+
+ log := ctrl.LoggerFrom(ctx, "ExtCommunitySet", klog.KObj(extCommunitySet))
+
+ routingPolicies := new(v1alpha1.RoutingPolicyList)
+ if err := r.List(ctx, routingPolicies, client.InNamespace(extCommunitySet.Namespace), client.MatchingFields{routingPolicyExtCommunitySetRefKey: extCommunitySet.Spec.Name}); err != nil {
+ log.Error(err, "Failed to list RoutingPolicies")
+ return nil
+ }
+
+ requests := []ctrl.Request{}
+ for i := range routingPolicies.Items {
+ rp := &routingPolicies.Items[i]
+ for _, stmt := range rp.Spec.Statements {
+ if stmt.Conditions != nil && stmt.Conditions.MatchExtCommunitySet != nil && stmt.Conditions.MatchExtCommunitySet.ExtCommunitySetRef.Name == extCommunitySet.Spec.Name {
+ log.V(2).Info("Enqueuing RoutingPolicy for reconciliation", "RoutingPolicy", klog.KObj(rp))
+ requests = append(requests, ctrl.Request{
+ Name: rp.Name,
+ Namespace: rp.Namespace,
+ })
+ break
+ }
+ }
+ }
+
+ return requests
+}
+
// deviceToRoutingPolicies is a [handler.MapFunc] to be used to enqueue requests for reconciliation
// for RoutingPolicies when their referenced Device's effective pause state changes.
func (r *RoutingPolicyReconciler) deviceToRoutingPolicies(ctx context.Context, obj client.Object) []ctrl.Request {
diff --git a/internal/controller/core/routingpolicy_controller_test.go b/internal/controller/core/routingpolicy_controller_test.go
index 4594cd67f..cbd782c28 100644
--- a/internal/controller/core/routingpolicy_controller_test.go
+++ b/internal/controller/core/routingpolicy_controller_test.go
@@ -52,6 +52,18 @@ var _ = Describe("RoutingPolicy Controller", func() {
ps.Namespace = metav1.NamespaceDefault
Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, ps))).To(Succeed())
+ 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("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 RoutingPolicy is removed from the provider")
Eventually(func(g Gomega) {
g.Expect(testProvider.RoutingPolicies.Has(name)).To(BeFalse(), "Provider shouldn't have RoutingPolicy configured anymore")
@@ -389,5 +401,484 @@ var _ = Describe("RoutingPolicy Controller", func() {
g.Expect(resource.Status.Conditions[1].Status).To(Equal(metav1.ConditionFalse))
}).Should(Succeed())
})
+
+ It("Should successfully reconcile a RoutingPolicy with CommunitySet match condition", func() {
+ By("Creating a CommunitySet resource")
+ cs := &v1alpha1.CommunitySet{
+ Name: name,
+ Namespace: metav1.NamespaceDefault,
+ Spec: v1alpha1.CommunitySetSpec{
+ DeviceRef: v1alpha1.LocalObjectReference{Name: name},
+ Name: "CS-BLUE",
+ Members: []v1alpha1.CommunityMember{{Sequence: 10, Regex: "65137:100"}},
+ },
+ }
+ Expect(k8sClient.Create(ctx, cs)).To(Succeed())
+
+ By("Creating a RoutingPolicy with CommunitySet match condition using matchSetOptions ALL")
+ rp := &v1alpha1.RoutingPolicy{
+ Name: name,
+ Namespace: metav1.NamespaceDefault,
+ Spec: v1alpha1.RoutingPolicySpec{
+ DeviceRef: v1alpha1.LocalObjectReference{Name: name},
+ Name: name,
+ Statements: []v1alpha1.PolicyStatement{
+ {
+ Sequence: 10,
+ Conditions: &v1alpha1.PolicyConditions{
+ MatchCommunitySet: &v1alpha1.CommunitySetMatchCondition{
+ CommunitySetRef: v1alpha1.LocalObjectReference{Name: name},
+ MatchSetOptions: v1alpha1.MatchSetOptionsAll,
+ },
+ },
+ Actions: v1alpha1.PolicyActions{
+ RouteDisposition: v1alpha1.AcceptRoute,
+ },
+ },
+ },
+ },
+ }
+ Expect(k8sClient.Create(ctx, rp)).To(Succeed())
+
+ By("Verifying the controller sets successful status conditions")
+ Eventually(func(g Gomega) {
+ resource := &v1alpha1.RoutingPolicy{}
+ 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("Verifying the RoutingPolicy is configured in the provider")
+ Eventually(func(g Gomega) {
+ g.Expect(testProvider.RoutingPolicies.Has(name)).To(BeTrue(), "Provider should have RoutingPolicy configured")
+ }).Should(Succeed())
+ })
+
+ It("Should handle non-existing CommunitySet reference", func() {
+ By("Creating a RoutingPolicy referencing non-existing CommunitySet")
+ rp := &v1alpha1.RoutingPolicy{
+ Name: name,
+ Namespace: metav1.NamespaceDefault,
+ Spec: v1alpha1.RoutingPolicySpec{
+ DeviceRef: v1alpha1.LocalObjectReference{Name: name},
+ Name: name,
+ Statements: []v1alpha1.PolicyStatement{
+ {
+ Sequence: 10,
+ Conditions: &v1alpha1.PolicyConditions{
+ MatchCommunitySet: &v1alpha1.CommunitySetMatchCondition{
+ CommunitySetRef: v1alpha1.LocalObjectReference{Name: "non-existing-communityset"},
+ },
+ },
+ Actions: v1alpha1.PolicyActions{
+ RouteDisposition: v1alpha1.AcceptRoute,
+ },
+ },
+ },
+ },
+ }
+ Expect(k8sClient.Create(ctx, rp)).To(Succeed())
+
+ By("Verifying the controller sets CommunitySet not found status")
+ Eventually(func(g Gomega) {
+ resource := &v1alpha1.RoutingPolicy{}
+ 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.ConditionFalse))
+ g.Expect(resource.Status.Conditions[0].Reason).To(Equal(v1alpha1.CommunitySetNotFoundReason))
+ g.Expect(resource.Status.Conditions[1].Type).To(Equal(v1alpha1.PausedCondition))
+ g.Expect(resource.Status.Conditions[1].Status).To(Equal(metav1.ConditionFalse))
+ }).Should(Succeed())
+ })
+
+ It("Should handle CommunitySet on different device", func() {
+ By("Creating a CommunitySet on a different device")
+ cs := &v1alpha1.CommunitySet{
+ Name: name,
+ Namespace: metav1.NamespaceDefault,
+ Spec: v1alpha1.CommunitySetSpec{
+ DeviceRef: v1alpha1.LocalObjectReference{Name: "different-device"},
+ Name: "CS-BLUE",
+ Members: []v1alpha1.CommunityMember{{Sequence: 10, Regex: "65137:100"}},
+ },
+ }
+ Expect(k8sClient.Create(ctx, cs)).To(Succeed())
+
+ By("Creating a RoutingPolicy referencing the cross-device CommunitySet")
+ rp := &v1alpha1.RoutingPolicy{
+ Name: name,
+ Namespace: metav1.NamespaceDefault,
+ Spec: v1alpha1.RoutingPolicySpec{
+ DeviceRef: v1alpha1.LocalObjectReference{Name: name},
+ Name: name,
+ Statements: []v1alpha1.PolicyStatement{
+ {
+ Sequence: 10,
+ Conditions: &v1alpha1.PolicyConditions{
+ MatchCommunitySet: &v1alpha1.CommunitySetMatchCondition{
+ CommunitySetRef: v1alpha1.LocalObjectReference{Name: name},
+ },
+ },
+ Actions: v1alpha1.PolicyActions{
+ RouteDisposition: v1alpha1.AcceptRoute,
+ },
+ },
+ },
+ },
+ }
+ Expect(k8sClient.Create(ctx, rp)).To(Succeed())
+
+ By("Verifying the controller sets cross-device reference status")
+ Eventually(func(g Gomega) {
+ resource := &v1alpha1.RoutingPolicy{}
+ 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.ConditionFalse))
+ g.Expect(resource.Status.Conditions[0].Reason).To(Equal(v1alpha1.CrossDeviceReferenceReason))
+ g.Expect(resource.Status.Conditions[1].Type).To(Equal(v1alpha1.PausedCondition))
+ g.Expect(resource.Status.Conditions[1].Status).To(Equal(metav1.ConditionFalse))
+ }).Should(Succeed())
+ })
+
+ It("Should successfully reconcile a RoutingPolicy with ExtCommunitySet match condition", func() {
+ By("Creating an ExtCommunitySet resource")
+ ecs := &v1alpha1.ExtCommunitySet{
+ Name: name,
+ Namespace: metav1.NamespaceDefault,
+ Spec: v1alpha1.ExtCommunitySetSpec{
+ DeviceRef: v1alpha1.LocalObjectReference{Name: name},
+ Name: "RT-BLUE",
+ Members: []v1alpha1.CommunityMember{{Sequence: 10, Regex: "65137:100"}},
+ },
+ }
+ Expect(k8sClient.Create(ctx, ecs)).To(Succeed())
+
+ By("Creating a RoutingPolicy with ExtCommunitySet match condition")
+ rp := &v1alpha1.RoutingPolicy{
+ Name: name,
+ Namespace: metav1.NamespaceDefault,
+ Spec: v1alpha1.RoutingPolicySpec{
+ DeviceRef: v1alpha1.LocalObjectReference{Name: name},
+ Name: name,
+ Statements: []v1alpha1.PolicyStatement{
+ {
+ Sequence: 10,
+ Conditions: &v1alpha1.PolicyConditions{
+ MatchExtCommunitySet: &v1alpha1.ExtCommunitySetMatchCondition{
+ ExtCommunitySetRef: v1alpha1.LocalObjectReference{Name: name},
+ },
+ },
+ Actions: v1alpha1.PolicyActions{
+ RouteDisposition: v1alpha1.AcceptRoute,
+ },
+ },
+ },
+ },
+ }
+ Expect(k8sClient.Create(ctx, rp)).To(Succeed())
+
+ By("Verifying the controller sets successful status conditions")
+ Eventually(func(g Gomega) {
+ resource := &v1alpha1.RoutingPolicy{}
+ 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("Verifying the RoutingPolicy is configured in the provider")
+ Eventually(func(g Gomega) {
+ g.Expect(testProvider.RoutingPolicies.Has(name)).To(BeTrue(), "Provider should have RoutingPolicy configured")
+ }).Should(Succeed())
+ })
+
+ It("Should handle non-existing ExtCommunitySet reference", func() {
+ By("Creating a RoutingPolicy referencing non-existing ExtCommunitySet")
+ rp := &v1alpha1.RoutingPolicy{
+ Name: name,
+ Namespace: metav1.NamespaceDefault,
+ Spec: v1alpha1.RoutingPolicySpec{
+ DeviceRef: v1alpha1.LocalObjectReference{Name: name},
+ Name: name,
+ Statements: []v1alpha1.PolicyStatement{
+ {
+ Sequence: 10,
+ Conditions: &v1alpha1.PolicyConditions{
+ MatchExtCommunitySet: &v1alpha1.ExtCommunitySetMatchCondition{
+ ExtCommunitySetRef: v1alpha1.LocalObjectReference{Name: "non-existing-extcommunityset"},
+ },
+ },
+ Actions: v1alpha1.PolicyActions{
+ RouteDisposition: v1alpha1.AcceptRoute,
+ },
+ },
+ },
+ },
+ }
+ Expect(k8sClient.Create(ctx, rp)).To(Succeed())
+
+ By("Verifying the controller sets ExtCommunitySet not found status")
+ Eventually(func(g Gomega) {
+ resource := &v1alpha1.RoutingPolicy{}
+ 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.ConditionFalse))
+ g.Expect(resource.Status.Conditions[0].Reason).To(Equal(v1alpha1.ExtCommunitySetNotFoundReason))
+ g.Expect(resource.Status.Conditions[1].Type).To(Equal(v1alpha1.PausedCondition))
+ g.Expect(resource.Status.Conditions[1].Status).To(Equal(metav1.ConditionFalse))
+ }).Should(Succeed())
+ })
+
+ It("Should successfully reconcile a RoutingPolicy combining prefix, community and ext-community matches", func() {
+ By("Creating a PrefixSet resource")
+ ps := &v1alpha1.PrefixSet{
+ Name: name,
+ Namespace: metav1.NamespaceDefault,
+ Spec: v1alpha1.PrefixSetSpec{
+ DeviceRef: v1alpha1.LocalObjectReference{Name: name},
+ Name: "PL-DEVICE-V4",
+ Entries: []v1alpha1.PrefixEntry{
+ {
+ Sequence: 10,
+ Prefix: v1alpha1.IPPrefix{Prefix: netip.MustParsePrefix("10.0.0.0/8")},
+ },
+ },
+ },
+ }
+ Expect(k8sClient.Create(ctx, ps)).To(Succeed())
+
+ By("Creating a CommunitySet resource")
+ cs := &v1alpha1.CommunitySet{
+ Name: name,
+ Namespace: metav1.NamespaceDefault,
+ Spec: v1alpha1.CommunitySetSpec{
+ DeviceRef: v1alpha1.LocalObjectReference{Name: name},
+ Name: "CS-BLUE",
+ Members: []v1alpha1.CommunityMember{{Sequence: 10, Regex: "65137:100"}},
+ },
+ }
+ Expect(k8sClient.Create(ctx, cs)).To(Succeed())
+
+ By("Creating an ExtCommunitySet resource")
+ ecs := &v1alpha1.ExtCommunitySet{
+ Name: name,
+ Namespace: metav1.NamespaceDefault,
+ Spec: v1alpha1.ExtCommunitySetSpec{
+ DeviceRef: v1alpha1.LocalObjectReference{Name: name},
+ Name: "RT-BLUE",
+ Members: []v1alpha1.CommunityMember{{Sequence: 10, Regex: "65137:100"}},
+ },
+ }
+ Expect(k8sClient.Create(ctx, ecs)).To(Succeed())
+
+ By("Creating a RoutingPolicy combining all three match conditions in one statement")
+ rp := &v1alpha1.RoutingPolicy{
+ Name: name,
+ Namespace: metav1.NamespaceDefault,
+ Spec: v1alpha1.RoutingPolicySpec{
+ DeviceRef: v1alpha1.LocalObjectReference{Name: name},
+ Name: name,
+ Statements: []v1alpha1.PolicyStatement{
+ {
+ Sequence: 10,
+ Conditions: &v1alpha1.PolicyConditions{
+ MatchPrefixSet: &v1alpha1.PrefixSetMatchCondition{
+ PrefixSetRef: v1alpha1.LocalObjectReference{Name: name},
+ },
+ MatchCommunitySet: &v1alpha1.CommunitySetMatchCondition{
+ CommunitySetRef: v1alpha1.LocalObjectReference{Name: name},
+ },
+ MatchExtCommunitySet: &v1alpha1.ExtCommunitySetMatchCondition{
+ ExtCommunitySetRef: v1alpha1.LocalObjectReference{Name: name},
+ MatchSetOptions: v1alpha1.MatchSetOptionsAll,
+ },
+ },
+ Actions: v1alpha1.PolicyActions{
+ RouteDisposition: v1alpha1.AcceptRoute,
+ },
+ },
+ },
+ },
+ }
+ Expect(k8sClient.Create(ctx, rp)).To(Succeed())
+
+ By("Verifying the controller sets successful status conditions")
+ Eventually(func(g Gomega) {
+ resource := &v1alpha1.RoutingPolicy{}
+ 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("Verifying the RoutingPolicy is configured in the provider")
+ Eventually(func(g Gomega) {
+ g.Expect(testProvider.RoutingPolicies.Has(name)).To(BeTrue(), "Provider should have RoutingPolicy configured")
+ }).Should(Succeed())
+ })
+
+ It("Should re-evaluate the RoutingPolicy when CommunitySet membership changes", func() {
+ By("Creating a CommunitySet resource")
+ cs := &v1alpha1.CommunitySet{
+ Name: name,
+ Namespace: metav1.NamespaceDefault,
+ Spec: v1alpha1.CommunitySetSpec{
+ DeviceRef: v1alpha1.LocalObjectReference{Name: name},
+ Name: "CS-BLUE",
+ Members: []v1alpha1.CommunityMember{{Sequence: 10, Regex: "65137:100"}},
+ },
+ }
+ Expect(k8sClient.Create(ctx, cs)).To(Succeed())
+
+ By("Creating a RoutingPolicy referencing the CommunitySet")
+ rp := &v1alpha1.RoutingPolicy{
+ Name: name,
+ Namespace: metav1.NamespaceDefault,
+ Spec: v1alpha1.RoutingPolicySpec{
+ DeviceRef: v1alpha1.LocalObjectReference{Name: name},
+ Name: name,
+ Statements: []v1alpha1.PolicyStatement{
+ {
+ Sequence: 10,
+ Conditions: &v1alpha1.PolicyConditions{
+ MatchCommunitySet: &v1alpha1.CommunitySetMatchCondition{
+ CommunitySetRef: v1alpha1.LocalObjectReference{Name: name},
+ },
+ },
+ Actions: v1alpha1.PolicyActions{
+ RouteDisposition: v1alpha1.AcceptRoute,
+ },
+ },
+ },
+ },
+ }
+ Expect(k8sClient.Create(ctx, rp)).To(Succeed())
+
+ By("Verifying the RoutingPolicy is configured in the provider")
+ Eventually(func(g Gomega) {
+ g.Expect(testProvider.RoutingPolicies.Has(name)).To(BeTrue(), "Provider should have RoutingPolicy configured")
+ }).Should(Succeed())
+
+ By("Recording the current generation observed by the controller")
+ resource := &v1alpha1.RoutingPolicy{}
+ Expect(k8sClient.Get(ctx, key, resource)).To(Succeed())
+ var initialConditionTime metav1.Time
+ Eventually(func(g Gomega) {
+ g.Expect(k8sClient.Get(ctx, key, resource)).To(Succeed())
+ g.Expect(resource.Status.Conditions).ToNot(BeEmpty())
+ initialConditionTime = resource.Status.Conditions[0].LastTransitionTime
+ g.Expect(initialConditionTime.IsZero()).To(BeFalse())
+ }).Should(Succeed())
+
+ By("Updating the CommunitySet membership")
+ Eventually(func(g Gomega) {
+ current := &v1alpha1.CommunitySet{}
+ g.Expect(k8sClient.Get(ctx, key, current)).To(Succeed())
+ current.Spec.Members = []v1alpha1.CommunityMember{{Sequence: 10, Regex: "65137:100"}, {Sequence: 20, Regex: "65137:200"}}
+ g.Expect(k8sClient.Update(ctx, current)).To(Succeed())
+ }).Should(Succeed())
+
+ By("Verifying the RoutingPolicy remains configured after the membership change")
+ Eventually(func(g Gomega) {
+ g.Expect(testProvider.RoutingPolicies.Has(name)).To(BeTrue(), "Provider should still have RoutingPolicy configured")
+ }).Should(Succeed())
+ })
+
+ It("Should reconverge when a CommunitySet match condition is removed from a statement", func() {
+ By("Creating a CommunitySet resource")
+ cs := &v1alpha1.CommunitySet{
+ Name: name,
+ Namespace: metav1.NamespaceDefault,
+ Spec: v1alpha1.CommunitySetSpec{
+ DeviceRef: v1alpha1.LocalObjectReference{Name: name},
+ Name: "CS-BLUE",
+ Members: []v1alpha1.CommunityMember{{Sequence: 10, Regex: "65137:100"}},
+ },
+ }
+ Expect(k8sClient.Create(ctx, cs)).To(Succeed())
+
+ By("Creating an ExtCommunitySet resource")
+ ecs := &v1alpha1.ExtCommunitySet{
+ Name: name,
+ Namespace: metav1.NamespaceDefault,
+ Spec: v1alpha1.ExtCommunitySetSpec{
+ DeviceRef: v1alpha1.LocalObjectReference{Name: name},
+ Name: "RT-BLUE",
+ Members: []v1alpha1.CommunityMember{{Sequence: 10, Regex: "65137:100"}},
+ },
+ }
+ Expect(k8sClient.Create(ctx, ecs)).To(Succeed())
+
+ By("Creating a RoutingPolicy matching both the CommunitySet and ExtCommunitySet")
+ rp := &v1alpha1.RoutingPolicy{
+ Name: name,
+ Namespace: metav1.NamespaceDefault,
+ Spec: v1alpha1.RoutingPolicySpec{
+ DeviceRef: v1alpha1.LocalObjectReference{Name: name},
+ Name: name,
+ Statements: []v1alpha1.PolicyStatement{
+ {
+ Sequence: 10,
+ Conditions: &v1alpha1.PolicyConditions{
+ MatchCommunitySet: &v1alpha1.CommunitySetMatchCondition{
+ CommunitySetRef: v1alpha1.LocalObjectReference{Name: name},
+ },
+ MatchExtCommunitySet: &v1alpha1.ExtCommunitySetMatchCondition{
+ ExtCommunitySetRef: v1alpha1.LocalObjectReference{Name: name},
+ },
+ },
+ Actions: v1alpha1.PolicyActions{
+ RouteDisposition: v1alpha1.AcceptRoute,
+ },
+ },
+ },
+ },
+ }
+ Expect(k8sClient.Create(ctx, rp)).To(Succeed())
+
+ By("Verifying the RoutingPolicy reaches Ready with both conditions")
+ Eventually(func(g Gomega) {
+ resource := &v1alpha1.RoutingPolicy{}
+ 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))
+ }).Should(Succeed())
+
+ By("Removing the matchCommunitySet condition from the statement")
+ Eventually(func(g Gomega) {
+ current := &v1alpha1.RoutingPolicy{}
+ g.Expect(k8sClient.Get(ctx, key, current)).To(Succeed())
+ current.Spec.Statements[0].Conditions.MatchCommunitySet = nil
+ g.Expect(k8sClient.Update(ctx, current)).To(Succeed())
+ }).Should(Succeed())
+
+ By("Deleting the CommunitySet to prove it is no longer referenced")
+ Expect(k8sClient.Delete(ctx, cs)).To(Succeed())
+
+ By("Verifying the RoutingPolicy stays Ready driven only by the remaining ExtCommunitySet match")
+ Eventually(func(g Gomega) {
+ resource := &v1alpha1.RoutingPolicy{}
+ g.Expect(k8sClient.Get(ctx, key, resource)).To(Succeed())
+ g.Expect(resource.Spec.Statements[0].Conditions.MatchCommunitySet).To(BeNil())
+ g.Expect(resource.Status.Conditions[0].Type).To(Equal(v1alpha1.ReadyCondition))
+ g.Expect(resource.Status.Conditions[0].Status).To(Equal(metav1.ConditionTrue))
+ }).Should(Succeed())
+
+ By("Verifying the RoutingPolicy remains configured in the provider")
+ Eventually(func(g Gomega) {
+ g.Expect(testProvider.RoutingPolicies.Has(name)).To(BeTrue(), "Provider should still have RoutingPolicy configured")
+ }).Should(Succeed())
+ })
})
})
diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go
index 697eec851..d52c553b8 100644
--- a/internal/provider/cisco/nxos/provider.go
+++ b/internal/provider/cisco/nxos/provider.go
@@ -2556,6 +2556,10 @@ func (p *Provider) EnsureRoutingPolicy(ctx context.Context, req *provider.Ensure
switch v := cond.(type) {
case provider.MatchPrefixSetCondition:
e.SetPrefixSet(v.PrefixSet.Spec.Name, v.PrefixSet.Is6())
+ case provider.MatchCommunitySetCondition:
+ e.SetCommunitySet(v.CommunitySet.Spec.Name, v.MatchAll)
+ case provider.MatchExtCommunitySetCondition:
+ e.SetExtCommunitySet(v.ExtCommunitySet.Spec.Name, v.MatchAll)
default:
return fmt.Errorf("routing policy: unsupported condition type %T", cond)
}
diff --git a/internal/provider/cisco/nxos/routemap.go b/internal/provider/cisco/nxos/routemap.go
index 475ba7ee8..687711992 100644
--- a/internal/provider/cisco/nxos/routemap.go
+++ b/internal/provider/cisco/nxos/routemap.go
@@ -41,6 +41,18 @@ type RouteMapEntry struct {
RsRtDstAttList gnmiext.List[string, *RsRtDstAtt] `json:"RsRtDstAtt-list,omitzero"`
} `json:"rsrtDstAtt-items,omitzero"`
} `json:"mrtdst-items,omitzero"`
+ MregcommItems struct {
+ Criteria string `json:"criteria,omitempty"`
+ RsregCommAttItems struct {
+ RsRegCommAttList gnmiext.List[string, *RsRegCommAtt] `json:"RsRegCommAtt-list,omitzero"`
+ } `json:"rsregCommAtt-items,omitzero"`
+ } `json:"mregcomm-items,omitzero"`
+ MextcommItems struct {
+ Criteria string `json:"criteria,omitempty"`
+ RsextCommAttItems struct {
+ RsExtCommAttList gnmiext.List[string, *RsExtCommAtt] `json:"RsExtCommAtt-list,omitzero"`
+ } `json:"rsextCommAtt-items,omitzero"`
+ } `json:"mextcomm-items,omitzero"`
SetASPathPrependItems struct {
AS string `json:"as"`
} `json:"setaspathprepend-items,omitzero"`
@@ -91,12 +103,46 @@ func (e *RouteMapEntry) SetPrefixSet(name string, isV6 bool) {
e.MrtdstItems.RsrtDstAttItems.RsRtDstAttList.Set(&RsRtDstAtt{TDn: tdn})
}
+// criteriaExact realizes matchSetOptions ALL: a route must carry every member of the set.
+// ANY (the default) leaves criteria absent, which is the device default.
+const criteriaExact = "exact"
+
+func (e *RouteMapEntry) SetCommunitySet(name string, all bool) {
+ e.MregcommItems.RsregCommAttItems.RsRegCommAttList.Set(&RsRegCommAtt{
+ TDn: "/System/rpm-items/rtregcom-items/Rule-list[name='" + name + "']",
+ })
+ if all {
+ e.MregcommItems.Criteria = criteriaExact
+ }
+}
+
+func (e *RouteMapEntry) SetExtCommunitySet(name string, all bool) {
+ e.MextcommItems.RsextCommAttItems.RsExtCommAttList.Set(&RsExtCommAtt{
+ TDn: "/System/rpm-items/rtextcom-items/Rule-list[name='" + name + "']",
+ })
+ if all {
+ e.MextcommItems.Criteria = criteriaExact
+ }
+}
+
type RsRtDstAtt struct {
TDn string `json:"tDn"`
}
func (r *RsRtDstAtt) Key() string { return r.TDn }
+type RsRegCommAtt struct {
+ TDn string `json:"tDn"`
+}
+
+func (r *RsRegCommAtt) Key() string { return r.TDn }
+
+type RsExtCommAtt struct {
+ TDn string `json:"tDn"`
+}
+
+func (r *RsExtCommAtt) Key() string { return r.TDn }
+
type CommItem struct {
Community string `json:"community"`
}
diff --git a/internal/provider/cisco/nxos/routemap_test.go b/internal/provider/cisco/nxos/routemap_test.go
index a08ae2018..40994da34 100644
--- a/internal/provider/cisco/nxos/routemap_test.go
+++ b/internal/provider/cisco/nxos/routemap_test.go
@@ -87,4 +87,56 @@ func init() {
pfxV6RM.Name = "RM-PREFIXSET-V6"
pfxV6RM.EntItems.EntryList.Set(pfxV6Entry)
Register("route_map_prefixset_v6", pfxV6RM)
+
+ commAnyEntry := &RouteMapEntry{}
+ commAnyEntry.Order = 10
+ commAnyEntry.Action = ActionPermit
+ commAnyEntry.SetCommunitySet("CS-BLUE", false)
+
+ commAnyRM := &RouteMap{}
+ commAnyRM.Name = "RM-COMMUNITYSET-ANY"
+ commAnyRM.EntItems.EntryList.Set(commAnyEntry)
+ Register("route_map_communityset_any", commAnyRM)
+
+ commAllEntry := &RouteMapEntry{}
+ commAllEntry.Order = 10
+ commAllEntry.Action = ActionPermit
+ commAllEntry.SetCommunitySet("CS-BLUE", true)
+
+ commAllRM := &RouteMap{}
+ commAllRM.Name = "RM-COMMUNITYSET-ALL"
+ commAllRM.EntItems.EntryList.Set(commAllEntry)
+ Register("route_map_communityset_all", commAllRM)
+
+ extCommAnyEntry := &RouteMapEntry{}
+ extCommAnyEntry.Order = 10
+ extCommAnyEntry.Action = ActionPermit
+ extCommAnyEntry.SetExtCommunitySet("RT-BLUE", false)
+
+ extCommAnyRM := &RouteMap{}
+ extCommAnyRM.Name = "RM-EXTCOMMUNITYSET-ANY"
+ extCommAnyRM.EntItems.EntryList.Set(extCommAnyEntry)
+ Register("route_map_extcommunityset_any", extCommAnyRM)
+
+ extCommAllEntry := &RouteMapEntry{}
+ extCommAllEntry.Order = 10
+ extCommAllEntry.Action = ActionPermit
+ extCommAllEntry.SetExtCommunitySet("RT-BLUE", true)
+
+ extCommAllRM := &RouteMap{}
+ extCommAllRM.Name = "RM-EXTCOMMUNITYSET-ALL"
+ extCommAllRM.EntItems.EntryList.Set(extCommAllEntry)
+ Register("route_map_extcommunityset_all", extCommAllRM)
+
+ comboEntry := &RouteMapEntry{}
+ comboEntry.Order = 10
+ comboEntry.Action = ActionPermit
+ comboEntry.SetPrefixSet("PL-DEVICE-V4", false)
+ comboEntry.SetCommunitySet("CS-BLUE", false)
+ comboEntry.SetExtCommunitySet("RT-BLUE", true)
+
+ comboRM := &RouteMap{}
+ comboRM.Name = "RM-COMBINED-MATCH"
+ comboRM.EntItems.EntryList.Set(comboEntry)
+ Register("route_map_combined_match", comboRM)
}
diff --git a/internal/provider/cisco/nxos/testdata/route_map_combined_match.json b/internal/provider/cisco/nxos/testdata/route_map_combined_match.json
new file mode 100644
index 000000000..ec3996a0f
--- /dev/null
+++ b/internal/provider/cisco/nxos/testdata/route_map_combined_match.json
@@ -0,0 +1,47 @@
+{
+ "rpm-items": {
+ "rtmap-items": {
+ "Rule-list": [
+ {
+ "name": "RM-COMBINED-MATCH",
+ "ent-items": {
+ "Entry-list": [
+ {
+ "action": "permit",
+ "order": 10,
+ "mrtdst-items": {
+ "rsrtDstAtt-items": {
+ "RsRtDstAtt-list": [
+ {
+ "tDn": "/System/rpm-items/pfxlistv4-items/RuleV4-list[name='PL-DEVICE-V4']"
+ }
+ ]
+ }
+ },
+ "mregcomm-items": {
+ "rsregCommAtt-items": {
+ "RsRegCommAtt-list": [
+ {
+ "tDn": "/System/rpm-items/rtregcom-items/Rule-list[name='CS-BLUE']"
+ }
+ ]
+ }
+ },
+ "mextcomm-items": {
+ "criteria": "exact",
+ "rsextCommAtt-items": {
+ "RsExtCommAtt-list": [
+ {
+ "tDn": "/System/rpm-items/rtextcom-items/Rule-list[name='RT-BLUE']"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/internal/provider/cisco/nxos/testdata/route_map_combined_match.json.txt b/internal/provider/cisco/nxos/testdata/route_map_combined_match.json.txt
new file mode 100644
index 000000000..3bd757232
--- /dev/null
+++ b/internal/provider/cisco/nxos/testdata/route_map_combined_match.json.txt
@@ -0,0 +1,4 @@
+route-map RM-COMBINED-MATCH permit 10
+ match ip address prefix-list PL-DEVICE-V4
+ match community CS-BLUE
+ match extcommunity RT-BLUE exact-match
diff --git a/internal/provider/cisco/nxos/testdata/route_map_communityset_all.json b/internal/provider/cisco/nxos/testdata/route_map_communityset_all.json
new file mode 100644
index 000000000..48ed23943
--- /dev/null
+++ b/internal/provider/cisco/nxos/testdata/route_map_communityset_all.json
@@ -0,0 +1,29 @@
+{
+ "rpm-items": {
+ "rtmap-items": {
+ "Rule-list": [
+ {
+ "name": "RM-COMMUNITYSET-ALL",
+ "ent-items": {
+ "Entry-list": [
+ {
+ "action": "permit",
+ "order": 10,
+ "mregcomm-items": {
+ "criteria": "exact",
+ "rsregCommAtt-items": {
+ "RsRegCommAtt-list": [
+ {
+ "tDn": "/System/rpm-items/rtregcom-items/Rule-list[name='CS-BLUE']"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/internal/provider/cisco/nxos/testdata/route_map_communityset_all.json.txt b/internal/provider/cisco/nxos/testdata/route_map_communityset_all.json.txt
new file mode 100644
index 000000000..53d9fc754
--- /dev/null
+++ b/internal/provider/cisco/nxos/testdata/route_map_communityset_all.json.txt
@@ -0,0 +1,2 @@
+route-map RM-COMMUNITYSET-ALL permit 10
+ match community CS-BLUE exact-match
diff --git a/internal/provider/cisco/nxos/testdata/route_map_communityset_any.json b/internal/provider/cisco/nxos/testdata/route_map_communityset_any.json
new file mode 100644
index 000000000..7f8640f04
--- /dev/null
+++ b/internal/provider/cisco/nxos/testdata/route_map_communityset_any.json
@@ -0,0 +1,28 @@
+{
+ "rpm-items": {
+ "rtmap-items": {
+ "Rule-list": [
+ {
+ "name": "RM-COMMUNITYSET-ANY",
+ "ent-items": {
+ "Entry-list": [
+ {
+ "action": "permit",
+ "order": 10,
+ "mregcomm-items": {
+ "rsregCommAtt-items": {
+ "RsRegCommAtt-list": [
+ {
+ "tDn": "/System/rpm-items/rtregcom-items/Rule-list[name='CS-BLUE']"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/internal/provider/cisco/nxos/testdata/route_map_communityset_any.json.txt b/internal/provider/cisco/nxos/testdata/route_map_communityset_any.json.txt
new file mode 100644
index 000000000..162225b11
--- /dev/null
+++ b/internal/provider/cisco/nxos/testdata/route_map_communityset_any.json.txt
@@ -0,0 +1,2 @@
+route-map RM-COMMUNITYSET-ANY permit 10
+ match community CS-BLUE
diff --git a/internal/provider/cisco/nxos/testdata/route_map_extcommunityset_all.json b/internal/provider/cisco/nxos/testdata/route_map_extcommunityset_all.json
new file mode 100644
index 000000000..1bea3d6c3
--- /dev/null
+++ b/internal/provider/cisco/nxos/testdata/route_map_extcommunityset_all.json
@@ -0,0 +1,29 @@
+{
+ "rpm-items": {
+ "rtmap-items": {
+ "Rule-list": [
+ {
+ "name": "RM-EXTCOMMUNITYSET-ALL",
+ "ent-items": {
+ "Entry-list": [
+ {
+ "action": "permit",
+ "order": 10,
+ "mextcomm-items": {
+ "criteria": "exact",
+ "rsextCommAtt-items": {
+ "RsExtCommAtt-list": [
+ {
+ "tDn": "/System/rpm-items/rtextcom-items/Rule-list[name='RT-BLUE']"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/internal/provider/cisco/nxos/testdata/route_map_extcommunityset_all.json.txt b/internal/provider/cisco/nxos/testdata/route_map_extcommunityset_all.json.txt
new file mode 100644
index 000000000..6a941ad1e
--- /dev/null
+++ b/internal/provider/cisco/nxos/testdata/route_map_extcommunityset_all.json.txt
@@ -0,0 +1,2 @@
+route-map RM-EXTCOMMUNITYSET-ALL permit 10
+ match extcommunity RT-BLUE exact-match
diff --git a/internal/provider/cisco/nxos/testdata/route_map_extcommunityset_any.json b/internal/provider/cisco/nxos/testdata/route_map_extcommunityset_any.json
new file mode 100644
index 000000000..df0bdaab3
--- /dev/null
+++ b/internal/provider/cisco/nxos/testdata/route_map_extcommunityset_any.json
@@ -0,0 +1,28 @@
+{
+ "rpm-items": {
+ "rtmap-items": {
+ "Rule-list": [
+ {
+ "name": "RM-EXTCOMMUNITYSET-ANY",
+ "ent-items": {
+ "Entry-list": [
+ {
+ "action": "permit",
+ "order": 10,
+ "mextcomm-items": {
+ "rsextCommAtt-items": {
+ "RsExtCommAtt-list": [
+ {
+ "tDn": "/System/rpm-items/rtextcom-items/Rule-list[name='RT-BLUE']"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/internal/provider/cisco/nxos/testdata/route_map_extcommunityset_any.json.txt b/internal/provider/cisco/nxos/testdata/route_map_extcommunityset_any.json.txt
new file mode 100644
index 000000000..9f335db62
--- /dev/null
+++ b/internal/provider/cisco/nxos/testdata/route_map_extcommunityset_any.json.txt
@@ -0,0 +1,2 @@
+route-map RM-EXTCOMMUNITYSET-ANY permit 10
+ match extcommunity RT-BLUE
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index bec196641..6c204968b 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -716,6 +716,22 @@ type MatchPrefixSetCondition struct {
func (MatchPrefixSetCondition) isPolicyCondition() {}
+type MatchCommunitySetCondition struct {
+ CommunitySet *v1alpha1.CommunitySet
+ // MatchAll is true when matchSetOptions is ALL (criteria "exact"); false for ANY.
+ MatchAll bool
+}
+
+func (MatchCommunitySetCondition) isPolicyCondition() {}
+
+type MatchExtCommunitySetCondition struct {
+ ExtCommunitySet *v1alpha1.ExtCommunitySet
+ // MatchAll is true when matchSetOptions is ALL (criteria "exact"); false for ANY.
+ MatchAll bool
+}
+
+func (MatchExtCommunitySetCondition) isPolicyCondition() {}
+
type DeleteRoutingPolicyRequest struct {
Name string
}
diff --git a/test/gnmi/testdata/cisco-nxos-gnmi/routingpolicy.txtar b/test/gnmi/testdata/cisco-nxos-gnmi/routingpolicy.txtar
index 7f2d8460f..f608d737d 100644
--- a/test/gnmi/testdata/cisco-nxos-gnmi/routingpolicy.txtar
+++ b/test/gnmi/testdata/cisco-nxos-gnmi/routingpolicy.txtar
@@ -26,6 +26,34 @@ spec:
- sequence: 10
prefix: "2001:db8::/32"
+-- 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]"
+
+-- 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]"
+
-- routingpolicies/rm-import --
apiVersion: networking.metal.ironcore.dev/v1alpha1
kind: RoutingPolicy
@@ -51,6 +79,21 @@ spec:
name: test-prefixset-v6
actions:
routeDisposition: AcceptRoute
+ - sequence: 30
+ conditions:
+ matchCommunitySet:
+ communitySetRef:
+ name: test-communityset
+ matchSetOptions: ALL
+ actions:
+ routeDisposition: AcceptRoute
+ - sequence: 40
+ conditions:
+ matchExtCommunitySet:
+ extCommunitySetRef:
+ name: test-extcommunityset
+ actions:
+ routeDisposition: AcceptRoute
-- state/preload --
{
@@ -107,6 +150,40 @@ spec:
}
]
},
+ "rtregcom-items": {
+ "Rule-list": [
+ {
+ "name": "BGP-COMMUNITY",
+ "mode": "regex",
+ "ent-items": {
+ "Entry-list": [
+ {
+ "order": 5,
+ "action": "permit",
+ "regex": "50000:[0-9][0-9]"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "rtextcom-items": {
+ "Rule-list": [
+ {
+ "name": "BGP-EXT-COMMUNITY",
+ "mode": "regex",
+ "ent-items": {
+ "Entry-list": [
+ {
+ "order": 5,
+ "action": "permit",
+ "regex": "65200:[0-9][0-9]"
+ }
+ ]
+ }
+ }
+ ]
+ },
"rtmap-items": {
"Rule-list": [
{
@@ -138,6 +215,33 @@ spec:
]
}
}
+ },
+ {
+ "action": "permit",
+ "order": 30,
+ "mregcomm-items": {
+ "criteria": "exact",
+ "rsregCommAtt-items": {
+ "RsRegCommAtt-list": [
+ {
+ "tDn": "/System/rpm-items/rtregcom-items/Rule-list[name='BGP-COMMUNITY']"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "action": "permit",
+ "order": 40,
+ "mextcomm-items": {
+ "rsextCommAtt-items": {
+ "RsExtCommAtt-list": [
+ {
+ "tDn": "/System/rpm-items/rtextcom-items/Rule-list[name='BGP-EXT-COMMUNITY']"
+ }
+ ]
+ }
+ }
}
]
}
@@ -162,6 +266,12 @@ spec:
"pfxlistv6-items": {
"RuleV6-list": []
},
+ "rtregcom-items": {
+ "Rule-list": []
+ },
+ "rtextcom-items": {
+ "Rule-list": []
+ },
"rtmap-items": {
"Rule-list": []
}