diff --git a/test/e2e/framework.go b/test/e2e/framework.go index 09d3d20b70e..7ff7e351768 100644 --- a/test/e2e/framework.go +++ b/test/e2e/framework.go @@ -348,6 +348,12 @@ func (f *Framework) CreateTLSRouteAndWaitFor(route *gatewayapi_v1alpha2.TLSRoute return createAndWaitFor(f.t, f.Client, route, condition, f.RetryInterval, f.RetryTimeout) } +// CreateTCPRouteAndWaitFor creates the provided TCPRoute in the Kubernetes API +// and then waits for the specified condition to be true. +func (f *Framework) CreateTCPRouteAndWaitFor(route *gatewayapi_v1alpha2.TCPRoute, condition func(*gatewayapi_v1alpha2.TCPRoute) bool) (*gatewayapi_v1alpha2.TCPRoute, bool) { + return createAndWaitFor(f.t, f.Client, route, condition, f.RetryInterval, f.RetryTimeout) +} + // CreateNamespace creates a namespace with the given name in the // Kubernetes API or fails the test if it encounters an error. func (f *Framework) CreateNamespace(name string) { diff --git a/test/e2e/gateway/gateway_test.go b/test/e2e/gateway/gateway_test.go index b6faa9224d9..dda16abe6c2 100644 --- a/test/e2e/gateway/gateway_test.go +++ b/test/e2e/gateway/gateway_test.go @@ -353,6 +353,36 @@ var _ = Describe("Gateway API", func() { f.NamespacedTest("gateway-multiple-https-listeners", testWithMultipleHTTPSListenersGateway(testMultipleHTTPSListeners)) }) + + Describe("Gateway with TCP listener", func() { + testWithTCPGateway := func(body e2e.NamespacedGatewayTestBody) e2e.NamespacedTestBody { + gatewayClass := getGatewayClass() + gw := &gatewayapi_v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tcp", + }, + Spec: gatewayapi_v1beta1.GatewaySpec{ + GatewayClassName: gatewayapi_v1beta1.ObjectName(gatewayClass.Name), + Listeners: []gatewayapi_v1beta1.Listener{ + { + Name: "tcp", + Protocol: gatewayapi_v1beta1.TCPProtocolType, + Port: gatewayapi_v1beta1.PortNumber(80), + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromSame), + }, + }, + }, + }, + }, + } + + return testWithGateway(gw, gatewayClass, body) + } + + f.NamespacedTest("gateway-tcproute", testWithTCPGateway(testTCPRoute)) + }) }) func getRandomNumber() int64 { diff --git a/test/e2e/gateway/tcproute_test.go b/test/e2e/gateway/tcproute_test.go new file mode 100644 index 00000000000..109d1f19e9c --- /dev/null +++ b/test/e2e/gateway/tcproute_test.go @@ -0,0 +1,74 @@ +// Copyright Project Contour Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build e2e + +package gateway + +import ( + . "github.com/onsi/ginkgo/v2" + "github.com/projectcontour/contour/internal/gatewayapi" + "github.com/projectcontour/contour/internal/ref" + "github.com/projectcontour/contour/test/e2e" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + gatewayapi_v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gatewayapi_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +func testTCPRoute(namespace string, gateway types.NamespacedName) { + Specify("A TCPRoute does L4 TCP proxying of traffic for its Listener port", func() { + t := f.T() + + f.Fixtures.Echo.Deploy(namespace, "echo") + + route := &gatewayapi_v1alpha2.TCPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "tcproute-1", + }, + Spec: gatewayapi_v1alpha2.TCPRouteSpec{ + CommonRouteSpec: gatewayapi_v1beta1.CommonRouteSpec{ + ParentRefs: []gatewayapi_v1alpha2.ParentReference{ + { + Namespace: ref.To(gatewayapi_v1beta1.Namespace(gateway.Namespace)), + Name: gatewayapi_v1beta1.ObjectName(gateway.Name), + }, + }, + }, + Rules: []gatewayapi_v1alpha2.TCPRouteRule{ + { + BackendRefs: gatewayapi.TLSRouteBackendRef("echo", 80, ref.To(int32(1))), + }, + }, + }, + } + route, ok := f.CreateTCPRouteAndWaitFor(route, e2e.TCPRouteAccepted) + require.True(t, ok) + require.NotNil(t, route) + + res, ok := f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Condition: e2e.HasStatusCode(200), + }) + assert.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) + assert.Equal(t, "echo", f.GetEchoResponseBody(res.Body).Service) + + // Envoy is expected to add the "server: envoy" and + // "x-envoy-upstream-service-time" HTTP headers when + // proxying HTTP; this ensures we are proxying TCP only. + assert.Equal(t, "", res.Headers.Get("server")) + assert.Equal(t, "", res.Headers.Get("x-envoy-upstream-service-time")) + }) +} diff --git a/test/e2e/gatewayapi_predicates.go b/test/e2e/gatewayapi_predicates.go index 62a91d3fbcd..702c6ae4f03 100644 --- a/test/e2e/gatewayapi_predicates.go +++ b/test/e2e/gatewayapi_predicates.go @@ -17,6 +17,7 @@ package e2e import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gatewayapi_v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gatewayapi_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) @@ -102,6 +103,22 @@ func HTTPRouteAccepted(route *gatewayapi_v1beta1.HTTPRoute) bool { return false } +// TCPRouteAccepted returns true if the route has a .status.conditions +// entry of "Accepted: true". +func TCPRouteAccepted(route *gatewayapi_v1alpha2.TCPRoute) bool { + if route == nil { + return false + } + + for _, gw := range route.Status.Parents { + if conditionExists(gw.Conditions, string(gatewayapi_v1beta1.RouteConditionAccepted), metav1.ConditionTrue) { + return true + } + } + + return false +} + func conditionExists(conditions []metav1.Condition, conditionType string, conditionStatus metav1.ConditionStatus) bool { for _, cond := range conditions { if cond.Type == conditionType && cond.Status == conditionStatus {