From 7425508170b8c67f3c19b250fd8e80b27dde5672 Mon Sep 17 00:00:00 2001 From: Tero Saarni Date: Mon, 15 Jun 2026 23:16:23 +0300 Subject: [PATCH] Replace contour-authserver in e2e with gRPC server running in test suite Signed-off-by: Tero Saarni --- test/e2e/deployment.go | 59 +----- test/e2e/httpproxy/external_auth_test.go | 247 ++++++++++++----------- test/e2e/httpproxy/httpproxy_test.go | 23 ++- test/e2e/local_grpc_auth_service.go | 158 +++++++++++++++ 4 files changed, 312 insertions(+), 175 deletions(-) create mode 100644 test/e2e/local_grpc_auth_service.go diff --git a/test/e2e/deployment.go b/test/e2e/deployment.go index 1e3a121875d..7a201d6bc78 100644 --- a/test/e2e/deployment.go +++ b/test/e2e/deployment.go @@ -111,11 +111,6 @@ type Deployment struct { RateLimitDeployment *apps_v1.Deployment RateLimitService *core_v1.Service RateLimitExtensionService *contour_v1alpha1.ExtensionService - - // Global External Authorization deployment. - GlobalExtAuthDeployment *apps_v1.Deployment - GlobalExtAuthService *core_v1.Service - GlobalExtAuthExtensionService *contour_v1alpha1.ExtensionService } // UnmarshalResources unmarshals resources from rendered Contour manifest in @@ -237,35 +232,7 @@ func (d *Deployment) UnmarshalResources() error { return err } - // Global external auth - globalExtAuthExamplePath := filepath.Join(filepath.Dir(thisFile), "..", "..", "examples", "global-external-auth") - globalExtAuthServerDeploymentFile := filepath.Join(globalExtAuthExamplePath, "01-authserver.yaml") - globalExtAuthExtSvcFile := filepath.Join(globalExtAuthExamplePath, "02-globalextauth-extsvc.yaml") - - rGlobalExtAuthDeploymentFile, err := os.Open(globalExtAuthServerDeploymentFile) - if err != nil { - return err - } - defer rGlobalExtAuthDeploymentFile.Close() - decoder = apimachinery_util_yaml.NewYAMLToJSONDecoder(rGlobalExtAuthDeploymentFile) - d.GlobalExtAuthDeployment = new(apps_v1.Deployment) - if err := decoder.Decode(d.GlobalExtAuthDeployment); err != nil { - return err - } - d.GlobalExtAuthService = new(core_v1.Service) - if err := decoder.Decode(d.GlobalExtAuthService); err != nil { - return err - } - - rGlobalExtAuthExtSvcFile, err := os.Open(globalExtAuthExtSvcFile) - if err != nil { - return err - } - defer rGlobalExtAuthExtSvcFile.Close() - decoder = apimachinery_util_yaml.NewYAMLToJSONDecoder(rGlobalExtAuthExtSvcFile) - d.GlobalExtAuthExtensionService = new(contour_v1alpha1.ExtensionService) - - return decoder.Decode(d.GlobalExtAuthExtensionService) + return nil } // Common case of updating object if exists, create otherwise. @@ -428,30 +395,6 @@ func (d *Deployment) EnsureRateLimitResources(namespace, configContents string) return d.ensureResource(extSvc, new(contour_v1alpha1.ExtensionService)) } -func (d *Deployment) EnsureGlobalExternalAuthResources(namespace string) error { - setNamespace := d.Namespace.Name - if len(namespace) > 0 { - setNamespace = namespace - } - - deployment := d.GlobalExtAuthDeployment.DeepCopy() - deployment.Namespace = setNamespace - if err := d.ensureResource(deployment, new(apps_v1.Deployment)); err != nil { - return err - } - - service := d.GlobalExtAuthService.DeepCopy() - service.Namespace = setNamespace - if err := d.ensureResource(service, new(core_v1.Service)); err != nil { - return err - } - - extSvc := d.GlobalExtAuthExtensionService.DeepCopy() - extSvc.Namespace = setNamespace - - return d.ensureResource(extSvc, new(contour_v1alpha1.ExtensionService)) -} - // Convenience method for deploying the pieces of the deployment needed for // testing Contour running locally, out of cluster. // Includes: diff --git a/test/e2e/httpproxy/external_auth_test.go b/test/e2e/httpproxy/external_auth_test.go index d08d7e612c2..56469713d76 100644 --- a/test/e2e/httpproxy/external_auth_test.go +++ b/test/e2e/httpproxy/external_auth_test.go @@ -19,13 +19,13 @@ import ( "context" "net/http" + envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" . "github.com/onsi/ginkgo/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - apps_v1 "k8s.io/api/apps/v1" core_v1 "k8s.io/api/core/v1" meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" contour_v1 "github.com/projectcontour/contour/apis/projectcontour/v1" contour_v1alpha1 "github.com/projectcontour/contour/apis/projectcontour/v1alpha1" @@ -33,103 +33,13 @@ import ( ) func testExternalAuth(namespace string) { - Specify("external auth can be configured on an HTTPRoute", func() { + Specify("external auth can be configured on an HTTPProxy", func() { t := f.T() f.Fixtures.Echo.Deploy(namespace, "echo") f.Certs.CreateSelfSignedCert(namespace, "echo", "externalauth.projectcontour.io") - f.Certs.CreateSelfSignedCert(namespace, "testserver-cert", "testserver") - - // auth testserver - deployment := &apps_v1.Deployment{ - ObjectMeta: meta_v1.ObjectMeta{ - Namespace: namespace, - Name: "testserver", - Labels: map[string]string{ - "app.kubernetes.io/name": "testserver", - }, - }, - Spec: apps_v1.DeploymentSpec{ - Selector: &meta_v1.LabelSelector{ - MatchLabels: map[string]string{"app.kubernetes.io/name": "testserver"}, - }, - Template: core_v1.PodTemplateSpec{ - ObjectMeta: meta_v1.ObjectMeta{ - Labels: map[string]string{"app.kubernetes.io/name": "testserver"}, - }, - Spec: core_v1.PodSpec{ - Containers: []core_v1.Container{ - { - Name: "testserver", - Image: "ghcr.io/projectcontour/contour-authserver:v4", - ImagePullPolicy: core_v1.PullIfNotPresent, - Command: []string{ - "/contour-authserver", - }, - Args: []string{ - "testserver", - "--address=:9443", - "--tls-ca-path=/tls/ca.crt", - "--tls-cert-path=/tls/tls.crt", - "--tls-key-path=/tls/tls.key", - }, - Ports: []core_v1.ContainerPort{ - { - Name: "auth", - ContainerPort: 9443, - Protocol: core_v1.ProtocolTCP, - }, - }, - VolumeMounts: []core_v1.VolumeMount{ - { - Name: "tls", - MountPath: "/tls", - ReadOnly: true, - }, - }, - }, - }, - Volumes: []core_v1.Volume{ - { - Name: "tls", - VolumeSource: core_v1.VolumeSource{ - Secret: &core_v1.SecretVolumeSource{ - SecretName: "testserver-cert", - }, - }, - }, - }, - }, - }, - }, - } - require.NoError(t, f.Client.Create(context.TODO(), deployment)) - - svc := &core_v1.Service{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "testserver", - Namespace: namespace, - Labels: map[string]string{ - "app.kubernetes.io/name": "testserver", - }, - }, - Spec: core_v1.ServiceSpec{ - Ports: []core_v1.ServicePort{ - { - Name: "auth", - Protocol: core_v1.ProtocolTCP, - Port: 9443, - TargetPort: intstr.FromInt(9443), - }, - }, - Selector: map[string]string{ - "app.kubernetes.io/name": "testserver", - }, - Type: core_v1.ServiceTypeClusterIP, - }, - } - require.NoError(t, f.Client.Create(context.TODO(), svc)) + auth := e2e.StartLocalGRPCAuthService(GinkgoT(), f.Client, namespace, "testserver") extSvc := &contour_v1alpha1.ExtensionService{ ObjectMeta: meta_v1.ObjectMeta{ @@ -137,6 +47,7 @@ func testExternalAuth(namespace string) { Namespace: namespace, }, Spec: contour_v1alpha1.ExtensionServiceSpec{ + Protocol: ptr.To("h2c"), Services: []contour_v1alpha1.ExtensionServiceTarget{ { Name: "testserver", @@ -246,6 +157,8 @@ func testExternalAuth(namespace string) { require.True(f.T(), f.CreateHTTPProxyAndWaitFor(p, e2e.HTTPProxyValid)) // By default requests to /first should not be authorized. + By("auth server denies request") + auth.Deny(http.StatusUnauthorized) res, ok := f.HTTP.SecureRequestUntil(&e2e.HTTPSRequestOpts{ Host: p.Spec.VirtualHost.Fqdn, Path: "/first", @@ -254,10 +167,12 @@ func testExternalAuth(namespace string) { require.NotNil(t, res, "request never succeeded") require.Truef(t, ok, "expected 401 response code, got %d", res.StatusCode) - // The `testserver` authorization server will accept any request with - // "allow" in the path, so this request should succeed. We can tell that - // the authorization server processed it by inspecting the context headers - // that it injects. + By("auth server allows request, context_extensions forwarded to auth service") + auth.Handle(func(req *envoy_service_auth_v3.CheckRequest) *envoy_service_auth_v3.CheckResponse { + assert.Equal(t, "first", req.Attributes.ContextExtensions["target"]) + assert.Equal(t, "externalauth.projectcontour.io", req.Attributes.ContextExtensions["hostname"]) + return e2e.AllowResponse().Build() + }) res, ok = f.HTTP.SecureRequestUntil(&e2e.HTTPSRequestOpts{ Host: p.Spec.VirtualHost.Fqdn, Path: "/first/allow", @@ -266,11 +181,8 @@ func testExternalAuth(namespace string) { require.NotNil(t, res, "request never succeeded") require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) - body := f.GetEchoResponseBody(res.Body) - assert.Equal(t, "first", body.RequestHeaders.Get("Auth-Context-Target")) - assert.Equal(t, "externalauth.projectcontour.io", body.RequestHeaders.Get("Auth-Context-Hostname")) - - // THe /second route disables authorization so this request should succeed. + By("route with AuthPolicy.Disabled=true bypasses auth") + auth.Deny(http.StatusUnauthorized) res, ok = f.HTTP.SecureRequestUntil(&e2e.HTTPSRequestOpts{ Host: p.Spec.VirtualHost.Fqdn, Path: "/second", @@ -288,10 +200,12 @@ func testExternalAuth(namespace string) { require.NotNil(t, res, "request never succeeded") require.Truef(t, ok, "expected 401 response code, got %d", res.StatusCode) - // The `testserver` authorization server will accept any request with - // "allow" in the path, so this request should succeed. We can tell that - // the authorization server processed it by inspecting the context headers - // that it injects. + By("context_extensions forwarded for the default route") + auth.Handle(func(req *envoy_service_auth_v3.CheckRequest) *envoy_service_auth_v3.CheckResponse { + assert.Equal(t, "default", req.Attributes.ContextExtensions["target"]) + assert.Equal(t, "externalauth.projectcontour.io", req.Attributes.ContextExtensions["hostname"]) + return e2e.AllowResponse().Build() + }) res, ok = f.HTTP.SecureRequestUntil(&e2e.HTTPSRequestOpts{ Host: p.Spec.VirtualHost.Fqdn, Path: "/matches-default-route/allow", @@ -300,11 +214,8 @@ func testExternalAuth(namespace string) { require.NotNil(t, res, "request never succeeded") require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) - body = f.GetEchoResponseBody(res.Body) - assert.Equal(t, "default", body.RequestHeaders.Get("Auth-Context-Target")) - assert.Equal(t, "externalauth.projectcontour.io", body.RequestHeaders.Get("Auth-Context-Hostname")) - - // Direct response with external auth enabled should get a 401. + By("direct-response route with auth enabled is subject to auth") + auth.Deny(http.StatusUnauthorized) res, ok = f.HTTP.SecureRequestUntil(&e2e.HTTPSRequestOpts{ Host: p.Spec.VirtualHost.Fqdn, Path: "/direct-response-auth-enabled", @@ -313,8 +224,8 @@ func testExternalAuth(namespace string) { require.NotNil(t, res, "request never succeeded") require.Truef(t, ok, "expected 401 response code, got %d", res.StatusCode) - // Direct response with external auth enabled with "allow" in the path - // should succeed. + By("direct-response route with auth enabled returns configured status on allow") + auth.Allow() res, ok = f.HTTP.SecureRequestUntil(&e2e.HTTPSRequestOpts{ Host: p.Spec.VirtualHost.Fqdn, Path: "/direct-response-auth-enabled/allow", @@ -323,7 +234,8 @@ func testExternalAuth(namespace string) { require.NotNil(t, res, "request never succeeded") require.Truef(t, ok, "expected 418 response code, got %d", res.StatusCode) - // Direct response with external auth disabled should succeed. + By("direct-response route with auth disabled bypasses auth") + auth.Deny(http.StatusUnauthorized) res, ok = f.HTTP.SecureRequestUntil(&e2e.HTTPSRequestOpts{ Host: p.Spec.VirtualHost.Fqdn, Path: "/direct-response-auth-disabled", @@ -331,5 +243,110 @@ func testExternalAuth(namespace string) { }) require.NotNil(t, res, "request never succeeded") require.Truef(t, ok, "expected 418 response code, got %d", res.StatusCode) + + // Create a Service with no endpoints so Envoy has nothing to connect to, + // simulating an unreachable auth server for FailOpen/FailClosed tests. + require.NoError(t, f.Client.Create(context.TODO(), &core_v1.Service{ + ObjectMeta: meta_v1.ObjectMeta{Name: "unreachable-authserver", Namespace: namespace}, + Spec: core_v1.ServiceSpec{ + Ports: []core_v1.ServicePort{{Name: "grpc", Protocol: core_v1.ProtocolTCP, Port: 9443}}, + }, + })) + + unreachableExtSvc := &contour_v1alpha1.ExtensionService{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "unreachable-authserver", + Namespace: namespace, + }, + Spec: contour_v1alpha1.ExtensionServiceSpec{ + Protocol: ptr.To("h2c"), + Services: []contour_v1alpha1.ExtensionServiceTarget{ + {Name: "unreachable-authserver", Port: 9443}, + }, + }, + } + require.NoError(t, f.Client.Create(context.TODO(), unreachableExtSvc)) + + By("FailOpen=false, unreachable auth server returns 503") + f.Certs.CreateSelfSignedCert(namespace, "failopen", "failopen.externalauth.projectcontour.io") + failOpenFalseProxy := &contour_v1.HTTPProxy{ + ObjectMeta: meta_v1.ObjectMeta{ + Namespace: namespace, + Name: "external-auth-failopen-false", + }, + Spec: contour_v1.HTTPProxySpec{ + VirtualHost: &contour_v1.VirtualHost{ + Fqdn: "failopen.externalauth.projectcontour.io", + TLS: &contour_v1.TLS{ + SecretName: "failopen", + }, + Authorization: &contour_v1.AuthorizationServer{ + FailOpen: false, + ResponseTimeout: "500ms", + ExtensionServiceRef: contour_v1.ExtensionServiceReference{ + Name: unreachableExtSvc.Name, + Namespace: unreachableExtSvc.Namespace, + }, + }, + }, + Routes: []contour_v1.Route{ + { + Services: []contour_v1.Service{ + {Name: "echo", Port: 80}, + }, + }, + }, + }, + } + require.True(f.T(), f.CreateHTTPProxyAndWaitFor(failOpenFalseProxy, e2e.HTTPProxyValid)) + + res, ok = f.HTTP.SecureRequestUntil(&e2e.HTTPSRequestOpts{ + Host: failOpenFalseProxy.Spec.VirtualHost.Fqdn, + Path: "/test", + Condition: e2e.HasStatusCode(503), + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 503 response code, got %d", res.StatusCode) + + By("FailOpen=true, unreachable auth server allows request through") + f.Certs.CreateSelfSignedCert(namespace, "failopen-true", "failopen-true.externalauth.projectcontour.io") + failOpenTrueProxy := &contour_v1.HTTPProxy{ + ObjectMeta: meta_v1.ObjectMeta{ + Namespace: namespace, + Name: "external-auth-failopen-true", + }, + Spec: contour_v1.HTTPProxySpec{ + VirtualHost: &contour_v1.VirtualHost{ + Fqdn: "failopen-true.externalauth.projectcontour.io", + TLS: &contour_v1.TLS{ + SecretName: "failopen-true", + }, + Authorization: &contour_v1.AuthorizationServer{ + FailOpen: true, + ResponseTimeout: "500ms", + ExtensionServiceRef: contour_v1.ExtensionServiceReference{ + Name: unreachableExtSvc.Name, + Namespace: unreachableExtSvc.Namespace, + }, + }, + }, + Routes: []contour_v1.Route{ + { + Services: []contour_v1.Service{ + {Name: "echo", Port: 80}, + }, + }, + }, + }, + } + require.True(f.T(), f.CreateHTTPProxyAndWaitFor(failOpenTrueProxy, e2e.HTTPProxyValid)) + + res, ok = f.HTTP.SecureRequestUntil(&e2e.HTTPSRequestOpts{ + Host: failOpenTrueProxy.Spec.VirtualHost.Fqdn, + Path: "/test", + Condition: e2e.HasStatusCode(200), + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) }) } diff --git a/test/e2e/httpproxy/httpproxy_test.go b/test/e2e/httpproxy/httpproxy_test.go index 050113eca3f..88115e14ca0 100644 --- a/test/e2e/httpproxy/httpproxy_test.go +++ b/test/e2e/httpproxy/httpproxy_test.go @@ -16,8 +16,10 @@ package httpproxy import ( + "context" "crypto/x509" "fmt" + "net/http" "strings" "testing" @@ -26,6 +28,7 @@ import ( "github.com/onsi/gomega/gexec" "github.com/stretchr/testify/require" "github.com/tsaarni/certyaml" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" contour_v1 "github.com/projectcontour/contour/apis/projectcontour/v1" @@ -775,8 +778,24 @@ descriptors: }, ResponseTimeout: "10s", } - require.NoError(f.T(), - f.Deployment.EnsureGlobalExternalAuthResources(namespace)) + + // Start the local gRPC auth service with a deny-all handler. + auth := e2e.StartLocalGRPCAuthService(GinkgoT(), f.Client, namespace, "testserver") + auth.Deny(http.StatusUnauthorized) + + extSvc := &contour_v1alpha1.ExtensionService{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "testserver", + Namespace: namespace, + }, + Spec: contour_v1alpha1.ExtensionServiceSpec{ + Protocol: ptr.To("h2c"), + Services: []contour_v1alpha1.ExtensionServiceTarget{ + {Name: "testserver", Port: 9443}, + }, + }, + } + require.NoError(f.T(), f.Client.Create(context.TODO(), extSvc)) }) body(namespace) }) diff --git a/test/e2e/local_grpc_auth_service.go b/test/e2e/local_grpc_auth_service.go new file mode 100644 index 00000000000..d7f1fa8d22b --- /dev/null +++ b/test/e2e/local_grpc_auth_service.go @@ -0,0 +1,158 @@ +// 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 e2e + +import ( + "context" + "net" + "os" + "sync/atomic" + + envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" + "github.com/onsi/ginkgo/v2" + "github.com/stretchr/testify/require" + rpc_status "google.golang.org/genproto/googleapis/rpc/status" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + core_v1 "k8s.io/api/core/v1" + discovery_v1 "k8s.io/api/discovery/v1" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// LocalGRPCAuthServer is a test helper that runs an in-process gRPC ext_authz +// server whose behavior can be changed on the fly. +type LocalGRPCAuthServer struct { + envoy_service_auth_v3.UnimplementedAuthorizationServer + handler atomic.Pointer[func(*envoy_service_auth_v3.CheckRequest) *envoy_service_auth_v3.CheckResponse] +} + +// Check implements the ext_authz Check function. +func (s *LocalGRPCAuthServer) Check(_ context.Context, req *envoy_service_auth_v3.CheckRequest) (*envoy_service_auth_v3.CheckResponse, error) { + return (*s.handler.Load())(req), nil +} + +// Deny sets the server to deny every request. +func (s *LocalGRPCAuthServer) Deny(httpStatusCode int32) { + s.RespondWith(DenyResponse(httpStatusCode)) +} + +// Allow sets the server to allow every request. +func (s *LocalGRPCAuthServer) Allow() { + s.RespondWith(AllowResponse().Build()) +} + +// RespondWith sets a static response for all requests. +func (s *LocalGRPCAuthServer) RespondWith(resp *envoy_service_auth_v3.CheckResponse) { + h := func(_ *envoy_service_auth_v3.CheckRequest) *envoy_service_auth_v3.CheckResponse { return resp } + s.handler.Store(&h) +} + +// Handle sets a custom handler that is called for every Check RPC. +func (s *LocalGRPCAuthServer) Handle(h func(*envoy_service_auth_v3.CheckRequest) *envoy_service_auth_v3.CheckResponse) { + s.handler.Store(&h) +} + +type CheckResponseBuilder struct { + resp *envoy_service_auth_v3.CheckResponse +} + +// AllowResponse starts building a CheckResponse that allows the request. +func AllowResponse() *CheckResponseBuilder { + return &CheckResponseBuilder{ + resp: &envoy_service_auth_v3.CheckResponse{ + Status: &rpc_status.Status{Code: int32(codes.OK)}, + HttpResponse: &envoy_service_auth_v3.CheckResponse_OkResponse{ + OkResponse: &envoy_service_auth_v3.OkHttpResponse{}, + }, + }, + } +} + +// DenyResponse starts building a CheckResponse that denies the request with +// the given HTTP status code. +func DenyResponse(httpStatusCode int32) *envoy_service_auth_v3.CheckResponse { + return &envoy_service_auth_v3.CheckResponse{ + Status: &rpc_status.Status{Code: int32(codes.PermissionDenied)}, + HttpResponse: &envoy_service_auth_v3.CheckResponse_DeniedResponse{ + DeniedResponse: &envoy_service_auth_v3.DeniedHttpResponse{ + Status: &envoy_type_v3.HttpStatus{Code: envoy_type_v3.StatusCode(httpStatusCode)}, + }, + }, + } +} + +// Build returns the finished CheckResponse. +func (b *CheckResponseBuilder) Build() *envoy_service_auth_v3.CheckResponse { + return b.resp +} + +// StartLocalGRPCAuthService starts a cleartext gRPC Authorization server on +// CONTOUR_E2E_LOCAL_HOST: and registers it in-cluster as a headless +// Service and EndpointSlice. +// +// CONTOUR_E2E_LOCAL_HOST must be set to a host IP reachable from within the +// cluster. The test is skipped if the variable is absent. +func StartLocalGRPCAuthService(t ginkgo.GinkgoTInterface, c client.Client, ns, name string) *LocalGRPCAuthServer { + hostIP := os.Getenv("CONTOUR_E2E_LOCAL_HOST") + if hostIP == "" { + ginkgo.Skip("CONTOUR_E2E_LOCAL_HOST must be set to a host IP reachable from within the cluster") + } + + srv := &LocalGRPCAuthServer{} + + grpcServer := grpc.NewServer() + envoy_service_auth_v3.RegisterAuthorizationServer(grpcServer, srv) + + listener, err := net.Listen("tcp", net.JoinHostPort(hostIP, "0")) + require.NoError(t, err) + + go func() { + _ = grpcServer.Serve(listener) + }() + t.Cleanup(func() { grpcServer.GracefulStop() }) + + port := listener.Addr().(*net.TCPAddr).Port + createHeadlessService(t, c, ns, name, hostIP, port) + + return srv +} + +// createHeadlessService creates a headless Service and EndpointSlice pointing +// at the given host:port, so in-cluster workloads can reach the local server. +func createHeadlessService(t ginkgo.GinkgoTInterface, c client.Client, ns, name, hostIP string, port int) { + require.NoError(t, c.Create(context.TODO(), &core_v1.Service{ + ObjectMeta: meta_v1.ObjectMeta{Name: name, Namespace: ns}, + Spec: core_v1.ServiceSpec{ + Ports: []core_v1.ServicePort{{Name: "grpc", Protocol: core_v1.ProtocolTCP, Port: 9443}}, + }, + })) + require.NoError(t, c.Create(context.TODO(), &discovery_v1.EndpointSlice{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: name, Namespace: ns, + Labels: map[string]string{discovery_v1.LabelServiceName: name}, + }, + AddressType: discovery_v1.AddressTypeIPv4, + Endpoints: []discovery_v1.Endpoint{ + {Addresses: []string{hostIP}, Conditions: discovery_v1.EndpointConditions{Ready: ptr.To(true)}}, + }, + Ports: []discovery_v1.EndpointPort{ + {Name: ptr.To("grpc"), Port: ptr.To(int32(port)), Protocol: ptr.To(core_v1.ProtocolTCP)}, //nolint:gosec + }, + })) +}