From c771e6bded9dd926414a8d0e04543a847eb016cd Mon Sep 17 00:00:00 2001 From: Ivo Gosemann Date: Tue, 15 Sep 2026 17:25:35 +0200 Subject: [PATCH] add nxos support to match extcommunities on routingpolicy Signed-off-by: Ivo Gosemann --- api/core/v1alpha1/groupversion_info.go | 4 + api/core/v1alpha1/routingpolicy_types.go | 49 ++ api/core/v1alpha1/zz_generated.deepcopy.go | 42 ++ ...olicies.networking.metal.ironcore.dev.yaml | 66 +++ ...ng.metal.ironcore.dev_routingpolicies.yaml | 66 +++ config/samples/v1alpha1_routingpolicy.yaml | 49 ++ docs/api-reference/index.md | 57 ++ .../core/routingpolicy_controller.go | 218 +++++++- .../core/routingpolicy_controller_test.go | 491 ++++++++++++++++++ internal/provider/cisco/nxos/provider.go | 4 + internal/provider/cisco/nxos/routemap.go | 46 ++ internal/provider/cisco/nxos/routemap_test.go | 52 ++ .../testdata/route_map_combined_match.json | 47 ++ .../route_map_combined_match.json.txt | 4 + .../testdata/route_map_communityset_all.json | 29 ++ .../route_map_communityset_all.json.txt | 2 + .../testdata/route_map_communityset_any.json | 28 + .../route_map_communityset_any.json.txt | 2 + .../route_map_extcommunityset_all.json | 29 ++ .../route_map_extcommunityset_all.json.txt | 2 + .../route_map_extcommunityset_any.json | 28 + .../route_map_extcommunityset_any.json.txt | 2 + internal/provider/provider.go | 16 + .../cisco-nxos-gnmi/routingpolicy.txtar | 110 ++++ 24 files changed, 1442 insertions(+), 1 deletion(-) create mode 100644 internal/provider/cisco/nxos/testdata/route_map_combined_match.json create mode 100644 internal/provider/cisco/nxos/testdata/route_map_combined_match.json.txt create mode 100644 internal/provider/cisco/nxos/testdata/route_map_communityset_all.json create mode 100644 internal/provider/cisco/nxos/testdata/route_map_communityset_all.json.txt create mode 100644 internal/provider/cisco/nxos/testdata/route_map_communityset_any.json create mode 100644 internal/provider/cisco/nxos/testdata/route_map_communityset_any.json.txt create mode 100644 internal/provider/cisco/nxos/testdata/route_map_extcommunityset_all.json create mode 100644 internal/provider/cisco/nxos/testdata/route_map_extcommunityset_all.json.txt create mode 100644 internal/provider/cisco/nxos/testdata/route_map_extcommunityset_any.json create mode 100644 internal/provider/cisco/nxos/testdata/route_map_extcommunityset_any.json.txt 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": [] }