diff --git a/api/v1alpha1/backendtrafficpolicy_types.go b/api/v1alpha1/backendtrafficpolicy_types.go index 9e861692..c8213c26 100644 --- a/api/v1alpha1/backendtrafficpolicy_types.go +++ b/api/v1alpha1/backendtrafficpolicy_types.go @@ -47,8 +47,11 @@ type BackendTrafficPolicySpec struct { LoadBalancer *LoadBalancer `json:"loadbalancer,omitempty" yaml:"loadbalancer,omitempty"` // Scheme is the protocol used to communicate with the upstream. // Default is `http`. - // Can be `http`, `https`, `grpc`, or `grpcs`. - // +kubebuilder:validation:Enum=http;https;grpc;grpcs; + // For L7 proxy, it can be `http`, `https`, `grpc`, or `grpcs`. + // For L4 proxy, it can be `tcp`, `tls`, or `udp`. + // The L4 values apply to stream routes only; using them for an HTTP route + // makes the upstream unreachable. + // +kubebuilder:validation:Enum=http;https;grpc;grpcs;tcp;tls;udp; // +kubebuilder:default=http Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` diff --git a/api/v2/apisixupstream_types.go b/api/v2/apisixupstream_types.go index 883c509f..e08ece26 100644 --- a/api/v2/apisixupstream_types.go +++ b/api/v2/apisixupstream_types.go @@ -106,9 +106,12 @@ type ApisixUpstreamConfig struct { // Scheme is the protocol used to communicate with the upstream. // Default is `http`. - // Can be `http`, `https`, `grpc`, or `grpcs`. + // For L7 proxy, it can be `http`, `https`, `grpc`, or `grpcs`. + // For L4 proxy, it can be `tcp`, `tls`, or `udp`. + // The L4 values apply to stream routes only; using them for an HTTP route + // makes the upstream unreachable. // +kubebuilder:validation:Optional - // +kubebuilder:validation:Enum=http;https;grpc;grpcs; + // +kubebuilder:validation:Enum=http;https;grpc;grpcs;tcp;tls;udp; Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` // Retries defines the number of retry attempts APISIX should make when a failure occurs. diff --git a/config/crd/bases/apisix.apache.org_apisixupstreams.yaml b/config/crd/bases/apisix.apache.org_apisixupstreams.yaml index a3a08cbd..2e572d01 100644 --- a/config/crd/bases/apisix.apache.org_apisixupstreams.yaml +++ b/config/crd/bases/apisix.apache.org_apisixupstreams.yaml @@ -591,12 +591,18 @@ spec: description: |- Scheme is the protocol used to communicate with the upstream. Default is `http`. - Can be `http`, `https`, `grpc`, or `grpcs`. + For L7 proxy, it can be `http`, `https`, `grpc`, or `grpcs`. + For L4 proxy, it can be `tcp`, `tls`, or `udp`. + The L4 values apply to stream routes only; using them for an HTTP route + makes the upstream unreachable. enum: - http - https - grpc - grpcs + - tcp + - tls + - udp type: string subsets: description: |- @@ -670,12 +676,18 @@ spec: description: |- Scheme is the protocol used to communicate with the upstream. Default is `http`. - Can be `http`, `https`, `grpc`, or `grpcs`. + For L7 proxy, it can be `http`, `https`, `grpc`, or `grpcs`. + For L4 proxy, it can be `tcp`, `tls`, or `udp`. + The L4 values apply to stream routes only; using them for an HTTP route + makes the upstream unreachable. enum: - http - https - grpc - grpcs + - tcp + - tls + - udp type: string subsets: description: |- diff --git a/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml b/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml index 0ea77069..b5af1854 100644 --- a/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml +++ b/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml @@ -284,12 +284,18 @@ spec: description: |- Scheme is the protocol used to communicate with the upstream. Default is `http`. - Can be `http`, `https`, `grpc`, or `grpcs`. + For L7 proxy, it can be `http`, `https`, `grpc`, or `grpcs`. + For L4 proxy, it can be `tcp`, `tls`, or `udp`. + The L4 values apply to stream routes only; using them for an HTTP route + makes the upstream unreachable. enum: - http - https - grpc - grpcs + - tcp + - tls + - udp type: string targetRefs: description: |- diff --git a/docs/en/latest/reference/api-reference.md b/docs/en/latest/reference/api-reference.md index 25b729c2..8def827e 100644 --- a/docs/en/latest/reference/api-reference.md +++ b/docs/en/latest/reference/api-reference.md @@ -254,7 +254,7 @@ _Appears in:_ | --- | --- | | `targetRefs` _[BackendPolicyTargetReferenceWithSectionName](#backendpolicytargetreferencewithsectionname) array_ | TargetRef identifies an API object to apply policy to. Currently, Backends (i.e. Service, ServiceImport, or any implementation-specific backendRef) are the only valid API target references. | | `loadbalancer` _[LoadBalancer](#loadbalancer)_ | LoadBalancer represents the load balancer configuration for Kubernetes Service. The default strategy is round robin. | -| `scheme` _string_ | Scheme is the protocol used to communicate with the upstream. Default is `http`. Can be `http`, `https`, `grpc`, or `grpcs`. | +| `scheme` _string_ | Scheme is the protocol used to communicate with the upstream. Default is `http`. For L7 proxy, it can be `http`, `https`, `grpc`, or `grpcs`. For L4 proxy, it can be `tcp`, `tls`, or `udp`. The L4 values apply to stream routes only; using them for an HTTP route makes the upstream unreachable. | | `retries` _integer_ | Retries specify the number of times the gateway should retry sending requests when errors such as timeouts or 502 errors occur. | | `timeout` _[Timeout](#timeout)_ | Timeout sets the read, send, and connect timeouts to the upstream. | | `passHost` _string_ | PassHost configures how the host header should be determined when a request is forwarded to the upstream. Default is `pass`. Can be `pass`, `node` or `rewrite`:
• `pass`: preserve the original Host header
• `node`: use the upstream node’s host
• `rewrite`: set to a custom host via `upstreamHost` | @@ -1489,7 +1489,7 @@ ApisixUpstreamConfig defines configuration for upstream services. | Field | Description | | --- | --- | | `loadbalancer` _[LoadBalancer](#loadbalancer)_ | LoadBalancer specifies the load balancer configuration for Kubernetes Service. | -| `scheme` _string_ | Scheme is the protocol used to communicate with the upstream. Default is `http`. Can be `http`, `https`, `grpc`, or `grpcs`. | +| `scheme` _string_ | Scheme is the protocol used to communicate with the upstream. Default is `http`. For L7 proxy, it can be `http`, `https`, `grpc`, or `grpcs`. For L4 proxy, it can be `tcp`, `tls`, or `udp`. The L4 values apply to stream routes only; using them for an HTTP route makes the upstream unreachable. | | `retries` _integer_ | Retries defines the number of retry attempts APISIX should make when a failure occurs. Failures include timeouts, network errors, or 5xx status codes. | | `timeout` _[UpstreamTimeout](#upstreamtimeout)_ | Timeout specifies the connection, send, and read timeouts for upstream requests. | | `healthCheck` _[HealthCheck](#healthcheck)_ | HealthCheck defines the active and passive health check configuration for the upstream. | @@ -1549,7 +1549,7 @@ definitions and custom configuration. | `ingressClassName` _string_ | IngressClassName is the name of an IngressClass cluster resource. Controller implementations use this field to determine whether they should process this ApisixUpstream resource. | | `externalNodes` _[ApisixUpstreamExternalNode](#apisixupstreamexternalnode) array_ | ExternalNodes defines a static list of backend nodes. These can be external hosts outside the cluster or cluster-internal Services specified by their DNS name. When this field is set, the upstream will route traffic directly to these nodes without DNS resolution or service discovery. | | `loadbalancer` _[LoadBalancer](#loadbalancer)_ | LoadBalancer specifies the load balancer configuration for Kubernetes Service. | -| `scheme` _string_ | Scheme is the protocol used to communicate with the upstream. Default is `http`. Can be `http`, `https`, `grpc`, or `grpcs`. | +| `scheme` _string_ | Scheme is the protocol used to communicate with the upstream. Default is `http`. For L7 proxy, it can be `http`, `https`, `grpc`, or `grpcs`. For L4 proxy, it can be `tcp`, `tls`, or `udp`. The L4 values apply to stream routes only; using them for an HTTP route makes the upstream unreachable. | | `retries` _integer_ | Retries defines the number of retry attempts APISIX should make when a failure occurs. Failures include timeouts, network errors, or 5xx status codes. | | `timeout` _[UpstreamTimeout](#upstreamtimeout)_ | Timeout specifies the connection, send, and read timeouts for upstream requests. | | `healthCheck` _[HealthCheck](#healthcheck)_ | HealthCheck defines the active and passive health check configuration for the upstream. | @@ -1719,7 +1719,7 @@ them if they are set on the port level. | Field | Description | | --- | --- | | `loadbalancer` _[LoadBalancer](#loadbalancer)_ | LoadBalancer specifies the load balancer configuration for Kubernetes Service. | -| `scheme` _string_ | Scheme is the protocol used to communicate with the upstream. Default is `http`. Can be `http`, `https`, `grpc`, or `grpcs`. | +| `scheme` _string_ | Scheme is the protocol used to communicate with the upstream. Default is `http`. For L7 proxy, it can be `http`, `https`, `grpc`, or `grpcs`. For L4 proxy, it can be `tcp`, `tls`, or `udp`. The L4 values apply to stream routes only; using them for an HTTP route makes the upstream unreachable. | | `retries` _integer_ | Retries defines the number of retry attempts APISIX should make when a failure occurs. Failures include timeouts, network errors, or 5xx status codes. | | `timeout` _[UpstreamTimeout](#upstreamtimeout)_ | Timeout specifies the connection, send, and read timeouts for upstream requests. | | `healthCheck` _[HealthCheck](#healthcheck)_ | HealthCheck defines the active and passive health check configuration for the upstream. | diff --git a/internal/adc/translator/apisixroute_test.go b/internal/adc/translator/apisixroute_test.go index 6ac355eb..89a80007 100644 --- a/internal/adc/translator/apisixroute_test.go +++ b/internal/adc/translator/apisixroute_test.go @@ -18,14 +18,22 @@ package translator import ( + "context" "testing" "github.com/go-logr/logr" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + discoveryv1 "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8stypes "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" adc "github.com/apache/apisix-ingress-controller/api/adc" apiv2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/internal/provider" ) func TestBuildRoute_HostsNotSet(t *testing.T) { @@ -119,3 +127,97 @@ func TestBuildRoute_MetadataLabelsDoNotOverwriteControllerLabels(t *testing.T) { assert.Equal(t, ar.Name, route.Labels["k8s/name"]) assert.Equal(t, "ApisixRoute/default/test-route", route.Labels["k8s/resource-key"]) } + +func TestTranslateApisixRouteStreamUpstreamScheme(t *testing.T) { + const ( + namespace = "default" + serviceName = "backend" + portName = "tcp" + portNumber = int32(6000) + ) + + tests := []struct { + scheme string + protocol string + }{ + {scheme: apiv2.SchemeTLS, protocol: "TCP"}, + {scheme: apiv2.SchemeTCP, protocol: "TCP"}, + {scheme: apiv2.SchemeUDP, protocol: "UDP"}, + } + + for _, tt := range tests { + t.Run(tt.scheme, func(t *testing.T) { + translator := NewTranslator(logr.Discard(), "") + tctx := provider.NewDefaultTranslateContext(context.Background()) + + serviceKey := k8stypes.NamespacedName{Namespace: namespace, Name: serviceName} + tctx.Services[serviceKey] = &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName, + Namespace: namespace, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{ + Name: portName, + Port: portNumber, + }}, + }, + } + tctx.EndpointSlices[serviceKey] = []discoveryv1.EndpointSlice{{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName + "-1", + Namespace: namespace, + }, + Ports: []discoveryv1.EndpointPort{{ + Name: ptr.To(portName), + Port: ptr.To(portNumber), + }}, + Endpoints: []discoveryv1.Endpoint{{ + Addresses: []string{"10.0.0.1"}, + Conditions: discoveryv1.EndpointConditions{ + Ready: ptr.To(true), + }, + }}, + }} + tctx.Upstreams[serviceKey] = &apiv2.ApisixUpstream{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName, + Namespace: namespace, + }, + Spec: apiv2.ApisixUpstreamSpec{ + ApisixUpstreamConfig: apiv2.ApisixUpstreamConfig{ + Scheme: tt.scheme, + }, + }, + } + + ar := &apiv2.ApisixRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-route", + Namespace: namespace, + }, + Spec: apiv2.ApisixRouteSpec{ + Stream: []apiv2.ApisixRouteStream{{ + Name: "rule1", + Protocol: tt.protocol, + Match: apiv2.ApisixRouteStreamMatch{ + IngressPort: 8000, + }, + Backend: apiv2.ApisixRouteStreamBackend{ + ServiceName: serviceName, + ServicePort: intstr.FromInt32(portNumber), + }, + }}, + }, + } + + result, err := translator.TranslateApisixRoute(tctx, ar) + require.NoError(t, err) + require.Len(t, result.Services, 1) + require.NotNil(t, result.Services[0].Upstream) + + assert.Equal(t, tt.scheme, result.Services[0].Upstream.Scheme) + assert.Equal(t, "10.0.0.1", result.Services[0].Upstream.Nodes[0].Host) + }) + } +} diff --git a/internal/adc/translator/l4route_test.go b/internal/adc/translator/l4route_test.go index fdf7a87d..c7901fe0 100644 --- a/internal/adc/translator/l4route_test.go +++ b/internal/adc/translator/l4route_test.go @@ -24,12 +24,18 @@ import ( "github.com/go-logr/logr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + discoveryv1 "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8stypes "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" "github.com/apache/apisix-ingress-controller/api/v1alpha1" + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" "github.com/apache/apisix-ingress-controller/internal/provider" + internaltypes "github.com/apache/apisix-ingress-controller/internal/types" ) func TestTranslateTCPRouteWithL4RoutePolicy(t *testing.T) { @@ -267,3 +273,85 @@ func TestTranslateTLSRouteWithL4RoutePolicy(t *testing.T) { }) } } + +func TestTranslateTCPRouteUpstreamScheme(t *testing.T) { + const ( + namespace = "default" + serviceName = "backend" + portName = "tcp" + portNumber = int32(6000) + ) + + translator := NewTranslator(logr.Discard(), "") + tctx := provider.NewDefaultTranslateContext(context.Background()) + + serviceKey := k8stypes.NamespacedName{Namespace: namespace, Name: serviceName} + tctx.Services[serviceKey] = &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName, + Namespace: namespace, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{ + Name: portName, + Port: portNumber, + }}, + }, + } + tctx.EndpointSlices[serviceKey] = []discoveryv1.EndpointSlice{{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName + "-1", + Namespace: namespace, + }, + Ports: []discoveryv1.EndpointPort{{ + Name: ptr.To(portName), + Port: ptr.To(portNumber), + }}, + Endpoints: []discoveryv1.Endpoint{{ + Addresses: []string{"10.0.0.1"}, + Conditions: discoveryv1.EndpointConditions{ + Ready: ptr.To(true), + }, + }}, + }} + tctx.BackendTrafficPolicies[serviceKey] = &v1alpha1.BackendTrafficPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backend-policy", + Namespace: namespace, + }, + Spec: v1alpha1.BackendTrafficPolicySpec{ + TargetRefs: []v1alpha1.BackendPolicyTargetReferenceWithSectionName{{ + LocalPolicyTargetReference: gatewayv1.LocalPolicyTargetReference{ + Name: gatewayv1.ObjectName(serviceName), + Kind: gatewayv1.Kind(internaltypes.KindService), + }, + }}, + Scheme: apiv2.SchemeTLS, + }, + } + + route := &gatewayv1alpha2.TCPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-tcp", + Namespace: namespace, + }, + Spec: gatewayv1alpha2.TCPRouteSpec{ + Rules: []gatewayv1alpha2.TCPRouteRule{{ + BackendRefs: []gatewayv1alpha2.BackendRef{{ + BackendObjectReference: gatewayv1.BackendObjectReference{ + Name: gatewayv1.ObjectName(serviceName), + Port: ptr.To(portNumber), + }, + }}, + }}, + }, + } + + result, err := translator.TranslateTCPRoute(tctx, route) + require.NoError(t, err) + require.Len(t, result.Services, 1) + require.NotNil(t, result.Services[0].Upstream) + + assert.Equal(t, apiv2.SchemeTLS, result.Services[0].Upstream.Scheme) + assert.Equal(t, "10.0.0.1", result.Services[0].Upstream.Nodes[0].Host) +} diff --git a/internal/adc/translator/tcproute.go b/internal/adc/translator/tcproute.go index 81e12a34..7327e911 100644 --- a/internal/adc/translator/tcproute.go +++ b/internal/adc/translator/tcproute.go @@ -68,7 +68,6 @@ func (t *Translator) TranslateTCPRoute(tctx *provider.TranslateContext, tcpRoute if len(upNodes) == 0 { continue } - // TODO: Confirm BackendTrafficPolicy attachment with e2e test case. t.AttachBackendTrafficPolicyToUpstream(backend, tctx.BackendTrafficPolicies, upstream, tctx.Services) upstream.Nodes = upNodes var ( diff --git a/test/e2e/crds/v2/streamroute.go b/test/e2e/crds/v2/streamroute.go index 151f6cd8..10ea85c1 100644 --- a/test/e2e/crds/v2/streamroute.go +++ b/test/e2e/crds/v2/streamroute.go @@ -25,6 +25,7 @@ import ( mqtt "github.com/eclipse/paho.mqtt.golang" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "k8s.io/utils/ptr" "github.com/apache/apisix-ingress-controller/test/e2e/framework" "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" @@ -107,6 +108,61 @@ spec: }) }) + Context("TCP Proxy with TLS upstream", func() { + apisixUpstream := ` +apiVersion: apisix.apache.org/v2 +kind: ApisixUpstream +metadata: + name: nginx +spec: + ingressClassName: %s + scheme: tls +` + apisixRoute := ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: nginx-tcp-route +spec: + ingressClassName: %s + stream: + - name: rule1 + protocol: TCP + match: + ingressPort: 9100 + backend: + serviceName: nginx + servicePort: 443 +` + BeforeEach(func() { + s.DeployNginx(framework.NginxOptions{ + Namespace: s.Namespace(), + Replicas: ptr.To(int32(1)), + }) + }) + + It("stream tcp proxy to tls upstream", func() { + // The client speaks plain TCP to APISIX; scheme: tls makes APISIX + // establish the TLS session with the upstream on its behalf. + err := s.CreateResourceFromString(fmt.Sprintf(apisixUpstream, s.Namespace())) + Expect(err).NotTo(HaveOccurred(), "creating ApisixUpstream") + + err = s.CreateResourceFromString(fmt.Sprintf(apisixRoute, s.Namespace())) + Expect(err).NotTo(HaveOccurred(), "creating ApisixRoute") + + s.RequestAssert(&scaffold.RequestAssert{ + Client: s.NewAPISIXClientWithTCPProxy(), + Method: "GET", + Path: "/", + Checks: []scaffold.ResponseCheckFunc{ + scaffold.WithExpectedStatus(200), + scaffold.WithExpectedHeader("X-Port", "443"), + scaffold.WithExpectedBodyContains("Hello, World!"), + }, + }) + }) + }) + Context("UDP Proxy", func() { apisixRoute := ` apiVersion: apisix.apache.org/v2 diff --git a/test/e2e/gatewayapi/tcproute.go b/test/e2e/gatewayapi/tcproute.go index 3af0d4ea..538f4aba 100644 --- a/test/e2e/gatewayapi/tcproute.go +++ b/test/e2e/gatewayapi/tcproute.go @@ -25,8 +25,10 @@ import ( . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + "github.com/apache/apisix-ingress-controller/test/e2e/framework" "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" ) @@ -108,6 +110,96 @@ spec: }) }) + Context("TCPRoute With BackendTrafficPolicy", 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-tls-upstream +spec: + parentRefs: + - name: %s + sectionName: tcp + rules: + - backendRefs: + - name: nginx + port: 443 +` + + var backendTrafficPolicy = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: nginx-tls +spec: + targetRefs: + - name: nginx + kind: Service + group: "" + scheme: tls +` + + BeforeEach(func() { + if framework.ProviderType == framework.ProviderTypeAPI7EE { + // The API7 EE Admin API only accepts tcp/udp as the upstream scheme + // of a stream service. + Skip("scheme: tls is not supported by the API7 EE provider") + } + 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") + s.DeployNginx(framework.NginxOptions{ + Namespace: s.Namespace(), + Replicas: ptr.To(int32(1)), + }) + }) + + It("BackendTrafficPolicy scheme tls connects to the upstream over TLS", func() { + By("creating BackendTrafficPolicy with scheme: tls") + Expect(s.CreateResourceFromString(backendTrafficPolicy)).NotTo(HaveOccurred(), "creating BackendTrafficPolicy") + + By("creating TCPRoute to the TLS port of nginx") + s.ResourceApplied("TCPRoute", "tcp-tls-upstream", fmt.Sprintf(tcpRoute, s.Namespace()), 1) + + // The client speaks plain HTTP over TCP; without scheme: tls nginx would + // reject the request on its TLS port instead of answering with 200. + s.RequestAssert(&scaffold.RequestAssert{ + Client: s.NewAPISIXClientOnTCPPort(), + Method: "GET", + Path: "/", + Checks: []scaffold.ResponseCheckFunc{ + scaffold.WithExpectedStatus(200), + scaffold.WithExpectedHeader("X-Port", "443"), + scaffold.WithExpectedBodyContains("Hello, World!"), + }, + Timeout: time.Minute * 3, + Interval: time.Second * 2, + }) + }) + }) + Context("TCPRoute With L4RoutePolicy", func() { var tcpGateway = ` apiVersion: gateway.networking.k8s.io/v1