diff --git a/api/v1alpha1/l4routepolicy_types.go b/api/v1alpha1/l4routepolicy_types.go
new file mode 100644
index 00000000..695d1982
--- /dev/null
+++ b/api/v1alpha1/l4routepolicy_types.go
@@ -0,0 +1,68 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package v1alpha1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+)
+
+// L4RoutePolicySpec defines the desired state of L4RoutePolicy.
+type L4RoutePolicySpec struct {
+ // TargetRefs identifies the L4 route resources (TCPRoute, UDPRoute, or TLSRoute)
+ // to which this policy applies. Only same-namespace targets are supported.
+ //
+ // +kubebuilder:validation:MinItems=1
+ // +kubebuilder:validation:MaxItems=16
+ // +kubebuilder:validation:XValidation:rule="self.all(r, r.kind == 'TCPRoute' || r.kind == 'UDPRoute' || r.kind == 'TLSRoute')",message="targetRefs kind must be TCPRoute, UDPRoute, or TLSRoute"
+ TargetRefs []gatewayv1alpha2.LocalPolicyTargetReferenceWithSectionName `json:"targetRefs"`
+
+ // Plugins is the list of APISIX stream plugins to attach to the targeted L4 routes.
+ // Plugin names should be valid APISIX stream plugin names (e.g., limit-conn, ip-restriction).
+ //
+ // +optional
+ Plugins []Plugin `json:"plugins,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
+
+// L4RoutePolicy defines plugin configuration for Gateway API L4 routes (TCPRoute, UDPRoute, TLSRoute).
+// It follows the Gateway API Policy Attachment pattern and attaches APISIX stream plugins
+// to the targeted L4 route resources.
+type L4RoutePolicy struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ // Spec defines the desired state of L4RoutePolicy.
+ Spec L4RoutePolicySpec `json:"spec,omitempty"`
+ Status PolicyStatus `json:"status,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// L4RoutePolicyList contains a list of L4RoutePolicy.
+type L4RoutePolicyList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata,omitempty"`
+ Items []L4RoutePolicy `json:"items"`
+}
+
+func init() {
+ SchemeBuilder.Register(&L4RoutePolicy{}, &L4RoutePolicyList{})
+}
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index f7b5383c..1c8fc80a 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -617,6 +617,94 @@ func (in *HTTPRoutePolicySpec) DeepCopy() *HTTPRoutePolicySpec {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *L4RoutePolicy) DeepCopyInto(out *L4RoutePolicy) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ in.Spec.DeepCopyInto(&out.Spec)
+ in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new L4RoutePolicy.
+func (in *L4RoutePolicy) DeepCopy() *L4RoutePolicy {
+ if in == nil {
+ return nil
+ }
+ out := new(L4RoutePolicy)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *L4RoutePolicy) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *L4RoutePolicyList) DeepCopyInto(out *L4RoutePolicyList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]L4RoutePolicy, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new L4RoutePolicyList.
+func (in *L4RoutePolicyList) DeepCopy() *L4RoutePolicyList {
+ if in == nil {
+ return nil
+ }
+ out := new(L4RoutePolicyList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *L4RoutePolicyList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *L4RoutePolicySpec) DeepCopyInto(out *L4RoutePolicySpec) {
+ *out = *in
+ if in.TargetRefs != nil {
+ in, out := &in.TargetRefs, &out.TargetRefs
+ *out = make([]v1alpha2.LocalPolicyTargetReferenceWithSectionName, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.Plugins != nil {
+ in, out := &in.Plugins, &out.Plugins
+ *out = make([]Plugin, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new L4RoutePolicySpec.
+func (in *L4RoutePolicySpec) DeepCopy() *L4RoutePolicySpec {
+ if in == nil {
+ return nil
+ }
+ out := new(L4RoutePolicySpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LoadBalancer) DeepCopyInto(out *LoadBalancer) {
*out = *in
diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go
index 8e659cd9..a4906250 100644
--- a/api/v2/zz_generated.deepcopy.go
+++ b/api/v2/zz_generated.deepcopy.go
@@ -1,19 +1,20 @@
//go:build !ignore_autogenerated
-// Licensed to the Apache Software Foundation (ASF) under one or more
-// contributor license agreements. See the NOTICE file distributed with
-// this work for additional information regarding copyright ownership.
-// The ASF licenses this file to You under the Apache License, Version 2.0
-// (the "License"); you may not use this file except in compliance with
-// the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
+/*
+Copyright 2024.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
// Code generated by controller-gen. DO NOT EDIT.
diff --git a/config/crd/bases/apisix.apache.org_l4routepolicies.yaml b/config/crd/bases/apisix.apache.org_l4routepolicies.yaml
new file mode 100644
index 00000000..43a966e9
--- /dev/null
+++ b/config/crd/bases/apisix.apache.org_l4routepolicies.yaml
@@ -0,0 +1,427 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.17.2
+ name: l4routepolicies.apisix.apache.org
+spec:
+ group: apisix.apache.org
+ names:
+ kind: L4RoutePolicy
+ listKind: L4RoutePolicyList
+ plural: l4routepolicies
+ singular: l4routepolicy
+ scope: Namespaced
+ versions:
+ - name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: |-
+ L4RoutePolicy defines plugin configuration for Gateway API L4 routes (TCPRoute, UDPRoute, TLSRoute).
+ It follows the Gateway API Policy Attachment pattern and attaches APISIX stream plugins
+ to the targeted L4 route resources.
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: Spec defines the desired state of L4RoutePolicy.
+ properties:
+ plugins:
+ description: |-
+ Plugins is the list of APISIX stream plugins to attach to the targeted L4 routes.
+ Plugin names should be valid APISIX stream plugin names (e.g., limit-conn, ip-restriction).
+ items:
+ properties:
+ config:
+ description: Config is plugin configuration details.
+ x-kubernetes-preserve-unknown-fields: true
+ name:
+ description: Name is the name of the plugin.
+ type: string
+ required:
+ - name
+ type: object
+ type: array
+ targetRefs:
+ description: |-
+ TargetRefs identifies the L4 route resources (TCPRoute, UDPRoute, or TLSRoute)
+ to which this policy applies. Only same-namespace targets are supported.
+ items:
+ description: |-
+ LocalPolicyTargetReferenceWithSectionName identifies an API object to apply a
+ direct policy to. This should be used as part of Policy resources that can
+ target single resources. For more information on how this policy attachment
+ mode works, and a sample Policy resource, refer to the policy attachment
+ documentation for Gateway API.
+
+ Note: This should only be used for direct policy attachment when references
+ to SectionName are actually needed. In all other cases,
+ LocalPolicyTargetReference should be used.
+ properties:
+ group:
+ description: Group is the group of the target resource.
+ maxLength: 253
+ pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ kind:
+ description: Kind is kind of the target resource.
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+ type: string
+ name:
+ description: Name is the name of the target resource.
+ maxLength: 253
+ minLength: 1
+ type: string
+ sectionName:
+ description: |-
+ SectionName is the name of a section within the target resource. When
+ unspecified, this targetRef targets the entire resource. In the following
+ resources, SectionName is interpreted as the following:
+
+ * Gateway: Listener name
+ * HTTPRoute: HTTPRouteRule name
+ * Service: Port name
+
+ If a SectionName is specified, but does not exist on the targeted object,
+ the Policy must fail to attach, and the policy implementation should record
+ a `ResolvedRefs` or similar Condition in the Policy's status.
+ maxLength: 253
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ required:
+ - group
+ - kind
+ - name
+ type: object
+ maxItems: 16
+ minItems: 1
+ type: array
+ x-kubernetes-validations:
+ - message: targetRefs kind must be TCPRoute, UDPRoute, or TLSRoute
+ rule: self.all(r, r.kind == 'TCPRoute' || r.kind == 'UDPRoute' ||
+ r.kind == 'TLSRoute')
+ required:
+ - targetRefs
+ type: object
+ status:
+ description: |-
+ PolicyStatus defines the common attributes that all Policies should include within
+ their status.
+ properties:
+ ancestors:
+ description: |-
+ Ancestors is a list of ancestor resources (usually Gateways) that are
+ associated with the policy, and the status of the policy with respect to
+ each ancestor. When this policy attaches to a parent, the controller that
+ manages the parent and the ancestors MUST add an entry to this list when
+ the controller first sees the policy and SHOULD update the entry as
+ appropriate when the relevant ancestor is modified.
+
+ Note that choosing the relevant ancestor is left to the Policy designers;
+ an important part of Policy design is designing the right object level at
+ which to namespace this status.
+
+ Note also that implementations MUST ONLY populate ancestor status for
+ the Ancestor resources they are responsible for. Implementations MUST
+ use the ControllerName field to uniquely identify the entries in this list
+ that they are responsible for.
+
+ Note that to achieve this, the list of PolicyAncestorStatus structs
+ MUST be treated as a map with a composite key, made up of the AncestorRef
+ and ControllerName fields combined.
+
+ A maximum of 16 ancestors will be represented in this list. An empty list
+ means the Policy is not relevant for any ancestors.
+
+ If this slice is full, implementations MUST NOT add further entries.
+ Instead they MUST consider the policy unimplementable and signal that
+ on any related resources such as the ancestor that would be referenced
+ here. For example, if this list was full on BackendTLSPolicy, no
+ additional Gateways would be able to reference the Service targeted by
+ the BackendTLSPolicy.
+ items:
+ description: |-
+ PolicyAncestorStatus describes the status of a route with respect to an
+ associated Ancestor.
+
+ Ancestors refer to objects that are either the Target of a policy or above it
+ in terms of object hierarchy. For example, if a policy targets a Service, the
+ Policy's Ancestors are, in order, the Service, the HTTPRoute, the Gateway, and
+ the GatewayClass. Almost always, in this hierarchy, the Gateway will be the most
+ useful object to place Policy status on, so we recommend that implementations
+ SHOULD use Gateway as the PolicyAncestorStatus object unless the designers
+ have a _very_ good reason otherwise.
+
+ In the context of policy attachment, the Ancestor is used to distinguish which
+ resource results in a distinct application of this policy. For example, if a policy
+ targets a Service, it may have a distinct result per attached Gateway.
+
+ Policies targeting the same resource may have different effects depending on the
+ ancestors of those resources. For example, different Gateways targeting the same
+ Service may have different capabilities, especially if they have different underlying
+ implementations.
+
+ For example, in BackendTLSPolicy, the Policy attaches to a Service that is
+ used as a backend in a HTTPRoute that is itself attached to a Gateway.
+ In this case, the relevant object for status is the Gateway, and that is the
+ ancestor object referred to in this status.
+
+ Note that a parent is also an ancestor, so for objects where the parent is the
+ relevant object for status, this struct SHOULD still be used.
+
+ This struct is intended to be used in a slice that's effectively a map,
+ with a composite key made up of the AncestorRef and the ControllerName.
+ properties:
+ ancestorRef:
+ description: |-
+ AncestorRef corresponds with a ParentRef in the spec that this
+ PolicyAncestorStatus struct describes the status of.
+ properties:
+ group:
+ default: gateway.networking.k8s.io
+ description: |-
+ Group is the group of the referent.
+ When unspecified, "gateway.networking.k8s.io" is inferred.
+ To set the core API group (such as for a "Service" kind referent),
+ Group must be explicitly set to "" (empty string).
+
+ Support: Core
+ maxLength: 253
+ pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ kind:
+ default: Gateway
+ description: |-
+ Kind is kind of the referent.
+
+ There are two kinds of parent resources with "Core" support:
+
+ * Gateway (Gateway conformance profile)
+ * Service (Mesh conformance profile, ClusterIP Services only)
+
+ Support for other resources is Implementation-Specific.
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+ type: string
+ name:
+ description: |-
+ Name is the name of the referent.
+
+ Support: Core
+ maxLength: 253
+ minLength: 1
+ type: string
+ namespace:
+ description: |-
+ Namespace is the namespace of the referent. When unspecified, this refers
+ to the local namespace of the Route.
+
+ Note that there are specific rules for ParentRefs which cross namespace
+ boundaries. Cross-namespace references are only valid if they are explicitly
+ allowed by something in the namespace they are referring to. For example:
+ Gateway has the AllowedRoutes field, and ReferenceGrant provides a
+ generic way to enable any other kind of cross-namespace reference.
+
+
+ ParentRefs from a Route to a Service in the same namespace are "producer"
+ routes, which apply default routing rules to inbound connections from
+ any namespace to the Service.
+
+ ParentRefs from a Route to a Service in a different namespace are
+ "consumer" routes, and these routing rules are only applied to outbound
+ connections originating from the same namespace as the Route, for which
+ the intended destination of the connections are a Service targeted as a
+ ParentRef of the Route.
+
+
+ Support: Core
+ maxLength: 63
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+ type: string
+ port:
+ description: |-
+ Port is the network port this Route targets. It can be interpreted
+ differently based on the type of parent resource.
+
+ When the parent resource is a Gateway, this targets all listeners
+ listening on the specified port that also support this kind of Route(and
+ select this Route). It's not recommended to set `Port` unless the
+ networking behaviors specified in a Route must apply to a specific port
+ as opposed to a listener(s) whose port(s) may be changed. When both Port
+ and SectionName are specified, the name and port of the selected listener
+ must match both specified values.
+
+
+ When the parent resource is a Service, this targets a specific port in the
+ Service spec. When both Port (experimental) and SectionName are specified,
+ the name and port of the selected port must match both specified values.
+
+
+ Implementations MAY choose to support other parent resources.
+ Implementations supporting other types of parent resources MUST clearly
+ document how/if Port is interpreted.
+
+ For the purpose of status, an attachment is considered successful as
+ long as the parent resource accepts it partially. For example, Gateway
+ listeners can restrict which Routes can attach to them by Route kind,
+ namespace, or hostname. If 1 of 2 Gateway listeners accept attachment
+ from the referencing Route, the Route MUST be considered successfully
+ attached. If no Gateway listeners accept attachment from this Route,
+ the Route MUST be considered detached from the Gateway.
+
+ Support: Extended
+ format: int32
+ maximum: 65535
+ minimum: 1
+ type: integer
+ sectionName:
+ description: |-
+ SectionName is the name of a section within the target resource. In the
+ following resources, SectionName is interpreted as the following:
+
+ * Gateway: Listener name. When both Port (experimental) and SectionName
+ are specified, the name and port of the selected listener must match
+ both specified values.
+ * Service: Port name. When both Port (experimental) and SectionName
+ are specified, the name and port of the selected listener must match
+ both specified values.
+
+ Implementations MAY choose to support attaching Routes to other resources.
+ If that is the case, they MUST clearly document how SectionName is
+ interpreted.
+
+ When unspecified (empty string), this will reference the entire resource.
+ For the purpose of status, an attachment is considered successful if at
+ least one section in the parent resource accepts it. For example, Gateway
+ listeners can restrict which Routes can attach to them by Route kind,
+ namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from
+ the referencing Route, the Route MUST be considered successfully
+ attached. If no Gateway listeners accept attachment from this Route, the
+ Route MUST be considered detached from the Gateway.
+
+ Support: Core
+ maxLength: 253
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+ type: string
+ required:
+ - name
+ type: object
+ conditions:
+ description: Conditions describes the status of the Policy with
+ respect to the given Ancestor.
+ items:
+ description: Condition contains details for one aspect of
+ the current state of this API Resource.
+ properties:
+ lastTransitionTime:
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False,
+ Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: type of condition in CamelCase or in foo.example.com/CamelCase.
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ maxItems: 8
+ minItems: 1
+ type: array
+ x-kubernetes-list-map-keys:
+ - type
+ x-kubernetes-list-type: map
+ controllerName:
+ description: |-
+ ControllerName is a domain/path string that indicates the name of the
+ controller that wrote this status. This corresponds with the
+ controllerName field on GatewayClass.
+
+ Example: "example.net/gateway-controller".
+
+ The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are
+ valid Kubernetes names
+ (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).
+
+ Controllers MUST populate this field when writing status. Controllers should ensure that
+ entries to status populated with their ControllerName are cleaned up when they are no
+ longer necessary.
+ maxLength: 253
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
+ type: string
+ required:
+ - ancestorRef
+ - controllerName
+ type: object
+ maxItems: 16
+ type: array
+ required:
+ - ancestors
+ type: object
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 07d8175b..ecf9372e 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -36,6 +36,7 @@ rules:
- consumers
- gatewayproxies
- httproutepolicies
+ - l4routepolicies
- pluginconfigs
verbs:
- get
@@ -53,6 +54,7 @@ rules:
- backendtrafficpolicies/status
- consumers/status
- httproutepolicies/status
+ - l4routepolicies/status
verbs:
- get
- update
diff --git a/docs/en/latest/reference/api-reference.md b/docs/en/latest/reference/api-reference.md
index 44b29323..6f658437 100644
--- a/docs/en/latest/reference/api-reference.md
+++ b/docs/en/latest/reference/api-reference.md
@@ -19,6 +19,7 @@ Package v1alpha1 contains API Schema definitions for the apisix.apache.org v1alp
- [Consumer](#consumer)
- [GatewayProxy](#gatewayproxy)
- [HTTPRoutePolicy](#httproutepolicy)
+- [L4RoutePolicy](#l4routepolicy)
- [PluginConfig](#pluginconfig)
### BackendTrafficPolicy
@@ -84,6 +85,24 @@ HTTPRoutePolicy defines configuration of traffic policies.
+### L4RoutePolicy
+
+
+L4RoutePolicy defines plugin configuration for Gateway API L4 routes (TCPRoute, UDPRoute, TLSRoute).
+It follows the Gateway API Policy Attachment pattern and attaches APISIX stream plugins
+to the targeted L4 route resources.
+
+
+
+| Field | Description |
+| --- | --- |
+| `apiVersion` _string_ | `apisix.apache.org/v1alpha1`
+| `kind` _string_ | `L4RoutePolicy`
+| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#objectmeta-v1-meta)_ | Please refer to the Kubernetes API documentation for details on the `metadata` field. |
+| `spec` _[L4RoutePolicySpec](#l4routepolicyspec)_ | Spec defines the desired state of L4RoutePolicy. |
+
+
+
### PluginConfig
@@ -356,6 +375,22 @@ _Base type:_ `string`
_Appears in:_
- [BackendTrafficPolicySpec](#backendtrafficpolicyspec)
+#### L4RoutePolicySpec
+
+
+L4RoutePolicySpec defines the desired state of L4RoutePolicy.
+
+
+
+| Field | Description |
+| --- | --- |
+| `targetRefs` _LocalPolicyTargetReferenceWithSectionName array_ | TargetRefs identifies the L4 route resources (TCPRoute, UDPRoute, or TLSRoute) to which this policy applies. Only same-namespace targets are supported. |
+| `plugins` _[Plugin](#plugin) array_ | Plugins is the list of APISIX stream plugins to attach to the targeted L4 routes. Plugin names should be valid APISIX stream plugin names (e.g., limit-conn, ip-restriction). |
+
+
+_Appears in:_
+- [L4RoutePolicy](#l4routepolicy)
+
#### LoadBalancer
@@ -388,6 +423,7 @@ _Appears in:_
_Appears in:_
- [ConsumerSpec](#consumerspec)
+- [L4RoutePolicySpec](#l4routepolicyspec)
- [PluginConfigSpec](#pluginconfigspec)
#### PluginConfigSpec
diff --git a/internal/adc/translator/apisixconsumer_test.go b/internal/adc/translator/apisixconsumer_test.go
index e6a1ee4a..ff42eef7 100644
--- a/internal/adc/translator/apisixconsumer_test.go
+++ b/internal/adc/translator/apisixconsumer_test.go
@@ -49,7 +49,7 @@ func TestTranslateApisixConsumer_UsesMetadataLabelsWithoutOverwritingControllerL
},
},
Spec: apiv2.ApisixConsumerSpec{
- AuthParameter: apiv2.ApisixConsumerAuthParameter{
+ AuthParameter: &apiv2.ApisixConsumerAuthParameter{
BasicAuth: &apiv2.ApisixConsumerBasicAuth{
Value: &apiv2.ApisixConsumerBasicAuthValue{
Username: "demo",
diff --git a/internal/adc/translator/l4route_test.go b/internal/adc/translator/l4route_test.go
new file mode 100644
index 00000000..904b918a
--- /dev/null
+++ b/internal/adc/translator/l4route_test.go
@@ -0,0 +1,264 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package translator
+
+import (
+ "context"
+ "testing"
+
+ "github.com/go-logr/logr"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ k8stypes "k8s.io/apimachinery/pkg/types"
+ gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+
+ "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+ "github.com/apache/apisix-ingress-controller/internal/provider"
+)
+
+func TestTranslateTCPRouteWithL4RoutePolicy(t *testing.T) {
+ tests := []struct {
+ name string
+ policy *v1alpha1.L4RoutePolicy
+ wantPlugins []string
+ wantNoPlugins bool
+ }{
+ {
+ name: "attaches plugins from matching L4RoutePolicy",
+ policy: makeL4RoutePolicy("default", "tcp-policy", "TCPRoute", "my-tcp", []v1alpha1.Plugin{
+ {Name: "limit-conn", Config: mustJSON(map[string]any{"conn": 100})},
+ {Name: "ip-restriction", Config: mustJSON(map[string]any{"whitelist": []string{"10.0.0.0/8"}})},
+ }),
+ wantPlugins: []string{"limit-conn", "ip-restriction"},
+ },
+ {
+ name: "does not attach plugins from policy targeting different route kind",
+ policy: makeL4RoutePolicy("default", "udp-policy", "UDPRoute", "my-tcp", []v1alpha1.Plugin{
+ {Name: "limit-conn", Config: mustJSON(map[string]any{"conn": 100})},
+ }),
+ wantNoPlugins: true,
+ },
+ {
+ name: "does not attach plugins from policy targeting different route name",
+ policy: makeL4RoutePolicy("default", "tcp-policy", "TCPRoute", "other-tcp", []v1alpha1.Plugin{
+ {Name: "limit-conn", Config: mustJSON(map[string]any{"conn": 100})},
+ }),
+ wantNoPlugins: true,
+ },
+ {
+ name: "succeeds with no policy in context",
+ policy: nil,
+ wantNoPlugins: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ translator := NewTranslator(logr.Discard())
+ tctx := provider.NewDefaultTranslateContext(context.Background())
+
+ if tt.policy != nil {
+ key := k8stypes.NamespacedName{Namespace: tt.policy.Namespace, Name: tt.policy.Name}
+ tctx.L4RoutePolicies[key] = tt.policy
+ }
+
+ route := &gatewayv1alpha2.TCPRoute{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "my-tcp",
+ Namespace: "default",
+ },
+ Spec: gatewayv1alpha2.TCPRouteSpec{
+ Rules: []gatewayv1alpha2.TCPRouteRule{
+ {BackendRefs: []gatewayv1alpha2.BackendRef{}},
+ },
+ },
+ }
+
+ result, err := translator.TranslateTCPRoute(tctx, route)
+ require.NoError(t, err)
+ require.Len(t, result.Services, 1)
+
+ plugins := result.Services[0].Plugins
+ if tt.wantNoPlugins {
+ assert.Empty(t, plugins)
+ } else {
+ for _, name := range tt.wantPlugins {
+ assert.Contains(t, plugins, name, "expected plugin %q to be attached", name)
+ }
+ }
+ })
+ }
+}
+
+func TestTranslateUDPRouteWithL4RoutePolicy(t *testing.T) {
+ tests := []struct {
+ name string
+ policy *v1alpha1.L4RoutePolicy
+ wantPlugins []string
+ wantNoPlugins bool
+ }{
+ {
+ name: "attaches plugins from matching L4RoutePolicy",
+ policy: makeL4RoutePolicy("default", "udp-policy", "UDPRoute", "my-udp", []v1alpha1.Plugin{
+ {Name: "limit-conn", Config: mustJSON(map[string]any{"conn": 50})},
+ }),
+ wantPlugins: []string{"limit-conn"},
+ },
+ {
+ name: "does not attach plugins from policy targeting TCPRoute",
+ policy: makeL4RoutePolicy("default", "tcp-policy", "TCPRoute", "my-udp", []v1alpha1.Plugin{
+ {Name: "limit-conn", Config: mustJSON(map[string]any{"conn": 50})},
+ }),
+ wantNoPlugins: true,
+ },
+ {
+ name: "succeeds with no policy in context",
+ policy: nil,
+ wantNoPlugins: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ translator := NewTranslator(logr.Discard())
+ tctx := provider.NewDefaultTranslateContext(context.Background())
+
+ if tt.policy != nil {
+ key := k8stypes.NamespacedName{Namespace: tt.policy.Namespace, Name: tt.policy.Name}
+ tctx.L4RoutePolicies[key] = tt.policy
+ }
+
+ route := &gatewayv1alpha2.UDPRoute{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "my-udp",
+ Namespace: "default",
+ },
+ Spec: gatewayv1alpha2.UDPRouteSpec{
+ Rules: []gatewayv1alpha2.UDPRouteRule{
+ {BackendRefs: []gatewayv1alpha2.BackendRef{}},
+ },
+ },
+ }
+
+ result, err := translator.TranslateUDPRoute(tctx, route)
+ require.NoError(t, err)
+ require.Len(t, result.Services, 1)
+
+ plugins := result.Services[0].Plugins
+ if tt.wantNoPlugins {
+ assert.Empty(t, plugins)
+ } else {
+ for _, name := range tt.wantPlugins {
+ assert.Contains(t, plugins, name, "expected plugin %q to be attached", name)
+ }
+ }
+ })
+ }
+}
+
+func TestTranslateTLSRouteWithL4RoutePolicy(t *testing.T) {
+ tests := []struct {
+ name string
+ policy *v1alpha1.L4RoutePolicy
+ hostnames []string
+ wantPlugins []string
+ wantNoPlugins bool
+ }{
+ {
+ name: "attaches plugins from matching L4RoutePolicy",
+ policy: makeL4RoutePolicy("default", "tls-policy", "TLSRoute", "my-tls", []v1alpha1.Plugin{
+ {Name: "ip-restriction", Config: mustJSON(map[string]any{"whitelist": []string{"192.168.0.0/16"}})},
+ }),
+ hostnames: []string{"example.com"},
+ wantPlugins: []string{"ip-restriction"},
+ },
+ {
+ name: "plugins attached once per rule even with multiple SNI hostnames",
+ policy: makeL4RoutePolicy("default", "tls-policy", "TLSRoute", "my-tls", []v1alpha1.Plugin{
+ {Name: "limit-conn", Config: mustJSON(map[string]any{"conn": 20})},
+ }),
+ hostnames: []string{"foo.example.com", "bar.example.com"},
+ wantPlugins: []string{"limit-conn"},
+ },
+ {
+ name: "does not attach plugins from policy targeting TCPRoute",
+ policy: makeL4RoutePolicy("default", "tcp-policy", "TCPRoute", "my-tls", []v1alpha1.Plugin{
+ {Name: "limit-conn", Config: mustJSON(map[string]any{"conn": 20})},
+ }),
+ hostnames: []string{"example.com"},
+ wantNoPlugins: true,
+ },
+ {
+ name: "succeeds with no policy in context",
+ policy: nil,
+ hostnames: []string{"example.com"},
+ wantNoPlugins: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ translator := NewTranslator(logr.Discard())
+ tctx := provider.NewDefaultTranslateContext(context.Background())
+
+ if tt.policy != nil {
+ key := k8stypes.NamespacedName{Namespace: tt.policy.Namespace, Name: tt.policy.Name}
+ tctx.L4RoutePolicies[key] = tt.policy
+ }
+
+ hostnames := make([]gatewayv1alpha2.Hostname, 0, len(tt.hostnames))
+ for _, h := range tt.hostnames {
+ hostnames = append(hostnames, gatewayv1alpha2.Hostname(h))
+ }
+
+ route := &gatewayv1alpha2.TLSRoute{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "my-tls",
+ Namespace: "default",
+ },
+ Spec: gatewayv1alpha2.TLSRouteSpec{
+ Hostnames: hostnames,
+ Rules: []gatewayv1alpha2.TLSRouteRule{
+ {BackendRefs: []gatewayv1alpha2.BackendRef{}},
+ },
+ },
+ }
+
+ result, err := translator.TranslateTLSRoute(tctx, route)
+ require.NoError(t, err)
+ require.Len(t, result.Services, 1)
+
+ // Verify stream routes are created per SNI hostname
+ if len(tt.hostnames) > 0 {
+ assert.Len(t, result.Services[0].StreamRoutes, len(tt.hostnames))
+ }
+
+ plugins := result.Services[0].Plugins
+ if tt.wantNoPlugins {
+ assert.Empty(t, plugins)
+ } else {
+ for _, name := range tt.wantPlugins {
+ assert.Contains(t, plugins, name, "expected plugin %q to be attached", name)
+ }
+ // Plugins are on the service, not duplicated per stream route
+ assert.Len(t, result.Services, 1, "plugins should be on service level, not duplicated")
+ }
+ })
+ }
+}
diff --git a/internal/adc/translator/l4routepolicy_test.go b/internal/adc/translator/l4routepolicy_test.go
new file mode 100644
index 00000000..180106e4
--- /dev/null
+++ b/internal/adc/translator/l4routepolicy_test.go
@@ -0,0 +1,143 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package translator
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/go-logr/logr"
+ "github.com/stretchr/testify/assert"
+ apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ k8stypes "k8s.io/apimachinery/pkg/types"
+ gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+
+ adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+ "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+)
+
+func makeL4RoutePolicy(namespace, name, targetKind, targetName string, plugins []v1alpha1.Plugin) *v1alpha1.L4RoutePolicy {
+ return &v1alpha1.L4RoutePolicy{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: namespace,
+ Name: name,
+ },
+ Spec: v1alpha1.L4RoutePolicySpec{
+ TargetRefs: []gatewayv1alpha2.LocalPolicyTargetReferenceWithSectionName{
+ {
+ LocalPolicyTargetReference: gatewayv1alpha2.LocalPolicyTargetReference{
+ Group: gatewayv1alpha2.GroupName,
+ Kind: gatewayv1alpha2.Kind(targetKind),
+ Name: gatewayv1alpha2.ObjectName(targetName),
+ },
+ },
+ },
+ Plugins: plugins,
+ },
+ }
+}
+
+func mustJSON(v any) apiextensionsv1.JSON {
+ b, err := json.Marshal(v)
+ if err != nil {
+ panic(err)
+ }
+ return apiextensionsv1.JSON{Raw: b}
+}
+
+func TestAttachL4RoutePolicyPlugins_AttachesMatchingPolicy(t *testing.T) {
+ tr := NewTranslator(logr.Discard())
+
+ policy := makeL4RoutePolicy("default", "my-policy", "TCPRoute", "my-tcp-route", []v1alpha1.Plugin{
+ {Name: "limit-conn", Config: mustJSON(map[string]any{"conn": 100, "burst": 50})},
+ {Name: "ip-restriction", Config: mustJSON(map[string]any{"whitelist": []string{"10.0.0.0/8"}})},
+ })
+
+ policies := map[k8stypes.NamespacedName]*v1alpha1.L4RoutePolicy{
+ {Namespace: "default", Name: "my-policy"}: policy,
+ }
+
+ plugins := adctypes.Plugins{}
+ tr.AttachL4RoutePolicyPlugins(policies, "default", "my-tcp-route", "TCPRoute", plugins)
+
+ assert.Len(t, plugins, 2)
+ assert.Contains(t, plugins, "limit-conn")
+ assert.Contains(t, plugins, "ip-restriction")
+
+ cfg := plugins["limit-conn"].(map[string]any)
+ assert.EqualValues(t, 100, cfg["conn"])
+}
+
+func TestAttachL4RoutePolicyPlugins_NoMatchOnKind(t *testing.T) {
+ tr := NewTranslator(logr.Discard())
+
+ policy := makeL4RoutePolicy("default", "udp-policy", "UDPRoute", "my-udp-route", []v1alpha1.Plugin{
+ {Name: "limit-conn", Config: mustJSON(map[string]any{"conn": 10})},
+ })
+
+ policies := map[k8stypes.NamespacedName]*v1alpha1.L4RoutePolicy{
+ {Namespace: "default", Name: "udp-policy"}: policy,
+ }
+
+ plugins := adctypes.Plugins{}
+ // Looking for TCPRoute, but policy targets UDPRoute — should not match.
+ tr.AttachL4RoutePolicyPlugins(policies, "default", "my-udp-route", "TCPRoute", plugins)
+
+ assert.Empty(t, plugins)
+}
+
+func TestAttachL4RoutePolicyPlugins_NoMatchOnNamespace(t *testing.T) {
+ tr := NewTranslator(logr.Discard())
+
+ policy := makeL4RoutePolicy("other-ns", "my-policy", "TCPRoute", "my-tcp-route", []v1alpha1.Plugin{
+ {Name: "limit-conn", Config: mustJSON(map[string]any{"conn": 10})},
+ })
+
+ policies := map[k8stypes.NamespacedName]*v1alpha1.L4RoutePolicy{
+ {Namespace: "other-ns", Name: "my-policy"}: policy,
+ }
+
+ plugins := adctypes.Plugins{}
+ // Route is in "default" namespace, policy is in "other-ns" — should not match.
+ tr.AttachL4RoutePolicyPlugins(policies, "default", "my-tcp-route", "TCPRoute", plugins)
+
+ assert.Empty(t, plugins)
+}
+
+func TestAttachL4RoutePolicyPlugins_EmptyPlugins(t *testing.T) {
+ tr := NewTranslator(logr.Discard())
+
+ policy := makeL4RoutePolicy("default", "empty-policy", "TCPRoute", "my-tcp-route", nil)
+
+ policies := map[k8stypes.NamespacedName]*v1alpha1.L4RoutePolicy{
+ {Namespace: "default", Name: "empty-policy"}: policy,
+ }
+
+ plugins := adctypes.Plugins{}
+ tr.AttachL4RoutePolicyPlugins(policies, "default", "my-tcp-route", "TCPRoute", plugins)
+
+ assert.Empty(t, plugins)
+}
+
+func TestAttachL4RoutePolicyPlugins_EmptyPolicies(t *testing.T) {
+ tr := NewTranslator(logr.Discard())
+ plugins := adctypes.Plugins{}
+ tr.AttachL4RoutePolicyPlugins(nil, "default", "my-tcp-route", "TCPRoute", plugins)
+ assert.Empty(t, plugins)
+}
diff --git a/internal/adc/translator/policies.go b/internal/adc/translator/policies.go
index 41706964..05ca8115 100644
--- a/internal/adc/translator/policies.go
+++ b/internal/adc/translator/policies.go
@@ -18,9 +18,12 @@
package translator
import (
+ "encoding/json"
+
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+ gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
adctypes "github.com/apache/apisix-ingress-controller/api/adc"
"github.com/apache/apisix-ingress-controller/api/v1alpha1"
@@ -80,3 +83,47 @@ func (t *Translator) attachBackendTrafficPolicyToUpstream(policy *v1alpha1.Backe
upstream.Key = policy.Spec.LoadBalancer.Key
}
}
+
+// AttachL4RoutePolicyPlugins merges plugins from the matching L4RoutePolicy (if any) into the
+// provided plugins map. It looks up policies targeting the route identified by routeNamespace,
+// routeName, and routeKind.
+func (t *Translator) AttachL4RoutePolicyPlugins(
+ policies map[types.NamespacedName]*v1alpha1.L4RoutePolicy,
+ routeNamespace, routeName, routeKind string,
+ plugins adctypes.Plugins,
+) {
+ if len(policies) == 0 {
+ return
+ }
+ for _, policy := range policies {
+ if policy.Namespace != routeNamespace {
+ continue
+ }
+ for _, ref := range policy.Spec.TargetRefs {
+ if string(ref.Group) != gatewayv1alpha2.GroupName {
+ continue
+ }
+ if string(ref.Kind) != routeKind {
+ continue
+ }
+ if string(ref.Name) != routeName {
+ continue
+ }
+ t.mergeL4PolicyPlugins(policy, plugins)
+ return
+ }
+ }
+}
+
+func (t *Translator) mergeL4PolicyPlugins(policy *v1alpha1.L4RoutePolicy, plugins adctypes.Plugins) {
+ for _, plugin := range policy.Spec.Plugins {
+ cfg := make(map[string]any)
+ if len(plugin.Config.Raw) > 0 {
+ if err := json.Unmarshal(plugin.Config.Raw, &cfg); err != nil {
+ t.Log.Error(err, "failed to unmarshal L4RoutePolicy plugin config", "plugin", plugin.Name, "policy", policy.Name)
+ continue
+ }
+ }
+ plugins[plugin.Name] = cfg
+ }
+}
diff --git a/internal/adc/translator/tcproute.go b/internal/adc/translator/tcproute.go
index fc9c0b13..dc9ec2e1 100644
--- a/internal/adc/translator/tcproute.go
+++ b/internal/adc/translator/tcproute.go
@@ -157,6 +157,12 @@ func (t *Translator) TranslateTCPRoute(tctx *provider.TranslateContext, tcpRoute
streamRoute.Labels = labels
// TODO: support remote_addr, server_addr, sni, server_port
service.StreamRoutes = append(service.StreamRoutes, streamRoute)
+
+ if service.Plugins == nil {
+ service.Plugins = make(adctypes.Plugins)
+ }
+ t.AttachL4RoutePolicyPlugins(tctx.L4RoutePolicies, tcpRoute.Namespace, tcpRoute.Name, "TCPRoute", service.Plugins)
+
result.Services = append(result.Services, service)
}
return result, nil
diff --git a/internal/adc/translator/tlsroute.go b/internal/adc/translator/tlsroute.go
index b1eb5fa0..b43b9835 100644
--- a/internal/adc/translator/tlsroute.go
+++ b/internal/adc/translator/tlsroute.go
@@ -153,6 +153,12 @@ func (t *Translator) TranslateTLSRoute(tctx *provider.TranslateContext, tlsRoute
streamRoute.Labels = labels
service.StreamRoutes = append(service.StreamRoutes, streamRoute)
}
+
+ if service.Plugins == nil {
+ service.Plugins = make(adctypes.Plugins)
+ }
+ t.AttachL4RoutePolicyPlugins(tctx.L4RoutePolicies, tlsRoute.Namespace, tlsRoute.Name, "TLSRoute", service.Plugins)
+
result.Services = append(result.Services, service)
}
return result, nil
diff --git a/internal/adc/translator/udproute.go b/internal/adc/translator/udproute.go
index 5cc09a10..a899a648 100644
--- a/internal/adc/translator/udproute.go
+++ b/internal/adc/translator/udproute.go
@@ -146,6 +146,12 @@ func (t *Translator) TranslateUDPRoute(tctx *provider.TranslateContext, udpRoute
streamRoute.Labels = labels
// TODO: support remote_addr, server_addr, sni, server_port
service.StreamRoutes = append(service.StreamRoutes, streamRoute)
+
+ if service.Plugins == nil {
+ service.Plugins = make(adctypes.Plugins)
+ }
+ t.AttachL4RoutePolicyPlugins(tctx.L4RoutePolicies, udpRoute.Namespace, udpRoute.Name, "UDPRoute", service.Plugins)
+
result.Services = append(result.Services, service)
}
return result, nil
diff --git a/internal/controller/indexer/indexer.go b/internal/controller/indexer/indexer.go
index 5d4b4998..61a64c5d 100644
--- a/internal/controller/indexer/indexer.go
+++ b/internal/controller/indexer/indexer.go
@@ -89,6 +89,7 @@ func SetupIndexer(mgr ctrl.Manager) error {
&networkingv1beta1.IngressClass{}: setupIngressClassV1beta1Indexer,
&v1alpha1.BackendTrafficPolicy{}: setupBackendTrafficPolicyIndexer,
&v1alpha1.HTTPRoutePolicy{}: setHTTPRoutePolicyIndexer,
+ &v1alpha1.L4RoutePolicy{}: setupL4RoutePolicyIndexer,
} {
if utils.HasAPIResource(mgr, resource) {
if err := setup(mgr); err != nil {
@@ -518,6 +519,18 @@ func IngressClassV1beta1IndexFunc(rawObj client.Object) []string {
return []string{controllerName}
}
+func setupL4RoutePolicyIndexer(mgr ctrl.Manager) error {
+ if err := mgr.GetFieldIndexer().IndexField(
+ context.Background(),
+ &v1alpha1.L4RoutePolicy{},
+ PolicyTargetRefs,
+ L4RoutePolicyIndexFunc,
+ ); err != nil {
+ return err
+ }
+ return nil
+}
+
func IngressClassIndexFunc(rawObj client.Object) []string {
ingressClass := rawObj.(*networkingv1.IngressClass)
if ingressClass.Spec.Controller == "" {
@@ -884,6 +897,20 @@ func BackendTrafficPolicyIndexFunc(rawObj client.Object) []string {
return keys
}
+func L4RoutePolicyIndexFunc(rawObj client.Object) []string {
+ lrp := rawObj.(*v1alpha1.L4RoutePolicy)
+ keys := make([]string, 0, len(lrp.Spec.TargetRefs))
+ m := make(map[string]struct{})
+ for _, ref := range lrp.Spec.TargetRefs {
+ key := GenIndexKeyWithGK(string(ref.Group), string(ref.Kind), lrp.GetNamespace(), string(ref.Name))
+ if _, ok := m[key]; !ok {
+ m[key] = struct{}{}
+ keys = append(keys, key)
+ }
+ }
+ return keys
+}
+
func IngressClassParametersRefIndexFunc(rawObj client.Object) []string {
ingressClass := rawObj.(*networkingv1.IngressClass)
// check if the IngressClass references this gateway proxy
diff --git a/internal/controller/policies.go b/internal/controller/policies.go
index f117cdea..34cb1dac 100644
--- a/internal/controller/policies.go
+++ b/internal/controller/policies.go
@@ -18,7 +18,9 @@
package controller
import (
+ "context"
"fmt"
+ "sort"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/api/meta"
@@ -224,3 +226,105 @@ func parentRefValueEqual(a, b gatewayv1.ParentReference) bool {
ptr.Equal(a.Namespace, b.Namespace) &&
a.Name == b.Name
}
+
+// ProcessL4RoutePolicy finds L4RoutePolicy resources that target the given L4 route
+// (identified by namespace, name, and kind), resolves conflicts deterministically,
+// populates tctx.L4RoutePolicies with the winning policy, and queues status updates.
+func ProcessL4RoutePolicy(
+ c client.Client,
+ log logr.Logger,
+ tctx *provider.TranslateContext,
+ routeNamespace, routeName, routeKind string,
+) {
+ var list v1alpha1.L4RoutePolicyList
+ key := indexer.GenIndexKeyWithGK(gatewayv1alpha2.GroupName, routeKind, routeNamespace, routeName)
+ if err := c.List(context.Background(), &list, client.MatchingFields{indexer.PolicyTargetRefs: key}); err != nil {
+ log.Error(err, "failed to list L4RoutePolicy", "namespace", routeNamespace, "name", routeName, "kind", routeKind)
+ return
+ }
+ if len(list.Items) == 0 {
+ return
+ }
+
+ // Deterministic conflict resolution: oldest creationTimestamp wins; tie-break by namespace/name.
+ sort.Slice(list.Items, func(i, j int) bool {
+ ti := list.Items[i].CreationTimestamp.Time
+ tj := list.Items[j].CreationTimestamp.Time
+ if ti.Equal(tj) {
+ ki := list.Items[i].Namespace + "/" + list.Items[i].Name
+ kj := list.Items[j].Namespace + "/" + list.Items[j].Name
+ return ki < kj
+ }
+ return ti.Before(tj)
+ })
+
+ winner := list.Items[0].DeepCopy()
+ tctx.L4RoutePolicies[types.NamespacedName{Namespace: winner.Namespace, Name: winner.Name}] = winner
+
+ for i := range list.Items {
+ policy := list.Items[i]
+ var condition metav1.Condition
+ if i == 0 {
+ condition = metav1.Condition{
+ Type: string(gatewayv1alpha2.PolicyConditionAccepted),
+ Status: metav1.ConditionTrue,
+ ObservedGeneration: policy.GetGeneration(),
+ LastTransitionTime: metav1.Now(),
+ Reason: string(gatewayv1alpha2.PolicyReasonAccepted),
+ Message: "Policy has been accepted",
+ }
+ } else {
+ condition = metav1.Condition{
+ Type: string(gatewayv1alpha2.PolicyConditionAccepted),
+ Status: metav1.ConditionFalse,
+ ObservedGeneration: policy.GetGeneration(),
+ LastTransitionTime: metav1.Now(),
+ Reason: string(gatewayv1alpha2.PolicyReasonConflicted),
+ Message: fmt.Sprintf("Conflicts with L4RoutePolicy %s/%s which was created earlier", winner.Namespace, winner.Name),
+ }
+ }
+
+ if updated := SetAncestors(&policy.Status, tctx.RouteParentRefs, condition); updated {
+ policyCopy := policy.DeepCopy()
+ tctx.StatusUpdaters = append(tctx.StatusUpdaters, status.Update{
+ NamespacedName: utils.NamespacedName(policyCopy),
+ Resource: policyCopy,
+ Mutator: status.MutatorFunc(func(obj client.Object) client.Object {
+ cp := obj.(*v1alpha1.L4RoutePolicy).DeepCopy()
+ cp.Status = policyCopy.Status
+ return cp
+ }),
+ })
+ }
+ }
+}
+
+// updateL4RoutePolicyStatusOnDeleting clears ancestor status entries for L4RoutePolicy
+// resources that target the deleted route.
+func updateL4RoutePolicyStatusOnDeleting(ctx context.Context, c client.Client, updater status.Updater, log logr.Logger, nn types.NamespacedName, routeKind string) {
+ var list v1alpha1.L4RoutePolicyList
+ key := indexer.GenIndexKeyWithGK(gatewayv1alpha2.GroupName, routeKind, nn.Namespace, nn.Name)
+ if err := c.List(ctx, &list, client.MatchingFields{indexer.PolicyTargetRefs: key}); err != nil {
+ log.Error(err, "failed to list L4RoutePolicy on route deletion", "namespace", nn.Namespace, "name", nn.Name)
+ return
+ }
+ for _, policy := range list.Items {
+ updateL4RoutePolicyDeleteAncestors(updater, policy)
+ }
+}
+
+func updateL4RoutePolicyDeleteAncestors(updater status.Updater, policy v1alpha1.L4RoutePolicy) {
+ if len(policy.Status.Ancestors) == 0 {
+ return
+ }
+ policy.Status.Ancestors = nil
+ updater.Update(status.Update{
+ NamespacedName: utils.NamespacedName(&policy),
+ Resource: policy.DeepCopy(),
+ Mutator: status.MutatorFunc(func(obj client.Object) client.Object {
+ cp := obj.(*v1alpha1.L4RoutePolicy).DeepCopy()
+ cp.Status = policy.Status
+ return cp
+ }),
+ })
+}
diff --git a/internal/controller/tcproute_controller.go b/internal/controller/tcproute_controller.go
index 125a14a9..70bc16d9 100644
--- a/internal/controller/tcproute_controller.go
+++ b/internal/controller/tcproute_controller.go
@@ -93,6 +93,9 @@ func (r *TCPRouteReconciler) SetupWithManager(mgr ctrl.Manager) error {
).
Watches(&v1alpha1.GatewayProxy{},
handler.EnqueueRequestsFromMapFunc(r.listTCPRoutesForGatewayProxy),
+ ).
+ Watches(&v1alpha1.L4RoutePolicy{},
+ handler.EnqueueRequestsFromMapFunc(r.listTCPRoutesForL4RoutePolicy),
)
if GetEnableReferenceGrant() {
@@ -240,6 +243,7 @@ func (r *TCPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
r.Log.Error(err, "failed to delete tcproute", "tcproute", tr)
return ctrl.Result{}, err
}
+ updateL4RoutePolicyStatusOnDeleting(ctx, r.Client, r.Updater, r.Log, req.NamespacedName, KindTCPRoute)
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
@@ -293,6 +297,7 @@ func (r *TCPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
}
ProcessBackendTrafficPolicy(r.Client, r.Log, tctx)
+ ProcessL4RoutePolicy(r.Client, r.Log, tctx, tr.Namespace, tr.Name, KindTCPRoute)
tr.Status.Parents = make([]gatewayv1.RouteParentStatus, 0, len(gateways))
for _, gateway := range gateways {
parentStatus := gatewayv1.RouteParentStatus{}
@@ -503,3 +508,28 @@ func (r *TCPRouteReconciler) listTCPRoutesByServiceRef(ctx context.Context, obj
}
return requests
}
+
+func (r *TCPRouteReconciler) listTCPRoutesForL4RoutePolicy(ctx context.Context, obj client.Object) []reconcile.Request {
+ policy, ok := obj.(*v1alpha1.L4RoutePolicy)
+ if !ok {
+ r.Log.Error(fmt.Errorf("unexpected object type"), "failed to convert object to L4RoutePolicy")
+ return nil
+ }
+ requests := make([]reconcile.Request, 0, len(policy.Spec.TargetRefs))
+ seen := make(map[k8stypes.NamespacedName]struct{})
+ for _, ref := range policy.Spec.TargetRefs {
+ if string(ref.Kind) != KindTCPRoute {
+ continue
+ }
+ nn := k8stypes.NamespacedName{
+ Namespace: policy.Namespace,
+ Name: string(ref.Name),
+ }
+ if _, ok := seen[nn]; ok {
+ continue
+ }
+ seen[nn] = struct{}{}
+ requests = append(requests, reconcile.Request{NamespacedName: nn})
+ }
+ return requests
+}
diff --git a/internal/controller/tlsroute_controller.go b/internal/controller/tlsroute_controller.go
index f5f97721..779f70c2 100644
--- a/internal/controller/tlsroute_controller.go
+++ b/internal/controller/tlsroute_controller.go
@@ -93,6 +93,9 @@ func (r *TLSRouteReconciler) SetupWithManager(mgr ctrl.Manager) error {
).
Watches(&v1alpha1.GatewayProxy{},
handler.EnqueueRequestsFromMapFunc(r.listTLSRoutesForGatewayProxy),
+ ).
+ Watches(&v1alpha1.L4RoutePolicy{},
+ handler.EnqueueRequestsFromMapFunc(r.listTLSRoutesForL4RoutePolicy),
)
if GetEnableReferenceGrant() {
@@ -240,6 +243,7 @@ func (r *TLSRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
r.Log.Error(err, "failed to delete tlsroute", "tlsroute", tr)
return ctrl.Result{}, err
}
+ updateL4RoutePolicyStatusOnDeleting(ctx, r.Client, r.Updater, r.Log, req.NamespacedName, types.KindTLSRoute)
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
@@ -293,6 +297,7 @@ func (r *TLSRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
}
ProcessBackendTrafficPolicy(r.Client, r.Log, tctx)
+ ProcessL4RoutePolicy(r.Client, r.Log, tctx, tr.Namespace, tr.Name, types.KindTLSRoute)
tr.Status.Parents = make([]gatewayv1.RouteParentStatus, 0, len(gateways))
for _, gateway := range gateways {
parentStatus := gatewayv1.RouteParentStatus{}
@@ -503,3 +508,28 @@ func (r *TLSRouteReconciler) listTLSRoutesByServiceRef(ctx context.Context, obj
}
return requests
}
+
+func (r *TLSRouteReconciler) listTLSRoutesForL4RoutePolicy(ctx context.Context, obj client.Object) []reconcile.Request {
+ policy, ok := obj.(*v1alpha1.L4RoutePolicy)
+ if !ok {
+ r.Log.Error(fmt.Errorf("unexpected object type"), "failed to convert object to L4RoutePolicy")
+ return nil
+ }
+ requests := make([]reconcile.Request, 0, len(policy.Spec.TargetRefs))
+ seen := make(map[k8stypes.NamespacedName]struct{})
+ for _, ref := range policy.Spec.TargetRefs {
+ if string(ref.Kind) != types.KindTLSRoute {
+ continue
+ }
+ nn := k8stypes.NamespacedName{
+ Namespace: policy.Namespace,
+ Name: string(ref.Name),
+ }
+ if _, ok := seen[nn]; ok {
+ continue
+ }
+ seen[nn] = struct{}{}
+ requests = append(requests, reconcile.Request{NamespacedName: nn})
+ }
+ return requests
+}
diff --git a/internal/controller/udproute_controller.go b/internal/controller/udproute_controller.go
index 2a4a7a4a..a9cf1424 100644
--- a/internal/controller/udproute_controller.go
+++ b/internal/controller/udproute_controller.go
@@ -93,6 +93,9 @@ func (r *UDPRouteReconciler) SetupWithManager(mgr ctrl.Manager) error {
).
Watches(&v1alpha1.GatewayProxy{},
handler.EnqueueRequestsFromMapFunc(r.listUDPRoutesForGatewayProxy),
+ ).
+ Watches(&v1alpha1.L4RoutePolicy{},
+ handler.EnqueueRequestsFromMapFunc(r.listUDPRoutesForL4RoutePolicy),
)
if GetEnableReferenceGrant() {
@@ -240,6 +243,7 @@ func (r *UDPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
r.Log.Error(err, "failed to delete udproute", "udproute", tr)
return ctrl.Result{}, err
}
+ updateL4RoutePolicyStatusOnDeleting(ctx, r.Client, r.Updater, r.Log, req.NamespacedName, KindUDPRoute)
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
@@ -293,6 +297,7 @@ func (r *UDPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
}
ProcessBackendTrafficPolicy(r.Client, r.Log, tctx)
+ ProcessL4RoutePolicy(r.Client, r.Log, tctx, tr.Namespace, tr.Name, KindUDPRoute)
tr.Status.Parents = make([]gatewayv1.RouteParentStatus, 0, len(gateways))
for _, gateway := range gateways {
parentStatus := gatewayv1.RouteParentStatus{}
@@ -503,3 +508,28 @@ func (r *UDPRouteReconciler) listUDPRoutesByServiceRef(ctx context.Context, obj
}
return requests
}
+
+func (r *UDPRouteReconciler) listUDPRoutesForL4RoutePolicy(ctx context.Context, obj client.Object) []reconcile.Request {
+ policy, ok := obj.(*v1alpha1.L4RoutePolicy)
+ if !ok {
+ r.Log.Error(fmt.Errorf("unexpected object type"), "failed to convert object to L4RoutePolicy")
+ return nil
+ }
+ requests := make([]reconcile.Request, 0, len(policy.Spec.TargetRefs))
+ seen := make(map[k8stypes.NamespacedName]struct{})
+ for _, ref := range policy.Spec.TargetRefs {
+ if string(ref.Kind) != KindUDPRoute {
+ continue
+ }
+ nn := k8stypes.NamespacedName{
+ Namespace: policy.Namespace,
+ Name: string(ref.Name),
+ }
+ if _, ok := seen[nn]; ok {
+ continue
+ }
+ seen[nn] = struct{}{}
+ requests = append(requests, reconcile.Request{NamespacedName: nn})
+ }
+ return requests
+}
diff --git a/internal/manager/controllers.go b/internal/manager/controllers.go
index c4db6f77..f8e70890 100644
--- a/internal/manager/controllers.go
+++ b/internal/manager/controllers.go
@@ -79,6 +79,8 @@ import (
// +kubebuilder:rbac:groups=apisix.apache.org,resources=backendtrafficpolicies/status,verbs=get;update
// +kubebuilder:rbac:groups=apisix.apache.org,resources=httproutepolicies,verbs=get;list;watch
// +kubebuilder:rbac:groups=apisix.apache.org,resources=httproutepolicies/status,verbs=get;update
+// +kubebuilder:rbac:groups=apisix.apache.org,resources=l4routepolicies,verbs=get;list;watch
+// +kubebuilder:rbac:groups=apisix.apache.org,resources=l4routepolicies/status,verbs=get;update
// GatewayAPI
// +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gatewayclasses,verbs=get;list;watch;update
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index 92ada510..92f36a87 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -54,6 +54,7 @@ type TranslateContext struct {
ApisixPluginConfigs map[k8stypes.NamespacedName]*apiv2.ApisixPluginConfig
Services map[k8stypes.NamespacedName]*corev1.Service
BackendTrafficPolicies map[k8stypes.NamespacedName]*v1alpha1.BackendTrafficPolicy
+ L4RoutePolicies map[k8stypes.NamespacedName]*v1alpha1.L4RoutePolicy
Upstreams map[k8stypes.NamespacedName]*apiv2.ApisixUpstream
GatewayProxies map[types.NamespacedNameKind]v1alpha1.GatewayProxy
ResourceParentRefs map[types.NamespacedNameKind][]types.NamespacedNameKind
@@ -73,6 +74,7 @@ func NewDefaultTranslateContext(ctx context.Context) *TranslateContext {
ApisixPluginConfigs: make(map[k8stypes.NamespacedName]*apiv2.ApisixPluginConfig),
Services: make(map[k8stypes.NamespacedName]*corev1.Service),
BackendTrafficPolicies: make(map[k8stypes.NamespacedName]*v1alpha1.BackendTrafficPolicy),
+ L4RoutePolicies: make(map[k8stypes.NamespacedName]*v1alpha1.L4RoutePolicy),
Upstreams: make(map[k8stypes.NamespacedName]*apiv2.ApisixUpstream),
GatewayProxies: make(map[types.NamespacedNameKind]v1alpha1.GatewayProxy),
ResourceParentRefs: make(map[types.NamespacedNameKind][]types.NamespacedNameKind),
diff --git a/test/e2e/framework/assertion.go b/test/e2e/framework/assertion.go
index fe069b4e..7fcb5f50 100644
--- a/test/e2e/framework/assertion.go
+++ b/test/e2e/framework/assertion.go
@@ -102,6 +102,38 @@ func PollUntilHTTPRoutePolicyHaveStatus(cli client.Client, timeout time.Duration
return genericPollResource(new(v1alpha1.HTTPRoutePolicy), cli, timeout, hrpNN, f)
}
+func L4RoutePolicyMustHaveCondition(t testing.TestingT, client client.Client, timeout time.Duration, refNN, policyNN types.NamespacedName,
+ condition metav1.Condition) {
+ err := PollUntilL4RoutePolicyHaveStatus(client, timeout, policyNN, func(policy *v1alpha1.L4RoutePolicy) bool {
+ for _, ancestor := range policy.Status.Ancestors {
+ if err := kubernetes.ConditionsHaveLatestObservedGeneration(policy, ancestor.Conditions); err != nil {
+ log.Printf("L4RoutePolicy %s (ancestorRef=%v) %v", policyNN, parentRefToString(ancestor.AncestorRef), err)
+ return false
+ }
+
+ if ancestor.AncestorRef.Name == gatewayv1.ObjectName(refNN.Name) &&
+ (refNN.Namespace == "" || (ancestor.AncestorRef.Namespace != nil && string(*ancestor.AncestorRef.Namespace) == refNN.Namespace)) {
+ if findConditionInList(ancestor.Conditions, condition) {
+ log.Printf("found condition %v in list %v for %s reference %s", condition, ancestor.Conditions, policyNN, refNN)
+ return true
+ }
+ log.Printf("NOT FOUND condition %v in %v for %s reference %s", condition, ancestor.Conditions, policyNN, refNN)
+ }
+ }
+ return false
+ })
+
+ require.NoError(t, err, "error waiting for L4RoutePolicy %s status to have a Condition matching %+v", policyNN, condition)
+}
+
+func PollUntilL4RoutePolicyHaveStatus(cli client.Client, timeout time.Duration, policyNN types.NamespacedName,
+ f func(policy *v1alpha1.L4RoutePolicy) bool) error {
+ if err := v1alpha1.AddToScheme(cli.Scheme()); err != nil {
+ return err
+ }
+ return genericPollResource(new(v1alpha1.L4RoutePolicy), cli, timeout, policyNN, f)
+}
+
func APIv2MustHaveCondition(t testing.TestingT, cli client.Client, timeout time.Duration, nn types.NamespacedName, obj client.Object, cond metav1.Condition) {
f := func(object client.Object) bool {
value := reflect.Indirect(reflect.ValueOf(object))
diff --git a/test/e2e/gatewayapi/tcproute.go b/test/e2e/gatewayapi/tcproute.go
index 7ed01449..c8ac7ba7 100644
--- a/test/e2e/gatewayapi/tcproute.go
+++ b/test/e2e/gatewayapi/tcproute.go
@@ -23,6 +23,9 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+ gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
)
@@ -105,4 +108,95 @@ spec:
s.HTTPOverTCPConnectAssert(false, time.Minute*3)
})
})
+
+ Context("TCPRoute With L4RoutePolicy", func() {
+ var tcpGateway = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+ name: %s
+spec:
+ gatewayClassName: %s
+ listeners:
+ - name: tcp
+ protocol: TCP
+ port: 80
+ allowedRoutes:
+ kinds:
+ - kind: TCPRoute
+ infrastructure:
+ parametersRef:
+ group: apisix.apache.org
+ kind: GatewayProxy
+ name: apisix-proxy-config
+`
+
+ var tcpRoute = `
+apiVersion: gateway.networking.k8s.io/v1alpha2
+kind: TCPRoute
+metadata:
+ name: tcp-l4policy
+spec:
+ parentRefs:
+ - name: %s
+ sectionName: tcp
+ rules:
+ - backendRefs:
+ - name: httpbin-service-e2e-test
+ port: 80
+`
+
+ // ip-restriction with blacklist covering all IPv4 addresses blocks all TCP connections.
+ var l4RoutePolicyBlockAll = `
+apiVersion: apisix.apache.org/v1alpha1
+kind: L4RoutePolicy
+metadata:
+ name: tcp-block-all
+spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: TCPRoute
+ name: tcp-l4policy
+ plugins:
+ - name: ip-restriction
+ config:
+ blacklist:
+ - "0.0.0.0/0"
+`
+
+ BeforeEach(func() {
+ Expect(s.CreateResourceFromString(s.GetGatewayProxySpec())).NotTo(HaveOccurred(), "creating GatewayProxy")
+ Expect(s.CreateResourceFromString(s.GetGatewayClassYaml())).NotTo(HaveOccurred(), "creating GatewayClass")
+ Expect(s.CreateResourceFromString(fmt.Sprintf(tcpGateway, s.Namespace(), s.Namespace()))).
+ NotTo(HaveOccurred(), "creating Gateway")
+ })
+
+ It("L4RoutePolicy blocks traffic via ip-restriction plugin", func() {
+ By("creating TCPRoute")
+ s.ResourceApplied("TCPRoute", "tcp-l4policy", fmt.Sprintf(tcpRoute, s.Namespace()), 1)
+
+ By("verifying TCP traffic works before applying L4RoutePolicy")
+ s.HTTPOverTCPConnectAssert(true, time.Minute*3)
+
+ By("applying L4RoutePolicy with ip-restriction blacklist")
+ s.ApplyL4RoutePolicy(
+ types.NamespacedName{Name: s.Namespace()},
+ types.NamespacedName{Namespace: s.Namespace(), Name: "tcp-block-all"},
+ l4RoutePolicyBlockAll,
+ metav1.Condition{
+ Type: string(gatewayv1alpha2.PolicyConditionAccepted),
+ Status: metav1.ConditionTrue,
+ },
+ )
+
+ By("verifying TCP traffic is blocked by the L4RoutePolicy")
+ s.HTTPOverTCPConnectAssert(false, time.Minute*3)
+
+ By("deleting L4RoutePolicy")
+ Expect(s.DeleteResource("L4RoutePolicy", "tcp-block-all")).NotTo(HaveOccurred(), "deleting L4RoutePolicy")
+
+ By("verifying TCP traffic recovers after L4RoutePolicy deletion")
+ s.HTTPOverTCPConnectAssert(true, time.Minute*3)
+ })
+ })
})
diff --git a/test/e2e/scaffold/k8s.go b/test/e2e/scaffold/k8s.go
index 2694832a..bb5be9fd 100644
--- a/test/e2e/scaffold/k8s.go
+++ b/test/e2e/scaffold/k8s.go
@@ -292,6 +292,22 @@ func (s *Scaffold) ApplyHTTPRoutePolicy(refNN, hrpNN types.NamespacedName, spec
}
}
+func (s *Scaffold) ApplyL4RoutePolicy(refNN, policyNN types.NamespacedName, spec string, conditions ...metav1.Condition) {
+ err := s.CreateResourceFromString(spec)
+ Expect(err).NotTo(HaveOccurred(), "creating L4RoutePolicy %s", policyNN)
+ if len(conditions) == 0 {
+ conditions = []metav1.Condition{
+ {
+ Type: string(v1alpha2.PolicyConditionAccepted),
+ Status: metav1.ConditionTrue,
+ },
+ }
+ }
+ for _, condition := range conditions {
+ framework.L4RoutePolicyMustHaveCondition(s.GinkgoT, s.K8sClient, 8*time.Second, refNN, policyNN, condition)
+ }
+}
+
func (s *Scaffold) GetGatewayProxySpec() string {
var gatewayProxyYaml = `
apiVersion: apisix.apache.org/v1alpha1