This is an automated email from the ASF dual-hosted git repository.

alinsran pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


The following commit(s) were added to refs/heads/master by this push:
     new 1afb9ace feat(gateway-api): support TLSRoute (#2594)
1afb9ace is described below

commit 1afb9acea8f55270055e7274e18647731d6150f1
Author: AlinsRan <[email protected]>
AuthorDate: Mon Oct 13 00:37:41 2025 +0800

    feat(gateway-api): support TLSRoute (#2594)
---
 Makefile                                           |   2 +-
 api/v2/shared_types.go                             |   2 +
 config/rbac/role.yaml                              |   2 +
 docs/en/latest/concepts/gateway-api.md             |   2 +-
 internal/adc/translator/tlsroute.go                | 159 +++++++
 internal/controller/indexer/indexer.go             |   1 +
 internal/controller/indexer/tlsroute.go            |  79 ++++
 internal/controller/tlsroute_controller.go         | 505 +++++++++++++++++++++
 internal/controller/utils.go                       |   6 +
 internal/manager/controllers.go                    |  11 +
 internal/provider/apisix/provider.go               |   5 +-
 internal/types/k8s.go                              |   7 +-
 test/conformance/conformance_test.go               |   3 +
 .../e2e/framework/manifests/apisix-standalone.yaml |   9 +
 test/e2e/framework/manifests/apisix.yaml           |   9 +
 test/e2e/framework/manifests/ingress.yaml          |  23 +-
 test/e2e/gatewayapi/tlsroute.go                    | 117 +++++
 test/e2e/scaffold/scaffold.go                      |  39 ++
 18 files changed, 958 insertions(+), 23 deletions(-)

diff --git a/Makefile b/Makefile
index adffa3b1..322af687 100644
--- a/Makefile
+++ b/Makefile
@@ -55,7 +55,7 @@ GATEAY_API_VERSION ?= v1.3.0
 SUPPORTED_EXTENDED_FEATURES = 
"HTTPRouteDestinationPortMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteRequestMirror,HTTPRouteSchemeRedirect,GatewayAddressEmpty,HTTPRouteResponseHeaderModification,GatewayPort8080,HTTPRouteHostRewrite,HTTPRouteQueryParamMatching"
 CONFORMANCE_TEST_REPORT_OUTPUT ?= 
$(DIR)/apisix-ingress-controller-conformance-report.yaml
 ## 
https://github.com/kubernetes-sigs/gateway-api/blob/v1.3.0/conformance/utils/suite/profiles.go
-CONFORMANCE_PROFILES ?= GATEWAY-HTTP,GATEWAY-GRPC
+CONFORMANCE_PROFILES ?= GATEWAY-HTTP,GATEWAY-GRPC,GATEWAY-TLS
 
 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is 
set)
 ifeq (,$(shell go env GOBIN))
diff --git a/api/v2/shared_types.go b/api/v2/shared_types.go
index 06dae1d6..6c2c2934 100644
--- a/api/v2/shared_types.go
+++ b/api/v2/shared_types.go
@@ -101,6 +101,8 @@ const (
        SchemeTCP = "tcp"
        // SchemeUDP represents the UDP protocol.
        SchemeUDP = "udp"
+       // SchemeTLS represents the TLS protocol.
+       SchemeTLS = "tls"
 )
 
 const (
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 856bf7db..bfda220b 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -93,6 +93,7 @@ rules:
   - httproutes/status
   - referencegrants/status
   - tcproutes/status
+  - tlsroutes/status
   - udproutes/status
   verbs:
   - get
@@ -105,6 +106,7 @@ rules:
   - httproutes
   - referencegrants
   - tcproutes
+  - tlsroutes
   - udproutes
   verbs:
   - get
diff --git a/docs/en/latest/concepts/gateway-api.md 
b/docs/en/latest/concepts/gateway-api.md
index 4621dfe9..8a5e5864 100644
--- a/docs/en/latest/concepts/gateway-api.md
+++ b/docs/en/latest/concepts/gateway-api.md
@@ -50,7 +50,7 @@ By supporting Gateway API, the APISIX Ingress controller can 
realize richer func
 | HTTPRoute        | Supported           | Partially supported    | Not 
supported                         | v1          |
 | GRPCRoute        | Supported           | Supported              | Not 
supported                         | v1          |
 | ReferenceGrant   | Supported           | Not supported          | Not 
supported                         | v1beta1     |
-| TLSRoute         | Not supported       | Not supported          | Not 
supported                         | v1alpha2    |
+| TLSRoute         | Supported           | Supported              | Not 
supported                         | v1alpha2    |
 | TCPRoute         | Supported           | Supported              | Not 
supported                         | v1alpha2    |
 | UDPRoute         | Supported           | Supported              | Not 
supported                         | v1alpha2    |
 | BackendTLSPolicy | Not supported       | Not supported          | Not 
supported                         | v1alpha3    |
diff --git a/internal/adc/translator/tlsroute.go 
b/internal/adc/translator/tlsroute.go
new file mode 100644
index 00000000..85bfba0c
--- /dev/null
+++ b/internal/adc/translator/tlsroute.go
@@ -0,0 +1,159 @@
+// 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 (
+       "fmt"
+
+       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"
+       apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       "github.com/apache/apisix-ingress-controller/internal/controller/label"
+       "github.com/apache/apisix-ingress-controller/internal/id"
+       "github.com/apache/apisix-ingress-controller/internal/provider"
+       "github.com/apache/apisix-ingress-controller/internal/types"
+)
+
+func (t *Translator) TranslateTLSRoute(tctx *provider.TranslateContext, 
tlsRoute *gatewayv1alpha2.TLSRoute) (*TranslateResult, error) {
+       result := &TranslateResult{}
+       rules := tlsRoute.Spec.Rules
+       labels := label.GenLabel(tlsRoute)
+       hosts := make([]string, 0, len(tlsRoute.Spec.Hostnames))
+       for _, hostname := range tlsRoute.Spec.Hostnames {
+               hosts = append(hosts, string(hostname))
+       }
+       for ruleIndex, rule := range rules {
+               service := adctypes.NewDefaultService()
+               service.Labels = labels
+               service.Name = 
adctypes.ComposeServiceNameWithStream(tlsRoute.Namespace, tlsRoute.Name, 
fmt.Sprintf("%d", ruleIndex), "TLS")
+               service.ID = id.GenID(service.Name)
+               var (
+                       upstreams         = make([]*adctypes.Upstream, 0)
+                       weightedUpstreams = 
make([]adctypes.TrafficSplitConfigRuleWeightedUpstream, 0)
+               )
+               for _, backend := range rule.BackendRefs {
+                       if backend.Namespace == nil {
+                               namespace := 
gatewayv1.Namespace(tlsRoute.Namespace)
+                               backend.Namespace = &namespace
+                       }
+                       upstream := newDefaultUpstreamWithoutScheme()
+                       upNodes, err := t.translateBackendRef(tctx, backend, 
DefaultEndpointFilter)
+                       if err != nil {
+                               continue
+                       }
+                       if len(upNodes) == 0 {
+                               continue
+                       }
+                       // TODO: Confirm BackendTrafficPolicy attachment with 
e2e test case.
+                       t.AttachBackendTrafficPolicyToUpstream(backend, 
tctx.BackendTrafficPolicies, upstream)
+                       upstream.Nodes = upNodes
+                       var (
+                               kind string
+                               port int32
+                       )
+                       if backend.Kind == nil {
+                               kind = types.KindService
+                       } else {
+                               kind = string(*backend.Kind)
+                       }
+                       if backend.Port != nil {
+                               port = int32(*backend.Port)
+                       }
+                       namespace := string(*backend.Namespace)
+                       name := string(backend.Name)
+                       upstreamName := 
adctypes.ComposeUpstreamNameForBackendRef(kind, namespace, name, port)
+                       upstream.Name = upstreamName
+                       upstream.ID = id.GenID(upstreamName)
+                       upstreams = append(upstreams, upstream)
+               }
+
+               // Handle multiple backends with traffic-split plugin
+               if len(upstreams) == 0 {
+                       // Create a default upstream if no valid backends
+                       upstream := adctypes.NewDefaultUpstream()
+                       service.Upstream = upstream
+               } else if len(upstreams) == 1 {
+                       // Single backend - use directly as service upstream
+                       service.Upstream = upstreams[0]
+                       // remove the id and name of the service.upstream, adc 
schema does not need id and name for it
+                       service.Upstream.ID = ""
+                       service.Upstream.Name = ""
+               } else {
+                       // Multiple backends - use traffic-split plugin
+                       service.Upstream = upstreams[0]
+                       // remove the id and name of the service.upstream, adc 
schema does not need id and name for it
+                       service.Upstream.ID = ""
+                       service.Upstream.Name = ""
+
+                       upstreams = upstreams[1:]
+
+                       if len(upstreams) > 0 {
+                               service.Upstreams = upstreams
+                       }
+
+                       // Set weight in traffic-split for the default upstream
+                       weight := apiv2.DefaultWeight
+                       if rule.BackendRefs[0].Weight != nil {
+                               weight = int(*rule.BackendRefs[0].Weight)
+                       }
+                       weightedUpstreams = append(weightedUpstreams, 
adctypes.TrafficSplitConfigRuleWeightedUpstream{
+                               Weight: weight,
+                       })
+
+                       // Set other upstreams in traffic-split using 
upstream_id
+                       for i, upstream := range upstreams {
+                               weight := apiv2.DefaultWeight
+                               // get weight from the backend refs starting 
from the second backend
+                               if i+1 < len(rule.BackendRefs) && 
rule.BackendRefs[i+1].Weight != nil {
+                                       weight = 
int(*rule.BackendRefs[i+1].Weight)
+                               }
+                               weightedUpstreams = append(weightedUpstreams, 
adctypes.TrafficSplitConfigRuleWeightedUpstream{
+                                       UpstreamID: upstream.ID,
+                                       Weight:     weight,
+                               })
+                       }
+
+                       if len(weightedUpstreams) > 0 {
+                               if service.Plugins == nil {
+                                       service.Plugins = make(map[string]any)
+                               }
+                               service.Plugins["traffic-split"] = 
&adctypes.TrafficSplitConfig{
+                                       Rules: 
[]adctypes.TrafficSplitConfigRule{
+                                               {
+                                                       WeightedUpstreams: 
weightedUpstreams,
+                                               },
+                                       },
+                               }
+                       }
+               }
+
+               for _, host := range hosts {
+                       streamRoute := adctypes.NewDefaultStreamRoute()
+                       streamRouteName := 
adctypes.ComposeStreamRouteName(tlsRoute.Namespace, tlsRoute.Name, 
fmt.Sprintf("%d", ruleIndex), "TLS")
+                       streamRoute.Name = streamRouteName
+                       streamRoute.ID = id.GenID(streamRouteName)
+                       streamRoute.SNI = host
+                       streamRoute.Labels = labels
+                       service.StreamRoutes = append(service.StreamRoutes, 
streamRoute)
+               }
+               result.Services = append(result.Services, service)
+       }
+       return result, nil
+}
diff --git a/internal/controller/indexer/indexer.go 
b/internal/controller/indexer/indexer.go
index ef3da206..8982630e 100644
--- a/internal/controller/indexer/indexer.go
+++ b/internal/controller/indexer/indexer.go
@@ -59,6 +59,7 @@ func SetupIndexer(mgr ctrl.Manager) error {
                setupTCPRouteIndexer,
                setupUDPRouteIndexer,
                setupGRPCRouteIndexer,
+               setupTLSRouteIndexer,
                setupIngressIndexer,
                setupConsumerIndexer,
                setupBackendTrafficPolicyIndexer,
diff --git a/internal/controller/indexer/tlsroute.go 
b/internal/controller/indexer/tlsroute.go
new file mode 100644
index 00000000..567131c4
--- /dev/null
+++ b/internal/controller/indexer/tlsroute.go
@@ -0,0 +1,79 @@
+// 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 indexer
+
+import (
+       "context"
+
+       internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
+       ctrl "sigs.k8s.io/controller-runtime"
+       "sigs.k8s.io/controller-runtime/pkg/client"
+       gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+)
+
+func setupTLSRouteIndexer(mgr ctrl.Manager) error {
+       if err := mgr.GetFieldIndexer().IndexField(
+               context.Background(),
+               &gatewayv1alpha2.TLSRoute{},
+               ParentRefs,
+               TLSRouteParentRefsIndexFunc,
+       ); err != nil {
+               return err
+       }
+
+       if err := mgr.GetFieldIndexer().IndexField(
+               context.Background(),
+               &gatewayv1alpha2.TLSRoute{},
+               ServiceIndexRef,
+               TLSPRouteServiceIndexFunc,
+       ); err != nil {
+               return err
+       }
+       return nil
+}
+
+func TLSRouteParentRefsIndexFunc(rawObj client.Object) []string {
+       tr := rawObj.(*gatewayv1alpha2.TLSRoute)
+       keys := make([]string, 0, len(tr.Spec.ParentRefs))
+       for _, ref := range tr.Spec.ParentRefs {
+               ns := tr.GetNamespace()
+               if ref.Namespace != nil {
+                       ns = string(*ref.Namespace)
+               }
+               keys = append(keys, GenIndexKey(ns, string(ref.Name)))
+       }
+       return keys
+}
+
+func TLSPRouteServiceIndexFunc(rawObj client.Object) []string {
+       tr := rawObj.(*gatewayv1alpha2.TLSRoute)
+       keys := make([]string, 0, len(tr.Spec.Rules))
+       for _, rule := range tr.Spec.Rules {
+               for _, backend := range rule.BackendRefs {
+                       namespace := tr.GetNamespace()
+                       if backend.Kind != nil && *backend.Kind != 
internaltypes.KindService {
+                               continue
+                       }
+                       if backend.Namespace != nil {
+                               namespace = string(*backend.Namespace)
+                       }
+                       keys = append(keys, GenIndexKey(namespace, 
string(backend.Name)))
+               }
+       }
+       return keys
+}
diff --git a/internal/controller/tlsroute_controller.go 
b/internal/controller/tlsroute_controller.go
new file mode 100644
index 00000000..f5f97721
--- /dev/null
+++ b/internal/controller/tlsroute_controller.go
@@ -0,0 +1,505 @@
+// 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 controller
+
+import (
+       "cmp"
+       "context"
+       "fmt"
+
+       "github.com/go-logr/logr"
+       corev1 "k8s.io/api/core/v1"
+       discoveryv1 "k8s.io/api/discovery/v1"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/apimachinery/pkg/runtime"
+       k8stypes "k8s.io/apimachinery/pkg/types"
+       ctrl "sigs.k8s.io/controller-runtime"
+       "sigs.k8s.io/controller-runtime/pkg/builder"
+       "sigs.k8s.io/controller-runtime/pkg/client"
+       "sigs.k8s.io/controller-runtime/pkg/event"
+       "sigs.k8s.io/controller-runtime/pkg/handler"
+       "sigs.k8s.io/controller-runtime/pkg/predicate"
+       "sigs.k8s.io/controller-runtime/pkg/reconcile"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+       gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+       "sigs.k8s.io/gateway-api/apis/v1beta1"
+
+       "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+       
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
+       "github.com/apache/apisix-ingress-controller/internal/controller/status"
+       "github.com/apache/apisix-ingress-controller/internal/manager/readiness"
+       "github.com/apache/apisix-ingress-controller/internal/provider"
+       "github.com/apache/apisix-ingress-controller/internal/types"
+       "github.com/apache/apisix-ingress-controller/internal/utils"
+)
+
+// TLSRouteReconciler reconciles a TLSRoute object.
+type TLSRouteReconciler struct { //nolint:revive
+       client.Client
+       Scheme *runtime.Scheme
+
+       Log logr.Logger
+
+       Provider provider.Provider
+
+       Updater status.Updater
+       Readier readiness.ReadinessManager
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *TLSRouteReconciler) SetupWithManager(mgr ctrl.Manager) error {
+
+       bdr := ctrl.NewControllerManagedBy(mgr).
+               For(&gatewayv1alpha2.TLSRoute{}).
+               WithEventFilter(predicate.GenerationChangedPredicate{}).
+               Watches(&discoveryv1.EndpointSlice{},
+                       
handler.EnqueueRequestsFromMapFunc(r.listTLSRoutesByServiceRef),
+               ).
+               Watches(&gatewayv1.Gateway{},
+                       
handler.EnqueueRequestsFromMapFunc(r.listTLSRoutesForGateway),
+                       builder.WithPredicates(
+                               predicate.Funcs{
+                                       GenericFunc: func(e event.GenericEvent) 
bool {
+                                               return false
+                                       },
+                                       DeleteFunc: func(e event.DeleteEvent) 
bool {
+                                               return false
+                                       },
+                                       CreateFunc: func(e event.CreateEvent) 
bool {
+                                               return true
+                                       },
+                                       UpdateFunc: func(e event.UpdateEvent) 
bool {
+                                               return true
+                                       },
+                               },
+                       ),
+               ).
+               Watches(&v1alpha1.BackendTrafficPolicy{},
+                       
handler.EnqueueRequestsFromMapFunc(r.listTLSRoutesForBackendTrafficPolicy),
+               ).
+               Watches(&v1alpha1.GatewayProxy{},
+                       
handler.EnqueueRequestsFromMapFunc(r.listTLSRoutesForGatewayProxy),
+               )
+
+       if GetEnableReferenceGrant() {
+               bdr.Watches(&v1beta1.ReferenceGrant{},
+                       
handler.EnqueueRequestsFromMapFunc(r.listTLSRoutesForReferenceGrant),
+                       
builder.WithPredicates(referenceGrantPredicates(types.KindTLSRoute)),
+               )
+       }
+
+       return bdr.Complete(r)
+}
+
+func (r *TLSRouteReconciler) listTLSRoutesForBackendTrafficPolicy(ctx 
context.Context, obj client.Object) []reconcile.Request {
+       policy, ok := obj.(*v1alpha1.BackendTrafficPolicy)
+       if !ok {
+               r.Log.Error(fmt.Errorf("unexpected object type"), "failed to 
convert object to BackendTrafficPolicy")
+               return nil
+       }
+
+       tlsrouteList := []gatewayv1alpha2.TLSRoute{}
+       for _, targetRef := range policy.Spec.TargetRefs {
+               service := &corev1.Service{}
+               if err := r.Get(ctx, client.ObjectKey{
+                       Namespace: policy.Namespace,
+                       Name:      string(targetRef.Name),
+               }, service); err != nil {
+                       if client.IgnoreNotFound(err) != nil {
+                               r.Log.Error(err, "failed to get service", 
"namespace", policy.Namespace, "name", targetRef.Name)
+                       }
+                       continue
+               }
+               trList := &gatewayv1alpha2.TLSRouteList{}
+               if err := r.List(ctx, trList, client.MatchingFields{
+                       indexer.ServiceIndexRef: 
indexer.GenIndexKey(policy.Namespace, string(targetRef.Name)),
+               }); err != nil {
+                       r.Log.Error(err, "failed to list tlsroutes by service 
reference", "service", targetRef.Name)
+                       return nil
+               }
+               tlsrouteList = append(tlsrouteList, trList.Items...)
+       }
+       var namespacedNameMap = make(map[k8stypes.NamespacedName]struct{})
+       requests := make([]reconcile.Request, 0, len(tlsrouteList))
+       for _, tr := range tlsrouteList {
+               key := k8stypes.NamespacedName{
+                       Namespace: tr.Namespace,
+                       Name:      tr.Name,
+               }
+               if _, ok := namespacedNameMap[key]; !ok {
+                       namespacedNameMap[key] = struct{}{}
+                       requests = append(requests, reconcile.Request{
+                               NamespacedName: client.ObjectKey{
+                                       Namespace: tr.Namespace,
+                                       Name:      tr.Name,
+                               },
+                       })
+               }
+       }
+       return requests
+}
+
+func (r *TLSRouteReconciler) listTLSRoutesForGateway(ctx context.Context, obj 
client.Object) []reconcile.Request {
+       gateway, ok := obj.(*gatewayv1.Gateway)
+       if !ok {
+               r.Log.Error(fmt.Errorf("unexpected object type"), "failed to 
convert object to Gateway")
+       }
+       trList := &gatewayv1alpha2.TLSRouteList{}
+       if err := r.List(ctx, trList, client.MatchingFields{
+               indexer.ParentRefs: indexer.GenIndexKey(gateway.Namespace, 
gateway.Name),
+       }); err != nil {
+               r.Log.Error(err, "failed to list tlsroutes by gateway", 
"gateway", gateway.Name)
+               return nil
+       }
+
+       requests := make([]reconcile.Request, 0, len(trList.Items))
+       for _, tcr := range trList.Items {
+               requests = append(requests, reconcile.Request{
+                       NamespacedName: client.ObjectKey{
+                               Namespace: tcr.Namespace,
+                               Name:      tcr.Name,
+                       },
+               })
+       }
+       return requests
+}
+
+// listTLSRoutesForGatewayProxy list all TLSRoute resources that are affected 
by a given GatewayProxy
+func (r *TLSRouteReconciler) listTLSRoutesForGatewayProxy(ctx context.Context, 
obj client.Object) []reconcile.Request {
+       gatewayProxy, ok := obj.(*v1alpha1.GatewayProxy)
+       if !ok {
+               r.Log.Error(fmt.Errorf("unexpected object type"), "failed to 
convert object to GatewayProxy")
+               return nil
+       }
+
+       namespace := gatewayProxy.GetNamespace()
+       name := gatewayProxy.GetName()
+
+       // find all gateways that reference this gateway proxy
+       gatewayList := &gatewayv1.GatewayList{}
+       if err := r.List(ctx, gatewayList, client.MatchingFields{
+               indexer.ParametersRef: indexer.GenIndexKey(namespace, name),
+       }); err != nil {
+               r.Log.Error(err, "failed to list gateways for gateway proxy", 
"gatewayproxy", gatewayProxy.GetName())
+               return nil
+       }
+
+       var requests []reconcile.Request
+
+       // for each gateway, find all TLSRoute resources that reference it
+       for _, gateway := range gatewayList.Items {
+               tlsRouteList := &gatewayv1alpha2.TLSRouteList{}
+               if err := r.List(ctx, tlsRouteList, client.MatchingFields{
+                       indexer.ParentRefs: 
indexer.GenIndexKey(gateway.Namespace, gateway.Name),
+               }); err != nil {
+                       r.Log.Error(err, "failed to list tlsroutes for 
gateway", "gateway", gateway.Name)
+                       continue
+               }
+
+               for _, tlsRoute := range tlsRouteList.Items {
+                       requests = append(requests, reconcile.Request{
+                               NamespacedName: client.ObjectKey{
+                                       Namespace: tlsRoute.Namespace,
+                                       Name:      tlsRoute.Name,
+                               },
+                       })
+               }
+       }
+
+       return requests
+}
+
+func (r *TLSRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) 
(ctrl.Result, error) {
+       defer r.Readier.Done(&gatewayv1alpha2.TLSRoute{}, req.NamespacedName)
+       tr := new(gatewayv1alpha2.TLSRoute)
+       if err := r.Get(ctx, req.NamespacedName, tr); err != nil {
+               if client.IgnoreNotFound(err) == nil {
+                       tr.Namespace = req.Namespace
+                       tr.Name = req.Name
+
+                       tr.TypeMeta = metav1.TypeMeta{
+                               Kind:       types.KindTLSRoute,
+                               APIVersion: 
gatewayv1alpha2.GroupVersion.String(),
+                       }
+
+                       if err := r.Provider.Delete(ctx, tr); err != nil {
+                               r.Log.Error(err, "failed to delete tlsroute", 
"tlsroute", tr)
+                               return ctrl.Result{}, err
+                       }
+                       return ctrl.Result{}, nil
+               }
+               return ctrl.Result{}, err
+       }
+
+       type ResourceStatus struct {
+               status bool
+               msg    string
+       }
+
+       acceptStatus := ResourceStatus{
+               status: true,
+               msg:    "Route is accepted",
+       }
+
+       gateways, err := ParseRouteParentRefs(ctx, r.Client, r.Log, tr, 
tr.Spec.ParentRefs)
+       if err != nil {
+               return ctrl.Result{}, err
+       }
+
+       if len(gateways) == 0 {
+               return ctrl.Result{}, nil
+       }
+
+       tctx := provider.NewDefaultTranslateContext(ctx)
+
+       tctx.RouteParentRefs = tr.Spec.ParentRefs
+       rk := utils.NamespacedNameKind(tr)
+       for _, gateway := range gateways {
+               if err := ProcessGatewayProxy(r.Client, r.Log, tctx, 
gateway.Gateway, rk); err != nil {
+                       acceptStatus.status = false
+                       acceptStatus.msg = err.Error()
+               }
+       }
+
+       var backendRefErr error
+       if err := r.processTLSRoute(tctx, tr); err != nil {
+               // When encountering a backend reference error, it should not 
affect the acceptance status
+               if types.IsSomeReasonError(err, 
gatewayv1.RouteReasonInvalidKind) {
+                       backendRefErr = err
+               } else {
+                       acceptStatus.status = false
+                       acceptStatus.msg = err.Error()
+               }
+       }
+
+       // Store the backend reference error for later use.
+       // If the backend reference error is because of an invalid kind, use 
this error first
+       if err := r.processTLSRouteBackendRefs(tctx, req.NamespacedName); err 
!= nil && backendRefErr == nil {
+               backendRefErr = err
+       }
+
+       ProcessBackendTrafficPolicy(r.Client, r.Log, tctx)
+       tr.Status.Parents = make([]gatewayv1.RouteParentStatus, 0, 
len(gateways))
+       for _, gateway := range gateways {
+               parentStatus := gatewayv1.RouteParentStatus{}
+               SetRouteParentRef(&parentStatus, gateway.Gateway.Name, 
gateway.Gateway.Namespace)
+               for _, condition := range gateway.Conditions {
+                       parentStatus.Conditions = 
MergeCondition(parentStatus.Conditions, condition)
+               }
+               SetRouteConditionAccepted(&parentStatus, tr.GetGeneration(), 
acceptStatus.status, acceptStatus.msg)
+               SetRouteConditionResolvedRefs(&parentStatus, 
tr.GetGeneration(), backendRefErr)
+
+               tr.Status.Parents = append(tr.Status.Parents, parentStatus)
+       }
+
+       r.Updater.Update(status.Update{
+               NamespacedName: utils.NamespacedName(tr),
+               Resource:       &gatewayv1alpha2.TLSRoute{},
+               Mutator: status.MutatorFunc(func(obj client.Object) 
client.Object {
+                       t, ok := obj.(*gatewayv1alpha2.TLSRoute)
+                       if !ok {
+                               err := fmt.Errorf("unsupported object type %T", 
obj)
+                               panic(err)
+                       }
+                       tCopy := t.DeepCopy()
+                       tCopy.Status = tr.Status
+                       return tCopy
+               }),
+       })
+       UpdateStatus(r.Updater, r.Log, tctx)
+       if isRouteAccepted(gateways) {
+               routeToUpdate := tr
+               if err := r.Provider.Update(ctx, tctx, routeToUpdate); err != 
nil {
+                       return ctrl.Result{}, err
+               }
+       }
+       return ctrl.Result{}, nil
+}
+
+func (r *TLSRouteReconciler) processTLSRoute(tctx *provider.TranslateContext, 
tlsRoute *gatewayv1alpha2.TLSRoute) error {
+       var terror error
+       for _, rule := range tlsRoute.Spec.Rules {
+               for _, backend := range rule.BackendRefs {
+                       if backend.Kind != nil && *backend.Kind != KindService {
+                               terror = 
types.NewInvalidKindError(*backend.Kind)
+                               continue
+                       }
+                       tctx.BackendRefs = append(tctx.BackendRefs, 
gatewayv1.BackendRef{
+                               BackendObjectReference: 
gatewayv1.BackendObjectReference{
+                                       Name:      backend.Name,
+                                       Namespace: cmp.Or(backend.Namespace, 
(*gatewayv1.Namespace)(&tlsRoute.Namespace)),
+                                       Port:      backend.Port,
+                               },
+                       })
+               }
+       }
+
+       return terror
+}
+
+func (r *TLSRouteReconciler) processTLSRouteBackendRefs(tctx 
*provider.TranslateContext, trNN k8stypes.NamespacedName) error {
+       var terr error
+       for _, backend := range tctx.BackendRefs {
+               targetNN := k8stypes.NamespacedName{
+                       Namespace: trNN.Namespace,
+                       Name:      string(backend.Name),
+               }
+               if backend.Namespace != nil {
+                       targetNN.Namespace = string(*backend.Namespace)
+               }
+
+               if backend.Kind != nil && *backend.Kind != KindService {
+                       terr = types.NewInvalidKindError(*backend.Kind)
+                       continue
+               }
+
+               if backend.Port == nil {
+                       terr = fmt.Errorf("port is required")
+                       continue
+               }
+
+               var service corev1.Service
+               if err := r.Get(tctx, targetNN, &service); err != nil {
+                       terr = err
+                       if client.IgnoreNotFound(err) == nil {
+                               terr = types.ReasonError{
+                                       Reason:  
string(gatewayv1.RouteReasonBackendNotFound),
+                                       Message: fmt.Sprintf("Service %s not 
found", targetNN),
+                               }
+                       }
+                       continue
+               }
+
+               // if cross namespaces between TLSRoute and referenced Service, 
check ReferenceGrant
+               if trNN.Namespace != targetNN.Namespace {
+                       if permitted := checkReferenceGrant(tctx,
+                               r.Client,
+                               v1beta1.ReferenceGrantFrom{
+                                       Group:     gatewayv1.GroupName,
+                                       Kind:      types.KindTLSRoute,
+                                       Namespace: 
v1beta1.Namespace(trNN.Namespace),
+                               },
+                               gatewayv1.ObjectReference{
+                                       Group:     corev1.GroupName,
+                                       Kind:      types.KindService,
+                                       Name:      
gatewayv1.ObjectName(targetNN.Name),
+                                       Namespace: 
(*gatewayv1.Namespace)(&targetNN.Namespace),
+                               },
+                       ); !permitted {
+                               terr = types.ReasonError{
+                                       Reason:  
string(v1beta1.RouteReasonRefNotPermitted),
+                                       Message: fmt.Sprintf("%s is in a 
different namespace than the TLSRoute %s and no ReferenceGrant allowing 
reference is configured", targetNN, trNN),
+                               }
+                               continue
+                       }
+               }
+
+               if service.Spec.Type == corev1.ServiceTypeExternalName {
+                       tctx.Services[targetNN] = &service
+                       continue
+               }
+
+               portExists := false
+               for _, port := range service.Spec.Ports {
+                       if port.Port == int32(*backend.Port) {
+                               portExists = true
+                               break
+                       }
+               }
+               if !portExists {
+                       terr = fmt.Errorf("port %d not found in service %s", 
*backend.Port, targetNN.Name)
+                       continue
+               }
+               tctx.Services[targetNN] = &service
+
+               endpointSliceList := new(discoveryv1.EndpointSliceList)
+               if err := r.List(tctx, endpointSliceList,
+                       client.InNamespace(targetNN.Namespace),
+                       client.MatchingLabels{
+                               discoveryv1.LabelServiceName: targetNN.Name,
+                       },
+               ); err != nil {
+                       r.Log.Error(err, "failed to list endpoint slices", 
"Service", targetNN)
+                       terr = err
+                       continue
+               }
+
+               tctx.EndpointSlices[targetNN] = endpointSliceList.Items
+       }
+       return terr
+}
+
+func (r *TLSRouteReconciler) listTLSRoutesForReferenceGrant(ctx 
context.Context, obj client.Object) (requests []reconcile.Request) {
+       grant, ok := obj.(*v1beta1.ReferenceGrant)
+       if !ok {
+               r.Log.Error(fmt.Errorf("unexpected object type"), "failed to 
convert object to ReferenceGrant")
+               return nil
+       }
+
+       var tlsRouteList gatewayv1alpha2.TLSRouteList
+       if err := r.List(ctx, &tlsRouteList); err != nil {
+               r.Log.Error(err, "failed to list tlsroutes for reference 
ReferenceGrant", "ReferenceGrant", k8stypes.NamespacedName{Namespace: 
obj.GetNamespace(), Name: obj.GetName()})
+               return nil
+       }
+
+       for _, tlsRoute := range tlsRouteList.Items {
+               tr := v1beta1.ReferenceGrantFrom{
+                       Group:     gatewayv1.GroupName,
+                       Kind:      types.KindTLSRoute,
+                       Namespace: v1beta1.Namespace(tlsRoute.GetNamespace()),
+               }
+               for _, from := range grant.Spec.From {
+                       if from == tr {
+                               requests = append(requests, reconcile.Request{
+                                       NamespacedName: client.ObjectKey{
+                                               Namespace: 
tlsRoute.GetNamespace(),
+                                               Name:      tlsRoute.GetName(),
+                                       },
+                               })
+                       }
+               }
+       }
+       return requests
+}
+
+func (r *TLSRouteReconciler) listTLSRoutesByServiceRef(ctx context.Context, 
obj client.Object) []reconcile.Request {
+       endpointSlice, ok := obj.(*discoveryv1.EndpointSlice)
+       if !ok {
+               r.Log.Error(fmt.Errorf("unexpected object type"), "failed to 
convert object to EndpointSlice")
+               return nil
+       }
+       namespace := endpointSlice.GetNamespace()
+       serviceName := endpointSlice.Labels[discoveryv1.LabelServiceName]
+
+       trList := &gatewayv1alpha2.TLSRouteList{}
+       if err := r.List(ctx, trList, client.MatchingFields{
+               indexer.ServiceIndexRef: indexer.GenIndexKey(namespace, 
serviceName),
+       }); err != nil {
+               r.Log.Error(err, "failed to list tlsroutes by service", 
"service", serviceName)
+               return nil
+       }
+       requests := make([]reconcile.Request, 0, len(trList.Items))
+       for _, tr := range trList.Items {
+               requests = append(requests, reconcile.Request{
+                       NamespacedName: client.ObjectKey{
+                               Namespace: tr.Namespace,
+                               Name:      tr.Name,
+                       },
+               })
+       }
+       return requests
+}
diff --git a/internal/controller/utils.go b/internal/controller/utils.go
index 530e7c4f..bed563af 100644
--- a/internal/controller/utils.go
+++ b/internal/controller/utils.go
@@ -500,6 +500,8 @@ func routeHostnamesIntersectsWithListenerHostname(route 
client.Object, listener
                return true // TCPRoute and UDPRoute don't have Hostnames to 
match
        case *gatewayv1.GRPCRoute:
                return listenerHostnameIntersectWithRouteHostnames(listener, 
r.Spec.Hostnames)
+       case *gatewayv1alpha2.TLSRoute:
+               return listenerHostnameIntersectWithRouteHostnames(listener, 
r.Spec.Hostnames)
        default:
                return false
        }
@@ -672,6 +674,10 @@ func routeMatchesListenerType(route client.Object, 
listener gatewayv1.Listener)
                if listener.Protocol != gatewayv1.UDPProtocolType {
                        return false
                }
+       case *gatewayv1alpha2.TLSRoute:
+               if listener.Protocol != gatewayv1.TLSProtocolType {
+                       return false
+               }
        default:
                return false
        }
diff --git a/internal/manager/controllers.go b/internal/manager/controllers.go
index 8420909c..9c5b7290 100644
--- a/internal/manager/controllers.go
+++ b/internal/manager/controllers.go
@@ -90,6 +90,8 @@ import (
 // 
+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=referencegrants/status,verbs=get;update
 // 
+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=grpcroutes,verbs=get;list;watch
 // 
+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=grpcroutes/status,verbs=get;update
+// 
+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=tlsroutes,verbs=get;list;watch
+// 
+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=tlsroutes/status,verbs=get;update
 
 // Networking
 // 
+kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch
@@ -150,6 +152,14 @@ func setupControllers(ctx context.Context, mgr 
manager.Manager, pro provider.Pro
                        Updater:  updater,
                        Readier:  readier,
                },
+               &controller.TLSRouteReconciler{
+                       Client:   mgr.GetClient(),
+                       Scheme:   mgr.GetScheme(),
+                       Log:      
ctrl.LoggerFrom(ctx).WithName("controllers").WithName(types.KindTLSRoute),
+                       Provider: pro,
+                       Updater:  updater,
+                       Readier:  readier,
+               },
                &controller.IngressReconciler{
                        Client:   mgr.GetClient(),
                        Scheme:   mgr.GetScheme(),
@@ -234,6 +244,7 @@ func registerReadinessGVK(c client.Client, readier 
readiness.ReadinessManager) {
                                types.GvkOf(&gatewayv1alpha2.TCPRoute{}),
                                types.GvkOf(&gatewayv1alpha2.UDPRoute{}),
                                types.GvkOf(&gatewayv1.GRPCRoute{}),
+                               types.GvkOf(&gatewayv1alpha2.TLSRoute{}),
                        },
                },
                {
diff --git a/internal/provider/apisix/provider.go 
b/internal/provider/apisix/provider.go
index 64d694c8..0151ad0b 100644
--- a/internal/provider/apisix/provider.go
+++ b/internal/provider/apisix/provider.go
@@ -116,6 +116,9 @@ func (d *apisixProvider) Update(ctx context.Context, tctx 
*provider.TranslateCon
        case *gatewayv1alpha2.UDPRoute:
                result, err = d.translator.TranslateUDPRoute(tctx, t.DeepCopy())
                resourceTypes = append(resourceTypes, adctypes.TypeService)
+       case *gatewayv1alpha2.TLSRoute:
+               result, err = d.translator.TranslateTLSRoute(tctx, t.DeepCopy())
+               resourceTypes = append(resourceTypes, adctypes.TypeService)
        case *gatewayv1.GRPCRoute:
                result, err = d.translator.TranslateGRPCRoute(tctx, 
t.DeepCopy())
                resourceTypes = append(resourceTypes, adctypes.TypeService)
@@ -189,7 +192,7 @@ func (d *apisixProvider) Delete(ctx context.Context, obj 
client.Object) error {
        var resourceTypes []string
        var labels map[string]string
        switch obj.(type) {
-       case *gatewayv1.HTTPRoute, *apiv2.ApisixRoute, *gatewayv1.GRPCRoute, 
*gatewayv1alpha2.TCPRoute, *gatewayv1alpha2.UDPRoute:
+       case *gatewayv1.HTTPRoute, *apiv2.ApisixRoute, *gatewayv1.GRPCRoute, 
*gatewayv1alpha2.TCPRoute, *gatewayv1alpha2.UDPRoute, *gatewayv1alpha2.TLSRoute:
                resourceTypes = append(resourceTypes, adctypes.TypeService)
                labels = label.GenLabel(obj)
        case *gatewayv1.Gateway:
diff --git a/internal/types/k8s.go b/internal/types/k8s.go
index 9396b940..914442b4 100644
--- a/internal/types/k8s.go
+++ b/internal/types/k8s.go
@@ -37,6 +37,7 @@ const (
        KindTCPRoute             = "TCPRoute"
        KindUDPRoute             = "UDPRoute"
        KindGRPCRoute            = "GRPCRoute"
+       KindTLSRoute             = "TLSRoute"
        KindGatewayClass         = "GatewayClass"
        KindIngress              = "Ingress"
        KindIngressClass         = "IngressClass"
@@ -68,6 +69,8 @@ func KindOf(obj any) string {
                return KindHTTPRoute
        case *gatewayv1.GRPCRoute:
                return KindGRPCRoute
+       case *gatewayv1alpha2.TLSRoute:
+               return KindTLSRoute
        case *gatewayv1.GatewayClass:
                return KindGatewayClass
        case *netv1.Ingress:
@@ -110,9 +113,7 @@ func GvkOf(obj any) schema.GroupVersionKind {
        switch obj.(type) {
        case *gatewayv1.Gateway, *gatewayv1.HTTPRoute, *gatewayv1.GatewayClass, 
*gatewayv1.GRPCRoute:
                return gatewayv1.SchemeGroupVersion.WithKind(kind)
-       case *gatewayv1alpha2.TCPRoute:
-               return gatewayv1alpha2.SchemeGroupVersion.WithKind(kind)
-       case *gatewayv1alpha2.UDPRoute:
+       case *gatewayv1alpha2.TCPRoute, *gatewayv1alpha2.UDPRoute, 
*gatewayv1alpha2.TLSRoute:
                return gatewayv1alpha2.SchemeGroupVersion.WithKind(kind)
        case *gatewayv1beta1.ReferenceGrant:
                return gatewayv1beta1.SchemeGroupVersion.WithKind(kind)
diff --git a/test/conformance/conformance_test.go 
b/test/conformance/conformance_test.go
index 42161ad5..8574f7f6 100644
--- a/test/conformance/conformance_test.go
+++ b/test/conformance/conformance_test.go
@@ -34,6 +34,9 @@ import (
 var skippedTestsForSSL = []string{
        tests.HTTPRouteHTTPSListener.ShortName,
        tests.HTTPRouteRedirectPortAndScheme.ShortName,
+
+       // TODO: APISIX does not support TLSRoute passthrough.
+       tests.TLSRouteSimpleSameNamespace.ShortName,
 }
 
 // TODO: HTTPRoute hostname intersection and listener hostname matching
diff --git a/test/e2e/framework/manifests/apisix-standalone.yaml 
b/test/e2e/framework/manifests/apisix-standalone.yaml
index 4b7adfe9..0eda2bc8 100644
--- a/test/e2e/framework/manifests/apisix-standalone.yaml
+++ b/test/e2e/framework/manifests/apisix-standalone.yaml
@@ -40,6 +40,8 @@ data:
       stream_proxy:                 # TCP/UDP proxy
         tcp:                        # TCP proxy port list
           - 9100
+          - addr: 9110
+            tls: true
         udp:                        # UDP proxy port list
           - 9200
     discovery:
@@ -101,6 +103,9 @@ spec:
             - name: udp
               containerPort: 9200
               protocol: UDP
+            - name: tls
+              containerPort: 9110
+              protocol: TCP
           volumeMounts:
             - name: config-writable
               mountPath: /usr/local/apisix/conf
@@ -139,6 +144,10 @@ spec:
       port: 9200
       protocol: UDP
       targetPort: 9200
+    - name: tls
+      port: 9110
+      protocol: TCP
+      targetPort: 9110
   selector:
     app.kubernetes.io/name: apisix 
   type: {{ .ServiceType | default "NodePort" }}
diff --git a/test/e2e/framework/manifests/apisix.yaml 
b/test/e2e/framework/manifests/apisix.yaml
index ae8a1396..31581bcc 100644
--- a/test/e2e/framework/manifests/apisix.yaml
+++ b/test/e2e/framework/manifests/apisix.yaml
@@ -47,6 +47,8 @@ data:
       stream_proxy:                 # TCP/UDP proxy
         tcp:                        # TCP proxy port list
           - 9100
+          - addr: 9110
+            tls: true
         udp:                        # UDP proxy port list
           - 9200
     discovery:
@@ -111,6 +113,9 @@ spec:
             - name: udp
               containerPort: 9200
               protocol: UDP
+            - name: tls
+              containerPort: 9110
+              protocol: TCP
           volumeMounts:
             - name: config-writable
               mountPath: /usr/local/apisix/conf
@@ -156,6 +161,10 @@ spec:
       port: 9200
       protocol: UDP
       targetPort: 9200
+    - name: tls
+      port: 9110
+      protocol: TCP
+      targetPort: 9110
   selector:
     app.kubernetes.io/name: apisix 
   type: {{ .ServiceType | default "NodePort" }}
diff --git a/test/e2e/framework/manifests/ingress.yaml 
b/test/e2e/framework/manifests/ingress.yaml
index e44cf1b0..a9d50d65 100644
--- a/test/e2e/framework/manifests/ingress.yaml
+++ b/test/e2e/framework/manifests/ingress.yaml
@@ -158,6 +158,9 @@ rules:
   - grpcroutes/status
   - httproutes/status
   - referencegrants/status
+  - tcproutes/status
+  - tlsroutes/status
+  - udproutes/status
   verbs:
   - get
   - update
@@ -167,29 +170,14 @@ rules:
   - gateways
   - grpcroutes
   - httproutes
+  - referencegrants
   - tcproutes
+  - tlsroutes
   - udproutes
   verbs:
   - get
   - list
   - watch
-- apiGroups:
-  - gateway.networking.k8s.io
-  resources:
-  - httproutes/status
-  - tcproutes/status
-  - udproutes/status
-  verbs:
-  - get
-  - update
-- apiGroups:
-  - gateway.networking.k8s.io
-  resources:
-  - referencegrants
-  verbs:
-  - get
-  - list
-  - watch
 - apiGroups:
   - networking.k8s.io
   resources:
@@ -206,6 +194,7 @@ rules:
   verbs:
   - get
   - update
+
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRole
diff --git a/test/e2e/gatewayapi/tlsroute.go b/test/e2e/gatewayapi/tlsroute.go
new file mode 100644
index 00000000..74fc1b93
--- /dev/null
+++ b/test/e2e/gatewayapi/tlsroute.go
@@ -0,0 +1,117 @@
+// 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 gatewayapi
+
+import (
+       "fmt"
+       "net/http"
+
+       . "github.com/onsi/ginkgo/v2"
+       . "github.com/onsi/gomega"
+
+       "github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
+)
+
+var _ = Describe("Test TLSRoute", Label("networking.k8s.io", "tlsroute"), 
func() {
+       s := scaffold.NewDefaultScaffold()
+
+       Context("TLSRoute Base", func() {
+               var (
+                       host       = "api6.com"
+                       secretName = _secretName
+                       tlsGateway = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: tls-gateway
+spec:
+  gatewayClassName: %s
+  listeners:
+    - name: https
+      protocol: TLS
+      port: 443
+      hostname: api6.com
+      tls:
+        certificateRefs:
+        - kind: Secret
+          group: ""
+          name: %s
+  infrastructure:
+    parametersRef:
+      group: apisix.apache.org
+      kind: GatewayProxy
+      name: apisix-proxy-config
+`
+                       tlsRoute = `
+apiVersion: gateway.networking.k8s.io/v1alpha2
+kind: TLSRoute
+metadata:
+  name: tls-route
+spec:
+  parentRefs:
+  - name: tls-gateway
+  hostnames: ["api6.com"]
+  rules:
+  - backendRefs:
+    - name: httpbin-service-e2e-test
+      port: 80
+`
+               )
+               BeforeEach(func() {
+                       createSecret(s, secretName)
+                       By("create GatewayProxy")
+                       
Expect(s.CreateResourceFromString(s.GetGatewayProxySpec())).NotTo(HaveOccurred(),
 "creating GatewayProxy")
+
+                       By("create GatewayClass")
+                       
Expect(s.CreateResourceFromString(s.GetGatewayClassYaml())).NotTo(HaveOccurred(),
 "creating GatewayClass")
+
+                       // Create Gateway with TCP listener
+                       By("create Gateway")
+                       
Expect(s.CreateResourceFromString(fmt.Sprintf(tlsGateway, s.Namespace(), 
secretName))).NotTo(HaveOccurred(), "creating Gateway")
+               })
+               It("Basic", func() {
+                       s.ResourceApplied("TLSRoute", "tls-route", tlsRoute, 1)
+
+                       client := s.NewAPISIXClientWithTLSProxy(host)
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Client: client,
+                               Method: http.MethodGet,
+                               Path:   "/ip",
+                               Check:  
scaffold.WithExpectedStatus(http.StatusOK),
+                       })
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Client: client,
+                               Method: http.MethodGet,
+                               Path:   "/notfound",
+                               Check:  
scaffold.WithExpectedStatus(http.StatusNotFound),
+                       })
+
+                       
Expect(s.DeleteResourceFromString(tlsRoute)).NotTo(HaveOccurred(), "deleting 
TLSRoute")
+
+                       s.RetryAssertion(func() string {
+                               var errMsg string
+                               reporter := &scaffold.ErrorReporter{}
+                               _ = 
client.GET("/ip").WithReporter(reporter).Expect()
+                               if reporter.Err() != nil {
+                                       errMsg = reporter.Err().Error()
+                               }
+                               return errMsg
+                       }).Should(ContainSubstring("EOF"), "should get EOF 
after deleting TLSRoute")
+               })
+       })
+})
diff --git a/test/e2e/scaffold/scaffold.go b/test/e2e/scaffold/scaffold.go
index a868ee88..432732ac 100644
--- a/test/e2e/scaffold/scaffold.go
+++ b/test/e2e/scaffold/scaffold.go
@@ -80,6 +80,7 @@ type Tunnels struct {
        HTTP  *k8s.Tunnel
        HTTPS *k8s.Tunnel
        TCP   *k8s.Tunnel
+       TLS   *k8s.Tunnel
 }
 
 func (t *Tunnels) Close() {
@@ -95,6 +96,10 @@ func (t *Tunnels) Close() {
                t.safeClose(t.TCP.Close)
                t.TCP = nil
        }
+       if t.TLS != nil {
+               t.safeClose(t.TLS.Close)
+               t.TLS = nil
+       }
 }
 
 func (t *Tunnels) safeClose(close func()) {
@@ -274,6 +279,31 @@ func (s *Scaffold) NewAPISIXClientWithTCPProxy() 
*httpexpect.Expect {
        })
 }
 
+func (s *Scaffold) NewAPISIXClientWithTLSProxy(host string) *httpexpect.Expect 
{
+       u := url.URL{
+               Scheme: apiv2.SchemeHTTPS,
+               Host:   s.apisixTunnels.TLS.Endpoint(),
+       }
+       return httpexpect.WithConfig(httpexpect.Config{
+               BaseURL: u.String(),
+               Client: &http.Client{
+                       Transport: &http.Transport{
+                               TLSClientConfig: &tls.Config{
+                                       // accept any certificate; for testing 
only!
+                                       InsecureSkipVerify: true,
+                                       ServerName:         host,
+                               },
+                       },
+                       CheckRedirect: func(req *http.Request, via 
[]*http.Request) error {
+                               return http.ErrUseLastResponse
+                       },
+               },
+               Reporter: httpexpect.NewAssertReporter(
+                       httpexpect.NewAssertReporter(s.GinkgoT),
+               ),
+       })
+}
+
 func (s *Scaffold) DefaultDataplaneResource() DataplaneResource {
        return s.Deployer.DefaultDataplaneResource()
 }
@@ -359,6 +389,7 @@ func (s *Scaffold) createDataplaneTunnels(
                httpPort  int
                httpsPort int
                tcpPort   int
+               tlsPort   int
        )
 
        for _, port := range svc.Spec.Ports {
@@ -369,6 +400,8 @@ func (s *Scaffold) createDataplaneTunnels(
                        httpsPort = int(port.Port)
                case apiv2.SchemeTCP:
                        tcpPort = int(port.Port)
+               case apiv2.SchemeTLS:
+                       tlsPort = int(port.Port)
                }
        }
 
@@ -381,6 +414,8 @@ func (s *Scaffold) createDataplaneTunnels(
                0, httpsPort)
        tcpTunnel := k8s.NewTunnel(kubectlOpts, k8s.ResourceTypeService, 
serviceName,
                0, tcpPort)
+       tlsTunnel := k8s.NewTunnel(kubectlOpts, k8s.ResourceTypeService, 
serviceName,
+               0, tlsPort)
 
        if err := httpTunnel.ForwardPortE(s.t); err != nil {
                return nil, err
@@ -396,6 +431,10 @@ func (s *Scaffold) createDataplaneTunnels(
                return nil, err
        }
        tunnels.TCP = tcpTunnel
+       if err := tlsTunnel.ForwardPortE(s.t); err != nil {
+               return nil, err
+       }
+       tunnels.TLS = tlsTunnel
 
        return tunnels, nil
 }


Reply via email to