From 6e34fbfcf146e548ba75388e1b582703c182a948 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Wed, 29 Jul 2026 15:17:36 +0800 Subject: [PATCH] feat: allow L4 upstream schemes (tcp/tls/udp) for stream backends APISIX accepts `tcp`, `tls` and `udp` as upstream schemes for L4 proxying, but both `ApisixUpstream.spec.scheme` and `BackendTrafficPolicy.spec.scheme` restricted the value to the L7 set, so an upstream that requires TLS from APISIX (plain TCP in, TLS out) could only be configured through the Admin API. The translation path already carries the scheme to the generated upstream for both ApisixRoute stream backends and Gateway API TCPRoute backends, so only the CRD validation needed to be relaxed. Sync of apache/apisix-ingress-controller#2830 (fixes apache/apisix-ingress-controller#2803). --- api/v1alpha1/backendtrafficpolicy_types.go | 7 +- api/v2/apisixupstream_types.go | 7 +- .../apisix.apache.org_apisixupstreams.yaml | 16 ++- ...six.apache.org_backendtrafficpolicies.yaml | 8 +- docs/en/latest/reference/api-reference.md | 8 +- internal/adc/translator/apisixroute_test.go | 102 ++++++++++++++++++ internal/adc/translator/l4route_test.go | 88 +++++++++++++++ internal/adc/translator/tcproute.go | 1 - test/e2e/crds/v2/streamroute.go | 56 ++++++++++ test/e2e/gatewayapi/tcproute.go | 92 ++++++++++++++++ 10 files changed, 373 insertions(+), 12 deletions(-) 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