From 8e7b0906fdda69d57d97a280857b8162874fbbbf Mon Sep 17 00:00:00 2001 From: Ciara Stacke Date: Wed, 29 Oct 2025 10:41:34 +0000 Subject: [PATCH] Support mixed externalname and local services in single route --- examples/externalname-service/README.md | 155 +++++++++++++++--- .../backendtlspolicy.yaml | 12 ++ examples/externalname-service/cafe.yaml | 49 ++++++ .../external-service.yaml | 16 -- examples/externalname-service/route.yaml | 46 ++++-- internal/controller/nginx/config/servers.go | 25 ++- .../controller/nginx/config/servers_test.go | 54 +++++- .../state/dataplane/configuration.go | 42 ++++- .../state/dataplane/configuration_test.go | 4 +- internal/controller/state/dataplane/types.go | 5 + 10 files changed, 332 insertions(+), 76 deletions(-) create mode 100644 examples/externalname-service/backendtlspolicy.yaml create mode 100644 examples/externalname-service/cafe.yaml delete mode 100644 examples/externalname-service/external-service.yaml diff --git a/examples/externalname-service/README.md b/examples/externalname-service/README.md index 59e5aae979..425c9b8f56 100644 --- a/examples/externalname-service/README.md +++ b/examples/externalname-service/README.md @@ -7,9 +7,12 @@ This example demonstrates how to use NGINX Gateway Fabric to route traffic to ex In this example, we will: 1. Deploy NGINX Gateway Fabric with DNS resolver configuration -2. Create an ExternalName service pointing to an external API -3. Configure HTTP and TLS routes to route traffic to this external service -4. Test both HTTP and TLS routing functionality +2. Create an ExternalName service pointing to an external API (httpbin.org) +3. Deploy an internal service (coffee) to demonstrate mixed routing +4. Configure an HTTPRoute that routes to both external and internal services +5. (Optional) Configure BackendTLSPolicy for HTTPS connections to external services +6. Test HTTP routing with both external and internal services +7. (Optional) Test HTTPS backend connections and TLS passthrough ## Running the Example @@ -17,6 +20,11 @@ In this example, we will: 1. Follow the [installation instructions](https://docs.nginx.com/nginx-gateway-fabric/install/) to deploy NGINX Gateway Fabric. + **Note**: To use BackendTLSPolicy or TLSRoute for HTTPS connections to external services, you must: + + - Install NGINX Gateway Fabric with `enableExperimental=true` + - Install the experimental Gateway APIs on the cluster + ## 2. Deploy the Gateway with DNS Resolver Create the Gateway and NginxProxy configuration that enables DNS resolution for ExternalName services: @@ -30,19 +38,22 @@ This creates: - A Gateway with HTTP and TLS listeners - An NginxProxy resource with DNS resolver configuration -## 3. Deploy ExternalName Services +## 3. Deploy Services -Create the ExternalName service that points to external APIs: +Create the ExternalName service and internal service: ```shell -kubectl apply -f external-service.yaml +kubectl apply -f cafe.yaml ``` -This creates an ExternalName service which points to `httpbin.org` with both HTTP and HTTPS ports +This creates: + +- An ExternalName service which points to `httpbin.org` with both HTTP and HTTPS ports +- A coffee deployment and service for internal routing ## 4. Configure Routing -Create the HTTPRoute and TLSRoute that route traffic to the external service: +Create the HTTPRoute and TLSRoute that route traffic to the services: ```shell kubectl apply -f route.yaml @@ -50,10 +61,41 @@ kubectl apply -f route.yaml This creates: -- An HTTPRoute that routes HTTP traffic to the httpbin service -- A TLSRoute that routes TLS traffic to the httpbin service +- An HTTPRoute with two paths: + - `/external/*` - Routes to the ExternalName service (httpbin.org) with a URLRewrite filter that strips the `/external` prefix + - `/coffee` - Routes to the internal coffee service +- A TLSRoute for TLS passthrough to the external httpbin service (commented out by default) + +## 5. (Optional) Configure HTTPS Backend + +If your external service requires HTTPS connections (like `https://httpbin.org`), you need to: + +1. Update the HTTPRoute to use port 443 for the httpbin service in `route.yaml`: + + ```yaml + backendRefs: + - name: httpbin + port: 443 # Change from 80 to 443 + ``` -## 5. Test the Configuration +2. Create a BackendTLSPolicy: + + ```shell + kubectl apply -f backendtlspolicy.yaml + ``` + +This configures NGINX Gateway Fabric to: + +- Establish TLS connections to the backend service +- Verify the backend's TLS certificate using system CA certificates +- Match the certificate's hostname against the external service hostname + +**Note**: BackendTLSPolicy is different from TLSRoute: + +- **BackendTLSPolicy**: NGF terminates client TLS/HTTP and establishes HTTPS to the backend. Allows HTTP-level routing (paths, headers, etc.) +- **TLSRoute**: TLS passthrough where the client establishes TLS directly with the backend. No HTTP-level routing possible. + +## 6. Test the Configuration Wait for the Gateway to be ready: @@ -66,43 +108,106 @@ Save the public IP address and ports of the NGINX Service into shell variables: ```text GW_IP=XXX.YYY.ZZZ.III GW_PORT_HTTP= -GW_PORT_HTTPS= ``` -Test the httpbin service via HTTP: +Test the ExternalName service (httpbin.org) via HTTP: ```shell -curl --resolve httpbin.example.com:$GW_PORT_HTTP:$GW_IP http://httpbin.example.com:$GW_PORT_HTTP/get +curl --resolve cafe.example.com:$GW_PORT_HTTP:$GW_IP http://cafe.example.com:$GW_PORT_HTTP/external/get ``` -Test the httpbin service via TLS passthrough: - -```shell -curl -k --resolve httpbin.example.com:$GW_PORT_HTTPS:$GW_IP https://httpbin.example.com:$GW_PORT_HTTPS/get -``` - -You should see a JSON response from httpbin.org showing request details e.g. +You should see a JSON response from httpbin.org. Notice the `Host` header is correctly set to `httpbin.org`: ```json { "args": {}, "headers": { "Accept": "*/*", - "Host": "httpbin.example.com", + "Host": "httpbin.org", "User-Agent": "curl/8.7.1", - "X-Amzn-Trace-Id": "Root=1-68a49086-1e1dabb51155e05c1ebc1f63" + "X-Amzn-Trace-Id": "Root=1-6901e342-2ce43cf518ee439d4e4d4867", + "X-Forwarded-Host": "cafe.example.com" }, "origin": "xxx.xxx.xxx.xx", - "url": "https://httpbin.example.com/get" + "url": "http://cafe.example.com/get" } ``` +Test the internal coffee service: + +```shell +curl --resolve cafe.example.com:$GW_PORT_HTTP:$GW_IP http://cafe.example.com:$GW_PORT_HTTP/coffee +``` + +You should see a response from the coffee service: + +```text +Server address: 10.244.0.7:8080 +Server name: coffee- +Date: ... +URI: /coffee +Request ID: ... +``` + +You can also test other httpbin.org endpoints: + +```shell +# Test /anything endpoint +curl --resolve cafe.example.com:$GW_PORT_HTTP:$GW_IP http://cafe.example.com:$GW_PORT_HTTP/external/anything + +# Test /headers endpoint +curl --resolve cafe.example.com:$GW_PORT_HTTP:$GW_IP http://cafe.example.com:$GW_PORT_HTTP/external/headers +``` + +### Testing HTTPS Backend (Optional) + +If you configured BackendTLSPolicy in step 5, NGF will establish HTTPS connections to the external service. The client connection remains HTTP, but the backend connection uses HTTPS: + +```shell +curl --resolve cafe.example.com:$GW_PORT_HTTP:$GW_IP http://cafe.example.com:$GW_PORT_HTTP/external/get +``` + +The response will show the request was successfully proxied to `https://httpbin.org` with proper TLS verification. + +### Testing TLS Passthrough (Optional) + +To test TLS passthrough, first uncomment the TLSRoute section in `route.yaml` and reapply: + +```shell +kubectl apply -f route.yaml +``` + +Then save the HTTPS port: + +```text +GW_PORT_HTTPS= +``` + +Test the httpbin service via TLS passthrough: + +```shell +curl -k --resolve httpbin.example.com:$GW_PORT_HTTPS:$GW_IP https://httpbin.example.com:$GW_PORT_HTTPS/get +``` + +You should see a JSON response from httpbin.org via HTTPS. + +## How It Works + +This example demonstrates key features for routing to external services: + +1. **DNS Resolution**: The NginxProxy resource configures DNS resolvers (8.8.8.8, 1.1.1.1) so NGINX can resolve external hostnames +2. **Host Header Handling**: NGF automatically detects ExternalName services and sets the `Host` header to the external hostname (`httpbin.org`) instead of the Gateway hostname (`cafe.example.com`), ensuring external services receive the correct Host header +3. **URL Rewriting**: The URLRewrite filter strips the `/external` prefix before proxying to httpbin.org, so `/external/get` becomes `/get` on the external service +4. **Mixed Routing**: The same HTTPRoute can route to both ExternalName services and internal Kubernetes services seamlessly +5. **HTTPS Backends**: BackendTLSPolicy enables secure HTTPS connections to external services while allowing HTTP-level routing based on paths, headers, etc. +6. **TLS Passthrough**: The TLSRoute allows direct TLS connections to external services without termination at the Gateway (no HTTP-level routing) + ## Cleanup Remove all resources created in this example: ```shell kubectl delete -f route.yaml -kubectl delete -f external-service.yaml +kubectl delete -f cafe.yaml kubectl delete -f gateway.yaml ``` diff --git a/examples/externalname-service/backendtlspolicy.yaml b/examples/externalname-service/backendtlspolicy.yaml new file mode 100644 index 0000000000..5b43110e1a --- /dev/null +++ b/examples/externalname-service/backendtlspolicy.yaml @@ -0,0 +1,12 @@ +apiVersion: gateway.networking.k8s.io/v1alpha3 +kind: BackendTLSPolicy +metadata: + name: httpbin-tls +spec: + targetRefs: + - group: "" + kind: Service + name: httpbin + validation: + hostname: httpbin.org + wellKnownCACertificates: System diff --git a/examples/externalname-service/cafe.yaml b/examples/externalname-service/cafe.yaml new file mode 100644 index 0000000000..dc9ea27e1c --- /dev/null +++ b/examples/externalname-service/cafe.yaml @@ -0,0 +1,49 @@ +apiVersion: v1 +kind: Service +metadata: + name: httpbin +spec: + type: ExternalName + externalName: httpbin.org + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: http + - port: 443 + targetPort: 443 + protocol: TCP + name: https +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: coffee +spec: + replicas: 1 + selector: + matchLabels: + app: coffee + template: + metadata: + labels: + app: coffee + spec: + containers: + - name: coffee + image: nginxdemos/nginx-hello:plain-text + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: coffee +spec: + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: coffee diff --git a/examples/externalname-service/external-service.yaml b/examples/externalname-service/external-service.yaml deleted file mode 100644 index 379e569251..0000000000 --- a/examples/externalname-service/external-service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: httpbin -spec: - type: ExternalName - externalName: httpbin.org - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: http - - port: 443 - targetPort: 443 - protocol: TCP - name: https diff --git a/examples/externalname-service/route.yaml b/examples/externalname-service/route.yaml index 774a44bd47..f5e5ec10ca 100644 --- a/examples/externalname-service/route.yaml +++ b/examples/externalname-service/route.yaml @@ -6,27 +6,39 @@ spec: parentRefs: - name: external-gateway hostnames: - - httpbin.example.com + - cafe.example.com rules: - matches: - path: type: PathPrefix - value: / + value: /external + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / backendRefs: - name: httpbin port: 80 - ---- -apiVersion: gateway.networking.k8s.io/v1alpha2 -kind: TLSRoute -metadata: - name: httpbin-tls-route -spec: - parentRefs: - - name: external-gateway - hostnames: - - httpbin.example.com - rules: - - backendRefs: - - name: httpbin - port: 443 + - matches: + - path: + type: PathPrefix + value: /coffee + backendRefs: + - name: coffee + port: 80 +# --- +# apiVersion: gateway.networking.k8s.io/v1alpha2 +# kind: TLSRoute +# metadata: +# name: httpbin-tls-route +# spec: +# parentRefs: +# - name: external-gateway +# hostnames: +# - httpbin.example.com +# rules: +# - backendRefs: +# - name: httpbin +# port: 443 diff --git a/internal/controller/nginx/config/servers.go b/internal/controller/nginx/config/servers.go index 1a9ad995d9..717664c87c 100644 --- a/internal/controller/nginx/config/servers.go +++ b/internal/controller/nginx/config/servers.go @@ -802,7 +802,19 @@ func updateLocationProxySettings( extraHeaders = append(extraHeaders, getConnectionHeader(keepAliveCheck, matchRule.BackendGroup.Backends)) } - proxySetHeaders := generateProxySetHeaders(&matchRule.Filters, createBaseProxySetHeaders(extraHeaders...)) + // Check if we have an ExternalName service backend + var externalHostname string + for _, backend := range matchRule.BackendGroup.Backends { + if backend.ExternalHostname != "" { + externalHostname = backend.ExternalHostname + break + } + } + + proxySetHeaders := generateProxySetHeaders( + &matchRule.Filters, + createBaseProxySetHeaders(externalHostname, extraHeaders...), + ) responseHeaders := generateResponseHeaders(&matchRule.Filters) location.ProxySetHeaders = proxySetHeaders @@ -1283,11 +1295,18 @@ func getRewriteClientIPSettings(rewriteIPConfig dataplane.RewriteClientIPSetting } } -func createBaseProxySetHeaders(extraHeaders ...http.Header) []http.Header { +func createBaseProxySetHeaders(externalHostname string, extraHeaders ...http.Header) []http.Header { + // For ExternalName services, use the external hostname as the Host header + // For regular services, use the Gateway API compliant host header + hostValue := "$gw_api_compliant_host" + if externalHostname != "" { + hostValue = externalHostname + } + baseHeaders := []http.Header{ { Name: "Host", - Value: "$gw_api_compliant_host", + Value: hostValue, }, { Name: "X-Forwarded-For", diff --git a/internal/controller/nginx/config/servers_test.go b/internal/controller/nginx/config/servers_test.go index e5a8ee9132..2e35224362 100644 --- a/internal/controller/nginx/config/servers_test.go +++ b/internal/controller/nginx/config/servers_test.go @@ -20,8 +20,8 @@ import ( ) var ( - httpBaseHeaders = createBaseProxySetHeaders(httpUpgradeHeader, httpConnectionHeader) - grpcBaseHeaders = createBaseProxySetHeaders(grpcAuthorityHeader) + httpBaseHeaders = createBaseProxySetHeaders("", httpUpgradeHeader, httpConnectionHeader) + grpcBaseHeaders = createBaseProxySetHeaders("", grpcAuthorityHeader) alwaysFalseKeepAliveChecker = func(_ string) bool { return false } ) @@ -1814,7 +1814,7 @@ func TestCreateServers(t *testing.T) { { Path: "= /keep-alive-enabled", ProxyPass: "http://test_keep_alive_80$request_uri", - ProxySetHeaders: createBaseProxySetHeaders(httpUpgradeHeader, unsetHTTPConnectionHeader), + ProxySetHeaders: createBaseProxySetHeaders("", httpUpgradeHeader, unsetHTTPConnectionHeader), Type: http.ExternalLocationType, Includes: externalIncludes, }, @@ -4102,7 +4102,7 @@ func TestGenerateProxySetHeaders(t *testing.T) { Value: "$connection_upgrade", }, }, - baseHeaders: createBaseProxySetHeaders(httpUpgradeHeader, httpConnectionHeader), + baseHeaders: createBaseProxySetHeaders("", httpUpgradeHeader, httpConnectionHeader), }, { msg: "header filter with gRPC", @@ -4222,7 +4222,7 @@ func TestCreateBaseProxySetHeaders(t *testing.T) { t.Parallel() g := NewWithT(t) - result := createBaseProxySetHeaders(test.additionalHeaders...) + result := createBaseProxySetHeaders("", test.additionalHeaders...) g.Expect(result).To(Equal(test.expBaseHeaders)) }) } @@ -4589,3 +4589,47 @@ func TestExecuteServers_DisableSNIHostValidation(t *testing.T) { g.Expect(serverConf).NotTo(ContainSubstring("if ($ssl_server_name != $host)"), "Expected SNI host validation block to be absent when DisableSNIHostValidation is true") } + +func TestCreateBaseProxySetHeadersWithExternalName(t *testing.T) { + t.Parallel() + + tests := []struct { + msg string + externalHostname string + expectedHostHeader string + additionalHeaders []http.Header + }{ + { + msg: "ExternalName service with httpbin.org", + externalHostname: "httpbin.org", + additionalHeaders: []http.Header{httpUpgradeHeader, httpConnectionHeader}, + expectedHostHeader: "httpbin.org", + }, + { + msg: "Regular service (no ExternalName)", + externalHostname: "", + additionalHeaders: []http.Header{httpUpgradeHeader, httpConnectionHeader}, + expectedHostHeader: "$gw_api_compliant_host", + }, + } + + for _, test := range tests { + t.Run(test.msg, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + result := createBaseProxySetHeaders(test.externalHostname, test.additionalHeaders...) + + // Find the Host header and verify it has the expected value + var hostHeaderFound bool + for _, header := range result { + if header.Name == "Host" { + hostHeaderFound = true + g.Expect(header.Value).To(Equal(test.expectedHostHeader)) + break + } + } + g.Expect(hostHeaderFound).To(BeTrue(), "Host header should be present") + }) + } +} diff --git a/internal/controller/state/dataplane/configuration.go b/internal/controller/state/dataplane/configuration.go index 92b119e052..88a04a0d54 100644 --- a/internal/controller/state/dataplane/configuration.go +++ b/internal/controller/state/dataplane/configuration.go @@ -54,7 +54,7 @@ func BuildConfiguration( baseHTTPConfig := buildBaseHTTPConfig(gateway, gatewaySnippetsFilters) baseStreamConfig := buildBaseStreamConfig(gateway) - httpServers, sslServers := buildServers(gateway) + httpServers, sslServers := buildServers(gateway, g.ReferencedServices) backendGroups := buildBackendGroups(append(httpServers, sslServers...)) upstreams := buildUpstreams( ctx, @@ -374,6 +374,7 @@ func newBackendGroup( gatewayName types.NamespacedName, sourceNsName types.NamespacedName, ruleIdx int, + referencedServices map[types.NamespacedName]*graph.ReferencedService, ) (BackendGroup, bool) { var backends []Backend @@ -402,12 +403,16 @@ func newBackendGroup( } } + // Check if this backend is an ExternalName service + externalHostname := getExternalHostname(ref.SvcNsName, referencedServices) + backends = append(backends, Backend{ UpstreamName: ref.ServicePortReference(), Weight: ref.Weight, Valid: valid, VerifyTLS: convertBackendTLS(ref.BackendTLSPolicy, gatewayName), EndpointPickerConfig: eppRef, + ExternalHostname: externalHostname, }) } @@ -437,7 +442,10 @@ func convertBackendTLS(btp *graph.BackendTLSPolicy, gwNsName types.NamespacedNam return verify } -func buildServers(gateway *graph.Gateway) (http, ssl []VirtualServer) { +func buildServers( + gateway *graph.Gateway, + referencedServices map[types.NamespacedName]*graph.ReferencedService, +) (http, ssl []VirtualServer) { rulesForProtocol := map[v1.ProtocolType]portPathRules{ v1.HTTPProtocolType: make(portPathRules), v1.HTTPSProtocolType: make(portPathRules), @@ -454,7 +462,7 @@ func buildServers(gateway *graph.Gateway) (http, ssl []VirtualServer) { rulesForProtocol[l.Source.Protocol][l.Source.Port] = rules } - rules.upsertListener(l, gateway) + rules.upsertListener(l, gateway, referencedServices) } } @@ -515,7 +523,11 @@ func newHostPathRules() *hostPathRules { } } -func (hpr *hostPathRules) upsertListener(l *graph.Listener, gateway *graph.Gateway) { +func (hpr *hostPathRules) upsertListener( + l *graph.Listener, + gateway *graph.Gateway, + referencedServices map[types.NamespacedName]*graph.ReferencedService, +) { hpr.listenersExist = true hpr.port = int32(l.Source.Port) @@ -528,7 +540,7 @@ func (hpr *hostPathRules) upsertListener(l *graph.Listener, gateway *graph.Gatew continue } - hpr.upsertRoute(r, l, gateway) + hpr.upsertRoute(r, l, gateway, referencedServices) } } @@ -536,6 +548,7 @@ func (hpr *hostPathRules) upsertRoute( route *graph.L7Route, listener *graph.Listener, gateway *graph.Gateway, + referencedServices map[types.NamespacedName]*graph.ReferencedService, ) { var hostnames []string GRPC := route.RouteType == graph.RouteTypeGRPC @@ -612,6 +625,7 @@ func (hpr *hostPathRules) upsertRoute( listener.GatewayName, routeNsName, idx, + referencedServices, ) if inferencePoolBackendExists { hostRule.HasInferenceBackends = true @@ -1292,6 +1306,18 @@ func buildDNSResolverConfig(dnsResolver *ngfAPIv1alpha2.DNSResolver) *DNSResolve return config } +// getExternalHostname returns the external hostname if the service is an ExternalName type. +// Returns an empty string if the service is not found or is not an ExternalName service. +func getExternalHostname( + svcNsName types.NamespacedName, + referencedServices map[types.NamespacedName]*graph.ReferencedService, +) string { + if graphSvc, exists := referencedServices[svcNsName]; exists && graphSvc.IsExternalName { + return graphSvc.ExternalName + } + return "" +} + // resolveUpstreamEndpoints handles service resolution for both regular and ExternalName services. func resolveUpstreamEndpoints( ctx context.Context, @@ -1302,10 +1328,10 @@ func resolveUpstreamEndpoints( allowedAddressType []discoveryV1.AddressType, ) ([]resolver.Endpoint, error) { // Check if this is an ExternalName service - if graphSvc, exists := referencedServices[br.SvcNsName]; exists && graphSvc.IsExternalName { + if externalName := getExternalHostname(br.SvcNsName, referencedServices); externalName != "" { // For ExternalName services, create an endpoint directly with the external name endpoint := resolver.Endpoint{ - Address: graphSvc.ExternalName, + Address: externalName, Port: br.ServicePort.Port, IPv6: false, // DNS names are neither IPv4 nor IPv6 Resolve: true, // ExternalName services require DNS resolution @@ -1313,7 +1339,7 @@ func resolveUpstreamEndpoints( logger.V(1).Info("resolved ExternalName service", "service", br.SvcNsName, - "externalName", graphSvc.ExternalName, + "externalName", externalName, "port", br.ServicePort.Port) return []resolver.Endpoint{endpoint}, nil diff --git a/internal/controller/state/dataplane/configuration_test.go b/internal/controller/state/dataplane/configuration_test.go index 3e1697590d..6e9a529048 100644 --- a/internal/controller/state/dataplane/configuration_test.go +++ b/internal/controller/state/dataplane/configuration_test.go @@ -2849,7 +2849,7 @@ func TestUpsertRoute_PathRuleHasInferenceBackend(t *testing.T) { } hpr := newHostPathRules() - hpr.upsertRoute(route, listener, gateway) + hpr.upsertRoute(route, listener, gateway, nil) // Find the PathRule for "/infer" found := false @@ -2875,7 +2875,7 @@ func TestNewBackendGroup_Mirror(t *testing.T) { IsMirrorBackend: true, } - group, _ := newBackendGroup([]graph.BackendRef{backendRef}, types.NamespacedName{}, types.NamespacedName{}, 0) + group, _ := newBackendGroup([]graph.BackendRef{backendRef}, types.NamespacedName{}, types.NamespacedName{}, 0, nil) g.Expect(group.Backends).To(BeEmpty()) } diff --git a/internal/controller/state/dataplane/types.go b/internal/controller/state/dataplane/types.go index 76dd970205..987c021c7f 100644 --- a/internal/controller/state/dataplane/types.go +++ b/internal/controller/state/dataplane/types.go @@ -333,6 +333,11 @@ type Backend struct { EndpointPickerConfig *EndpointPickerConfig // UpstreamName is the name of the upstream for this backend. UpstreamName string + // ExternalHostname is the external hostname for ExternalName type services. + // This is used to set the Host header when proxying to external services. + // Note: The upstream address is also set to this hostname (see resolveUpstreamEndpoints). + // Both the Host header and upstream address use the same external hostname to ensure consistency. + ExternalHostname string // Weight is the weight of the BackendRef. // The possible values of weight are 0-1,000,000. // If weight is 0, no traffic should be forwarded for this entry.