diff --git a/Makefile b/Makefile index f26c1aef5e..af7cda6c20 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ MANIFEST_DIR = $(CURDIR)/deploy/manifests CHART_DIR = $(SELF_DIR)charts/nginx-gateway-fabric NGINX_CONF_DIR = internal/mode/static/nginx/conf NJS_DIR = internal/mode/static/nginx/modules/src +KIND_CONFIG_FILE = $(SELF_DIR)config/cluster/kind-cluster.yaml NGINX_DOCKER_BUILD_PLUS_ARGS = --secret id=nginx-repo.crt,src=nginx-repo.crt --secret id=nginx-repo.key,src=nginx-repo.key BUILD_AGENT=local PLUS_ENABLED ?= false @@ -160,7 +161,7 @@ deps: ## Add missing and remove unused modules, verify deps and download them to .PHONY: create-kind-cluster create-kind-cluster: ## Create a kind cluster $(eval KIND_IMAGE=$(shell grep -m1 'FROM kindest/node' <$(SELF_DIR)tests/Dockerfile | awk -F'[ ]' '{print $$2}')) - kind create cluster --image $(KIND_IMAGE) + kind create cluster --image $(KIND_IMAGE) --config $(KIND_CONFIG_FILE) .PHONY: delete-kind-cluster delete-kind-cluster: ## Delete kind cluster diff --git a/apis/v1alpha1/nginxproxy_types.go b/apis/v1alpha1/nginxproxy_types.go index 2c2a125946..018911da7e 100644 --- a/apis/v1alpha1/nginxproxy_types.go +++ b/apis/v1alpha1/nginxproxy_types.go @@ -27,8 +27,28 @@ type NginxProxyList struct { Items []NginxProxy `json:"items"` } +// IPFamilyType specifies the IP family to be used by NGINX. +// +// +kubebuilder:validation:Enum=dual;ipv4;ipv6 +type IPFamilyType string + +const ( + // Dual specifies that NGINX will use both IPv4 and IPv6. + Dual IPFamilyType = "dual" + // IPv4 specifies that NGINX will use only IPv4. + IPv4 IPFamilyType = "ipv4" + // IPv6 specifies that NGINX will use only IPv6. + IPv6 IPFamilyType = "ipv6" +) + // NginxProxySpec defines the desired state of the NginxProxy. type NginxProxySpec struct { + // IPFamily specifies the IP family to be used by the NGINX. + // Default is "dual", meaning the server will use both IPv4 and IPv6. + // + // +optional + // +kubebuilder:default:=dual + IPFamily *IPFamilyType `json:"ipFamily,omitempty"` // Telemetry specifies the OpenTelemetry configuration. // // +optional diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index a341140561..90cdca7a41 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -357,6 +357,11 @@ func (in *NginxProxyList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NginxProxySpec) DeepCopyInto(out *NginxProxySpec) { *out = *in + if in.IPFamily != nil { + in, out := &in.IPFamily, &out.IPFamily + *out = new(IPFamilyType) + **out = **in + } if in.Telemetry != nil { in, out := &in.Telemetry, &out.Telemetry *out = new(Telemetry) diff --git a/charts/nginx-gateway-fabric/values.yaml b/charts/nginx-gateway-fabric/values.yaml index 827d414fab..9cfc2064b2 100644 --- a/charts/nginx-gateway-fabric/values.yaml +++ b/charts/nginx-gateway-fabric/values.yaml @@ -92,6 +92,7 @@ nginx: config: {} # disableHTTP2: false + # ipFamily: dual # telemetry: # exporter: # endpoint: otel-collector.default.svc:4317 diff --git a/config/cluster/kind-cluster.yaml b/config/cluster/kind-cluster.yaml new file mode 100644 index 0000000000..d61c10ca6f --- /dev/null +++ b/config/cluster/kind-cluster.yaml @@ -0,0 +1,7 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: +- role: control-plane +networking: + ipFamily: dual + apiServerAddress: 127.0.0.1 diff --git a/config/crd/bases/gateway.nginx.org_nginxproxies.yaml b/config/crd/bases/gateway.nginx.org_nginxproxies.yaml index b42d562f75..73acae5e84 100644 --- a/config/crd/bases/gateway.nginx.org_nginxproxies.yaml +++ b/config/crd/bases/gateway.nginx.org_nginxproxies.yaml @@ -52,6 +52,16 @@ spec: DisableHTTP2 defines if http2 should be disabled for all servers. Default is false, meaning http2 will be enabled for all servers. type: boolean + ipFamily: + default: dual + description: |- + IPFamily specifies the IP family to be used by the NGINX. + Default is "dual", meaning the server will use both IPv4 and IPv6. + enum: + - dual + - ipv4 + - ipv6 + type: string telemetry: description: Telemetry specifies the OpenTelemetry configuration. properties: diff --git a/deploy/crds.yaml b/deploy/crds.yaml index 51ae8b34bf..547c912748 100644 --- a/deploy/crds.yaml +++ b/deploy/crds.yaml @@ -697,6 +697,16 @@ spec: DisableHTTP2 defines if http2 should be disabled for all servers. Default is false, meaning http2 will be enabled for all servers. type: boolean + ipFamily: + default: dual + description: |- + IPFamily specifies the IP family to be used by the NGINX. + Default is "dual", meaning the server will use both IPv4 and IPv6. + enum: + - dual + - ipv4 + - ipv6 + type: string telemetry: description: Telemetry specifies the OpenTelemetry configuration. properties: diff --git a/docs/developer/quickstart.md b/docs/developer/quickstart.md index 448123a973..e64706b174 100644 --- a/docs/developer/quickstart.md +++ b/docs/developer/quickstart.md @@ -134,10 +134,24 @@ This will build the docker images `nginx-gateway-fabric:` and `nginx- 1. Create a `kind` cluster: + To create a `kind` cluster with dual (IPv4 and IPv6) enabled: + ```makefile make create-kind-cluster ``` + To create a `kind` cluster with IPv6 or IPv4 only, edit kind cluster config located at `nginx-gateway-fabric/config/cluster/kind-cluster.yaml`: + + ```yaml + kind: Cluster + apiVersion: kind.x-k8s.io/v1alpha4 + nodes: + - role: control-plane + networking: + ipFamily: ipv6 + apiServerAddress: 127.0.0.1 + ``` + 2. Load the previously built images onto your `kind` cluster: ```shell diff --git a/internal/mode/static/nginx/config/http/config.go b/internal/mode/static/nginx/config/http/config.go index 19e7db223d..9326ebb439 100644 --- a/internal/mode/static/nginx/config/http/config.go +++ b/internal/mode/static/nginx/config/http/config.go @@ -12,6 +12,12 @@ type Server struct { GRPC bool } +// IPFamily holds the IP family configuration to be used by NGINX. +type IPFamily struct { + IPv4 bool + IPv6 bool +} + // Location holds all configuration for an HTTP location. type Location struct { Path string @@ -106,3 +112,9 @@ type ProxySSLVerify struct { TrustedCertificate string Name string } + +// ServerConfig holds configuration for an HTTP server and IP family to be used by NGINX. +type ServerConfig struct { + Servers []Server + IPFamily IPFamily +} diff --git a/internal/mode/static/nginx/config/servers.go b/internal/mode/static/nginx/config/servers.go index 54bbe709ed..3aeefa47c7 100644 --- a/internal/mode/static/nginx/config/servers.go +++ b/internal/mode/static/nginx/config/servers.go @@ -60,9 +60,14 @@ var grpcBaseHeaders = []http.Header{ func executeServers(conf dataplane.Configuration) []executeResult { servers, httpMatchPairs := createServers(conf.HTTPServers, conf.SSLServers) + serverConfig := http.ServerConfig{ + Servers: servers, + IPFamily: getIPFamily(conf.BaseHTTPConfig), + } + serverResult := executeResult{ dest: httpConfigFile, - data: helpers.MustExecuteTemplate(serversTemplate, servers), + data: helpers.MustExecuteTemplate(serversTemplate, serverConfig), } // create httpMatchPair conf @@ -86,6 +91,18 @@ func executeServers(conf dataplane.Configuration) []executeResult { return allResults } +// getIPFamily returns whether the server should be configured for IPv4, IPv6, or both. +func getIPFamily(baseHTTPConfig dataplane.BaseHTTPConfig) http.IPFamily { + switch baseHTTPConfig.IPFamily { + case dataplane.IPv4: + return http.IPFamily{IPv4: true} + case dataplane.IPv6: + return http.IPFamily{IPv6: true} + } + + return http.IPFamily{IPv4: true, IPv6: true} +} + func createAdditionFileResults(conf dataplane.Configuration) []executeResult { uniqueAdditions := make(map[string][]byte) diff --git a/internal/mode/static/nginx/config/servers_template.go b/internal/mode/static/nginx/config/servers_template.go index 465bd6fc81..4d8196b180 100644 --- a/internal/mode/static/nginx/config/servers_template.go +++ b/internal/mode/static/nginx/config/servers_template.go @@ -2,16 +2,26 @@ package config const serversTemplateText = ` js_preload_object matches from /etc/nginx/conf.d/matches.json; -{{- range $s := . -}} +{{- range $s := .Servers -}} {{ if $s.IsDefaultSSL -}} server { + {{- if $.IPFamily.IPv4 }} listen {{ $s.Port }} ssl default_server; + {{- end }} + {{- if $.IPFamily.IPv6 }} + listen [::]:{{ $s.Port }} ssl default_server; + {{- end }} ssl_reject_handshake on; } {{- else if $s.IsDefaultHTTP }} server { + {{- if $.IPFamily.IPv4 }} listen {{ $s.Port }} default_server; + {{- end }} + {{- if $.IPFamily.IPv6 }} + listen [::]:{{ $s.Port }} default_server; + {{- end }} default_type text/html; return 404; @@ -19,7 +29,12 @@ server { {{- else }} server { {{- if $s.SSL }} + {{- if $.IPFamily.IPv4 }} listen {{ $s.Port }} ssl; + {{- end }} + {{- if $.IPFamily.IPv6 }} + listen [::]:{{ $s.Port }} ssl; + {{- end }} ssl_certificate {{ $s.SSL.Certificate }}; ssl_certificate_key {{ $s.SSL.CertificateKey }}; @@ -27,7 +42,12 @@ server { return 421; } {{- else }} + {{- if $.IPFamily.IPv4 }} listen {{ $s.Port }}; + {{- end }} + {{- if $.IPFamily.IPv6 }} + listen [::]:{{ $s.Port }}; + {{- end }} {{- end }} server_name {{ $s.ServerName }}; diff --git a/internal/mode/static/nginx/config/servers_test.go b/internal/mode/static/nginx/config/servers_test.go index ae458e8f03..effb0099ab 100644 --- a/internal/mode/static/nginx/config/servers_test.go +++ b/internal/mode/static/nginx/config/servers_test.go @@ -137,6 +137,117 @@ func TestExecuteServers(t *testing.T) { } } +func TestExecuteServersForIPFamily(t *testing.T) { + httpServers := []dataplane.VirtualServer{ + { + IsDefault: true, + Port: 8080, + }, + { + Hostname: "example.com", + Port: 8080, + }, + } + sslServers := []dataplane.VirtualServer{ + { + IsDefault: true, + Port: 8443, + }, + { + Hostname: "example.com", + SSL: &dataplane.SSL{ + KeyPairID: "test-keypair", + }, + Port: 8443, + }, + } + tests := []struct { + msg string + expectedHTTPConfig map[string]int + config dataplane.Configuration + }{ + { + msg: "http and ssl servers with IPv4 IP family", + config: dataplane.Configuration{ + HTTPServers: httpServers, + SSLServers: sslServers, + BaseHTTPConfig: dataplane.BaseHTTPConfig{ + IPFamily: dataplane.IPv4, + }, + }, + expectedHTTPConfig: map[string]int{ + "listen 8080 default_server;": 1, + "listen 8080;": 1, + "listen 8443 ssl default_server;": 1, + "listen 8443 ssl;": 1, + "server_name example.com;": 2, + "ssl_certificate /etc/nginx/secrets/test-keypair.pem;": 1, + "ssl_certificate_key /etc/nginx/secrets/test-keypair.pem;": 1, + "ssl_reject_handshake on;": 1, + }, + }, + { + msg: "http and ssl servers with IPv6 IP family", + config: dataplane.Configuration{ + HTTPServers: httpServers, + SSLServers: sslServers, + BaseHTTPConfig: dataplane.BaseHTTPConfig{ + IPFamily: dataplane.IPv6, + }, + }, + expectedHTTPConfig: map[string]int{ + "listen [::]:8080 default_server;": 1, + "listen [::]:8080;": 1, + "listen [::]:8443 ssl default_server;": 1, + "listen [::]:8443 ssl;": 1, + "server_name example.com;": 2, + "ssl_certificate /etc/nginx/secrets/test-keypair.pem;": 1, + "ssl_certificate_key /etc/nginx/secrets/test-keypair.pem;": 1, + "ssl_reject_handshake on;": 1, + }, + }, + { + msg: "http and ssl servers with Dual IP family", + config: dataplane.Configuration{ + HTTPServers: httpServers, + SSLServers: sslServers, + BaseHTTPConfig: dataplane.BaseHTTPConfig{ + IPFamily: dataplane.Dual, + }, + }, + expectedHTTPConfig: map[string]int{ + "listen 8080 default_server;": 1, + "listen 8080;": 1, + "listen 8443 ssl default_server;": 1, + "listen 8443 ssl;": 1, + "server_name example.com;": 2, + "ssl_certificate /etc/nginx/secrets/test-keypair.pem;": 1, + "ssl_certificate_key /etc/nginx/secrets/test-keypair.pem;": 1, + "ssl_reject_handshake on;": 1, + "listen [::]:8080 default_server;": 1, + "listen [::]:8080;": 1, + "listen [::]:8443 ssl default_server;": 1, + "listen [::]:8443 ssl;": 1, + }, + }, + } + + for _, test := range tests { + t.Run(test.msg, func(t *testing.T) { + g := NewWithT(t) + results := executeServers(test.config) + g.Expect(results).To(HaveLen(2)) + serverConf := string(results[0].data) + httpMatchConf := string(results[1].data) + g.Expect(httpMatchConf).To(Equal("{}")) + + for expSubStr, expCount := range test.expectedHTTPConfig { + g.Expect(strings.Count(serverConf, expSubStr)).To(Equal(expCount)) + } + }) + } +} + func TestExecuteForDefaultServers(t *testing.T) { testcases := []struct { msg string @@ -2515,3 +2626,35 @@ func TestAdditionFilename(t *testing.T) { name := createAdditionFileName(dataplane.Addition{Identifier: "my-addition"}) g.Expect(name).To(Equal(includesFolder + "/" + "my-addition.conf")) } + +func TestGetIPFamily(t *testing.T) { + test := []struct { + msg string + baseHTTPConfig dataplane.BaseHTTPConfig + expected http.IPFamily + }{ + { + msg: "ipv4", + baseHTTPConfig: dataplane.BaseHTTPConfig{IPFamily: dataplane.IPv4}, + expected: http.IPFamily{IPv4: true, IPv6: false}, + }, + { + msg: "ipv6", + baseHTTPConfig: dataplane.BaseHTTPConfig{IPFamily: dataplane.IPv6}, + expected: http.IPFamily{IPv4: false, IPv6: true}, + }, + { + msg: "dual", + baseHTTPConfig: dataplane.BaseHTTPConfig{IPFamily: dataplane.Dual}, + expected: http.IPFamily{IPv4: true, IPv6: true}, + }, + } + + for _, tc := range test { + t.Run(tc.msg, func(t *testing.T) { + g := NewWithT(t) + result := getIPFamily(tc.baseHTTPConfig) + g.Expect(result).To(Equal(tc.expected)) + }) + } +} diff --git a/internal/mode/static/nginx/config/upstreams.go b/internal/mode/static/nginx/config/upstreams.go index 2e1906234e..a76ee23a6a 100644 --- a/internal/mode/static/nginx/config/upstreams.go +++ b/internal/mode/static/nginx/config/upstreams.go @@ -68,8 +68,12 @@ func (g GeneratorImpl) createUpstream(up dataplane.Upstream) http.Upstream { upstreamServers := make([]http.UpstreamServer, len(up.Endpoints)) for idx, ep := range up.Endpoints { + format := "%s:%d" + if ep.IPv6 { + format = "[%s]:%d" + } upstreamServers[idx] = http.UpstreamServer{ - Address: fmt.Sprintf("%s:%d", ep.Address, ep.Port), + Address: fmt.Sprintf(format, ep.Address, ep.Port), } } diff --git a/internal/mode/static/nginx/config/upstreams_test.go b/internal/mode/static/nginx/config/upstreams_test.go index b0a631858d..9b6dbbf7ec 100644 --- a/internal/mode/static/nginx/config/upstreams_test.go +++ b/internal/mode/static/nginx/config/upstreams_test.go @@ -35,15 +35,27 @@ func TestExecuteUpstreams(t *testing.T) { Name: "up3", Endpoints: []resolver.Endpoint{}, }, + { + Name: "up4-ipv6", + Endpoints: []resolver.Endpoint{ + { + Address: "2001:db8::1", + Port: 80, + IPv6: true, + }, + }, + }, } expectedSubStrings := []string{ "upstream up1", "upstream up2", "upstream up3", + "upstream up4-ipv6", "upstream invalid-backend-ref", "server 10.0.0.0:80;", "server 11.0.0.0:80;", + "server [2001:db8::1]:80", "server unix:/var/run/nginx/nginx-502-server.sock;", } @@ -91,6 +103,16 @@ func TestCreateUpstreams(t *testing.T) { Name: "up3", Endpoints: []resolver.Endpoint{}, }, + { + Name: "up4-ipv6", + Endpoints: []resolver.Endpoint{ + { + Address: "fd00:10:244:1::7", + Port: 80, + IPv6: true, + }, + }, + }, } expUpstreams := []http.Upstream{ @@ -127,6 +149,15 @@ func TestCreateUpstreams(t *testing.T) { }, }, }, + { + Name: "up4-ipv6", + ZoneSize: ossZoneSize, + Servers: []http.UpstreamServer{ + { + Address: "[fd00:10:244:1::7]:80", + }, + }, + }, { Name: invalidBackendRef, Servers: []http.UpstreamServer{ @@ -216,6 +247,28 @@ func TestCreateUpstream(t *testing.T) { }, msg: "multiple endpoints", }, + { + stateUpstream: dataplane.Upstream{ + Name: "endpoint-ipv6", + Endpoints: []resolver.Endpoint{ + { + Address: "fd00:10:244:1::7", + Port: 80, + IPv6: true, + }, + }, + }, + expectedUpstream: http.Upstream{ + Name: "endpoint-ipv6", + ZoneSize: ossZoneSize, + Servers: []http.UpstreamServer{ + { + Address: "[fd00:10:244:1::7]:80", + }, + }, + }, + msg: "endpoint ipv6", + }, } for _, test := range tests { diff --git a/internal/mode/static/state/conditions/conditions.go b/internal/mode/static/state/conditions/conditions.go index bb53854c68..5b075a6446 100644 --- a/internal/mode/static/state/conditions/conditions.go +++ b/internal/mode/static/state/conditions/conditions.go @@ -40,6 +40,11 @@ const ( // Used with Accepted (false). RouteReasonUnsupportedConfiguration v1.RouteConditionReason = "UnsupportedConfiguration" + // RouteReasonInvalidIPFamily is used when the Service associated with the Route is not configured with + // the same IP family as the NGINX server. + // Used with ResolvedRefs (false). + RouteReasonInvalidIPFamily v1.RouteConditionReason = "InvalidServiceIPFamily" + // GatewayReasonGatewayConflict indicates there are multiple Gateway resources to choose from, // and we ignored the resource in question and picked another Gateway as the winner. // This reason is used with GatewayConditionAccepted (false). @@ -276,6 +281,17 @@ func NewRouteGatewayNotProgrammed(msg string) conditions.Condition { } } +// NewRouteInvalidIPFamily returns a Condition that indicates that the Service associated with the Route +// is not configured with the same IP family as the NGINX server. +func NewRouteInvalidIPFamily(msg string) conditions.Condition { + return conditions.Condition{ + Type: string(v1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + Reason: string(RouteReasonInvalidIPFamily), + Message: msg, + } +} + // NewDefaultListenerConditions returns the default Conditions that must be present in the status of a Listener. func NewDefaultListenerConditions() []conditions.Condition { return []conditions.Condition{ diff --git a/internal/mode/static/state/dataplane/configuration.go b/internal/mode/static/state/dataplane/configuration.go index 56569aa432..b81dbfe7a0 100644 --- a/internal/mode/static/state/dataplane/configuration.go +++ b/internal/mode/static/state/dataplane/configuration.go @@ -7,6 +7,7 @@ import ( "sort" apiv1 "k8s.io/api/core/v1" + discoveryV1 "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -41,13 +42,13 @@ func BuildConfiguration( return Configuration{Version: configVersion} } - upstreams := buildUpstreams(ctx, g.Gateway.Listeners, resolver) + baseHTTPConfig := buildBaseHTTPConfig(g) + upstreams := buildUpstreams(ctx, g.Gateway.Listeners, resolver, baseHTTPConfig.IPFamily) httpServers, sslServers := buildServers(g, generator) backendGroups := buildBackendGroups(append(httpServers, sslServers...)) keyPairs := buildSSLKeyPairs(g.ReferencedSecrets, g.Gateway.Listeners) certBundles := buildCertBundles(g.ReferencedCaCertConfigMaps, backendGroups) telemetry := buildTelemetry(g) - baseHTTPConfig := buildBaseHTTPConfig(g) config := Configuration{ HTTPServers: httpServers, @@ -472,11 +473,15 @@ func buildUpstreams( ctx context.Context, listeners []*graph.Listener, resolver resolver.ServiceResolver, + ipFamily IPFamilyType, ) []Upstream { // There can be duplicate upstreams if multiple routes reference the same upstream. // We use a map to deduplicate them. uniqueUpstreams := make(map[string]Upstream) + // We need to build endpoints based on the IPFamily of NGINX. + allowedAddressType := getAllowedAddressType(ipFamily) + for _, l := range listeners { if !l.Valid { continue @@ -503,7 +508,7 @@ func buildUpstreams( var errMsg string - eps, err := resolver.Resolve(ctx, br.SvcNsName, br.ServicePort) + eps, err := resolver.Resolve(ctx, br.SvcNsName, br.ServicePort, allowedAddressType) if err != nil { errMsg = err.Error() } @@ -531,6 +536,19 @@ func buildUpstreams( return upstreams } +func getAllowedAddressType(ipFamily IPFamilyType) []discoveryV1.AddressType { + switch ipFamily { + case IPv4: + return []discoveryV1.AddressType{discoveryV1.AddressTypeIPv4} + case IPv6: + return []discoveryV1.AddressType{discoveryV1.AddressTypeIPv6} + case Dual: + return []discoveryV1.AddressType{discoveryV1.AddressTypeIPv4, discoveryV1.AddressTypeIPv6} + default: + return []discoveryV1.AddressType{} + } +} + func getListenerHostname(h *v1.Hostname) string { if h == nil || *h == "" { return wildcardHostname @@ -658,7 +676,8 @@ func buildTelemetry(g *graph.Graph) Telemetry { func buildBaseHTTPConfig(g *graph.Graph) BaseHTTPConfig { baseConfig := BaseHTTPConfig{ // HTTP2 should be enabled by default - HTTP2: true, + HTTP2: true, + IPFamily: Dual, } if g.NginxProxy == nil || !g.NginxProxy.Valid { return baseConfig @@ -668,6 +687,15 @@ func buildBaseHTTPConfig(g *graph.Graph) BaseHTTPConfig { baseConfig.HTTP2 = false } + if g.NginxProxy.Source.Spec.IPFamily != nil { + switch *g.NginxProxy.Source.Spec.IPFamily { + case ngfAPI.IPv4: + baseConfig.IPFamily = IPv4 + case ngfAPI.IPv6: + baseConfig.IPFamily = IPv6 + } + } + return baseConfig } diff --git a/internal/mode/static/state/dataplane/configuration_test.go b/internal/mode/static/state/dataplane/configuration_test.go index e053a42d96..83d5873ce0 100644 --- a/internal/mode/static/state/dataplane/configuration_test.go +++ b/internal/mode/static/state/dataplane/configuration_test.go @@ -9,6 +9,7 @@ import ( . "github.com/onsi/gomega" apiv1 "k8s.io/api/core/v1" + discoveryV1 "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" @@ -26,6 +27,64 @@ import ( "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/resolver/resolverfakes" ) +func getNormalBackendRef() graph.BackendRef { + return graph.BackendRef{ + SvcNsName: types.NamespacedName{Name: "foo", Namespace: "test"}, + ServicePort: apiv1.ServicePort{Port: 80}, + Valid: true, + Weight: 1, + } +} + +func getExpectedConfiguration() Configuration { + return Configuration{ + BaseHTTPConfig: BaseHTTPConfig{HTTP2: true, IPFamily: Dual}, + HTTPServers: []VirtualServer{{ + IsDefault: true, + Port: 80, + }}, + SSLServers: []VirtualServer{ + { + IsDefault: true, + Port: 443, + }, + }, + Upstreams: []Upstream{}, + BackendGroups: []BackendGroup{}, + SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{ + "ssl_keypair_test_secret-1": { + Cert: []byte("cert-1"), + Key: []byte("privateKey-1"), + }, + }, + CertBundles: map[CertBundleID]CertBundle{}, + } +} + +func getNormalGraph() *graph.Graph { + return &graph.Graph{ + GatewayClass: &graph.GatewayClass{ + Source: &v1.GatewayClass{}, + Valid: true, + }, + Gateway: &graph.Gateway{ + Source: &v1.Gateway{}, + Listeners: []*graph.Listener{}, + }, + Routes: map[graph.RouteKey]*graph.L7Route{}, + ReferencedSecrets: map[types.NamespacedName]*graph.Secret{}, + ReferencedCaCertConfigMaps: map[types.NamespacedName]*graph.CaCertConfigMap{}, + } +} + +func getModifiedGraph(mod func(g *graph.Graph) *graph.Graph) *graph.Graph { + return mod(getNormalGraph()) +} + +func getModifiedExpectedConfiguration(mod func(conf Configuration) Configuration) Configuration { + return mod(getExpectedConfiguration()) +} + func createFakePolicy(name string, kind string) policies.Policy { fakeKind := &policiesfakes.FakeObjectKind{ GroupVersionKindStub: func() schema.GroupVersionKind { @@ -120,12 +179,7 @@ func TestBuildConfiguration(t *testing.T) { fakeResolver := &resolverfakes.FakeServiceResolver{} fakeResolver.ResolveReturns(fooEndpoints, nil) - validBackendRef := graph.BackendRef{ - SvcNsName: types.NamespacedName{Name: "foo", Namespace: "test"}, - ServicePort: apiv1.ServicePort{Port: 80}, - Valid: true, - Weight: 1, - } + validBackendRef := getNormalBackendRef() expValidBackend := Backend{ UpstreamName: fooUpstreamName, @@ -633,6 +687,27 @@ func TestBuildConfiguration(t *testing.T) { ServiceName: helpers.GetPointer("my-svc"), }, DisableHTTP2: true, + IPFamily: helpers.GetPointer(ngfAPI.Dual), + }, + }, + Valid: true, + } + + nginxProxyIPv4 := &graph.NginxProxy{ + Source: &ngfAPI.NginxProxy{ + Spec: ngfAPI.NginxProxySpec{ + Telemetry: &ngfAPI.Telemetry{}, + IPFamily: helpers.GetPointer(ngfAPI.IPv4), + }, + }, + Valid: true, + } + + nginxProxyIPv6 := &graph.NginxProxy{ + Source: &ngfAPI.NginxProxy{ + Spec: ngfAPI.NginxProxySpec{ + Telemetry: &ngfAPI.Telemetry{}, + IPFamily: helpers.GetPointer(ngfAPI.IPv6), }, }, Valid: true, @@ -644,161 +719,97 @@ func TestBuildConfiguration(t *testing.T) { expConf Configuration }{ { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, - Valid: true, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{}, - Listeners: []*graph.Listener{}, - }, - Routes: map[graph.RouteKey]*graph.L7Route{}, - }, - expConf: Configuration{ - HTTPServers: []VirtualServer{}, - SSLServers: []VirtualServer{}, - SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{}, - CertBundles: map[CertBundleID]CertBundle{}, - BaseHTTPConfig: BaseHTTPConfig{HTTP2: true}, - }, + graph: getNormalGraph(), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.HTTPServers = []VirtualServer{} + conf.SSLServers = []VirtualServer{} + conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{} + return conf + }), msg: "no listeners and routes", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ + Name: "listener-80-1", + Source: listener80, Valid: true, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{}, - Listeners: []*graph.Listener{ - { - Name: "listener-80-1", - Source: listener80, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{}, - }, - }, - }, - Routes: map[graph.RouteKey]*graph.L7Route{}, - }, - expConf: Configuration{ - HTTPServers: []VirtualServer{ - { - IsDefault: true, - Port: 80, - }, - }, - SSLServers: []VirtualServer{}, - SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{}, - CertBundles: map[CertBundleID]CertBundle{}, - BaseHTTPConfig: BaseHTTPConfig{HTTP2: true}, - }, + }) + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.SSLServers = []VirtualServer{} + conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{} + return conf + }), msg: "http listener with no routes", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, - Valid: true, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{}, - Listeners: []*graph.Listener{ - { - Name: "listener-80-1", - Source: listener80, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(hr1Invalid): routeHR1Invalid, - }, - }, - { - Name: "listener-443-1", - Source: listener443, // nil hostname - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(httpsHR1Invalid): httpsRouteHR1Invalid, - }, - ResolvedSecret: &secret1NsName, - }, - }, - }, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(hr1Invalid): routeHR1Invalid, - }, - ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ - secret1NsName: secret1, - }, - }, - expConf: Configuration{ - HTTPServers: []VirtualServer{ + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Listeners = append(g.Gateway.Listeners, []*graph.Listener{ { - IsDefault: true, - Port: 80, - }, - }, - SSLServers: []VirtualServer{ - { - IsDefault: true, - Port: 443, + Name: "listener-80-1", + Source: listener80, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr1Invalid): routeHR1Invalid, + }, }, { - Hostname: wildcardHostname, - SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, - Port: 443, - }, - }, - SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{ - "ssl_keypair_test_secret-1": { - Cert: []byte("cert-1"), - Key: []byte("privateKey-1"), - }, - }, - CertBundles: map[CertBundleID]CertBundle{}, - BaseHTTPConfig: BaseHTTPConfig{HTTP2: true}, - }, + Name: "listener-443-1", + Source: listener443, // nil hostname + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR1Invalid): httpsRouteHR1Invalid, + }, + ResolvedSecret: &secret1NsName, + }, + }...) + g.Routes[graph.CreateRouteKey(hr1Invalid)] = routeHR1Invalid + g.ReferencedSecrets[secret1NsName] = secret1 + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.HTTPServers = []VirtualServer{{ + IsDefault: true, + Port: 80, + }} + conf.SSLServers = append(conf.SSLServers, VirtualServer{ + Hostname: wildcardHostname, + SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, + Port: 443, + }) + return conf + }), msg: "http and https listeners with no valid routes", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, - Valid: true, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{}, - Listeners: []*graph.Listener{ - { - Name: "listener-443-1", - Source: listener443, // nil hostname - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{}, - ResolvedSecret: &secret1NsName, - }, - { - Name: "listener-443-with-hostname", - Source: listener443WithHostname, // non-nil hostname - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{}, - ResolvedSecret: &secret2NsName, - }, + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Listeners = append(g.Gateway.Listeners, []*graph.Listener{ + { + Name: "listener-443-1", + Source: listener443, // nil hostname + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{}, + ResolvedSecret: &secret1NsName, }, - }, - Routes: map[graph.RouteKey]*graph.L7Route{}, - ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ + { + Name: "listener-443-with-hostname", + Source: listener443WithHostname, // non-nil hostname + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{}, + ResolvedSecret: &secret2NsName, + }, + }...) + g.ReferencedSecrets = map[types.NamespacedName]*graph.Secret{ secret1NsName: secret1, secret2NsName: secret2, - }, - }, - expConf: Configuration{ - HTTPServers: []VirtualServer{}, - SSLServers: []VirtualServer{ - { - IsDefault: true, - Port: 443, - }, + } + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.HTTPServers = []VirtualServer{} + conf.SSLServers = append(conf.SSLServers, []VirtualServer{ { Hostname: string(hostname), SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-2"}, @@ -809,87 +820,57 @@ func TestBuildConfiguration(t *testing.T) { SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, Port: 443, }, - }, - SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{ - "ssl_keypair_test_secret-1": { - Cert: []byte("cert-1"), - Key: []byte("privateKey-1"), - }, - "ssl_keypair_test_secret-2": { - Cert: []byte("cert-2"), - Key: []byte("privateKey-2"), - }, - }, - CertBundles: map[CertBundleID]CertBundle{}, - BaseHTTPConfig: BaseHTTPConfig{HTTP2: true}, - }, + }...) + conf.SSLKeyPairs["ssl_keypair_test_secret-2"] = SSLKeyPair{ + Cert: []byte("cert-2"), + Key: []byte("privateKey-2"), + } + return conf + }), msg: "https listeners with no routes", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, - Valid: true, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{}, - Listeners: []*graph.Listener{ - { - Name: "invalid-listener", - Source: invalidListener, - Valid: false, - ResolvedSecret: &secret1NsName, - }, - }, - }, - Routes: map[graph.RouteKey]*graph.L7Route{ + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ + Name: "invalid-listener", + Source: invalidListener, + Valid: false, + ResolvedSecret: &secret1NsName, + }) + g.Routes = map[graph.RouteKey]*graph.L7Route{ graph.CreateRouteKey(httpsHR1): httpsRouteHR1, graph.CreateRouteKey(httpsHR2): httpsRouteHR2, - }, - ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ - secret1NsName: secret1, - }, - }, - expConf: Configuration{ - HTTPServers: []VirtualServer{}, - SSLServers: []VirtualServer{}, - SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{}, - CertBundles: map[CertBundleID]CertBundle{}, - BaseHTTPConfig: BaseHTTPConfig{HTTP2: true}, - }, + } + g.ReferencedSecrets[secret1NsName] = secret1 + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.HTTPServers = []VirtualServer{} + conf.SSLServers = []VirtualServer{} + conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{} + return conf + }), msg: "invalid https listener with resolved secret", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ + Name: "listener-80-1", + Source: listener80, Valid: true, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{}, - Listeners: []*graph.Listener{ - { - Name: "listener-80-1", - Source: listener80, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(hr1): routeHR1, - graph.CreateRouteKey(hr2): routeHR2, - }, - }, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr1): routeHR1, + graph.CreateRouteKey(hr2): routeHR2, }, - }, - Routes: map[graph.RouteKey]*graph.L7Route{ + }) + g.Routes = map[graph.RouteKey]*graph.L7Route{ graph.CreateRouteKey(hr1): routeHR1, graph.CreateRouteKey(hr2): routeHR2, - }, - }, - expConf: Configuration{ - HTTPServers: []VirtualServer{ - { - IsDefault: true, - Port: 80, - }, + } + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.HTTPServers = append(conf.HTTPServers, []VirtualServer{ { Hostname: "bar.example.com", PathRules: []PathRule{ @@ -922,119 +903,93 @@ func TestBuildConfiguration(t *testing.T) { }, Port: 80, }, - }, - SSLServers: []VirtualServer{}, - Upstreams: []Upstream{fooUpstream}, - BackendGroups: []BackendGroup{expHR1Groups[0], expHR2Groups[0]}, - SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{}, - CertBundles: map[CertBundleID]CertBundle{}, - BaseHTTPConfig: BaseHTTPConfig{HTTP2: true}, - }, + }...) + conf.SSLServers = []VirtualServer{} + conf.Upstreams = []Upstream{fooUpstream} + conf.BackendGroups = []BackendGroup{expHR1Groups[0], expHR2Groups[0]} + conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{} + + return conf + }), msg: "one http listener with two routes for different hostnames", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ + Name: "listener-80-1", + Source: listener80, Valid: true, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{}, - Listeners: []*graph.Listener{ - { - Name: "listener-80-1", - Source: listener80, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(gr): routeGR, - }, - }, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(gr): routeGR, }, - }, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(gr): routeGR, - }, - }, - expConf: Configuration{ - HTTPServers: []VirtualServer{ - { - IsDefault: true, - Port: 80, - }, - { - Hostname: "foo.example.com", - PathRules: []PathRule{ - { - Path: "/", - PathType: PathTypePrefix, - GRPC: true, - MatchRules: []MatchRule{ - { - BackendGroup: expGRGroups[0], - Source: &gr.ObjectMeta, - }, + }) + g.Routes[graph.CreateRouteKey(gr)] = routeGR + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.HTTPServers = append(conf.HTTPServers, VirtualServer{ + Hostname: "foo.example.com", + PathRules: []PathRule{ + { + Path: "/", + PathType: PathTypePrefix, + GRPC: true, + MatchRules: []MatchRule{ + { + BackendGroup: expGRGroups[0], + Source: &gr.ObjectMeta, }, }, }, - Port: 80, }, + Port: 80, }, - SSLServers: []VirtualServer{}, - Upstreams: []Upstream{fooUpstream}, - BackendGroups: []BackendGroup{expGRGroups[0]}, - SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{}, - CertBundles: map[CertBundleID]CertBundle{}, - BaseHTTPConfig: BaseHTTPConfig{HTTP2: true}, - }, + ) + conf.SSLServers = []VirtualServer{} + conf.Upstreams = append(conf.Upstreams, fooUpstream) + conf.BackendGroups = []BackendGroup{expGRGroups[0]} + conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{} + return conf + }), msg: "one http listener with one grpc route", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, - Valid: true, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{}, - Listeners: []*graph.Listener{ - { - Name: "listener-443-1", - Source: listener443, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(httpsHR1): httpsRouteHR1, - graph.CreateRouteKey(httpsHR2): httpsRouteHR2, - }, - ResolvedSecret: &secret1NsName, + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Listeners = append(g.Gateway.Listeners, []*graph.Listener{ + { + Name: "listener-443-1", + Source: listener443, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR1): httpsRouteHR1, + graph.CreateRouteKey(httpsHR2): httpsRouteHR2, }, - { - Name: "listener-443-with-hostname", - Source: listener443WithHostname, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(httpsHR5): httpsRouteHR5, - }, - ResolvedSecret: &secret2NsName, + ResolvedSecret: &secret1NsName, + }, + { + Name: "listener-443-with-hostname", + Source: listener443WithHostname, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR5): httpsRouteHR5, }, + ResolvedSecret: &secret2NsName, }, - }, - Routes: map[graph.RouteKey]*graph.L7Route{ + }...) + g.Routes = map[graph.RouteKey]*graph.L7Route{ graph.CreateRouteKey(hr1): httpsRouteHR1, graph.CreateRouteKey(hr2): httpsRouteHR2, graph.CreateRouteKey(httpsHR5): httpsRouteHR5, - }, - ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ + } + g.ReferencedSecrets = map[types.NamespacedName]*graph.Secret{ secret1NsName: secret1, secret2NsName: secret2, - }, - }, - expConf: Configuration{ - HTTPServers: []VirtualServer{}, - SSLServers: []VirtualServer{ - { - IsDefault: true, - Port: 443, - }, + } + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.HTTPServers = []VirtualServer{} + conf.SSLServers = append(conf.SSLServers, []VirtualServer{ { Hostname: "bar.example.com", PathRules: []PathRule{ @@ -1091,10 +1046,10 @@ func TestBuildConfiguration(t *testing.T) { SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, Port: 443, }, - }, - Upstreams: []Upstream{fooUpstream}, - BackendGroups: []BackendGroup{expHTTPSHR1Groups[0], expHTTPSHR2Groups[0], expHTTPSHR5Groups[0]}, - SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{ + }...) + conf.Upstreams = []Upstream{fooUpstream} + conf.BackendGroups = []BackendGroup{expHTTPSHR1Groups[0], expHTTPSHR2Groups[0], expHTTPSHR5Groups[0]} + conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{ "ssl_keypair_test_secret-1": { Cert: []byte("cert-1"), Key: []byte("privateKey-1"), @@ -1103,58 +1058,47 @@ func TestBuildConfiguration(t *testing.T) { Cert: []byte("cert-2"), Key: []byte("privateKey-2"), }, - }, - CertBundles: map[CertBundleID]CertBundle{}, - BaseHTTPConfig: BaseHTTPConfig{HTTP2: true}, - }, + } + return conf + }), msg: "two https listeners each with routes for different hostnames", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, - Valid: true, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{}, - Listeners: []*graph.Listener{ - { - Name: "listener-80-1", - Source: listener80, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(hr3): routeHR3, - graph.CreateRouteKey(hr4): routeHR4, - }, + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Listeners = append(g.Gateway.Listeners, []*graph.Listener{ + { + Name: "listener-80-1", + Source: listener80, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr3): routeHR3, + graph.CreateRouteKey(hr4): routeHR4, }, - { - Name: "listener-443-1", - Source: listener443, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(httpsHR3): httpsRouteHR3, - graph.CreateRouteKey(httpsHR4): httpsRouteHR4, - }, - ResolvedSecret: &secret1NsName, + }, + { + Name: "listener-443-1", + Source: listener443, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR3): httpsRouteHR3, + graph.CreateRouteKey(httpsHR4): httpsRouteHR4, }, + ResolvedSecret: &secret1NsName, }, - }, - Routes: map[graph.RouteKey]*graph.L7Route{ + }...) + g.Routes = map[graph.RouteKey]*graph.L7Route{ graph.CreateRouteKey(hr3): routeHR3, graph.CreateRouteKey(hr4): routeHR4, graph.CreateRouteKey(httpsHR3): httpsRouteHR3, graph.CreateRouteKey(httpsHR4): httpsRouteHR4, - }, - ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ + } + g.ReferencedSecrets = map[types.NamespacedName]*graph.Secret{ secret1NsName: secret1, - }, - }, - expConf: Configuration{ - HTTPServers: []VirtualServer{ - { - IsDefault: true, - Port: 80, - }, + } + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.HTTPServers = append(conf.HTTPServers, []VirtualServer{ { Hostname: "foo.example.com", PathRules: []PathRule{ @@ -1195,12 +1139,8 @@ func TestBuildConfiguration(t *testing.T) { }, Port: 80, }, - }, - SSLServers: []VirtualServer{ - { - IsDefault: true, - Port: 443, - }, + }...) + conf.SSLServers = append(conf.SSLServers, []VirtualServer{ { Hostname: "foo.example.com", SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, @@ -1247,9 +1187,9 @@ func TestBuildConfiguration(t *testing.T) { SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, Port: 443, }, - }, - Upstreams: []Upstream{fooUpstream}, - BackendGroups: []BackendGroup{ + }...) + conf.Upstreams = append(conf.Upstreams, fooUpstream) + conf.BackendGroups = []BackendGroup{ expHR3Groups[0], expHR3Groups[1], expHR4Groups[0], @@ -1258,79 +1198,62 @@ func TestBuildConfiguration(t *testing.T) { expHTTPSHR3Groups[1], expHTTPSHR4Groups[0], expHTTPSHR4Groups[1], - }, - SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{ - "ssl_keypair_test_secret-1": { - Cert: []byte("cert-1"), - Key: []byte("privateKey-1"), - }, - }, - CertBundles: map[CertBundleID]CertBundle{}, - BaseHTTPConfig: BaseHTTPConfig{HTTP2: true}, - }, + } + return conf + }), msg: "one http and one https listener with two routes with the same hostname with and without collisions", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, - Valid: true, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{}, - Listeners: []*graph.Listener{ - { - Name: "listener-80-1", - Source: listener80, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(hr3): routeHR3, - }, + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Listeners = append(g.Gateway.Listeners, []*graph.Listener{ + { + Name: "listener-80-1", + Source: listener80, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr3): routeHR3, }, - { - Name: "listener-8080", - Source: listener8080, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(hr8): routeHR8, - }, + }, + { + Name: "listener-8080", + Source: listener8080, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr8): routeHR8, }, - { - Name: "listener-443-1", - Source: listener443, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(httpsHR3): httpsRouteHR3, - }, - ResolvedSecret: &secret1NsName, + }, + { + Name: "listener-443-1", + Source: listener443, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR3): httpsRouteHR3, }, - { - Name: "listener-8443", - Source: listener8443, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(httpsHR7): httpsRouteHR7, - }, - ResolvedSecret: &secret1NsName, + ResolvedSecret: &secret1NsName, + }, + { + Name: "listener-8443", + Source: listener8443, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR7): httpsRouteHR7, }, + ResolvedSecret: &secret1NsName, }, - }, - Routes: map[graph.RouteKey]*graph.L7Route{ + }...) + g.Routes = map[graph.RouteKey]*graph.L7Route{ graph.CreateRouteKey(hr3): routeHR3, graph.CreateRouteKey(hr8): routeHR8, graph.CreateRouteKey(httpsHR3): httpsRouteHR3, graph.CreateRouteKey(httpsHR7): httpsRouteHR7, - }, - ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ + } + g.ReferencedSecrets = map[types.NamespacedName]*graph.Secret{ secret1NsName: secret1, - }, - }, - expConf: Configuration{ - HTTPServers: []VirtualServer{ - { - IsDefault: true, - Port: 80, - }, + } + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.HTTPServers = append(conf.HTTPServers, []VirtualServer{ { Hostname: "foo.example.com", PathRules: []PathRule{ @@ -1387,12 +1310,8 @@ func TestBuildConfiguration(t *testing.T) { }, Port: 8080, }, - }, - SSLServers: []VirtualServer{ - { - IsDefault: true, - Port: 443, - }, + }...) + conf.SSLServers = append(conf.SSLServers, []VirtualServer{ { Hostname: "foo.example.com", SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, @@ -1461,9 +1380,9 @@ func TestBuildConfiguration(t *testing.T) { SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, Port: 8443, }, - }, - Upstreams: []Upstream{fooUpstream}, - BackendGroups: []BackendGroup{ + }...) + conf.Upstreams = append(conf.Upstreams, fooUpstream) + conf.BackendGroups = []BackendGroup{ expHR3Groups[0], expHR3Groups[1], expHR8Groups[0], @@ -1472,109 +1391,74 @@ func TestBuildConfiguration(t *testing.T) { expHTTPSHR3Groups[1], expHTTPSHR7Groups[0], expHTTPSHR7Groups[1], - }, - SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{ - "ssl_keypair_test_secret-1": { - Cert: []byte("cert-1"), - Key: []byte("privateKey-1"), - }, - }, - CertBundles: map[CertBundleID]CertBundle{}, - BaseHTTPConfig: BaseHTTPConfig{HTTP2: true}, - }, - + } + return conf + }), msg: "multiple http and https listener; different ports", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, - Valid: false, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{}, - Listeners: []*graph.Listener{ - { - Name: "listener-80-1", - Source: listener80, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(hr1): routeHR1, - }, - }, + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.GatewayClass.Valid = false + g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ + Name: "listener-80-1", + Source: listener80, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr1): routeHR1, }, - }, - Routes: map[graph.RouteKey]*graph.L7Route{ + }) + g.Routes = map[graph.RouteKey]*graph.L7Route{ graph.CreateRouteKey(hr1): routeHR1, - }, - }, + } + return g + }), expConf: Configuration{}, msg: "invalid gatewayclass", }, { - graph: &graph.Graph{ - GatewayClass: nil, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{}, - Listeners: []*graph.Listener{ - { - Name: "listener-80-1", - Source: listener80, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(hr1): routeHR1, - }, - }, + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.GatewayClass.Valid = false + g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ + Name: "listener-80-1", + Source: listener80, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr1): routeHR1, }, - }, - Routes: map[graph.RouteKey]*graph.L7Route{ + }) + g.Routes = map[graph.RouteKey]*graph.L7Route{ graph.CreateRouteKey(hr1): routeHR1, - }, - }, + } + return g + }), expConf: Configuration{}, msg: "missing gatewayclass", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, - Valid: true, - }, - Gateway: nil, - Routes: map[graph.RouteKey]*graph.L7Route{}, - }, + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway = nil + return g + }), expConf: Configuration{}, msg: "missing gateway", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ + Name: "listener-80-1", + Source: listener80, Valid: true, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{}, - Listeners: []*graph.Listener{ - { - Name: "listener-80-1", - Source: listener80, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(hr5): routeHR5, - }, - }, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr5): routeHR5, }, - }, - Routes: map[graph.RouteKey]*graph.L7Route{ + }) + g.Routes = map[graph.RouteKey]*graph.L7Route{ graph.CreateRouteKey(hr5): routeHR5, - }, - }, - expConf: Configuration{ - HTTPServers: []VirtualServer{ - { - IsDefault: true, - Port: 80, - }, + } + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.HTTPServers = append(conf.HTTPServers, []VirtualServer{ { Hostname: "foo.example.com", PathRules: []PathRule{ @@ -1607,58 +1491,47 @@ func TestBuildConfiguration(t *testing.T) { }, Port: 80, }, - }, - SSLServers: []VirtualServer{}, - Upstreams: []Upstream{fooUpstream}, - BackendGroups: []BackendGroup{expHR5Groups[0], expHR5Groups[1]}, - SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{}, - CertBundles: map[CertBundleID]CertBundle{}, - BaseHTTPConfig: BaseHTTPConfig{HTTP2: true}, - }, + }...) + conf.SSLServers = []VirtualServer{} + conf.Upstreams = []Upstream{fooUpstream} + conf.BackendGroups = []BackendGroup{expHR5Groups[0], expHR5Groups[1]} + conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{} + return conf + }), msg: "one http listener with one route with filters", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, - Valid: true, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{}, - Listeners: []*graph.Listener{ - { - Name: "listener-80-1", - Source: listener80, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(hr6): routeHR6, - }, + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Listeners = append(g.Gateway.Listeners, []*graph.Listener{ + { + Name: "listener-80-1", + Source: listener80, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr6): routeHR6, }, - { - Name: "listener-443-1", - Source: listener443, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(httpsHR6): httpsRouteHR6, - }, - ResolvedSecret: &secret1NsName, + }, + { + Name: "listener-443-1", + Source: listener443, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR6): httpsRouteHR6, }, + ResolvedSecret: &secret1NsName, }, - }, - Routes: map[graph.RouteKey]*graph.L7Route{ + }...) + g.Routes = map[graph.RouteKey]*graph.L7Route{ graph.CreateRouteKey(hr6): routeHR6, graph.CreateRouteKey(httpsHR6): httpsRouteHR6, - }, - ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ + } + g.ReferencedSecrets = map[types.NamespacedName]*graph.Secret{ secret1NsName: secret1, - }, - }, - expConf: Configuration{ - HTTPServers: []VirtualServer{ - { - IsDefault: true, - Port: 80, - }, + } + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.HTTPServers = append(conf.HTTPServers, []VirtualServer{ { Hostname: "foo.example.com", PathRules: []PathRule{ @@ -1675,12 +1548,8 @@ func TestBuildConfiguration(t *testing.T) { }, Port: 80, }, - }, - SSLServers: []VirtualServer{ - { - IsDefault: true, - Port: 443, - }, + }...) + conf.SSLServers = append(conf.SSLServers, []VirtualServer{ { Hostname: "foo.example.com", SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, @@ -1703,52 +1572,30 @@ func TestBuildConfiguration(t *testing.T) { SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, Port: 443, }, - }, - Upstreams: []Upstream{fooUpstream}, - BackendGroups: []BackendGroup{ - expHR6Groups[0], - expHTTPSHR6Groups[0], - }, - SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{ - "ssl_keypair_test_secret-1": { - Cert: []byte("cert-1"), - Key: []byte("privateKey-1"), - }, - }, - CertBundles: map[CertBundleID]CertBundle{}, - BaseHTTPConfig: BaseHTTPConfig{HTTP2: true}, - }, + }...) + conf.Upstreams = []Upstream{fooUpstream} + conf.BackendGroups = []BackendGroup{expHR6Groups[0], expHTTPSHR6Groups[0]} + return conf + }), msg: "one http and one https listener with routes with valid and invalid rules", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ + Name: "listener-80-1", + Source: listener80, Valid: true, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{}, - Listeners: []*graph.Listener{ - { - Name: "listener-80-1", - Source: listener80, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(hr7): routeHR7, - }, - }, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr7): routeHR7, }, - }, - Routes: map[graph.RouteKey]*graph.L7Route{ + }) + g.Routes = map[graph.RouteKey]*graph.L7Route{ graph.CreateRouteKey(hr7): routeHR7, - }, - }, - expConf: Configuration{ - HTTPServers: []VirtualServer{ - { - IsDefault: true, - Port: 80, - }, + } + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.HTTPServers = append(conf.HTTPServers, []VirtualServer{ { Hostname: "foo.example.com", PathRules: []PathRule{ @@ -1775,60 +1622,48 @@ func TestBuildConfiguration(t *testing.T) { }, Port: 80, }, - }, - SSLServers: []VirtualServer{}, - Upstreams: []Upstream{fooUpstream}, - BackendGroups: []BackendGroup{expHR7Groups[0], expHR7Groups[1]}, - SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{}, - CertBundles: map[CertBundleID]CertBundle{}, - BaseHTTPConfig: BaseHTTPConfig{HTTP2: true}, - }, + }...) + conf.SSLServers = []VirtualServer{} + conf.Upstreams = []Upstream{fooUpstream} + conf.BackendGroups = []BackendGroup{expHR7Groups[0], expHR7Groups[1]} + conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{} + return conf + }), msg: "duplicate paths with different types", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, - Valid: true, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{}, - Listeners: []*graph.Listener{ - { - Name: "listener-443-with-hostname", - Source: listener443WithHostname, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(httpsHR5): httpsRouteHR5, - }, - ResolvedSecret: &secret2NsName, - }, - { - Name: "listener-443-1", - Source: listener443, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(httpsHR5): httpsRouteHR5, - }, - ResolvedSecret: &secret1NsName, + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Listeners = append(g.Gateway.Listeners, []*graph.Listener{ + { + Name: "listener-443-with-hostname", + Source: listener443WithHostname, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR5): httpsRouteHR5, }, + ResolvedSecret: &secret2NsName, }, - }, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(httpsHR5): httpsRouteHR5, - }, - ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ - secret1NsName: secret1, - secret2NsName: secret2, - }, - }, - expConf: Configuration{ - HTTPServers: []VirtualServer{}, - SSLServers: []VirtualServer{ { - IsDefault: true, - Port: 443, + Name: "listener-443-1", + Source: listener443, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR5): httpsRouteHR5, + }, + ResolvedSecret: &secret1NsName, }, + }...) + g.Routes = map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR5): httpsRouteHR5, + } + g.ReferencedSecrets = map[types.NamespacedName]*graph.Secret{ + secret1NsName: secret1, + secret2NsName: secret2, + } + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.SSLServers = append(conf.SSLServers, []VirtualServer{ { Hostname: "example.com", PathRules: []PathRule{ @@ -1856,10 +1691,11 @@ func TestBuildConfiguration(t *testing.T) { SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, Port: 443, }, - }, - Upstreams: []Upstream{fooUpstream}, - BackendGroups: []BackendGroup{expHTTPSHR5Groups[0]}, - SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{ + }...) + conf.HTTPServers = []VirtualServer{} + conf.Upstreams = []Upstream{fooUpstream} + conf.BackendGroups = []BackendGroup{expHTTPSHR5Groups[0]} + conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{ "ssl_keypair_test_secret-1": { Cert: []byte("cert-1"), Key: []byte("privateKey-1"), @@ -1868,47 +1704,33 @@ func TestBuildConfiguration(t *testing.T) { Cert: []byte("cert-2"), Key: []byte("privateKey-2"), }, - }, - CertBundles: map[CertBundleID]CertBundle{}, - BaseHTTPConfig: BaseHTTPConfig{HTTP2: true}, - }, + } + return conf + }), msg: "two https listeners with different hostnames but same route; chooses listener with more specific hostname", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ + Name: "listener-443", + Source: listener443, Valid: true, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{}, - Listeners: []*graph.Listener{ - { - Name: "listener-443", - Source: listener443, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(httpsHR8): httpsRouteHR8, - }, - ResolvedSecret: &secret1NsName, - }, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR8): httpsRouteHR8, }, - }, - Routes: map[graph.RouteKey]*graph.L7Route{ + ResolvedSecret: &secret1NsName, + }) + g.Routes = map[graph.RouteKey]*graph.L7Route{ graph.CreateRouteKey(httpsHR8): httpsRouteHR8, - }, - ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ + } + g.ReferencedSecrets = map[types.NamespacedName]*graph.Secret{ secret1NsName: secret1, - }, - ReferencedCaCertConfigMaps: referencedConfigMaps, - }, - expConf: Configuration{ - HTTPServers: []VirtualServer{}, - SSLServers: []VirtualServer{ - { - IsDefault: true, - Port: 443, - }, + } + g.ReferencedCaCertConfigMaps = referencedConfigMaps + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.SSLServers = append(conf.SSLServers, []VirtualServer{ { Hostname: "foo.example.com", PathRules: []PathRule{ @@ -1935,57 +1757,39 @@ func TestBuildConfiguration(t *testing.T) { SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, Port: 443, }, - }, - Upstreams: []Upstream{fooUpstream}, - BackendGroups: []BackendGroup{expHTTPSHR8Groups[0], expHTTPSHR8Groups[1]}, - SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{ - "ssl_keypair_test_secret-1": { - Cert: []byte("cert-1"), - Key: []byte("privateKey-1"), - }, - }, - CertBundles: map[CertBundleID]CertBundle{ + }...) + conf.HTTPServers = []VirtualServer{} + conf.Upstreams = []Upstream{fooUpstream} + conf.BackendGroups = []BackendGroup{expHTTPSHR8Groups[0], expHTTPSHR8Groups[1]} + conf.CertBundles = map[CertBundleID]CertBundle{ "cert_bundle_test_configmap-1": []byte("cert-1"), - }, - BaseHTTPConfig: BaseHTTPConfig{HTTP2: true}, - }, + } + return conf + }), msg: "https listener with httproute with backend that has a backend TLS policy attached", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ + Name: "listener-443", + Source: listener443, Valid: true, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{}, - Listeners: []*graph.Listener{ - { - Name: "listener-443", - Source: listener443, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(httpsHR9): httpsRouteHR9, - }, - ResolvedSecret: &secret1NsName, - }, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR9): httpsRouteHR9, }, - }, - Routes: map[graph.RouteKey]*graph.L7Route{ + ResolvedSecret: &secret1NsName, + }) + g.Routes = map[graph.RouteKey]*graph.L7Route{ graph.CreateRouteKey(httpsHR9): httpsRouteHR9, - }, - ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ + } + g.ReferencedSecrets = map[types.NamespacedName]*graph.Secret{ secret1NsName: secret1, - }, - ReferencedCaCertConfigMaps: referencedConfigMaps, - }, - expConf: Configuration{ - HTTPServers: []VirtualServer{}, - SSLServers: []VirtualServer{ - { - IsDefault: true, - Port: 443, - }, + } + g.ReferencedCaCertConfigMaps = referencedConfigMaps + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.SSLServers = append(conf.SSLServers, []VirtualServer{ { Hostname: "foo.example.com", PathRules: []PathRule{ @@ -2012,97 +1816,66 @@ func TestBuildConfiguration(t *testing.T) { SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, Port: 443, }, - }, - Upstreams: []Upstream{fooUpstream}, - BackendGroups: []BackendGroup{expHTTPSHR9Groups[0], expHTTPSHR9Groups[1]}, - SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{ - "ssl_keypair_test_secret-1": { - Cert: []byte("cert-1"), - Key: []byte("privateKey-1"), - }, - }, - CertBundles: map[CertBundleID]CertBundle{ + }...) + conf.HTTPServers = []VirtualServer{} + conf.Upstreams = []Upstream{fooUpstream} + conf.BackendGroups = []BackendGroup{expHTTPSHR9Groups[0], expHTTPSHR9Groups[1]} + conf.CertBundles = map[CertBundleID]CertBundle{ "cert_bundle_test_configmap-2": []byte("cert-2"), - }, - BaseHTTPConfig: BaseHTTPConfig{HTTP2: true}, - }, + } + return conf + }), msg: "https listener with httproute with backend that has a backend TLS policy with binaryData attached", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Source.ObjectMeta = metav1.ObjectMeta{ + Name: "gw", + Namespace: "ns", + } + g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ + Name: "listener-80-1", + Source: listener80, Valid: true, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gw", - Namespace: "ns", - }, - }, - Listeners: []*graph.Listener{ - { - Name: "listener-80-1", - Source: listener80, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{}, - }, - }, - }, - Routes: map[graph.RouteKey]*graph.L7Route{}, - NginxProxy: nginxProxy, - }, - expConf: Configuration{ - HTTPServers: []VirtualServer{ - { - IsDefault: true, - Port: 80, - }, - }, - SSLServers: []VirtualServer{}, - SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{}, - CertBundles: map[CertBundleID]CertBundle{}, - BaseHTTPConfig: BaseHTTPConfig{HTTP2: false}, - Telemetry: Telemetry{ + Routes: map[graph.RouteKey]*graph.L7Route{}, + }) + g.NginxProxy = nginxProxy + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.SSLServers = []VirtualServer{} + conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{} + conf.Telemetry = Telemetry{ Endpoint: "my-otel.svc:4563", Interval: "5s", BatchSize: 512, BatchCount: 4, ServiceName: "ngf:ns:gw:my-svc", Ratios: []Ratio{}, - }, - }, + } + conf.BaseHTTPConfig = BaseHTTPConfig{HTTP2: false, IPFamily: Dual} + return conf + }), msg: "NginxProxy with tracing config and http2 disabled", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Source.ObjectMeta = metav1.ObjectMeta{ + Name: "gw", + Namespace: "ns", + } + g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ + Name: "listener-80-1", + Source: listener80, Valid: true, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gw", - Namespace: "ns", - }, - }, - Listeners: []*graph.Listener{ - { - Name: "listener-80-1", - Source: listener80, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{}, - }, - }, - }, - Routes: map[graph.RouteKey]*graph.L7Route{}, - NginxProxy: &graph.NginxProxy{ + Routes: map[graph.RouteKey]*graph.L7Route{}, + }) + g.NginxProxy = &graph.NginxProxy{ Valid: false, Source: &ngfAPI.NginxProxy{ Spec: ngfAPI.NginxProxySpec{ DisableHTTP2: true, + IPFamily: helpers.GetPointer(ngfAPI.Dual), Telemetry: &ngfAPI.Telemetry{ Exporter: &ngfAPI.TelemetryExporter{ Endpoint: "some-endpoint", @@ -2110,72 +1883,52 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, - }, - }, - expConf: Configuration{ - HTTPServers: []VirtualServer{ - { - IsDefault: true, - Port: 80, - }, - }, - SSLServers: []VirtualServer{}, - SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{}, - CertBundles: map[CertBundleID]CertBundle{}, - BaseHTTPConfig: BaseHTTPConfig{HTTP2: true}, - Telemetry: Telemetry{}, - }, + } + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.SSLServers = []VirtualServer{} + conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{} + return conf + }), msg: "invalid NginxProxy", }, { - graph: &graph.Graph{ - GatewayClass: &graph.GatewayClass{ - Source: &v1.GatewayClass{}, - Valid: true, - }, - Gateway: &graph.Gateway{ - Source: &v1.Gateway{}, - Listeners: []*graph.Listener{ - { - Name: "listener-80-1", - Source: listener80, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(hrWithPolicy): l7RouteWithPolicy, - }, + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Listeners = append(g.Gateway.Listeners, []*graph.Listener{ + { + Name: "listener-80-1", + Source: listener80, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hrWithPolicy): l7RouteWithPolicy, }, - { - Name: "listener-443", - Source: listener443, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(httpsHRWithPolicy): l7HTTPSRouteWithPolicy, - }, - ResolvedSecret: &secret1NsName, + }, + { + Name: "listener-443", + Source: listener443, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHRWithPolicy): l7HTTPSRouteWithPolicy, }, + ResolvedSecret: &secret1NsName, }, - Policies: []*graph.Policy{gwPolicy1, gwPolicy2}, - }, - Routes: map[graph.RouteKey]*graph.L7Route{ + }...) + g.Gateway.Policies = []*graph.Policy{gwPolicy1, gwPolicy2} + g.Routes = map[graph.RouteKey]*graph.L7Route{ graph.CreateRouteKey(hrWithPolicy): l7RouteWithPolicy, graph.CreateRouteKey(httpsHRWithPolicy): l7HTTPSRouteWithPolicy, - }, - ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ + } + g.ReferencedSecrets = map[types.NamespacedName]*graph.Secret{ secret1NsName: secret1, - }, - }, - expConf: Configuration{ - SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{ - "ssl_keypair_test_secret-1": { - Cert: []byte("cert-1"), - Key: []byte("privateKey-1"), - }, - }, - CertBundles: map[CertBundleID]CertBundle{}, - HTTPServers: []VirtualServer{ + } + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.SSLServers = []VirtualServer{ { IsDefault: true, - Port: 80, + Port: 443, Additions: []Addition{ { Bytes: []byte("apple"), @@ -2195,19 +1948,20 @@ func TestBuildConfiguration(t *testing.T) { PathType: PathTypePrefix, MatchRules: []MatchRule{ { - Source: &hrWithPolicy.ObjectMeta, - BackendGroup: expHRWithPolicyGroups[0], + BackendGroup: expHTTPSHRWithPolicyGroups[0], + Source: &httpsHRWithPolicy.ObjectMeta, Additions: []Addition{ { - Bytes: []byte("lemon"), - Identifier: "LemonPolicy_default_attach-hr", + Bytes: []byte("lime"), + Identifier: "LimePolicy_default_attach-hr", }, }, }, }, }, }, - Port: 80, + SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, + Port: 443, Additions: []Addition{ { Bytes: []byte("apple"), @@ -2219,11 +1973,26 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, - }, - SSLServers: []VirtualServer{ + { + Hostname: wildcardHostname, + SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, + Port: 443, + Additions: []Addition{ + { + Bytes: []byte("apple"), + Identifier: "ApplePolicy_default_attach-gw", + }, + { + Bytes: []byte("orange"), + Identifier: "OrangePolicy_default_attach-gw", + }, + }, + }, + } + conf.HTTPServers = []VirtualServer{ { IsDefault: true, - Port: 443, + Port: 80, Additions: []Addition{ { Bytes: []byte("apple"), @@ -2243,35 +2012,19 @@ func TestBuildConfiguration(t *testing.T) { PathType: PathTypePrefix, MatchRules: []MatchRule{ { - BackendGroup: expHTTPSHRWithPolicyGroups[0], - Source: &httpsHRWithPolicy.ObjectMeta, + Source: &hrWithPolicy.ObjectMeta, + BackendGroup: expHRWithPolicyGroups[0], Additions: []Addition{ { - Bytes: []byte("lime"), - Identifier: "LimePolicy_default_attach-hr", + Bytes: []byte("lemon"), + Identifier: "LemonPolicy_default_attach-hr", }, }, }, }, }, }, - SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, - Port: 443, - Additions: []Addition{ - { - Bytes: []byte("apple"), - Identifier: "ApplePolicy_default_attach-gw", - }, - { - Bytes: []byte("orange"), - Identifier: "OrangePolicy_default_attach-gw", - }, - }, - }, - { - Hostname: wildcardHostname, - SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, - Port: 443, + Port: 80, Additions: []Addition{ { Bytes: []byte("apple"), @@ -2283,18 +2036,59 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, - }, - Upstreams: []Upstream{fooUpstream}, - BackendGroups: []BackendGroup{ - expHRWithPolicyGroups[0], - expHTTPSHRWithPolicyGroups[0], - }, - BaseHTTPConfig: BaseHTTPConfig{ - HTTP2: true, - }, - }, + } + conf.Upstreams = []Upstream{fooUpstream} + conf.BackendGroups = []BackendGroup{expHRWithPolicyGroups[0], expHTTPSHRWithPolicyGroups[0]} + return conf + }), msg: "Simple Gateway and HTTPRoute with policies attached", }, + { + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Source.ObjectMeta = metav1.ObjectMeta{ + Name: "gw", + Namespace: "ns", + } + g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ + Name: "listener-80-1", + Source: listener80, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{}, + }) + g.NginxProxy = nginxProxyIPv4 + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.SSLServers = []VirtualServer{} + conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{} + conf.BaseHTTPConfig = BaseHTTPConfig{HTTP2: true, IPFamily: IPv4} + return conf + }), + msg: "NginxProxy with IPv4 IPFamily and no routes", + }, + { + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Source.ObjectMeta = metav1.ObjectMeta{ + Name: "gw", + Namespace: "ns", + } + g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ + Name: "listener-80-1", + Source: listener80, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{}, + }) + g.NginxProxy = nginxProxyIPv6 + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.SSLServers = []VirtualServer{} + conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{} + conf.BaseHTTPConfig = BaseHTTPConfig{HTTP2: true, IPFamily: IPv6} + return conf + }), + msg: "NginxProxy with IPv6 IPFamily and no routes", + }, } for _, test := range tests { @@ -2607,6 +2401,10 @@ func TestBuildUpstreams(t *testing.T) { Address: "10.0.0.2", Port: 8080, }, + { + Address: "fd00:10:244::6", + Port: 8080, + }, } barEndpoints := []resolver.Endpoint{ @@ -2633,6 +2431,10 @@ func TestBuildUpstreams(t *testing.T) { Address: "12.0.0.0", Port: 80, }, + { + Address: "fd00:10:244::9", + Port: 80, + }, } baz2Endpoints := []resolver.Endpoint{ @@ -2649,6 +2451,21 @@ func TestBuildUpstreams(t *testing.T) { }, } + ipv6Endpoints := []resolver.Endpoint{ + { + Address: "fd00:10:244::7", + Port: 80, + }, + { + Address: "fd00:10:244::8", + Port: 80, + }, + { + Address: "fd00:10:244::9", + Port: 80, + }, + } + createBackendRefs := func(serviceNames ...string) []graph.BackendRef { var backends []graph.BackendRef for _, name := range serviceNames { @@ -2675,6 +2492,8 @@ func TestBuildUpstreams(t *testing.T) { hr4Refs1 := createBackendRefs("baz2") + hr5Refs0 := createBackendRefs("ipv6-endpoints") + nonExistingRefs := createBackendRefs("non-existing") invalidHRRefs := createBackendRefs("abc") @@ -2709,6 +2528,15 @@ func TestBuildUpstreams(t *testing.T) { }, } + routes3 := map[graph.RouteKey]*graph.L7Route{ + {NamespacedName: types.NamespacedName{Name: "hr4", Namespace: "test"}}: { + Valid: true, + Spec: graph.L7RouteSpec{ + Rules: refsToValidRules(hr5Refs0, hr2Refs1), + }, + }, + } + routesWithNonExistingRefs := map[graph.RouteKey]*graph.L7Route{ {NamespacedName: types.NamespacedName{Name: "non-existing", Namespace: "test"}}: { Valid: true, @@ -2748,6 +2576,11 @@ func TestBuildUpstreams(t *testing.T) { Valid: true, Routes: invalidRoutes, // shouldn't be included since routes are invalid }, + { + Name: "listener-4", + Valid: true, + Routes: routes3, + }, } emptyEndpointsErrMsg := "empty endpoints error" @@ -2780,6 +2613,10 @@ func TestBuildUpstreams(t *testing.T) { Endpoints: nil, ErrorMsg: nilEndpointsErrMsg, }, + { + Name: "test_ipv6-endpoints_80", + Endpoints: ipv6Endpoints, + }, } fakeResolver := &resolverfakes.FakeServiceResolver{} @@ -2787,6 +2624,7 @@ func TestBuildUpstreams(t *testing.T) { _ context.Context, svcNsName types.NamespacedName, _ apiv1.ServicePort, + _ []discoveryV1.AddressType, ) ([]resolver.Endpoint, error) { switch svcNsName.Name { case "bar": @@ -2803,6 +2641,8 @@ func TestBuildUpstreams(t *testing.T) { return nil, errors.New(nilEndpointsErrMsg) case "abc": return abcEndpoints, nil + case "ipv6-endpoints": + return ipv6Endpoints, nil default: return nil, fmt.Errorf("unexpected service %s", svcNsName.Name) } @@ -2810,7 +2650,7 @@ func TestBuildUpstreams(t *testing.T) { g := NewWithT(t) - upstreams := buildUpstreams(context.TODO(), listeners, fakeResolver) + upstreams := buildUpstreams(context.TODO(), listeners, fakeResolver, Dual) g.Expect(upstreams).To(ConsistOf(expUpstreams)) } @@ -3339,3 +3179,39 @@ func TestBuildAdditions(t *testing.T) { }) } } + +func TestGetAllowedAddressType(t *testing.T) { + test := []struct { + msg string + ipFamily IPFamilyType + expected []discoveryV1.AddressType + }{ + { + msg: "dual ip family", + ipFamily: Dual, + expected: []discoveryV1.AddressType{discoveryV1.AddressTypeIPv4, discoveryV1.AddressTypeIPv6}, + }, + { + msg: "ipv4 ip family", + ipFamily: IPv4, + expected: []discoveryV1.AddressType{discoveryV1.AddressTypeIPv4}, + }, + { + msg: "ipv6 ip family", + ipFamily: IPv6, + expected: []discoveryV1.AddressType{discoveryV1.AddressTypeIPv6}, + }, + { + msg: "unknown ip family", + ipFamily: "unknown", + expected: []discoveryV1.AddressType{}, + }, + } + + for _, tc := range test { + t.Run(tc.msg, func(t *testing.T) { + g := NewWithT(t) + g.Expect(getAllowedAddressType(tc.ipFamily)).To(Equal(tc.expected)) + }) + } +} diff --git a/internal/mode/static/state/dataplane/types.go b/internal/mode/static/state/dataplane/types.go index 77cd66c008..d342ff3b0c 100644 --- a/internal/mode/static/state/dataplane/types.go +++ b/internal/mode/static/state/dataplane/types.go @@ -33,10 +33,10 @@ type Configuration struct { Upstreams []Upstream // BackendGroups holds all unique BackendGroups. BackendGroups []BackendGroup - // Telemetry holds the Otel configuration. - Telemetry Telemetry // BaseHTTPConfig holds the configuration options at the http context. BaseHTTPConfig BaseHTTPConfig + // Telemetry holds the Otel configuration. + Telemetry Telemetry // Version represents the version of the generated configuration. Version int } @@ -296,10 +296,24 @@ type SpanAttribute struct { // BaseHTTPConfig holds the configuration options at the http context. type BaseHTTPConfig struct { + // IPFamily specifies the IP family for all servers. + IPFamily IPFamilyType // HTTP2 specifies whether http2 should be enabled for all servers. HTTP2 bool } +// IPFamilyType specifies the IP family to be used by NGINX. +type IPFamilyType string + +const ( + // Dual specifies that the server will use both IPv4 and IPv6. + Dual IPFamilyType = "dual" + // IPv4 specifies that the server will use only IPv4. + IPv4 IPFamilyType = "ipv4" + // IPv6 specifies that the server will use only IPv6. + IPv6 IPFamilyType = "ipv6" +) + // Ratio represents a tracing sampling ratio used in an nginx config with the otel_module. type Ratio struct { // Name is based on the associated ObservabilityPolicy's NamespacedName, diff --git a/internal/mode/static/state/graph/backend_refs.go b/internal/mode/static/state/graph/backend_refs.go index 2673fa4365..2b79569dc3 100644 --- a/internal/mode/static/state/graph/backend_refs.go +++ b/internal/mode/static/state/graph/backend_refs.go @@ -1,6 +1,7 @@ package graph import ( + "errors" "fmt" "slices" @@ -10,6 +11,8 @@ import ( gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" "sigs.k8s.io/gateway-api/apis/v1alpha3" + ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/sort" @@ -44,9 +47,10 @@ func addBackendRefsToRouteRules( refGrantResolver *referenceGrantResolver, services map[types.NamespacedName]*v1.Service, backendTLSPolicies map[types.NamespacedName]*BackendTLSPolicy, + npCfg *NginxProxy, ) { for _, r := range routes { - addBackendRefsToRules(r, refGrantResolver, services, backendTLSPolicies) + addBackendRefsToRules(r, refGrantResolver, services, backendTLSPolicies, npCfg) } } @@ -57,6 +61,7 @@ func addBackendRefsToRules( refGrantResolver *referenceGrantResolver, services map[types.NamespacedName]*v1.Service, backendTLSPolicies map[types.NamespacedName]*BackendTLSPolicy, + npCfg *NginxProxy, ) { if !route.Valid { return @@ -87,6 +92,7 @@ func addBackendRefsToRules( services, refPath, backendTLSPolicies, + npCfg, ) backendRefs = append(backendRefs, ref) @@ -116,6 +122,7 @@ func createBackendRef( services map[types.NamespacedName]*v1.Service, refPath *field.Path, backendTLSPolicies map[types.NamespacedName]*BackendTLSPolicy, + npCfg *NginxProxy, ) (BackendRef, *conditions.Condition) { // Data plane will handle invalid ref by responding with 500. // Because of that, we always need to add a BackendRef to group.Backends, even if the ref is invalid. @@ -142,8 +149,25 @@ func createBackendRef( return backendRef, &cond } - svcNsName, svcPort, err := getServiceAndPortFromRef(ref.BackendRef, sourceNamespace, services, refPath) + ns := sourceNamespace + if ref.BackendRef.Namespace != nil { + ns = string(*ref.Namespace) + } + svcNsName := types.NamespacedName{Name: string(ref.BackendRef.Name), Namespace: ns} + svcIPFamily, svcPort, err := getIPFamilyAndPortFromRef(ref.BackendRef, svcNsName, services, refPath) if err != nil { + backendRef = BackendRef{ + Weight: weight, + Valid: false, + SvcNsName: svcNsName, + ServicePort: v1.ServicePort{}, + } + + cond := staticConds.NewRouteBackendRefRefBackendNotFound(err.Error()) + return backendRef, &cond + } + + if err := verifyIPFamily(npCfg, svcIPFamily); err != nil { backendRef = BackendRef{ SvcNsName: svcNsName, ServicePort: svcPort, @@ -151,7 +175,7 @@ func createBackendRef( Valid: false, } - cond := staticConds.NewRouteBackendRefRefBackendNotFound(err.Error()) + cond := staticConds.NewRouteInvalidIPFamily(err.Error()) return backendRef, &cond } @@ -269,35 +293,55 @@ func findBackendTLSPolicyForService( return beTLSPolicy, err } -// getServiceAndPortFromRef extracts the NamespacedName of the Service and the port from a BackendRef. +// getIPFamilyAndPortFromRef extracts the IPFamily of the Service and the port from a BackendRef. // It can return an error and an empty v1.ServicePort in two cases: // 1. The Service referenced from the BackendRef does not exist in the cluster/state. // 2. The Port on the BackendRef does not match any of the ServicePorts on the Service. -func getServiceAndPortFromRef( +func getIPFamilyAndPortFromRef( ref gatewayv1.BackendRef, - routeNamespace string, + svcNsName types.NamespacedName, services map[types.NamespacedName]*v1.Service, refPath *field.Path, -) (types.NamespacedName, v1.ServicePort, error) { - ns := routeNamespace - if ref.Namespace != nil { - ns = string(*ref.Namespace) - } - - svcNsName := types.NamespacedName{Name: string(ref.Name), Namespace: ns} - +) ([]v1.IPFamily, v1.ServicePort, error) { svc, ok := services[svcNsName] if !ok { - return svcNsName, v1.ServicePort{}, field.NotFound(refPath.Child("name"), ref.Name) + return []v1.IPFamily{}, v1.ServicePort{}, field.NotFound(refPath.Child("name"), ref.Name) } // safe to dereference port here because we already validated that the port is not nil in validateBackendRef. svcPort, err := getServicePort(svc, int32(*ref.Port)) if err != nil { - return svcNsName, v1.ServicePort{}, err + return []v1.IPFamily{}, v1.ServicePort{}, err + } + + return svc.Spec.IPFamilies, svcPort, nil +} + +func verifyIPFamily(npCfg *NginxProxy, svcIPFamily []v1.IPFamily) error { + if npCfg == nil || npCfg.Source == nil || !npCfg.Valid { + return nil } - return svcNsName, svcPort, nil + // we can access this field since we have already validated that ipFamily is not nil in validateNginxProxy. + npIPFamily := npCfg.Source.Spec.IPFamily + if *npIPFamily == ngfAPI.IPv4 { + if slices.Contains(svcIPFamily, v1.IPv6Protocol) { + // capitalizing error message to match the rest of the error messages associated with a condition + return errors.New( //nolint: stylecheck + "Service configured with IPv6 family but NginxProxy is configured with IPv4", + ) + } + } + if *npIPFamily == ngfAPI.IPv6 { + if slices.Contains(svcIPFamily, v1.IPv4Protocol) { + // capitalizing error message to match the rest of the error messages associated with a condition + return errors.New( //nolint: stylecheck + "Service configured with IPv4 family but NginxProxy is configured with IPv6", + ) + } + } + + return nil } func validateRouteBackendRef( diff --git a/internal/mode/static/state/graph/backend_refs_test.go b/internal/mode/static/state/graph/backend_refs_test.go index 140d718864..c260408c43 100644 --- a/internal/mode/static/state/graph/backend_refs_test.go +++ b/internal/mode/static/state/graph/backend_refs_test.go @@ -1,6 +1,7 @@ package graph import ( + "errors" "testing" . "github.com/onsi/gomega" @@ -14,6 +15,7 @@ import ( "sigs.k8s.io/gateway-api/apis/v1alpha3" "sigs.k8s.io/gateway-api/apis/v1beta1" + ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" @@ -254,7 +256,7 @@ func TestValidateWeight(t *testing.T) { } } -func TestGetServiceAndPortFromRef(t *testing.T) { +func TestGetIPFamilyAndPortFromRef(t *testing.T) { svc1 := &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "service1", @@ -266,12 +268,9 @@ func TestGetServiceAndPortFromRef(t *testing.T) { Port: 80, }, }, + IPFamilies: []v1.IPFamily{v1.IPv4Protocol}, }, } - svc1NsName := types.NamespacedName{ - Namespace: "test", - Name: "service1", - } svc2 := &v1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -281,17 +280,19 @@ func TestGetServiceAndPortFromRef(t *testing.T) { } tests := []struct { - ref gatewayv1.BackendRef - expServiceNsName types.NamespacedName - name string - expServicePort v1.ServicePort - expErr bool + ref gatewayv1.BackendRef + svcNsName types.NamespacedName + expSvcIPFamily []v1.IPFamily + name string + expServicePort v1.ServicePort + expErr bool }{ { - name: "normal case", - ref: getNormalRef(), - expServiceNsName: svc1NsName, - expServicePort: v1.ServicePort{Port: 80}, + name: "normal case", + ref: getNormalRef(), + expServicePort: v1.ServicePort{Port: 80}, + expSvcIPFamily: []v1.IPFamily{v1.IPv4Protocol}, + svcNsName: types.NamespacedName{Namespace: "test", Name: "service1"}, }, { name: "service does not exist", @@ -299,9 +300,10 @@ func TestGetServiceAndPortFromRef(t *testing.T) { backend.Name = "does-not-exist" return backend }), - expErr: true, - expServiceNsName: types.NamespacedName{Name: "does-not-exist", Namespace: "test"}, - expServicePort: v1.ServicePort{}, + expErr: true, + expServicePort: v1.ServicePort{}, + expSvcIPFamily: []v1.IPFamily{}, + svcNsName: types.NamespacedName{Namespace: "test", Name: "does-not-exist"}, }, { name: "no matching port for service and port", @@ -309,9 +311,10 @@ func TestGetServiceAndPortFromRef(t *testing.T) { backend.Port = helpers.GetPointer[gatewayv1.PortNumber](504) return backend }), - expErr: true, - expServiceNsName: svc1NsName, - expServicePort: v1.ServicePort{}, + expErr: true, + expServicePort: v1.ServicePort{}, + expSvcIPFamily: []v1.IPFamily{}, + svcNsName: types.NamespacedName{Namespace: "test", Name: "service1"}, }, } @@ -326,11 +329,87 @@ func TestGetServiceAndPortFromRef(t *testing.T) { t.Run(test.name, func(t *testing.T) { g := NewWithT(t) - svcNsName, servicePort, err := getServiceAndPortFromRef(test.ref, "test", services, refPath) + svcIPFamily, servicePort, err := getIPFamilyAndPortFromRef(test.ref, test.svcNsName, services, refPath) g.Expect(err != nil).To(Equal(test.expErr)) - g.Expect(svcNsName).To(Equal(test.expServiceNsName)) g.Expect(servicePort).To(Equal(test.expServicePort)) + g.Expect(svcIPFamily).To(Equal(test.expSvcIPFamily)) + }) + } +} + +func TestVerifyIPFamily(t *testing.T) { + test := []struct { + name string + expErr error + npCfg *NginxProxy + svcIPFamily []v1.IPFamily + }{ + { + name: "Valid - IPv6 and IPv4 configured for NGINX, service has only IPv4", + npCfg: &NginxProxy{ + Source: &ngfAPI.NginxProxy{ + Spec: ngfAPI.NginxProxySpec{ + IPFamily: helpers.GetPointer(ngfAPI.Dual), + }, + }, + Valid: true, + }, + svcIPFamily: []v1.IPFamily{v1.IPv4Protocol}, + }, + { + name: "Valid - IPv6 and IPv4 configured for NGINX, service has only IPv6", + npCfg: &NginxProxy{ + Source: &ngfAPI.NginxProxy{ + Spec: ngfAPI.NginxProxySpec{ + IPFamily: helpers.GetPointer(ngfAPI.Dual), + }, + }, + Valid: true, + }, + svcIPFamily: []v1.IPFamily{v1.IPv6Protocol}, + }, + { + name: "Invalid - IPv4 configured for NGINX, service has only IPv6", + npCfg: &NginxProxy{ + Source: &ngfAPI.NginxProxy{ + Spec: ngfAPI.NginxProxySpec{ + IPFamily: helpers.GetPointer(ngfAPI.IPv4), + }, + }, + Valid: true, + }, + svcIPFamily: []v1.IPFamily{v1.IPv6Protocol}, + expErr: errors.New("Service configured with IPv6 family but NginxProxy is configured with IPv4"), + }, + { + name: "Invalid - IPv6 configured for NGINX, service has only IPv4", + npCfg: &NginxProxy{ + Source: &ngfAPI.NginxProxy{ + Spec: ngfAPI.NginxProxySpec{ + IPFamily: helpers.GetPointer(ngfAPI.IPv6), + }, + }, + Valid: true, + }, + svcIPFamily: []v1.IPFamily{v1.IPv4Protocol}, + expErr: errors.New("Service configured with IPv4 family but NginxProxy is configured with IPv6"), + }, + { + name: "Valid - When NginxProxy is nil", + svcIPFamily: []v1.IPFamily{v1.IPv4Protocol}, + }, + } + + for _, test := range test { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + err := verifyIPFamily(test.npCfg, test.svcIPFamily) + if test.expErr != nil { + g.Expect(err).To(Equal(test.expErr)) + } else { + g.Expect(err).ToNot(HaveOccurred()) + } }) } } @@ -680,7 +759,7 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { t.Run(test.name, func(t *testing.T) { g := NewWithT(t) resolver := newReferenceGrantResolver(nil) - addBackendRefsToRules(test.route, resolver, services, test.policies) + addBackendRefsToRules(test.route, resolver, services, test.policies, nil) var actual []BackendRef if test.route.Spec.Rules != nil { @@ -706,6 +785,7 @@ func TestCreateBackend(t *testing.T) { Port: 80, }, }, + IPFamilies: []v1.IPFamily{v1.IPv4Protocol}, }, } } @@ -771,6 +851,7 @@ func TestCreateBackend(t *testing.T) { tests := []struct { expectedCondition *conditions.Condition + nginxProxy *NginxProxy name string expectedServicePortReference string ref gatewayv1.HTTPBackendRef @@ -857,10 +938,12 @@ func TestCreateBackend(t *testing.T) { }), }, expectedBackend: BackendRef{ - SvcNsName: types.NamespacedName{Name: "not-exist", Namespace: "test"}, - ServicePort: v1.ServicePort{}, - Weight: 5, - Valid: false, + Weight: 5, + Valid: false, + SvcNsName: types.NamespacedName{ + Namespace: "test", + Name: "not-exist", + }, }, expectedServicePortReference: "", expectedCondition: helpers.GetPointer( @@ -868,6 +951,30 @@ func TestCreateBackend(t *testing.T) { ), name: "service doesn't exist", }, + { + ref: gatewayv1.HTTPBackendRef{ + BackendRef: getModifiedRef(func(backend gatewayv1.BackendRef) gatewayv1.BackendRef { + backend.Name = "service2" + return backend + }), + }, + expectedBackend: BackendRef{ + SvcNsName: svc2NamespacedName, + ServicePort: svc1.Spec.Ports[0], + Weight: 5, + Valid: false, + }, + nginxProxy: &NginxProxy{ + Source: &ngfAPI.NginxProxy{ + Spec: ngfAPI.NginxProxySpec{IPFamily: helpers.GetPointer(ngfAPI.IPv6)}, + }, + Valid: true, + }, + expectedCondition: helpers.GetPointer( + staticConds.NewRouteInvalidIPFamily(`Service configured with IPv4 family but NginxProxy is configured with IPv6`), + ), + name: "service IPFamily doesn't match NginxProxy IPFamily", + }, { ref: gatewayv1.HTTPBackendRef{ BackendRef: getModifiedRef(func(backend gatewayv1.BackendRef) gatewayv1.BackendRef { @@ -940,6 +1047,7 @@ func TestCreateBackend(t *testing.T) { services, refPath, policies, + test.nginxProxy, ) g.Expect(helpers.Diff(test.expectedBackend, backend)).To(BeEmpty()) diff --git a/internal/mode/static/state/graph/graph.go b/internal/mode/static/state/graph/graph.go index 4fc86119cb..d874d5d2c7 100644 --- a/internal/mode/static/state/graph/graph.go +++ b/internal/mode/static/state/graph/graph.go @@ -225,7 +225,7 @@ func BuildGraph( ) bindRoutesToListeners(routes, gw, state.Namespaces) - addBackendRefsToRouteRules(routes, refGrantResolver, state.Services, processedBackendTLSPolicies) + addBackendRefsToRouteRules(routes, refGrantResolver, state.Services, processedBackendTLSPolicies, npCfg) referencedNamespaces := buildReferencedNamespaces(state.Namespaces, gw) diff --git a/internal/mode/static/state/graph/nginxproxy.go b/internal/mode/static/state/graph/nginxproxy.go index 29cebdbea2..d36133308b 100644 --- a/internal/mode/static/state/graph/nginxproxy.go +++ b/internal/mode/static/state/graph/nginxproxy.go @@ -6,6 +6,7 @@ import ( v1 "sigs.k8s.io/gateway-api/apis/v1" ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation" ) @@ -108,5 +109,22 @@ func validateNginxProxy( } } + if npCfg.Spec.IPFamily != nil { + ipFamily := npCfg.Spec.IPFamily + ipFamilyPath := spec.Child("ipFamily") + switch *ipFamily { + case ngfAPI.Dual, ngfAPI.IPv4, ngfAPI.IPv6: + default: + allErrs = append( + allErrs, + field.NotSupported( + ipFamilyPath, + ipFamily, + []string{string(ngfAPI.Dual), string(ngfAPI.IPv4), string(ngfAPI.IPv6)})) + } + } else { + npCfg.Spec.IPFamily = helpers.GetPointer[ngfAPI.IPFamilyType](ngfAPI.Dual) + } + return allErrs } diff --git a/internal/mode/static/state/graph/nginxproxy_test.go b/internal/mode/static/state/graph/nginxproxy_test.go index 1aed0913fc..efec06e865 100644 --- a/internal/mode/static/state/graph/nginxproxy_test.go +++ b/internal/mode/static/state/graph/nginxproxy_test.go @@ -71,6 +71,9 @@ func TestGetNginxProxy(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "np2", }, + Spec: ngfAPI.NginxProxySpec{ + IPFamily: helpers.GetPointer(ngfAPI.Dual), + }, }, Valid: true, }, @@ -262,6 +265,7 @@ func TestValidateNginxProxy(t *testing.T) { {Key: "key", Value: "value"}, }, }, + IPFamily: helpers.GetPointer[ngfAPI.IPFamilyType](ngfAPI.Dual), }, }, expectErrCount: 0, @@ -326,6 +330,18 @@ func TestValidateNginxProxy(t *testing.T) { expErrSubstring: "telemetry.spanAttributes", expectErrCount: 2, }, + { + name: "invalid ipFamily type", + validator: createInvalidValidator(), + np: &ngfAPI.NginxProxy{ + Spec: ngfAPI.NginxProxySpec{ + Telemetry: &ngfAPI.Telemetry{}, + IPFamily: helpers.GetPointer[ngfAPI.IPFamilyType]("invalid"), + }, + }, + expErrSubstring: "spec.ipFamily", + expectErrCount: 1, + }, } for _, test := range tests { diff --git a/internal/mode/static/state/resolver/resolver.go b/internal/mode/static/state/resolver/resolver.go index 1200add7f3..6ceb9637cd 100644 --- a/internal/mode/static/state/resolver/resolver.go +++ b/internal/mode/static/state/resolver/resolver.go @@ -3,6 +3,7 @@ package resolver import ( "context" "fmt" + "slices" v1 "k8s.io/api/core/v1" discoveryV1 "k8s.io/api/discovery/v1" @@ -19,7 +20,12 @@ import ( // ServiceResolver resolves a Service's NamespacedName and ServicePort to a list of Endpoints. // Returns an error if the Service or Service Port cannot be resolved. type ServiceResolver interface { - Resolve(ctx context.Context, svcNsName types.NamespacedName, svcPort v1.ServicePort) ([]Endpoint, error) + Resolve( + ctx context.Context, + svcNsName types.NamespacedName, + svcPort v1.ServicePort, + allowedAddressType []discoveryV1.AddressType, + ) ([]Endpoint, error) } // Endpoint is the internal representation of a Kubernetes endpoint. @@ -28,6 +34,8 @@ type Endpoint struct { Address string // Port is the port of the endpoint. Port int32 + // IPv6 is true if the endpoint is an IPv6 address. + IPv6 bool } // ServiceResolverImpl implements ServiceResolver. @@ -46,6 +54,7 @@ func (e *ServiceResolverImpl) Resolve( ctx context.Context, svcNsName types.NamespacedName, svcPort v1.ServicePort, + allowedAddressType []discoveryV1.AddressType, ) ([]Endpoint, error) { if svcPort.Port == 0 || svcNsName.Name == "" || svcNsName.Namespace == "" { panic(fmt.Errorf("expected the following fields to be non-empty: name: %s, ns: %s, port: %d", @@ -66,7 +75,13 @@ func (e *ServiceResolverImpl) Resolve( return nil, fmt.Errorf("no endpoints found for Service %s", svcNsName) } - return resolveEndpoints(svcNsName, svcPort, endpointSliceList, initEndpointSetWithCalculatedSize) + return resolveEndpoints( + svcNsName, + svcPort, + endpointSliceList, + initEndpointSetWithCalculatedSize, + allowedAddressType, + ) } type initEndpointSetFunc func([]discoveryV1.EndpointSlice) map[Endpoint]struct{} @@ -97,8 +112,9 @@ func resolveEndpoints( svcPort v1.ServicePort, endpointSliceList discoveryV1.EndpointSliceList, initEndpointsSet initEndpointSetFunc, + allowedAddressType []discoveryV1.AddressType, ) ([]Endpoint, error) { - filteredSlices := filterEndpointSliceList(endpointSliceList, svcPort) + filteredSlices := filterEndpointSliceList(endpointSliceList, svcPort, allowedAddressType) if len(filteredSlices) == 0 { return nil, fmt.Errorf("no valid endpoints found for Service %s and port %d", svcNsName, svcPort.Port) @@ -109,6 +125,7 @@ func resolveEndpoints( endpointSet := initEndpointsSet(filteredSlices) for _, eps := range filteredSlices { + ipv6 := eps.AddressType == discoveryV1.AddressTypeIPv6 for _, endpoint := range eps.Endpoints { if !endpointReady(endpoint) { continue @@ -119,7 +136,7 @@ func resolveEndpoints( endpointPort := findPort(eps.Ports, svcPort) for _, address := range endpoint.Addresses { - ep := Endpoint{Address: address, Port: endpointPort} + ep := Endpoint{Address: address, Port: endpointPort, IPv6: ipv6} endpointSet[ep] = struct{}{} } } @@ -148,8 +165,16 @@ func getDefaultPort(svcPort v1.ServicePort) int32 { return svcPort.Port } -func ignoreEndpointSlice(endpointSlice discoveryV1.EndpointSlice, port v1.ServicePort) bool { - if endpointSlice.AddressType != discoveryV1.AddressTypeIPv4 { +func ignoreEndpointSlice( + endpointSlice discoveryV1.EndpointSlice, + port v1.ServicePort, + allowedAddressType []discoveryV1.AddressType, +) bool { + if endpointSlice.AddressType == discoveryV1.AddressTypeFQDN { + return true + } + + if !slices.Contains(allowedAddressType, endpointSlice.AddressType) { return true } @@ -165,11 +190,12 @@ func endpointReady(endpoint discoveryV1.Endpoint) bool { func filterEndpointSliceList( endpointSliceList discoveryV1.EndpointSliceList, port v1.ServicePort, + allowedAddressType []discoveryV1.AddressType, ) []discoveryV1.EndpointSlice { filtered := make([]discoveryV1.EndpointSlice, 0, len(endpointSliceList.Items)) for _, endpointSlice := range endpointSliceList.Items { - if !ignoreEndpointSlice(endpointSlice, port) { + if !ignoreEndpointSlice(endpointSlice, port, allowedAddressType) { filtered = append(filtered, endpointSlice) } } diff --git a/internal/mode/static/state/resolver/resolver_test.go b/internal/mode/static/state/resolver/resolver_test.go index 1b7c248621..adb87bb1ac 100644 --- a/internal/mode/static/state/resolver/resolver_test.go +++ b/internal/mode/static/state/resolver/resolver_test.go @@ -14,15 +14,25 @@ import ( ) var ( - svcPortName = "svc-port" + svcPortName = "svc-port" + dualAddressType = []discoveryV1.AddressType{ + discoveryV1.AddressTypeIPv4, + discoveryV1.AddressTypeIPv6, + } - addresses = []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"} + addresses = []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"} + addressesIPv6 = []string{"2001:db8::1", "2001:db8::2", "2001:db8::3"} readyEndpoint1 = discoveryV1.Endpoint{ Addresses: addresses, Conditions: discoveryV1.EndpointConditions{Ready: helpers.GetPointer(true)}, } + readyIPv6Endpoint2 = discoveryV1.Endpoint{ + Addresses: addressesIPv6, + Conditions: discoveryV1.EndpointConditions{Ready: helpers.GetPointer(true)}, + } + notReadyEndpoint = discoveryV1.Endpoint{ Addresses: addresses, Conditions: discoveryV1.EndpointConditions{Ready: helpers.GetPointer(false)}, @@ -44,7 +54,7 @@ var ( Endpoints: nil, } - validEndpointSlice = discoveryV1.EndpointSlice{ + validIPv4EndpontSlice = discoveryV1.EndpointSlice{ AddressType: discoveryV1.AddressTypeIPv4, Endpoints: []discoveryV1.Endpoint{ readyEndpoint1, @@ -59,8 +69,19 @@ var ( }, } - invalidAddressTypeEndpointSlice = discoveryV1.EndpointSlice{ + validIPv6EndpointSlice = discoveryV1.EndpointSlice{ AddressType: discoveryV1.AddressTypeIPv6, + Endpoints: []discoveryV1.Endpoint{readyIPv6Endpoint2}, + Ports: []discoveryV1.EndpointPort{ + { + Name: &svcPortName, + Port: helpers.GetPointer[int32](80), + }, + }, + } + + invalidAddressTypeEndpointSlice = discoveryV1.EndpointSlice{ + AddressType: discoveryV1.AddressTypeFQDN, Endpoints: []discoveryV1.Endpoint{readyEndpoint1}, Ports: []discoveryV1.EndpointPort{ { @@ -83,13 +104,54 @@ var ( ) func TestFilterEndpointSliceList(t *testing.T) { - sliceList := discoveryV1.EndpointSliceList{ - Items: []discoveryV1.EndpointSlice{ - validEndpointSlice, - invalidAddressTypeEndpointSlice, - invalidPortEndpointSlice, - nilEndpoints, - mixedValidityEndpointSlice, + test := []struct { + msg string + sliceList discoveryV1.EndpointSliceList + expList []discoveryV1.EndpointSlice + allowedAddressType []discoveryV1.AddressType + }{ + { + msg: "only ipv4 enabled", + sliceList: discoveryV1.EndpointSliceList{ + Items: []discoveryV1.EndpointSlice{ + validIPv4EndpontSlice, + validIPv6EndpointSlice, + }, + }, + expList: []discoveryV1.EndpointSlice{ + validIPv4EndpontSlice, + }, + allowedAddressType: []discoveryV1.AddressType{discoveryV1.AddressTypeIPv4}, + }, + { + msg: "only ipv6 enabled", + sliceList: discoveryV1.EndpointSliceList{ + Items: []discoveryV1.EndpointSlice{ + validIPv4EndpontSlice, + validIPv6EndpointSlice, + }, + }, + expList: []discoveryV1.EndpointSlice{ + validIPv6EndpointSlice, + }, + allowedAddressType: []discoveryV1.AddressType{discoveryV1.AddressTypeIPv6}, + }, + { + msg: "ipv4 and ipv6 enabled", + sliceList: discoveryV1.EndpointSliceList{ + Items: []discoveryV1.EndpointSlice{ + validIPv4EndpontSlice, + validIPv6EndpointSlice, + invalidAddressTypeEndpointSlice, + invalidPortEndpointSlice, + nilEndpoints, + mixedValidityEndpointSlice, + }, + }, + expList: []discoveryV1.EndpointSlice{ + validIPv4EndpontSlice, validIPv6EndpointSlice, mixedValidityEndpointSlice, + }, + allowedAddressType: dualAddressType, }, } @@ -99,11 +161,11 @@ func TestFilterEndpointSliceList(t *testing.T) { TargetPort: intstr.FromInt(80), } - expFilteredList := []discoveryV1.EndpointSlice{validEndpointSlice, mixedValidityEndpointSlice} - - filteredSliceList := filterEndpointSliceList(sliceList, svcPort) - g := NewWithT(t) - g.Expect(filteredSliceList).To(Equal(expFilteredList)) + for _, tc := range test { + filteredSliceList := filterEndpointSliceList(tc.sliceList, svcPort, tc.allowedAddressType) + g := NewWithT(t) + g.Expect(filteredSliceList).To(Equal(tc.expList)) + } } func TestGetDefaultPort(t *testing.T) { @@ -156,9 +218,9 @@ func TestIgnoreEndpointSlice(t *testing.T) { ignore bool }{ { - msg: "IPV6 address type", + msg: "FQDN address type", slice: discoveryV1.EndpointSlice{ - AddressType: discoveryV1.AddressTypeIPv6, + AddressType: discoveryV1.AddressTypeFQDN, Ports: []discoveryV1.EndpointPort{ { Name: &svcPortName, @@ -174,13 +236,13 @@ func TestIgnoreEndpointSlice(t *testing.T) { ignore: true, }, { - msg: "FQDN address type", + msg: "no matching port", slice: discoveryV1.EndpointSlice{ - AddressType: discoveryV1.AddressTypeFQDN, + AddressType: discoveryV1.AddressTypeIPv4, Ports: []discoveryV1.EndpointPort{ { - Name: &svcPortName, - Port: &port8080, + Name: helpers.GetPointer("other-svc-port"), + Port: &port4000, }, }, }, @@ -192,13 +254,12 @@ func TestIgnoreEndpointSlice(t *testing.T) { ignore: true, }, { - msg: "no matching port", + msg: "nil endpoint port", slice: discoveryV1.EndpointSlice{ AddressType: discoveryV1.AddressTypeIPv4, Ports: []discoveryV1.EndpointPort{ { - Name: helpers.GetPointer("other-svc-port"), - Port: &port4000, + Port: nil, }, }, }, @@ -207,15 +268,16 @@ func TestIgnoreEndpointSlice(t *testing.T) { Port: 80, TargetPort: intstr.FromInt(8080), }, - ignore: true, + ignore: false, }, { - msg: "nil endpoint port", + msg: "normal IPV4 address type", slice: discoveryV1.EndpointSlice{ AddressType: discoveryV1.AddressTypeIPv4, Ports: []discoveryV1.EndpointPort{ { - Port: nil, + Name: &svcPortName, + Port: &port8080, }, }, }, @@ -227,9 +289,9 @@ func TestIgnoreEndpointSlice(t *testing.T) { ignore: false, }, { - msg: "normal", + msg: "normal IPV6 address type", slice: discoveryV1.EndpointSlice{ - AddressType: discoveryV1.AddressTypeIPv4, + AddressType: discoveryV1.AddressTypeIPv6, Ports: []discoveryV1.EndpointPort{ { Name: &svcPortName, @@ -247,7 +309,7 @@ func TestIgnoreEndpointSlice(t *testing.T) { } for _, tc := range testcases { g := NewWithT(t) - g.Expect(ignoreEndpointSlice(tc.slice, tc.servicePort)).To(Equal(tc.ignore)) + g.Expect(ignoreEndpointSlice(tc.slice, tc.servicePort, dualAddressType)).To(Equal(tc.ignore)) } } @@ -542,7 +604,7 @@ func bench(b *testing.B, svcNsName types.NamespacedName, ) { b.Helper() for i := 0; i < b.N; i++ { - res, err := resolveEndpoints(svcNsName, v1.ServicePort{Port: 80}, list, initSet) + res, err := resolveEndpoints(svcNsName, v1.ServicePort{Port: 80}, list, initSet, dualAddressType) if len(res) != n { b.Fatalf("expected %d endpoints, got %d", n, len(res)) } diff --git a/internal/mode/static/state/resolver/resolverfakes/fake_service_resolver.go b/internal/mode/static/state/resolver/resolverfakes/fake_service_resolver.go index 8fa8a9fcdc..09e624cfe5 100644 --- a/internal/mode/static/state/resolver/resolverfakes/fake_service_resolver.go +++ b/internal/mode/static/state/resolver/resolverfakes/fake_service_resolver.go @@ -7,16 +7,18 @@ import ( "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/resolver" v1 "k8s.io/api/core/v1" + v1a "k8s.io/api/discovery/v1" "k8s.io/apimachinery/pkg/types" ) type FakeServiceResolver struct { - ResolveStub func(context.Context, types.NamespacedName, v1.ServicePort) ([]resolver.Endpoint, error) + ResolveStub func(context.Context, types.NamespacedName, v1.ServicePort, []v1a.AddressType) ([]resolver.Endpoint, error) resolveMutex sync.RWMutex resolveArgsForCall []struct { arg1 context.Context arg2 types.NamespacedName arg3 v1.ServicePort + arg4 []v1a.AddressType } resolveReturns struct { result1 []resolver.Endpoint @@ -30,20 +32,26 @@ type FakeServiceResolver struct { invocationsMutex sync.RWMutex } -func (fake *FakeServiceResolver) Resolve(arg1 context.Context, arg2 types.NamespacedName, arg3 v1.ServicePort) ([]resolver.Endpoint, error) { +func (fake *FakeServiceResolver) Resolve(arg1 context.Context, arg2 types.NamespacedName, arg3 v1.ServicePort, arg4 []v1a.AddressType) ([]resolver.Endpoint, error) { + var arg4Copy []v1a.AddressType + if arg4 != nil { + arg4Copy = make([]v1a.AddressType, len(arg4)) + copy(arg4Copy, arg4) + } fake.resolveMutex.Lock() ret, specificReturn := fake.resolveReturnsOnCall[len(fake.resolveArgsForCall)] fake.resolveArgsForCall = append(fake.resolveArgsForCall, struct { arg1 context.Context arg2 types.NamespacedName arg3 v1.ServicePort - }{arg1, arg2, arg3}) + arg4 []v1a.AddressType + }{arg1, arg2, arg3, arg4Copy}) stub := fake.ResolveStub fakeReturns := fake.resolveReturns - fake.recordInvocation("Resolve", []interface{}{arg1, arg2, arg3}) + fake.recordInvocation("Resolve", []interface{}{arg1, arg2, arg3, arg4Copy}) fake.resolveMutex.Unlock() if stub != nil { - return stub(arg1, arg2, arg3) + return stub(arg1, arg2, arg3, arg4) } if specificReturn { return ret.result1, ret.result2 @@ -57,17 +65,17 @@ func (fake *FakeServiceResolver) ResolveCallCount() int { return len(fake.resolveArgsForCall) } -func (fake *FakeServiceResolver) ResolveCalls(stub func(context.Context, types.NamespacedName, v1.ServicePort) ([]resolver.Endpoint, error)) { +func (fake *FakeServiceResolver) ResolveCalls(stub func(context.Context, types.NamespacedName, v1.ServicePort, []v1a.AddressType) ([]resolver.Endpoint, error)) { fake.resolveMutex.Lock() defer fake.resolveMutex.Unlock() fake.ResolveStub = stub } -func (fake *FakeServiceResolver) ResolveArgsForCall(i int) (context.Context, types.NamespacedName, v1.ServicePort) { +func (fake *FakeServiceResolver) ResolveArgsForCall(i int) (context.Context, types.NamespacedName, v1.ServicePort, []v1a.AddressType) { fake.resolveMutex.RLock() defer fake.resolveMutex.RUnlock() argsForCall := fake.resolveArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 } func (fake *FakeServiceResolver) ResolveReturns(result1 []resolver.Endpoint, result2 error) { diff --git a/internal/mode/static/state/resolver/service_resolver_test.go b/internal/mode/static/state/resolver/service_resolver_test.go index ef605eb5ba..51cdd29485 100644 --- a/internal/mode/static/state/resolver/service_resolver_test.go +++ b/internal/mode/static/state/resolver/service_resolver_test.go @@ -150,6 +150,10 @@ var _ = Describe("ServiceResolver", func() { "other-svc-port", discoveryV1.AddressTypeIPv4, ) + dualAddressType = []discoveryV1.AddressType{ + discoveryV1.AddressTypeIPv4, + discoveryV1.AddressTypeIPv6, + } ) var ( @@ -196,9 +200,14 @@ var _ = Describe("ServiceResolver", func() { Address: "12.0.0.1", Port: 8080, }, + { + Address: "FE80:CD00:0:CDE:1257:0:211E:729C", + Port: 8080, + IPv6: true, + }, } - endpoints, err := serviceResolver.Resolve(context.TODO(), svcNsName, svcPort) + endpoints, err := serviceResolver.Resolve(context.TODO(), svcNsName, svcPort, dualAddressType) Expect(err).ToNot(HaveOccurred()) Expect(endpoints).To(ConsistOf(expectedEndpoints)) }) @@ -207,29 +216,29 @@ var _ = Describe("ServiceResolver", func() { Expect(fakeK8sClient.Delete(context.TODO(), slice1)).To(Succeed()) Expect(fakeK8sClient.Delete(context.TODO(), slice2)).To(Succeed()) Expect(fakeK8sClient.Delete(context.TODO(), dupeEndpointSlice)).To(Succeed()) + Expect(fakeK8sClient.Delete(context.TODO(), sliceIPV6)).To(Succeed()) - endpoints, err := serviceResolver.Resolve(context.TODO(), svcNsName, svcPort) + endpoints, err := serviceResolver.Resolve(context.TODO(), svcNsName, svcPort, dualAddressType) Expect(err).To(HaveOccurred()) Expect(endpoints).To(BeNil()) }) It("returns an error if there are no endpoint slices for the service", func() { // delete remaining endpoint slices - Expect(fakeK8sClient.Delete(context.TODO(), sliceIPV6)).To(Succeed()) Expect(fakeK8sClient.Delete(context.TODO(), sliceNoMatchingPortName)).To(Succeed()) - endpoints, err := serviceResolver.Resolve(context.TODO(), svcNsName, svcPort) + endpoints, err := serviceResolver.Resolve(context.TODO(), svcNsName, svcPort, dualAddressType) Expect(err).To(HaveOccurred()) Expect(endpoints).To(BeNil()) }) It("panics if the service NamespacedName is empty", func() { resolve := func() { - _, _ = serviceResolver.Resolve(context.TODO(), types.NamespacedName{}, svcPort) + _, _ = serviceResolver.Resolve(context.TODO(), types.NamespacedName{}, svcPort, dualAddressType) } Expect(resolve).Should(Panic()) }) It("panics if the ServicePort is empty", func() { resolve := func() { - _, _ = serviceResolver.Resolve(context.TODO(), types.NamespacedName{}, v1.ServicePort{}) + _, _ = serviceResolver.Resolve(context.TODO(), types.NamespacedName{}, v1.ServicePort{}, dualAddressType) } Expect(resolve).Should(Panic()) }) diff --git a/site/content/how-to/monitoring/troubleshooting.md b/site/content/how-to/monitoring/troubleshooting.md index 6de1f8f76e..e15bee6a1c 100644 --- a/site/content/how-to/monitoring/troubleshooting.md +++ b/site/content/how-to/monitoring/troubleshooting.md @@ -405,6 +405,39 @@ Or view the following error message in the NGINX logs: The request body exceeds the [client_max_body_size](https://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size). To **resolve** this, you can configure the `client_max_body_size` using the `ClientSettingsPolicy` API. Read the [Client Settings Policy]({{< relref "how-to/traffic-management/client-settings.md" >}}) documentation for more information. +##### IP Family Mismatch Errors + +If you `describe` your HTTPRoute and see the following error: + +```text + Conditions: + Last Transition Time: 2024-07-14T23:36:37Z + Message: The route is accepted + Observed Generation: 1 + Reason: Accepted + Status: True + Type: Accepted + Last Transition Time: 2024-07-14T23:36:37Z + Message: Service configured with IPv4 family but NginxProxy is configured with IPv6 + Observed Generation: 1 + Reason: InvalidServiceIPFamily + Status: False + Type: ResolvedRefs + Controller Name: gateway.nginx.org/nginx-gateway-controller +``` + +The Service associated with your HTTPRoute is configured with a IP Family different than the one specified in the NginxProxy configuration. +To **resolve** this, you can do one of the following: + +- Update the NginxProxy configuration with the proper [`ipFamily`](({{< relref "reference/api.md" >}})) field. You can edit the NginxProxy configuration using `kubectl edit`. For example: + + ```shell + kubectl edit -n nginx-gateway nginxproxies.gateway.nginx.org ngf-proxy-config + ``` + +- When installing NGINX Gateway Fabric, change the IPFamily by modifying the field `nginx.config.ipFamily` in the `values.yaml` or add the `--set nginx.config.ipFamily=` flag to the `helm install` command. The supported IPFamilies are `ipv4`, `ipv6` and `dual` (default). + +- Adjust the IPFamily of your Service to match that of the NginxProxy configuration. ### Further reading diff --git a/site/content/overview/gateway-api-compatibility.md b/site/content/overview/gateway-api-compatibility.md index 50c118a41e..567b3c5af0 100644 --- a/site/content/overview/gateway-api-compatibility.md +++ b/site/content/overview/gateway-api-compatibility.md @@ -179,6 +179,7 @@ See the [static-mode]({{< relref "/reference/cli-help.md#static-mode">}}) comman - `ResolvedRefs/False/RefNotPermitted` - `ResolvedRefs/False/BackendNotFound` - `ResolvedRefs/False/UnsupportedValue`: Custom reason for when one of the HTTPRoute rules has a backendRef with an unsupported value. + - `ResolvedRefs/False/InvalidIPFamily`: Custom reason for when one of the HTTPRoute rules has a backendRef that has an invalid IPFamily. - `PartiallyInvalid/True/UnsupportedValue` --- diff --git a/site/content/reference/api.md b/site/content/reference/api.md index fef67d181f..b597e1a39e 100644 --- a/site/content/reference/api.md +++ b/site/content/reference/api.md @@ -299,6 +299,21 @@ NginxProxySpec + + + + + + + +
+ipFamily
+ + +IPFamilyType + + +
+(Optional) +

IPFamily specifies the IP family to be used by the NGINX. +Default is “dual”, meaning the server will use both IPv4 and IPv6.

+
telemetry
@@ -715,6 +730,34 @@ Support: Gateway, HTTPRoute, GRPCRoute.

Duration can be specified in milliseconds (ms) or seconds (s) A value without a suffix is seconds. Examples: 120s, 50ms.

+

IPFamilyType +(string alias)

+

+

+(Appears on: +NginxProxySpec) +

+

+

IPFamilyType specifies the IP family to be used by NGINX.

+

+ + + + + + + + + + + + + + +
ValueDescription

"dual"

Dual specifies that NGINX will use both IPv4 and IPv6.

+

"ipv4"

IPv4 specifies that NGINX will use only IPv4.

+

"ipv6"

IPv6 specifies that NGINX will use only IPv6.

+

Logging

@@ -878,6 +921,21 @@ Logging
+ipFamily
+ + +IPFamilyType + + +
+(Optional) +

IPFamily specifies the IP family to be used by the NGINX. +Default is “dual”, meaning the server will use both IPv4 and IPv6.

+
telemetry