diff --git a/Makefile b/Makefile index 83d51b2492..34cfefddcf 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ TELEMETRY_ENDPOINT=# if empty, NGF will report telemetry in its logs at debug le TELEMETRY_ENDPOINT_INSECURE = false GW_API_VERSION = 1.0.0 +ENABLE_EXPERIMENTAL = false NODE_VERSION = $(shell cat .nvmrc) # go build flags - should not be overridden by the user @@ -192,13 +193,13 @@ install-ngf-local-build-with-plus: build-images-with-plus load-images-with-plus .PHONY: helm-install-local helm-install-local: ## Helm install NGF on configured kind cluster with local images. To build, load, and install with helm run make install-ngf-local-build. - ./conformance/scripts/install-gateway.sh $(GW_API_VERSION) - helm install dev $(CHART_DIR) --create-namespace --wait --set service.type=NodePort --set nginxGateway.image.repository=$(PREFIX) --set nginxGateway.image.tag=$(TAG) --set nginxGateway.image.pullPolicy=Never --set nginx.image.repository=$(NGINX_PREFIX) --set nginx.image.tag=$(TAG) --set nginx.image.pullPolicy=Never -n nginx-gateway + ./conformance/scripts/install-gateway.sh $(GW_API_VERSION) $(ENABLE_EXPERIMENTAL) + helm install dev $(CHART_DIR) --create-namespace --wait --set service.type=NodePort --set nginxGateway.image.repository=$(PREFIX) --set nginxGateway.image.tag=$(TAG) --set nginxGateway.image.pullPolicy=Never --set nginx.image.repository=$(NGINX_PREFIX) --set nginx.image.tag=$(TAG) --set nginx.image.pullPolicy=Never --set nginxGateway.gwAPIExperimentalFeatures.enable=$(ENABLE_EXPERIMENTAL) -n nginx-gateway .PHONY: helm-install-local-with-plus helm-install-local-with-plus: ## Helm install NGF with NGINX Plus on configured kind cluster with local images. To build, load, and install with helm run make install-ngf-local-build-with-plus. - ./conformance/scripts/install-gateway.sh $(GW_API_VERSION) - helm install dev $(CHART_DIR) --create-namespace --wait --set service.type=NodePort --set nginxGateway.image.repository=$(PREFIX) --set nginxGateway.image.tag=$(TAG) --set nginxGateway.image.pullPolicy=Never --set nginx.image.repository=$(NGINX_PLUS_PREFIX) --set nginx.image.tag=$(TAG) --set nginx.image.pullPolicy=Never --set nginx.plus=true -n nginx-gateway + ./conformance/scripts/install-gateway.sh $(GW_API_VERSION) $(ENABLE_EXPERIMENTAL) + helm install dev $(CHART_DIR) --create-namespace --wait --set service.type=NodePort --set nginxGateway.image.repository=$(PREFIX) --set nginxGateway.image.tag=$(TAG) --set nginxGateway.image.pullPolicy=Never --set nginx.image.repository=$(NGINX_PLUS_PREFIX) --set nginx.image.tag=$(TAG) --set nginx.image.pullPolicy=Never --set nginx.plus=true --set nginxGateway.gwAPIExperimentalFeatures.enable=$(ENABLE_EXPERIMENTAL) -n nginx-gateway # Debug Targets .PHONY: debug-build diff --git a/README.md b/README.md index be4f85a056..18c7c0d864 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ NGINX Gateway Fabric is an open-source project that provides an implementation of the [Gateway API](https://gateway-api.sigs.k8s.io/) using [NGINX](https://nginx.org/) as the data plane. The goal of -this project is to implement the core Gateway APIs -- `Gateway`, `GatewayClass`, `HTTPRoute`, `TCPRoute`, `TLSRoute`, +this project is to implement the core Gateway APIs -- `Gateway`, `GatewayClass`, `HTTPRoute`, `GRPCRoute`, `TCPRoute`, `TLSRoute`, and `UDPRoute` -- to configure an HTTP or TCP/UDP load balancer, reverse-proxy, or API gateway for applications running on Kubernetes. NGINX Gateway Fabric supports a subset of the Gateway API. diff --git a/build/Dockerfile.nginx b/build/Dockerfile.nginx index ad12ffe2b3..85b304926f 100644 --- a/build/Dockerfile.nginx +++ b/build/Dockerfile.nginx @@ -13,6 +13,8 @@ RUN apk add --no-cache libcap \ COPY ${NJS_DIR}/httpmatches.js /usr/lib/nginx/modules/njs/httpmatches.js COPY ${NGINX_CONF_DIR}/nginx.conf /etc/nginx/nginx.conf +COPY ${NGINX_CONF_DIR}/grpc-error-locations.conf /etc/nginx/grpc-error-locations.conf +COPY ${NGINX_CONF_DIR}/grpc-error-pages.conf /etc/nginx/grpc-error-pages.conf RUN chown -R 101:1001 /etc/nginx /var/cache/nginx /var/lib/nginx diff --git a/build/Dockerfile.nginxplus b/build/Dockerfile.nginxplus index dd4ba09fca..7e62891e65 100644 --- a/build/Dockerfile.nginxplus +++ b/build/Dockerfile.nginxplus @@ -29,6 +29,8 @@ RUN --mount=type=secret,id=nginx-repo.crt,dst=/etc/apk/cert.pem,mode=0644 \ COPY ${NJS_DIR}/httpmatches.js /usr/lib/nginx/modules/njs/httpmatches.js COPY ${NGINX_CONF_DIR}/nginx-plus.conf /etc/nginx/nginx.conf +COPY ${NGINX_CONF_DIR}/grpc-error-locations.conf /etc/nginx/grpc-error-locations.conf +COPY ${NGINX_CONF_DIR}/grpc-error-pages.conf /etc/nginx/grpc-error-pages.conf RUN chown -R 101:1001 /etc/nginx /var/cache/nginx /var/lib/nginx diff --git a/charts/nginx-gateway-fabric/templates/rbac.yaml b/charts/nginx-gateway-fabric/templates/rbac.yaml index 8d29b828b9..69e4992605 100644 --- a/charts/nginx-gateway-fabric/templates/rbac.yaml +++ b/charts/nginx-gateway-fabric/templates/rbac.yaml @@ -92,6 +92,7 @@ rules: - referencegrants {{- if .Values.nginxGateway.gwAPIExperimentalFeatures.enable }} - backendtlspolicies + - grpcroutes {{- end }} verbs: - list @@ -104,6 +105,7 @@ rules: - gatewayclasses/status {{- if .Values.nginxGateway.gwAPIExperimentalFeatures.enable }} - backendtlspolicies/status + - grpcroutes/status {{- end }} verbs: - update diff --git a/conformance/Makefile b/conformance/Makefile index b7a3627924..58c2daa8e5 100644 --- a/conformance/Makefile +++ b/conformance/Makefile @@ -17,6 +17,10 @@ PROVISIONER_MANIFEST=provisioner/provisioner.yaml ENABLE_EXPERIMENTAL ?= false .DEFAULT_GOAL := help +ifeq ($(ENABLE_EXPERIMENTAL),true) + SUPPORTED_FEATURES +=,GRPCExactMethodMatching,GRPCRouteListenerHostnameMatching,GRPCRouteHeaderMatching +endif + .PHONY: help help: Makefile ## Display this help @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "; printf "Usage:\n\n make \033[36m\033[0m\n\nTargets:\n\n"}; {printf " \033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/deploy/manifests/nginx-gateway-experimental.yaml b/deploy/manifests/nginx-gateway-experimental.yaml index 5d6449a98f..a52689370a 100644 --- a/deploy/manifests/nginx-gateway-experimental.yaml +++ b/deploy/manifests/nginx-gateway-experimental.yaml @@ -77,6 +77,7 @@ rules: - httproutes - referencegrants - backendtlspolicies + - grpcroutes verbs: - list - watch @@ -87,6 +88,7 @@ rules: - gateways/status - gatewayclasses/status - backendtlspolicies/status + - grpcroutes/status verbs: - update - apiGroups: diff --git a/deploy/manifests/nginx-plus-gateway-experimental.yaml b/deploy/manifests/nginx-plus-gateway-experimental.yaml index f43819b427..824440d4e3 100644 --- a/deploy/manifests/nginx-plus-gateway-experimental.yaml +++ b/deploy/manifests/nginx-plus-gateway-experimental.yaml @@ -83,6 +83,7 @@ rules: - httproutes - referencegrants - backendtlspolicies + - grpcroutes verbs: - list - watch @@ -93,6 +94,7 @@ rules: - gateways/status - gatewayclasses/status - backendtlspolicies/status + - grpcroutes/status verbs: - update - apiGroups: diff --git a/go.mod b/go.mod index 380e967fb5..69f3ba5442 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/emicklei/go-restful/v3 v3.11.2 // indirect github.com/evanphx/json-patch v5.7.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/fatih/color v1.16.0 // indirect diff --git a/go.sum b/go.sum index f36e990ba8..dbd5c5555e 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.11.2 h1:1onLa9DcsMYO9P+CXaL0dStDqQ2EHHXLiz+BtnqkLAU= +github.com/emicklei/go-restful/v3 v3.11.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= diff --git a/internal/framework/gatewayclass/validate.go b/internal/framework/gatewayclass/validate.go index 828f8eb06e..78ac7ddd56 100644 --- a/internal/framework/gatewayclass/validate.go +++ b/internal/framework/gatewayclass/validate.go @@ -22,6 +22,7 @@ var gatewayCRDs = map[string]apiVersion{ "httproutes.gateway.networking.k8s.io": {}, "referencegrants.gateway.networking.k8s.io": {}, "backendtlspolicies.gateway.networking.k8s.io": {}, + "grpcroutes.gateway.networking.k8s.io": {}, } type apiVersion struct { diff --git a/internal/mode/static/handler.go b/internal/mode/static/handler.go index 03480bb9d0..fe4f971b71 100644 --- a/internal/mode/static/handler.go +++ b/internal/mode/static/handler.go @@ -245,7 +245,13 @@ func (h *eventHandlerImpl) updateStatuses(ctx context.Context, logger logr.Logge if h.cfg.updateGatewayClassStatus { gcReqs = status.PrepareGatewayClassRequests(graph.GatewayClass, graph.IgnoredGatewayClasses, transitionTime) } - routeReqs := status.PrepareRouteRequests(graph.Routes, transitionTime, h.latestReloadResult, h.cfg.gatewayCtlrName) + routeReqs := status.PrepareRouteRequests( + graph.Routes, + transitionTime, + h.latestReloadResult, + h.cfg.gatewayCtlrName, + ) + polReqs := status.PrepareBackendTLSPolicyRequests(graph.BackendTLSPolicies, transitionTime, h.cfg.gatewayCtlrName) reqs := make([]frameworkStatus.UpdateRequest, 0, len(gcReqs)+len(routeReqs)+len(polReqs)) diff --git a/internal/mode/static/manager.go b/internal/mode/static/manager.go index cb68557165..9069fecc54 100644 --- a/internal/mode/static/manager.go +++ b/internal/mode/static/manager.go @@ -424,7 +424,7 @@ func registerControllers( } if cfg.ExperimentalFeatures { - backendTLSObjs := []ctlrCfg{ + gwExpFeatures := []ctlrCfg{ { objectType: &gatewayv1alpha2.BackendTLSPolicy{}, options: []controller.Option{ @@ -436,8 +436,14 @@ func registerControllers( // https://github.com/nginxinc/nginx-gateway-fabric/issues/1545 objectType: &apiv1.ConfigMap{}, }, + { + objectType: &gatewayv1alpha2.GRPCRoute{}, + options: []controller.Option{ + controller.WithK8sPredicate(k8spredicate.GenerationChangedPredicate{}), + }, + }, } - controllerRegCfgs = append(controllerRegCfgs, backendTLSObjs...) + controllerRegCfgs = append(controllerRegCfgs, gwExpFeatures...) } if cfg.ConfigName != "" { @@ -604,7 +610,12 @@ func prepareFirstEventBatchPreparerArgs( } if enableExperimentalFeatures { - objectLists = append(objectLists, &gatewayv1alpha2.BackendTLSPolicyList{}, &apiv1.ConfigMapList{}) + objectLists = append( + objectLists, + &gatewayv1alpha2.BackendTLSPolicyList{}, + &apiv1.ConfigMapList{}, + &gatewayv1alpha2.GRPCRouteList{}, + ) } if gwNsName == nil { diff --git a/internal/mode/static/manager_test.go b/internal/mode/static/manager_test.go index e28fa3c7bb..a8693a9edc 100644 --- a/internal/mode/static/manager_test.go +++ b/internal/mode/static/manager_test.go @@ -99,6 +99,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) { &ngfAPI.NginxProxyList{}, partialObjectMetadataList, &gatewayv1alpha2.BackendTLSPolicyList{}, + &gatewayv1alpha2.GRPCRouteList{}, }, experimentalEnabled: true, }, diff --git a/internal/mode/static/nginx/conf/grpc-error-locations.conf b/internal/mode/static/nginx/conf/grpc-error-locations.conf new file mode 100644 index 0000000000..680b908cfb --- /dev/null +++ b/internal/mode/static/nginx/conf/grpc-error-locations.conf @@ -0,0 +1,55 @@ +location @grpc_deadline_exceeded { + default_type application/grpc; + add_header content-type application/grpc; + add_header grpc-status 4; + add_header grpc-message 'deadline exceeded'; + return 204; +} + +location @grpc_permission_denied { + default_type application/grpc; + add_header content-type application/grpc; + add_header grpc-status 7; + add_header grpc-message 'permission denied'; + return 204; +} + +location @grpc_resource_exhausted { + default_type application/grpc; + add_header content-type application/grpc; + add_header grpc-status 8; + add_header grpc-message 'resource exhausted'; + return 204; +} + +location @grpc_unimplemented { + default_type application/grpc; + add_header content-type application/grpc; + add_header grpc-status 12; + add_header grpc-message unimplemented; + return 204; +} + +location @grpc_internal { + default_type application/grpc; + add_header content-type application/grpc; + add_header grpc-status 13; + add_header grpc-message 'internal error'; + return 204; +} + +location @grpc_unavailable { + default_type application/grpc; + add_header content-type application/grpc; + add_header grpc-status 14; + add_header grpc-message unavailable; + return 204; +} + +location @grpc_unauthenticated { + default_type application/grpc; + add_header content-type application/grpc; + add_header grpc-status 16; + add_header grpc-message unauthenticated; + return 204; +} diff --git a/internal/mode/static/nginx/conf/grpc-error-pages.conf b/internal/mode/static/nginx/conf/grpc-error-pages.conf new file mode 100644 index 0000000000..ef0efe455a --- /dev/null +++ b/internal/mode/static/nginx/conf/grpc-error-pages.conf @@ -0,0 +1,19 @@ +error_page 400 = @grpc_internal; +error_page 401 = @grpc_unauthenticated; +error_page 403 = @grpc_permission_denied; +error_page 404 = @grpc_unimplemented; +error_page 429 = @grpc_unavailable; +error_page 502 = @grpc_unavailable; +error_page 503 = @grpc_unavailable; +error_page 504 = @grpc_unavailable; +error_page 405 = @grpc_internal; +error_page 408 = @grpc_deadline_exceeded; +error_page 413 = @grpc_resource_exhausted; +error_page 414 = @grpc_resource_exhausted; +error_page 415 = @grpc_internal; +error_page 426 = @grpc_internal; +error_page 495 = @grpc_unauthenticated; +error_page 496 = @grpc_unauthenticated; +error_page 497 = @grpc_internal; +error_page 500 = @grpc_internal; +error_page 501 = @grpc_internal; diff --git a/internal/mode/static/nginx/conf/nginx-plus.conf b/internal/mode/static/nginx/conf/nginx-plus.conf index b8b9cc77d8..009dba530d 100644 --- a/internal/mode/static/nginx/conf/nginx-plus.conf +++ b/internal/mode/static/nginx/conf/nginx-plus.conf @@ -27,6 +27,8 @@ http { sendfile on; tcp_nopush on; + http2 on; + server { listen 127.0.0.1:8765; root /usr/share/nginx/html; diff --git a/internal/mode/static/nginx/conf/nginx.conf b/internal/mode/static/nginx/conf/nginx.conf index b70e857e0d..33b2333eb2 100644 --- a/internal/mode/static/nginx/conf/nginx.conf +++ b/internal/mode/static/nginx/conf/nginx.conf @@ -27,6 +27,8 @@ http { sendfile on; tcp_nopush on; + http2 on; + server { listen unix:/var/run/nginx/nginx-status.sock; access_log off; diff --git a/internal/mode/static/nginx/config/http/config.go b/internal/mode/static/nginx/config/http/config.go index 5db1e061fe..f20a94f2cf 100644 --- a/internal/mode/static/nginx/config/http/config.go +++ b/internal/mode/static/nginx/config/http/config.go @@ -7,6 +7,7 @@ type Server struct { Locations []Location IsDefaultHTTP bool IsDefaultSSL bool + GRPC bool Port int32 } @@ -19,9 +20,10 @@ type Location struct { ProxySSLVerify *ProxySSLVerify Return *Return Rewrites []string + GRPC bool } -// Header defines a HTTP header to be passed to the proxied server. +// Header defines an HTTP header to be passed to the proxied server. type Header struct { Name string Value string diff --git a/internal/mode/static/nginx/config/servers.go b/internal/mode/static/nginx/config/servers.go index 58c1602c24..6884007630 100644 --- a/internal/mode/static/nginx/config/servers.go +++ b/internal/mode/static/nginx/config/servers.go @@ -90,7 +90,7 @@ func createSSLServer(virtualServer dataplane.VirtualServer, serverID int) (http. }, nil } - locs, matchPairs := createLocations(&virtualServer, serverID) + locs, matchPairs, grpc := createLocations(&virtualServer, serverID) return http.Server{ ServerName: virtualServer.Hostname, @@ -100,6 +100,7 @@ func createSSLServer(virtualServer dataplane.VirtualServer, serverID int) (http. }, Locations: locs, Port: virtualServer.Port, + GRPC: grpc, }, matchPairs } @@ -111,12 +112,13 @@ func createServer(virtualServer dataplane.VirtualServer, serverID int) (http.Ser }, nil } - locs, matchPairs := createLocations(&virtualServer, serverID) + locs, matchPairs, grpc := createLocations(&virtualServer, serverID) return http.Server{ ServerName: virtualServer.Hostname, Locations: locs, Port: virtualServer.Port, + GRPC: grpc, }, matchPairs } @@ -129,11 +131,12 @@ type rewriteConfig struct { type httpMatchPairs map[string][]routeMatch -func createLocations(server *dataplane.VirtualServer, serverID int) ([]http.Location, httpMatchPairs) { +func createLocations(server *dataplane.VirtualServer, serverID int) ([]http.Location, httpMatchPairs, bool) { maxLocs, pathsAndTypes := getMaxLocationCountAndPathMap(server.PathRules) locs := make([]http.Location, 0, maxLocs) matchPairs := make(httpMatchPairs) var rootPathExists bool + var grpc bool for pathRuleIdx, rule := range server.PathRules { matches := make([]routeMatch, 0, len(rule.MatchRules)) @@ -142,6 +145,10 @@ func createLocations(server *dataplane.VirtualServer, serverID int) ([]http.Loca rootPathExists = true } + if rule.GRPC { + grpc = true + } + extLocations := initializeExternalLocations(rule, pathsAndTypes) for matchRuleIdx, r := range rule.MatchRules { @@ -152,7 +159,7 @@ func createLocations(server *dataplane.VirtualServer, serverID int) ([]http.Loca matches = append(matches, match) } - buildLocations = updateLocationsForFilters(r.Filters, buildLocations, r, server.Port, rule.Path) + buildLocations = updateLocationsForFilters(r.Filters, buildLocations, r, server.Port, rule.Path, rule.GRPC) locs = append(locs, buildLocations...) } @@ -177,7 +184,7 @@ func createLocations(server *dataplane.VirtualServer, serverID int) ([]http.Loca locs = append(locs, createDefaultRootLocation()) } - return locs, matchPairs + return locs, matchPairs, grpc } // pathAndTypeMap contains a map of paths and any path types defined for that path @@ -268,6 +275,7 @@ func updateLocationsForFilters( matchRule dataplane.MatchRule, listenerPort int32, path string, + grpc bool, ) []http.Location { if filters.InvalidFilter != nil { for i := range buildLocations { @@ -285,7 +293,7 @@ func updateLocationsForFilters( } rewrites := createRewritesValForRewriteFilter(filters.RequestURLRewrite, path) - proxySetHeaders := generateProxySetHeaders(&matchRule.Filters) + proxySetHeaders := generateProxySetHeaders(&matchRule.Filters, grpc) for i := range buildLocations { if rewrites != nil { if rewrites.Rewrite != "" { @@ -297,19 +305,27 @@ func updateLocationsForFilters( proxyPass := createProxyPass( matchRule.BackendGroup, matchRule.Filters.RequestURLRewrite, - generateProtocolString(buildLocations[i].ProxySSLVerify), + generateProtocolString(buildLocations[i].ProxySSLVerify, grpc), + grpc, ) buildLocations[i].ProxyPass = proxyPass + buildLocations[i].GRPC = grpc } return buildLocations } -func generateProtocolString(ssl *http.ProxySSLVerify) string { +func generateProtocolString(ssl *http.ProxySSLVerify, grpc bool) string { + if !grpc { + if ssl != nil { + return "https" + } + return "http" + } if ssl != nil { - return "https" + return "grpcs" } - return "http" + return "grpc" } func createProxyTLSFromBackends(backends []dataplane.Backend) *http.ProxySSLVerify { @@ -514,10 +530,13 @@ func createProxyPass( backendGroup dataplane.BackendGroup, filter *dataplane.HTTPURLRewriteFilter, protocol string, + grpc bool, ) string { var requestURI string - if filter == nil || filter.Path == nil { - requestURI = "$request_uri" + if !grpc { + if filter == nil || filter.Path == nil { + requestURI = "$request_uri" + } } backendName := backendGroupName(backendGroup) @@ -534,9 +553,12 @@ func createMatchLocation(path string) http.Location { } } -func generateProxySetHeaders(filters *dataplane.HTTPFilters) []http.Header { - headers := make([]http.Header, len(baseHeaders)) - copy(headers, baseHeaders) +func generateProxySetHeaders(filters *dataplane.HTTPFilters, grpc bool) []http.Header { + var headers []http.Header + if !grpc { + headers = make([]http.Header, len(baseHeaders)) + copy(headers, baseHeaders) + } if filters != nil && filters.RequestURLRewrite != nil && filters.RequestURLRewrite.Hostname != nil { for i, header := range headers { diff --git a/internal/mode/static/nginx/config/servers_template.go b/internal/mode/static/nginx/config/servers_template.go index c05fa7cea7..d4ad022202 100644 --- a/internal/mode/static/nginx/config/servers_template.go +++ b/internal/mode/static/nginx/config/servers_template.go @@ -47,20 +47,30 @@ server { js_content httpmatches.redirect; {{- end }} + {{ $proxyOrGRPC := "proxy" }}{{ if $l.GRPC }}{{ $proxyOrGRPC = "grpc" }}{{ end }} + + {{- if $l.GRPC }} + include /etc/nginx/grpc-error-pages.conf; + {{- end }} + {{- if $l.ProxyPass -}} {{ range $h := $l.ProxySetHeaders }} - proxy_set_header {{ $h.Name }} "{{ $h.Value }}"; + {{ $proxyOrGRPC }}_set_header {{ $h.Name }} "{{ $h.Value }}"; {{- end }} + {{ $proxyOrGRPC }}_pass {{ $l.ProxyPass }}; proxy_http_version 1.1; - proxy_pass {{ $l.ProxyPass }}; {{- if $l.ProxySSLVerify }} - proxy_ssl_verify on; - proxy_ssl_name {{ $l.ProxySSLVerify.Name }}; - proxy_ssl_trusted_certificate {{ $l.ProxySSLVerify.TrustedCertificate }}; + {{ $proxyOrGRPC }}_ssl_verify on; + {{ $proxyOrGRPC }}_ssl_name {{ $l.ProxySSLVerify.Name }}; + {{ $proxyOrGRPC }}_ssl_trusted_certificate {{ $l.ProxySSLVerify.TrustedCertificate }}; {{- end }} {{- end }} } {{ end }} + + {{- if $s.GRPC }} + include /etc/nginx/grpc-error-locations.conf; + {{- end }} } {{- end }} {{ end }} diff --git a/internal/mode/static/nginx/config/servers_test.go b/internal/mode/static/nginx/config/servers_test.go index 3e931fe1e4..990a3a4c52 100644 --- a/internal/mode/static/nginx/config/servers_test.go +++ b/internal/mode/static/nginx/config/servers_test.go @@ -489,6 +489,28 @@ func TestCreateServers(t *testing.T) { }, }, }, + { + Path: "/grpc/method", + PathType: dataplane.PathTypeExact, + MatchRules: []dataplane.MatchRule{ + { + Match: dataplane.Match{}, + BackendGroup: fooGroup, + }, + }, + GRPC: true, + }, + { + Path: "/grpc-with-backend-tls-policy/method", + PathType: dataplane.PathTypeExact, + MatchRules: []dataplane.MatchRule{ + { + Match: dataplane.Match{}, + BackendGroup: btpGroup, + }, + }, + GRPC: true, + }, } httpServers := []dataplane.VirtualServer{ @@ -806,6 +828,22 @@ func TestCreateServers(t *testing.T) { }, }, }, + { + Path: "= /grpc/method", + ProxyPass: "grpc://test_foo_80", + GRPC: true, + ProxySetHeaders: nil, + }, + { + Path: "= /grpc-with-backend-tls-policy/method", + ProxyPass: "grpcs://test_btp_80", + ProxySSLVerify: &http.ProxySSLVerify{ + Name: "test-btp.example.com", + TrustedCertificate: "/etc/nginx/secrets/test-btp.crt", + }, + GRPC: true, + ProxySetHeaders: nil, + }, } } @@ -820,6 +858,7 @@ func TestCreateServers(t *testing.T) { ServerName: "cafe.example.com", Locations: getExpectedLocations(false), Port: 8080, + GRPC: true, }, { IsDefaultSSL: true, @@ -833,6 +872,7 @@ func TestCreateServers(t *testing.T) { }, Locations: getExpectedLocations(true), Port: 8443, + GRPC: true, }, } @@ -1068,7 +1108,7 @@ func TestCreateLocationsRootPath(t *testing.T) { }, } - getPathRules := func(rootPath bool) []dataplane.PathRule { + getPathRules := func(rootPath bool, grpc bool) []dataplane.PathRule { rules := []dataplane.PathRule{ { Path: "/path-1", @@ -1102,6 +1142,19 @@ func TestCreateLocationsRootPath(t *testing.T) { }) } + if grpc { + rules = append(rules, dataplane.PathRule{ + Path: "/grpc", + GRPC: true, + MatchRules: []dataplane.MatchRule{ + { + Match: dataplane.Match{}, + BackendGroup: fooGroup, + }, + }, + }) + } + return rules } @@ -1109,10 +1162,11 @@ func TestCreateLocationsRootPath(t *testing.T) { name string pathRules []dataplane.PathRule expLocations []http.Location + grpc bool }{ { name: "path rules with no root path should generate a default 404 root location", - pathRules: getPathRules(false /* rootPath */), + pathRules: getPathRules(false /* rootPath */, false /* grpc */), expLocations: []http.Location{ { Path: "/path-1", @@ -1132,9 +1186,37 @@ func TestCreateLocationsRootPath(t *testing.T) { }, }, }, + { + name: "path rules with grpc & with no root path should generate a default 404 root location and GRPC true", + pathRules: getPathRules(false /* rootPath */, true /* grpc */), + grpc: true, + expLocations: []http.Location{ + { + Path: "/path-1", + ProxyPass: "http://test_foo_80$request_uri", + ProxySetHeaders: baseHeaders, + }, + { + Path: "/path-2", + ProxyPass: "http://test_foo_80$request_uri", + ProxySetHeaders: baseHeaders, + }, + { + Path: "/grpc", + ProxyPass: "grpc://test_foo_80", + GRPC: true, + }, + { + Path: "/", + Return: &http.Return{ + Code: http.StatusNotFound, + }, + }, + }, + }, { name: "path rules with a root path should not generate a default 404 root path", - pathRules: getPathRules(true /* rootPath */), + pathRules: getPathRules(true /* rootPath */, false /* grpc */), expLocations: []http.Location{ { Path: "/path-1", @@ -1171,12 +1253,13 @@ func TestCreateLocationsRootPath(t *testing.T) { t.Run(test.name, func(t *testing.T) { g := NewWithT(t) - locs, httpMatchPair := createLocations(&dataplane.VirtualServer{ + locs, httpMatchPair, grpc := createLocations(&dataplane.VirtualServer{ PathRules: test.pathRules, Port: 80, }, 1) g.Expect(locs).To(Equal(test.expLocations)) g.Expect(httpMatchPair).To(BeEmpty()) + g.Expect(grpc).To(Equal(test.grpc)) }) } } @@ -1692,6 +1775,7 @@ func TestCreateProxyPass(t *testing.T) { rewrite *dataplane.HTTPURLRewriteFilter expected string grp dataplane.BackendGroup + GRPC bool }{ { expected: "http://10.0.0.1:80$request_uri", @@ -1738,10 +1822,23 @@ func TestCreateProxyPass(t *testing.T) { }, }, }, + { + expected: "grpc://10.0.0.1:80", + grp: dataplane.BackendGroup{ + Backends: []dataplane.Backend{ + { + UpstreamName: "10.0.0.1:80", + Valid: true, + Weight: 1, + }, + }, + }, + GRPC: true, + }, } for _, tc := range tests { - result := createProxyPass(tc.grp, tc.rewrite, generateProtocolString(nil)) + result := createProxyPass(tc.grp, tc.rewrite, generateProtocolString(nil, tc.GRPC), tc.GRPC) g.Expect(result).To(Equal(tc.expected)) } } @@ -1762,6 +1859,7 @@ func TestGenerateProxySetHeaders(t *testing.T) { filters *dataplane.HTTPFilters msg string expectedHeaders []http.Header + GRPC bool }{ { msg: "header filter", @@ -1851,13 +1949,18 @@ func TestGenerateProxySetHeaders(t *testing.T) { }, }, }, + { + msg: "grpc", + expectedHeaders: nil, + GRPC: true, + }, } for _, tc := range tests { t.Run(tc.msg, func(t *testing.T) { g := NewWithT(t) - headers := generateProxySetHeaders(tc.filters) + headers := generateProxySetHeaders(tc.filters, tc.GRPC) g.Expect(headers).To(Equal(tc.expectedHeaders)) }) } diff --git a/internal/mode/static/state/change_processor.go b/internal/mode/static/state/change_processor.go index e5ae41cc52..5a70be3a5f 100644 --- a/internal/mode/static/state/change_processor.go +++ b/internal/mode/static/state/change_processor.go @@ -107,6 +107,7 @@ func NewChangeProcessorImpl(cfg ChangeProcessorConfig) *ChangeProcessorImpl { BackendTLSPolicies: make(map[types.NamespacedName]*v1alpha2.BackendTLSPolicy), ConfigMaps: make(map[types.NamespacedName]*apiv1.ConfigMap), NginxProxies: make(map[types.NamespacedName]*ngfAPI.NginxProxy), + GRPCRoutes: make(map[types.NamespacedName]*v1alpha2.GRPCRoute), } extractGVK := func(obj client.Object) schema.GroupVersionKind { @@ -154,6 +155,11 @@ func NewChangeProcessorImpl(cfg ChangeProcessorConfig) *ChangeProcessorImpl { store: newObjectStoreMapAdapter(clusterStore.BackendTLSPolicies), predicate: nil, }, + { + gvk: extractGVK(&v1alpha2.GRPCRoute{}), + store: newObjectStoreMapAdapter(clusterStore.GRPCRoutes), + predicate: nil, + }, { gvk: extractGVK(&apiv1.Namespace{}), store: newObjectStoreMapAdapter(clusterStore.Namespaces), diff --git a/internal/mode/static/state/change_processor_test.go b/internal/mode/static/state/change_processor_test.go index aeb8d9e0bc..9c03a9137b 100644 --- a/internal/mode/static/state/change_processor_test.go +++ b/internal/mode/static/state/change_processor_test.go @@ -171,6 +171,17 @@ func createBackendRef( } } +func createRouteBackendRefs(refs []v1.HTTPBackendRef) []graph.RouteBackendRef { + rbrs := make([]graph.RouteBackendRef, 0, len(refs)) + for _, ref := range refs { + rbr := graph.RouteBackendRef{ + BackendRef: ref.BackendRef, + } + rbrs = append(rbrs, rbr) + } + return rbrs +} + func createAlwaysValidValidators() validation.Validators { return validation.Validators{ HTTPFieldsValidator: &validationfakes.FakeHTTPFieldsValidator{}, @@ -287,9 +298,9 @@ var _ = Describe("ChangeProcessor", func() { gw1, gw1Updated, gw2 *v1.Gateway refGrant1, refGrant2 *v1beta1.ReferenceGrant expGraph *graph.Graph - expRouteHR1, expRouteHR2 *graph.Route - hr1Name, hr2Name types.NamespacedName + expRouteHR1, expRouteHR2 *graph.L7Route gatewayAPICRD, gatewayAPICRDUpdated *metav1.PartialObjectMetadata + routeKey1, routeKey2 graph.RouteKey ) BeforeAll(func() { gcUpdated = gc.DeepCopy() @@ -307,13 +318,15 @@ var _ = Describe("ChangeProcessor", func() { } hr1 = createRoute("hr-1", "gateway-1", "foo.example.com", crossNsBackendRef) - hr1Name = types.NamespacedName{Namespace: hr1.Namespace, Name: hr1.Name} + + routeKey1 = graph.CreateRouteKey(hr1) hr1Updated = hr1.DeepCopy() hr1Updated.Generation++ hr2 = createRoute("hr-2", "gateway-2", "bar.example.com") - hr2Name = types.NamespacedName{Namespace: "test", Name: "hr-2"} + + routeKey2 = graph.CreateRouteKey(hr2) refGrant1 = &v1beta1.ReferenceGrant{ ObjectMeta: metav1.ObjectMeta{ @@ -405,35 +418,43 @@ var _ = Describe("ChangeProcessor", func() { gatewayAPICRDUpdated.Annotations[gatewayclass.BundleVersionAnnotation] = "v1.99.0" }) BeforeEach(func() { - expRouteHR1 = &graph.Route{ - Source: hr1, + expRouteHR1 = &graph.L7Route{ + Source: hr1, + RouteType: graph.RouteTypeHTTP, ParentRefs: []graph.ParentRef{ { Attachment: &graph.ParentRefAttachmentStatus{ AcceptedHostnames: map[string][]string{"listener-80-1": {"foo.example.com"}}, Attached: true, }, - Gateway: types.NamespacedName{Namespace: "test", Name: "gateway-1"}, + Gateway: types.NamespacedName{Namespace: "test", Name: "gateway-1"}, + SectionName: hr1.Spec.ParentRefs[0].SectionName, }, { Attachment: &graph.ParentRefAttachmentStatus{ AcceptedHostnames: map[string][]string{"listener-443-1": {"foo.example.com"}}, Attached: true, }, - Gateway: types.NamespacedName{Namespace: "test", Name: "gateway-1"}, - Idx: 1, + Gateway: types.NamespacedName{Namespace: "test", Name: "gateway-1"}, + Idx: 1, + SectionName: hr1.Spec.ParentRefs[1].SectionName, }, }, - Rules: []graph.Rule{ - { - BackendRefs: []graph.BackendRef{ - { - SvcNsName: types.NamespacedName{Namespace: "service-ns", Name: "service"}, - Weight: 1, + Spec: graph.L7RouteSpec{ + Hostnames: hr1.Spec.Hostnames, + Rules: []graph.RouteRule{ + { + BackendRefs: []graph.BackendRef{ + { + SvcNsName: types.NamespacedName{Namespace: "service-ns", Name: "service"}, + Weight: 1, + }, }, + ValidMatches: true, + ValidFilters: true, + Matches: hr1.Spec.Rules[0].Matches, + RouteBackendRefs: createRouteBackendRefs(hr1.Spec.Rules[0].BackendRefs), }, - ValidMatches: true, - ValidFilters: true, }, }, Valid: true, @@ -445,26 +466,39 @@ var _ = Describe("ChangeProcessor", func() { }, } - expRouteHR2 = &graph.Route{ - Source: hr2, + expRouteHR2 = &graph.L7Route{ + Source: hr2, + RouteType: graph.RouteTypeHTTP, ParentRefs: []graph.ParentRef{ { Attachment: &graph.ParentRefAttachmentStatus{ AcceptedHostnames: map[string][]string{"listener-80-1": {"bar.example.com"}}, Attached: true, }, - Gateway: types.NamespacedName{Namespace: "test", Name: "gateway-2"}, + Gateway: types.NamespacedName{Namespace: "test", Name: "gateway-2"}, + SectionName: hr2.Spec.ParentRefs[0].SectionName, }, { Attachment: &graph.ParentRefAttachmentStatus{ AcceptedHostnames: map[string][]string{"listener-443-1": {"bar.example.com"}}, Attached: true, }, - Gateway: types.NamespacedName{Namespace: "test", Name: "gateway-2"}, - Idx: 1, + Gateway: types.NamespacedName{Namespace: "test", Name: "gateway-2"}, + Idx: 1, + SectionName: hr2.Spec.ParentRefs[1].SectionName, + }, + }, + Spec: graph.L7RouteSpec{ + Hostnames: hr2.Spec.Hostnames, + Rules: []graph.RouteRule{ + { + ValidMatches: true, + ValidFilters: true, + Matches: hr2.Spec.Rules[0].Matches, + RouteBackendRefs: []graph.RouteBackendRef{}, + }, }, }, - Rules: []graph.Rule{{ValidMatches: true, ValidFilters: true}}, Valid: true, Attachable: true, } @@ -480,33 +514,27 @@ var _ = Describe("ChangeProcessor", func() { Source: gw1, Listeners: []*graph.Listener{ { - Name: "listener-80-1", - Source: gw1.Spec.Listeners[0], - Valid: true, - Attachable: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-1"}: expRouteHR1, - }, + Name: "listener-80-1", + Source: gw1.Spec.Listeners[0], + Valid: true, + Attachable: true, + Routes: map[graph.RouteKey]*graph.L7Route{routeKey1: expRouteHR1}, SupportedKinds: []v1.RouteGroupKind{{Kind: "HTTPRoute"}}, }, { - Name: "listener-443-1", - Source: gw1.Spec.Listeners[1], - Valid: true, - Attachable: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-1"}: expRouteHR1, - }, + Name: "listener-443-1", + Source: gw1.Spec.Listeners[1], + Valid: true, + Attachable: true, + Routes: map[graph.RouteKey]*graph.L7Route{routeKey1: expRouteHR1}, ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(diffNsTLSSecret)), SupportedKinds: []v1.RouteGroupKind{{Kind: "HTTPRoute"}}, }, }, Valid: true, }, - IgnoredGateways: map[types.NamespacedName]*v1.Gateway{}, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-1"}: expRouteHR1, - }, + IgnoredGateways: map[types.NamespacedName]*v1.Gateway{}, + Routes: map[graph.RouteKey]*graph.L7Route{routeKey1: expRouteHR1}, ReferencedSecrets: map[types.NamespacedName]*graph.Secret{}, ReferencedServices: map[types.NamespacedName]struct{}{ { @@ -567,16 +595,16 @@ var _ = Describe("ChangeProcessor", func() { expGraph.Gateway.Listeners = nil // no ref grant exists yet for hr1 - expGraph.Routes[hr1Name].Conditions = []conditions.Condition{ + expGraph.Routes[routeKey1].Conditions = []conditions.Condition{ staticConds.NewRouteBackendRefRefNotPermitted( "Backend ref to Service service-ns/service not permitted by any ReferenceGrant", ), } - expGraph.Routes[hr1Name].ParentRefs[0].Attachment = &graph.ParentRefAttachmentStatus{ + expGraph.Routes[routeKey1].ParentRefs[0].Attachment = &graph.ParentRefAttachmentStatus{ AcceptedHostnames: map[string][]string{}, FailedCondition: staticConds.NewRouteNoMatchingParent(), } - expGraph.Routes[hr1Name].ParentRefs[1].Attachment = &graph.ParentRefAttachmentStatus{ + expGraph.Routes[routeKey1].ParentRefs[1].Attachment = &graph.ParentRefAttachmentStatus{ AcceptedHostnames: map[string][]string{}, FailedCondition: staticConds.NewRouteNoMatchingParent(), } @@ -584,7 +612,7 @@ var _ = Describe("ChangeProcessor", func() { expGraph.ReferencedSecrets = nil expGraph.ReferencedServices = nil - expRouteHR1.Rules[0].BackendRefs[0].SvcNsName = types.NamespacedName{} + expRouteHR1.Spec.Rules[0].BackendRefs[0].SvcNsName = types.NamespacedName{} changed, graphCfg := processor.Process() Expect(changed).To(Equal(state.ClusterStateChange)) @@ -622,23 +650,23 @@ var _ = Describe("ChangeProcessor", func() { } listener80 := getListenerByName(expGraph.Gateway, "listener-80-1") - listener80.Routes[hr1Name].ParentRefs[0].Attachment = expAttachment80 - listener443.Routes[hr1Name].ParentRefs[1].Attachment = expAttachment443 + listener80.Routes[routeKey1].ParentRefs[0].Attachment = expAttachment80 + listener443.Routes[routeKey1].ParentRefs[1].Attachment = expAttachment443 // no ref grant exists yet for hr1 - expGraph.Routes[hr1Name].Conditions = []conditions.Condition{ + expGraph.Routes[routeKey1].Conditions = []conditions.Condition{ staticConds.NewRouteInvalidListener(), staticConds.NewRouteBackendRefRefNotPermitted( "Backend ref to Service service-ns/service not permitted by any ReferenceGrant", ), } - expGraph.Routes[hr1Name].ParentRefs[0].Attachment = expAttachment80 - expGraph.Routes[hr1Name].ParentRefs[1].Attachment = expAttachment443 + expGraph.Routes[routeKey1].ParentRefs[0].Attachment = expAttachment80 + expGraph.Routes[routeKey1].ParentRefs[1].Attachment = expAttachment443 expGraph.ReferencedSecrets = nil expGraph.ReferencedServices = nil - expRouteHR1.Rules[0].BackendRefs[0].SvcNsName = types.NamespacedName{} + expRouteHR1.Spec.Rules[0].BackendRefs[0].SvcNsName = types.NamespacedName{} changed, graphCfg := processor.Process() Expect(changed).To(Equal(state.ClusterStateChange)) @@ -651,7 +679,7 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(refGrant1) // no ref grant exists yet for hr1 - expGraph.Routes[hr1Name].Conditions = []conditions.Condition{ + expGraph.Routes[routeKey1].Conditions = []conditions.Condition{ staticConds.NewRouteBackendRefRefNotPermitted( "Backend ref to Service service-ns/service not permitted by any ReferenceGrant", ), @@ -661,7 +689,7 @@ var _ = Describe("ChangeProcessor", func() { } expGraph.ReferencedServices = nil - expRouteHR1.Rules[0].BackendRefs[0].SvcNsName = types.NamespacedName{} + expRouteHR1.Spec.Rules[0].BackendRefs[0].SvcNsName = types.NamespacedName{} changed, graphCfg := processor.Process() Expect(changed).To(Equal(state.ClusterStateChange)) @@ -741,10 +769,10 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(hr1Updated) listener443 := getListenerByName(expGraph.Gateway, "listener-443-1") - listener443.Routes[hr1Name].Source.Generation = hr1Updated.Generation + listener443.Routes[routeKey1].Source.SetGeneration(hr1Updated.Generation) listener80 := getListenerByName(expGraph.Gateway, "listener-80-1") - listener80.Routes[hr1Name].Source.Generation = hr1Updated.Generation + listener80.Routes[routeKey1].Source.SetGeneration(hr1Updated.Generation) expGraph.ReferencedSecrets[client.ObjectKeyFromObject(diffNsTLSSecret)] = &graph.Secret{ Source: diffNsTLSSecret, } @@ -850,12 +878,12 @@ var _ = Describe("ChangeProcessor", func() { expGraph.IgnoredGateways = map[types.NamespacedName]*v1.Gateway{ {Namespace: "test", Name: "gateway-2"}: gw2, } - expGraph.Routes[hr2Name] = expRouteHR2 - expGraph.Routes[hr2Name].ParentRefs[0].Attachment = &graph.ParentRefAttachmentStatus{ + expGraph.Routes[routeKey2] = expRouteHR2 + expGraph.Routes[routeKey2].ParentRefs[0].Attachment = &graph.ParentRefAttachmentStatus{ AcceptedHostnames: map[string][]string{}, FailedCondition: staticConds.NewTODO("Gateway is ignored"), } - expGraph.Routes[hr2Name].ParentRefs[1].Attachment = &graph.ParentRefAttachmentStatus{ + expGraph.Routes[routeKey2].ParentRefs[1].Attachment = &graph.ParentRefAttachmentStatus{ AcceptedHostnames: map[string][]string{}, FailedCondition: staticConds.NewTODO("Gateway is ignored"), } @@ -884,19 +912,19 @@ var _ = Describe("ChangeProcessor", func() { expGraph.Gateway.Source = gw2 listener80.Source = gw2.Spec.Listeners[0] listener443.Source = gw2.Spec.Listeners[1] - delete(listener80.Routes, hr1Name) - delete(listener443.Routes, hr1Name) - listener80.Routes[hr2Name] = expRouteHR2 - listener443.Routes[hr2Name] = expRouteHR2 - delete(expGraph.Routes, hr1Name) - expGraph.Routes[hr2Name] = expRouteHR2 + delete(listener80.Routes, routeKey1) + delete(listener443.Routes, routeKey1) + listener80.Routes[routeKey2] = expRouteHR2 + listener443.Routes[routeKey2] = expRouteHR2 + delete(expGraph.Routes, routeKey1) + expGraph.Routes[routeKey2] = expRouteHR2 sameNsTLSSecretRef := helpers.GetPointer(client.ObjectKeyFromObject(sameNsTLSSecret)) listener443.ResolvedSecret = sameNsTLSSecretRef expGraph.ReferencedSecrets[client.ObjectKeyFromObject(sameNsTLSSecret)] = &graph.Secret{ Source: sameNsTLSSecret, } - expRouteHR1.Rules[0].BackendRefs[0].SvcNsName = types.NamespacedName{} + expRouteHR1.Spec.Rules[0].BackendRefs[0].SvcNsName = types.NamespacedName{} expGraph.ReferencedServices = nil changed, graphCfg := processor.Process() @@ -920,16 +948,16 @@ var _ = Describe("ChangeProcessor", func() { expGraph.Gateway.Source = gw2 listener80.Source = gw2.Spec.Listeners[0] listener443.Source = gw2.Spec.Listeners[1] - delete(listener80.Routes, hr1Name) - delete(listener443.Routes, hr1Name) - expGraph.Routes = map[types.NamespacedName]*graph.Route{} + delete(listener80.Routes, routeKey1) + delete(listener443.Routes, routeKey1) + expGraph.Routes = map[graph.RouteKey]*graph.L7Route{} sameNsTLSSecretRef := helpers.GetPointer(client.ObjectKeyFromObject(sameNsTLSSecret)) listener443.ResolvedSecret = sameNsTLSSecretRef expGraph.ReferencedSecrets[client.ObjectKeyFromObject(sameNsTLSSecret)] = &graph.Secret{ Source: sameNsTLSSecret, } - expRouteHR1.Rules[0].BackendRefs[0].SvcNsName = types.NamespacedName{} + expRouteHR1.Spec.Rules[0].BackendRefs[0].SvcNsName = types.NamespacedName{} expGraph.ReferencedServices = nil changed, graphCfg := processor.Process() @@ -950,10 +978,10 @@ var _ = Describe("ChangeProcessor", func() { Source: gw2, Conditions: staticConds.NewGatewayInvalid("GatewayClass doesn't exist"), } - expGraph.Routes = map[types.NamespacedName]*graph.Route{} + expGraph.Routes = map[graph.RouteKey]*graph.L7Route{} expGraph.ReferencedSecrets = nil - expRouteHR1.Rules[0].BackendRefs[0].SvcNsName = types.NamespacedName{} + expRouteHR1.Spec.Rules[0].BackendRefs[0].SvcNsName = types.NamespacedName{} expGraph.ReferencedServices = nil changed, graphCfg := processor.Process() @@ -969,7 +997,7 @@ var _ = Describe("ChangeProcessor", func() { types.NamespacedName{Namespace: "test", Name: "gateway-2"}, ) - expRouteHR1.Rules[0].BackendRefs[0].SvcNsName = types.NamespacedName{} + expRouteHR1.Spec.Rules[0].BackendRefs[0].SvcNsName = types.NamespacedName{} expGraph.ReferencedServices = nil changed, graphCfg := processor.Process() @@ -985,7 +1013,7 @@ var _ = Describe("ChangeProcessor", func() { types.NamespacedName{Namespace: "test", Name: "hr-1"}, ) - expRouteHR1.Rules[0].BackendRefs[0].SvcNsName = types.NamespacedName{} + expRouteHR1.Spec.Rules[0].BackendRefs[0].SvcNsName = types.NamespacedName{} expGraph.ReferencedServices = nil changed, graphCfg := processor.Process() diff --git a/internal/mode/static/state/conditions/conditions.go b/internal/mode/static/state/conditions/conditions.go index 9675112b40..ec700f03a3 100644 --- a/internal/mode/static/state/conditions/conditions.go +++ b/internal/mode/static/state/conditions/conditions.go @@ -64,7 +64,7 @@ const ( // RouteMessageFailedNginxReload is a message used with RouteReasonGatewayNotProgrammed // when nginx fails to reload. RouteMessageFailedNginxReload = GatewayMessageFailedNginxReload + ". NGINX may still be configured " + - "for this HTTPRoute. However, future updates to this resource will not be configured until the Gateway " + + "for this Route. However, future updates to this resource will not be configured until the Gateway " + "is programmed again" // GatewayClassResolvedRefs condition indicates whether the controller was able to resolve the @@ -89,7 +89,7 @@ func NewTODO(msg string) conditions.Condition { } } -// NewDefaultRouteConditions returns the default conditions that must be present in the status of an HTTPRoute. +// NewDefaultRouteConditions returns the default conditions that must be present in the status of a Route. func NewDefaultRouteConditions() []conditions.Condition { return []conditions.Condition{ NewRouteAccepted(), @@ -97,29 +97,29 @@ func NewDefaultRouteConditions() []conditions.Condition { } } -// NewRouteNotAllowedByListeners returns a Condition that indicates that the HTTPRoute is not allowed by +// NewRouteNotAllowedByListeners returns a Condition that indicates that the Route is not allowed by // any listener. func NewRouteNotAllowedByListeners() conditions.Condition { return conditions.Condition{ Type: string(v1.RouteConditionAccepted), Status: metav1.ConditionFalse, Reason: string(v1.RouteReasonNotAllowedByListeners), - Message: "HTTPRoute is not allowed by any listener", + Message: "Route is not allowed by any listener", } } // NewRouteNoMatchingListenerHostname returns a Condition that indicates that the hostname of the listener -// does not match the hostnames of the HTTPRoute. +// does not match the hostnames of the Route. func NewRouteNoMatchingListenerHostname() conditions.Condition { return conditions.Condition{ Type: string(v1.RouteConditionAccepted), Status: metav1.ConditionFalse, Reason: string(v1.RouteReasonNoMatchingListenerHostname), - Message: "Listener hostname does not match the HTTPRoute hostnames", + Message: "Listener hostname does not match the Route hostnames", } } -// NewRouteAccepted returns a Condition that indicates that the HTTPRoute is accepted. +// NewRouteAccepted returns a Condition that indicates that the Route is accepted. func NewRouteAccepted() conditions.Condition { return conditions.Condition{ Type: string(v1.RouteConditionAccepted), @@ -129,7 +129,7 @@ func NewRouteAccepted() conditions.Condition { } } -// NewRouteUnsupportedValue returns a Condition that indicates that the HTTPRoute includes an unsupported value. +// NewRouteUnsupportedValue returns a Condition that indicates that the Route includes an unsupported value. func NewRouteUnsupportedValue(msg string) conditions.Condition { return conditions.Condition{ Type: string(v1.RouteConditionAccepted), @@ -139,7 +139,7 @@ func NewRouteUnsupportedValue(msg string) conditions.Condition { } } -// NewRoutePartiallyInvalid returns a Condition that indicates that the HTTPRoute contains a combination +// NewRoutePartiallyInvalid returns a Condition that indicates that the Route contains a combination // of both valid and invalid rules. // // // nolint:lll @@ -154,7 +154,7 @@ func NewRoutePartiallyInvalid(msg string) conditions.Condition { } } -// NewRouteInvalidListener returns a Condition that indicates that the HTTPRoute is not accepted because of an +// NewRouteInvalidListener returns a Condition that indicates that the Route is not accepted because of an // invalid listener. func NewRouteInvalidListener() conditions.Condition { return conditions.Condition{ @@ -242,7 +242,7 @@ func NewRouteNoMatchingParent() conditions.Condition { } // NewRouteGatewayNotProgrammed returns a Condition that indicates that the Gateway it references is not programmed, -// which does not guarantee that the HTTPRoute has been configured. +// which does not guarantee that the Route has been configured. func NewRouteGatewayNotProgrammed(msg string) conditions.Condition { return conditions.Condition{ Type: string(v1.RouteConditionAccepted), diff --git a/internal/mode/static/state/dataplane/configuration.go b/internal/mode/static/state/dataplane/configuration.go index d9f0e4264e..36999dcd4e 100644 --- a/internal/mode/static/state/dataplane/configuration.go +++ b/internal/mode/static/state/dataplane/configuration.go @@ -7,10 +7,13 @@ import ( "sort" apiv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" v1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/resolver" ) @@ -276,8 +279,18 @@ func (hpr *hostPathRules) upsertListener(l *graph.Listener) { } } -func (hpr *hostPathRules) upsertRoute(route *graph.Route, listener *graph.Listener) { +func (hpr *hostPathRules) upsertRoute(route *graph.L7Route, listener *graph.Listener) { var hostnames []string + GRPC := route.RouteType == graph.RouteTypeGRPC + + var objectSrc *metav1.ObjectMeta + + if GRPC { + objectSrc = &helpers.MustCastObject[*v1alpha2.GRPCRoute](route.Source).ObjectMeta + } else { + objectSrc = &helpers.MustCastObject[*v1.HTTPRoute](route.Source).ObjectMeta + } + for _, p := range route.ParentRefs { if val, exist := p.Attachment.AcceptedHostnames[string(listener.Source.Name)]; exist { hostnames = val @@ -299,13 +312,13 @@ func (hpr *hostPathRules) upsertRoute(route *graph.Route, listener *graph.Listen } } - for i, rule := range route.Source.Spec.Rules { - if !route.Rules[i].ValidMatches { + for i, rule := range route.Spec.Rules { + if !rule.ValidMatches { continue } var filters HTTPFilters - if route.Rules[i].ValidFilters { + if rule.ValidFilters { filters = createHTTPFilters(rule.Filters) } else { filters = HTTPFilters{ @@ -322,25 +335,24 @@ func (hpr *hostPathRules) upsertRoute(route *graph.Route, listener *graph.Listen pathType: *m.Path.Type, } - rule, exist := hpr.rulesPerHost[h][key] + hostRule, exist := hpr.rulesPerHost[h][key] if !exist { - rule.Path = path - rule.PathType = convertPathType(*m.Path.Type) + hostRule.Path = path + hostRule.PathType = convertPathType(*m.Path.Type) } - // create iteration variable inside the loop to fix implicit memory aliasing - om := route.Source.ObjectMeta - routeNsName := client.ObjectKeyFromObject(route.Source) - rule.MatchRules = append(rule.MatchRules, MatchRule{ - Source: &om, - BackendGroup: newBackendGroup(route.Rules[i].BackendRefs, routeNsName, i), + hostRule.GRPC = GRPC + + hostRule.MatchRules = append(hostRule.MatchRules, MatchRule{ + Source: objectSrc, + BackendGroup: newBackendGroup(rule.BackendRefs, routeNsName, i), Filters: filters, Match: convertMatch(m), }) - hpr.rulesPerHost[h][key] = rule + hpr.rulesPerHost[h][key] = hostRule } } } @@ -450,7 +462,7 @@ func buildUpstreams( continue } - for _, rule := range route.Rules { + for _, rule := range route.Spec.Rules { if !rule.ValidMatches || !rule.ValidFilters { // don't generate upstreams for rules that have invalid matches or filters continue diff --git a/internal/mode/static/state/dataplane/configuration_test.go b/internal/mode/static/state/dataplane/configuration_test.go index 5ffde9f604..def4563ab8 100644 --- a/internal/mode/static/state/dataplane/configuration_test.go +++ b/internal/mode/static/state/dataplane/configuration_test.go @@ -27,44 +27,27 @@ func TestBuildConfiguration(t *testing.T) { invalidFiltersPath = "/not-valid-filters" ) - createRoute := func(name, hostname, listenerName string, paths ...pathAndType) *v1.HTTPRoute { - rules := make([]v1.HTTPRouteRule, 0, len(paths)) - for _, p := range paths { - rules = append(rules, v1.HTTPRouteRule{ - Matches: []v1.HTTPRouteMatch{ - { - Path: &v1.HTTPPathMatch{ - Value: helpers.GetPointer(p.path), - Type: helpers.GetPointer(p.pathType), - }, - }, - }, - }) - } + createRoute := func(name string) *v1.HTTPRoute { return &v1.HTTPRoute{ ObjectMeta: metav1.ObjectMeta{ Namespace: "test", Name: name, }, - Spec: v1.HTTPRouteSpec{ - CommonRouteSpec: v1.CommonRouteSpec{ - ParentRefs: []v1.ParentReference{ - { - Namespace: (*v1.Namespace)(helpers.GetPointer("test")), - Name: "gateway", - SectionName: (*v1.SectionName)(helpers.GetPointer(listenerName)), - }, - }, - }, - Hostnames: []v1.Hostname{ - v1.Hostname(hostname), - }, - Rules: rules, + Spec: v1.HTTPRouteSpec{}, + } + } + + createGRPCRoute := func(name string) *v1alpha2.GRPCRoute { + return &v1alpha2.GRPCRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: name, }, + Spec: v1alpha2.GRPCRouteSpec{}, } } - addFilters := func(hr *v1.HTTPRoute, filters []v1.HTTPRouteFilter) { + addFilters := func(hr *graph.L7Route, filters []v1.HTTPRouteFilter) { for i := range hr.Spec.Rules { hr.Spec.Rules[i].Filters = filters } @@ -108,18 +91,28 @@ func TestBuildConfiguration(t *testing.T) { return []graph.BackendRef{validBackendRef} } - createRules := func(hr *v1.HTTPRoute, paths []pathAndType) []graph.Rule { - rules := make([]graph.Rule, len(hr.Spec.Rules)) + createRules := func(paths []pathAndType) []graph.RouteRule { + rules := make([]graph.RouteRule, len(paths)) for i := range paths { validMatches := paths[i].path != invalidMatchesPath validFilters := paths[i].path != invalidFiltersPath validRule := validMatches && validFilters - rules[i] = graph.Rule{ + m := []v1.HTTPRouteMatch{ + { + Path: &v1.HTTPPathMatch{ + Value: &paths[i].path, + Type: &paths[i].pathType, + }, + }, + } + + rules[i] = graph.RouteRule{ ValidMatches: validMatches, ValidFilters: validFilters, BackendRefs: createBackendRefs(validRule), + Matches: m, } } @@ -127,18 +120,19 @@ func TestBuildConfiguration(t *testing.T) { } createInternalRoute := func( - source *v1.HTTPRoute, + source client.Object, + routeType graph.RouteType, + hostnames []string, listenerName string, paths []pathAndType, - ) *graph.Route { - hostnames := make([]string, 0, len(source.Spec.Hostnames)) - for _, h := range source.Spec.Hostnames { - hostnames = append(hostnames, string(h)) - } - r := &graph.Route{ - Source: source, - Rules: createRules(source, paths), - Valid: true, + ) *graph.L7Route { + r := &graph.L7Route{ + RouteType: routeType, + Source: source, + Spec: graph.L7RouteSpec{ + Rules: createRules(paths), + }, + Valid: true, ParentRefs: []graph.ParentRef{ { Attachment: &graph.ParentRefAttachmentStatus{ @@ -152,10 +146,10 @@ func TestBuildConfiguration(t *testing.T) { return r } - createExpBackendGroupsForRoute := func(route *graph.Route) []BackendGroup { + createExpBackendGroupsForRoute := func(route *graph.L7Route) []BackendGroup { groups := make([]BackendGroup, 0) - for idx, r := range route.Rules { + for idx, r := range route.Spec.Rules { var backends []Backend if r.ValidFilters && r.ValidMatches { backends = []Backend{expValidBackend} @@ -172,10 +166,10 @@ func TestBuildConfiguration(t *testing.T) { } createTestResources := func(name, hostname, listenerName string, paths ...pathAndType) ( - *v1.HTTPRoute, []BackendGroup, *graph.Route, + *v1.HTTPRoute, []BackendGroup, *graph.L7Route, ) { - hr := createRoute(name, hostname, listenerName, paths...) - route := createInternalRoute(hr, listenerName, paths) + hr := createRoute(name) + route := createInternalRoute(hr, graph.RouteTypeHTTP, []string{hostname}, listenerName, paths) groups := createExpBackendGroupsForRoute(route) return hr, groups, route } @@ -230,7 +224,7 @@ func TestBuildConfiguration(t *testing.T) { Hostname: (*v1.PreciseHostname)(helpers.GetPointer("foo.example.com")), }, } - addFilters(hr5, []v1.HTTPRouteFilter{redirect}) + addFilters(routeHR5, []v1.HTTPRouteFilter{redirect}) expRedirect := HTTPRequestRedirectFilter{ Hostname: helpers.GetPointer("foo.example.com"), } @@ -322,7 +316,7 @@ func TestBuildConfiguration(t *testing.T) { pathAndType{path: "/", pathType: prefix}, pathAndType{path: "/", pathType: prefix}, ) - httpsRouteHR8.Rules[0].BackendRefs[0].BackendTLSPolicy = &graph.BackendTLSPolicy{ + httpsRouteHR8.Spec.Rules[0].BackendRefs[0].BackendTLSPolicy = &graph.BackendTLSPolicy{ Source: &v1alpha2.BackendTLSPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "btp", @@ -365,7 +359,17 @@ func TestBuildConfiguration(t *testing.T) { pathAndType{path: "/", pathType: prefix}, pathAndType{path: "/", pathType: prefix}, ) - httpsRouteHR9.Rules[0].BackendRefs[0].BackendTLSPolicy = &graph.BackendTLSPolicy{ + gr := createGRPCRoute("gr") + routeGR := createInternalRoute( + gr, + graph.RouteTypeGRPC, + []string{"foo.example.com"}, + "listener-80-1", + []pathAndType{{path: "/", pathType: prefix}}, + ) + expGRGroups := createExpBackendGroupsForRoute(routeGR) + + httpsRouteHR9.Spec.Rules[0].BackendRefs[0].BackendTLSPolicy = &graph.BackendTLSPolicy{ Source: &v1alpha2.BackendTLSPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "btp2", @@ -569,7 +573,7 @@ func TestBuildConfiguration(t *testing.T) { Source: &v1.Gateway{}, Listeners: []*graph.Listener{}, }, - Routes: map[types.NamespacedName]*graph.Route{}, + Routes: map[graph.RouteKey]*graph.L7Route{}, }, expConf: Configuration{ HTTPServers: []VirtualServer{}, @@ -592,11 +596,11 @@ func TestBuildConfiguration(t *testing.T) { Name: "listener-80-1", Source: listener80, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{}, + Routes: map[graph.RouteKey]*graph.L7Route{}, }, }, }, - Routes: map[types.NamespacedName]*graph.Route{}, + Routes: map[graph.RouteKey]*graph.L7Route{}, }, expConf: Configuration{ HTTPServers: []VirtualServer{ @@ -624,23 +628,23 @@ func TestBuildConfiguration(t *testing.T) { Name: "listener-80-1", Source: listener80, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - client.ObjectKeyFromObject(hr1Invalid): routeHR1Invalid, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr1Invalid): routeHR1Invalid, }, }, { Name: "listener-443-1", Source: listener443, // nil hostname Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - client.ObjectKeyFromObject(httpsHR1Invalid): httpsRouteHR1Invalid, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR1Invalid): httpsRouteHR1Invalid, }, ResolvedSecret: &secret1NsName, }, }, }, - Routes: map[types.NamespacedName]*graph.Route{ - client.ObjectKeyFromObject(hr1Invalid): routeHR1Invalid, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr1Invalid): routeHR1Invalid, }, ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ secret1NsName: secret1, @@ -687,19 +691,19 @@ func TestBuildConfiguration(t *testing.T) { Name: "listener-443-1", Source: listener443, // nil hostname Valid: true, - Routes: map[types.NamespacedName]*graph.Route{}, + Routes: map[graph.RouteKey]*graph.L7Route{}, ResolvedSecret: &secret1NsName, }, { Name: "listener-443-with-hostname", Source: listener443WithHostname, // non-nil hostname Valid: true, - Routes: map[types.NamespacedName]*graph.Route{}, + Routes: map[graph.RouteKey]*graph.L7Route{}, ResolvedSecret: &secret2NsName, }, }, }, - Routes: map[types.NamespacedName]*graph.Route{}, + Routes: map[graph.RouteKey]*graph.L7Route{}, ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ secret1NsName: secret1, secret2NsName: secret2, @@ -754,9 +758,9 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "https-hr-1"}: httpsRouteHR1, - {Namespace: "test", Name: "https-hr-2"}: httpsRouteHR2, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR1): httpsRouteHR1, + graph.CreateRouteKey(httpsHR2): httpsRouteHR2, }, ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ secret1NsName: secret1, @@ -783,16 +787,16 @@ func TestBuildConfiguration(t *testing.T) { Name: "listener-80-1", Source: listener80, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-1"}: routeHR1, - {Namespace: "test", Name: "hr-2"}: routeHR2, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr1): routeHR1, + graph.CreateRouteKey(hr2): routeHR2, }, }, }, }, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-1"}: routeHR1, - {Namespace: "test", Name: "hr-2"}: routeHR2, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr1): routeHR1, + graph.CreateRouteKey(hr2): routeHR2, }, }, expConf: Configuration{ @@ -842,6 +846,61 @@ func TestBuildConfiguration(t *testing.T) { }, msg: "one http listener with two 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(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, + }, + }, + }, + }, + Port: 80, + }, + }, + SSLServers: []VirtualServer{}, + Upstreams: []Upstream{fooUpstream}, + BackendGroups: []BackendGroup{expGRGroups[0]}, + SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{}, + CertBundles: map[CertBundleID]CertBundle{}, + }, + msg: "one http listener with one grpc route", + }, { graph: &graph.Graph{ GatewayClass: &graph.GatewayClass{ @@ -855,9 +914,9 @@ func TestBuildConfiguration(t *testing.T) { Name: "listener-443-1", Source: listener443, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "https-hr-1"}: httpsRouteHR1, - {Namespace: "test", Name: "https-hr-2"}: httpsRouteHR2, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR1): httpsRouteHR1, + graph.CreateRouteKey(httpsHR2): httpsRouteHR2, }, ResolvedSecret: &secret1NsName, }, @@ -865,17 +924,17 @@ func TestBuildConfiguration(t *testing.T) { Name: "listener-443-with-hostname", Source: listener443WithHostname, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "https-hr-5"}: httpsRouteHR5, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR5): httpsRouteHR5, }, ResolvedSecret: &secret2NsName, }, }, }, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "https-hr-1"}: httpsRouteHR1, - {Namespace: "test", Name: "https-hr-2"}: httpsRouteHR2, - {Namespace: "test", Name: "https-hr-5"}: httpsRouteHR5, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr1): httpsRouteHR1, + graph.CreateRouteKey(hr2): httpsRouteHR2, + graph.CreateRouteKey(httpsHR5): httpsRouteHR5, }, ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ secret1NsName: secret1, @@ -975,28 +1034,28 @@ func TestBuildConfiguration(t *testing.T) { Name: "listener-80-1", Source: listener80, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-3"}: routeHR3, - {Namespace: "test", Name: "hr-4"}: routeHR4, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr3): routeHR3, + graph.CreateRouteKey(hr4): routeHR4, }, }, { Name: "listener-443-1", Source: listener443, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "https-hr-3"}: httpsRouteHR3, - {Namespace: "test", Name: "https-hr-4"}: httpsRouteHR4, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR3): httpsRouteHR3, + graph.CreateRouteKey(httpsHR4): httpsRouteHR4, }, ResolvedSecret: &secret1NsName, }, }, }, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-3"}: routeHR3, - {Namespace: "test", Name: "hr-4"}: routeHR4, - {Namespace: "test", Name: "https-hr-3"}: httpsRouteHR3, - {Namespace: "test", Name: "https-hr-4"}: httpsRouteHR4, + 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{ secret1NsName: secret1, @@ -1135,24 +1194,24 @@ func TestBuildConfiguration(t *testing.T) { Name: "listener-80-1", Source: listener80, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-3"}: routeHR3, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr3): routeHR3, }, }, { Name: "listener-8080", Source: listener8080, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-8"}: routeHR8, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr8): routeHR8, }, }, { Name: "listener-443-1", Source: listener443, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "https-hr-3"}: httpsRouteHR3, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR3): httpsRouteHR3, }, ResolvedSecret: &secret1NsName, }, @@ -1160,18 +1219,18 @@ func TestBuildConfiguration(t *testing.T) { Name: "listener-8443", Source: listener8443, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "https-hr-7"}: httpsRouteHR7, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR7): httpsRouteHR7, }, ResolvedSecret: &secret1NsName, }, }, }, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-3"}: routeHR3, - {Namespace: "test", Name: "hr-8"}: routeHR8, - {Namespace: "test", Name: "https-hr-3"}: httpsRouteHR3, - {Namespace: "test", Name: "https-hr-7"}: httpsRouteHR7, + 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{ secret1NsName: secret1, @@ -1349,14 +1408,14 @@ func TestBuildConfiguration(t *testing.T) { Name: "listener-80-1", Source: listener80, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-1"}: routeHR1, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr1): routeHR1, }, }, }, }, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-1"}: routeHR1, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr1): routeHR1, }, }, expConf: Configuration{}, @@ -1372,14 +1431,14 @@ func TestBuildConfiguration(t *testing.T) { Name: "listener-80-1", Source: listener80, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-1"}: routeHR1, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr1): routeHR1, }, }, }, }, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-1"}: routeHR1, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr1): routeHR1, }, }, expConf: Configuration{}, @@ -1392,7 +1451,7 @@ func TestBuildConfiguration(t *testing.T) { Valid: true, }, Gateway: nil, - Routes: map[types.NamespacedName]*graph.Route{}, + Routes: map[graph.RouteKey]*graph.L7Route{}, }, expConf: Configuration{}, msg: "missing gateway", @@ -1410,14 +1469,14 @@ func TestBuildConfiguration(t *testing.T) { Name: "listener-80-1", Source: listener80, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-5"}: routeHR5, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr5): routeHR5, }, }, }, }, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-5"}: routeHR5, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr5): routeHR5, }, }, expConf: Configuration{ @@ -1480,24 +1539,24 @@ func TestBuildConfiguration(t *testing.T) { Name: "listener-80-1", Source: listener80, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-6"}: routeHR6, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr6): routeHR6, }, }, { Name: "listener-443-1", Source: listener443, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "https-hr-6"}: httpsRouteHR6, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR6): httpsRouteHR6, }, ResolvedSecret: &secret1NsName, }, }, }, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-6"}: routeHR6, - {Namespace: "test", Name: "https-hr-6"}: httpsRouteHR6, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr6): routeHR6, + graph.CreateRouteKey(httpsHR6): httpsRouteHR6, }, ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ secret1NsName: secret1, @@ -1582,14 +1641,14 @@ func TestBuildConfiguration(t *testing.T) { Name: "listener-80-1", Source: listener80, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-7"}: routeHR7, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr7): routeHR7, }, }, }, }, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-7"}: routeHR7, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hr7): routeHR7, }, }, expConf: Configuration{ @@ -1646,8 +1705,8 @@ func TestBuildConfiguration(t *testing.T) { Name: "listener-443-with-hostname", Source: listener443WithHostname, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "https-hr-5"}: httpsRouteHR5, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR5): httpsRouteHR5, }, ResolvedSecret: &secret2NsName, }, @@ -1655,15 +1714,15 @@ func TestBuildConfiguration(t *testing.T) { Name: "listener-443-1", Source: listener443, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "https-hr-5"}: httpsRouteHR5, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR5): httpsRouteHR5, }, ResolvedSecret: &secret1NsName, }, }, }, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "https-hr-5"}: httpsRouteHR5, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR5): httpsRouteHR5, }, ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ secret1NsName: secret1, @@ -1734,15 +1793,15 @@ func TestBuildConfiguration(t *testing.T) { Name: "listener-443", Source: listener443, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "https-hr-8"}: httpsRouteHR8, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR8): httpsRouteHR8, }, ResolvedSecret: &secret1NsName, }, }, }, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "https-hr-8"}: httpsRouteHR8, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR8): httpsRouteHR8, }, ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ secret1NsName: secret1, @@ -1810,15 +1869,15 @@ func TestBuildConfiguration(t *testing.T) { Name: "listener-443", Source: listener443, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "https-hr-9"}: httpsRouteHR9, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR9): httpsRouteHR9, }, ResolvedSecret: &secret1NsName, }, }, }, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "https-hr-9"}: httpsRouteHR9, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHR9): httpsRouteHR9, }, ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ secret1NsName: secret1, @@ -1891,11 +1950,11 @@ func TestBuildConfiguration(t *testing.T) { Name: "listener-80-1", Source: listener80, Valid: true, - Routes: map[types.NamespacedName]*graph.Route{}, + Routes: map[graph.RouteKey]*graph.L7Route{}, }, }, }, - Routes: map[types.NamespacedName]*graph.Route{}, + Routes: map[graph.RouteKey]*graph.L7Route{}, NginxProxy: nginxProxy, }, expConf: Configuration{ @@ -2143,11 +2202,11 @@ func TestGetListenerHostname(t *testing.T) { } } -func refsToValidRules(refs ...[]graph.BackendRef) []graph.Rule { - rules := make([]graph.Rule, 0, len(refs)) +func refsToValidRules(refs ...[]graph.BackendRef) []graph.RouteRule { + rules := make([]graph.RouteRule, 0, len(refs)) for _, ref := range refs { - rules = append(rules, graph.Rule{ + rules = append(rules, graph.RouteRule{ ValidMatches: true, ValidFilters: true, BackendRefs: ref, @@ -2243,39 +2302,51 @@ func TestBuildUpstreams(t *testing.T) { invalidHRRefs := createBackendRefs("abc") - routes := map[types.NamespacedName]*graph.Route{ - {Name: "hr1", Namespace: "test"}: { + routes := map[graph.RouteKey]*graph.L7Route{ + {NamespacedName: types.NamespacedName{Name: "hr1", Namespace: "test"}}: { Valid: true, - Rules: refsToValidRules(hr1Refs0, hr1Refs1), + Spec: graph.L7RouteSpec{ + Rules: refsToValidRules(hr1Refs0, hr1Refs1), + }, }, - {Name: "hr2", Namespace: "test"}: { + {NamespacedName: types.NamespacedName{Name: "hr2", Namespace: "test"}}: { Valid: true, - Rules: refsToValidRules(hr2Refs0, hr2Refs1), + Spec: graph.L7RouteSpec{ + Rules: refsToValidRules(hr2Refs0, hr2Refs1), + }, }, - {Name: "hr3", Namespace: "test"}: { + {NamespacedName: types.NamespacedName{Name: "hr3", Namespace: "test"}}: { Valid: true, - Rules: refsToValidRules(hr3Refs0), + Spec: graph.L7RouteSpec{ + Rules: refsToValidRules(hr3Refs0), + }, }, } - routes2 := map[types.NamespacedName]*graph.Route{ - {Name: "hr4", Namespace: "test"}: { + routes2 := map[graph.RouteKey]*graph.L7Route{ + {NamespacedName: types.NamespacedName{Name: "hr4", Namespace: "test"}}: { Valid: true, - Rules: refsToValidRules(hr4Refs0, hr4Refs1), + Spec: graph.L7RouteSpec{ + Rules: refsToValidRules(hr4Refs0, hr4Refs1), + }, }, } - routesWithNonExistingRefs := map[types.NamespacedName]*graph.Route{ - {Name: "non-existing", Namespace: "test"}: { + routesWithNonExistingRefs := map[graph.RouteKey]*graph.L7Route{ + {NamespacedName: types.NamespacedName{Name: "non-existing", Namespace: "test"}}: { Valid: true, - Rules: refsToValidRules(nonExistingRefs), + Spec: graph.L7RouteSpec{ + Rules: refsToValidRules(nonExistingRefs), + }, }, } - invalidRoutes := map[types.NamespacedName]*graph.Route{ - {Name: "invalid", Namespace: "test"}: { + invalidRoutes := map[graph.RouteKey]*graph.L7Route{ + {NamespacedName: types.NamespacedName{Name: "invalid", Namespace: "test"}}: { Valid: false, - Rules: refsToValidRules(invalidHRRefs), + Spec: graph.L7RouteSpec{ + Rules: refsToValidRules(invalidHRRefs), + }, }, } diff --git a/internal/mode/static/state/dataplane/sort.go b/internal/mode/static/state/dataplane/sort.go index 553d522768..13dcc3149c 100644 --- a/internal/mode/static/state/dataplane/sort.go +++ b/internal/mode/static/state/dataplane/sort.go @@ -7,7 +7,7 @@ import ( ) func sortMatchRules(matchRules []MatchRule) { - // stable sort is used so that the order of matches (as defined in each HTTPRoute rule) is preserved + // stable sort is used so that the order of matches (as defined in each Route rule) is preserved // this is important, because the winning match is the first match to win. sort.SliceStable( matchRules, func(i, j int) bool { @@ -21,12 +21,20 @@ Returns true if rule1 has a higher priority than rule2. From the spec: Precedence must be given to the Rule with the largest number of (Continuing on ties): +(HTTPRoute) - Characters in a matching non-wildcard hostname. - Characters in a matching hostname. - Characters in a matching path. - Method match. - Header matches. - Query param matches. +or +(GRPCRoute) +- Characters in a matching non-wildcard hostname. +- Characters in a matching hostname. +- Characters in a matching service. +- Characters in a matching method. +- Header matches. If ties still exist across multiple Routes, matching precedence MUST be determined in order of the following criteria, continuing on ties: @@ -38,6 +46,8 @@ matching precedence MUST be granted to the first matching rule meeting the above higherPriority will determine precedence by comparing len(headers), len(query parameters), creation timestamp, and namespace name. It gives higher priority to rules with a method match. The other criteria are handled by NGINX. +For GRPCRoute rules, match.Method and match.QueryParams are always nil/ 0 len. Our representation combines service +and method into a path so that we perform "characters in a matching path" for GRPCRoute. */ func higherPriority(rule1, rule2 MatchRule) bool { // Compare if a method exists on one of the matches but not the other. diff --git a/internal/mode/static/state/dataplane/types.go b/internal/mode/static/state/dataplane/types.go index 21c5749dfb..53453b0d82 100644 --- a/internal/mode/static/state/dataplane/types.go +++ b/internal/mode/static/state/dataplane/types.go @@ -96,6 +96,8 @@ type PathRule struct { PathType PathType // MatchRules holds routing rules. MatchRules []MatchRule + // GRPC indicates if this is a gRPC rule + GRPC bool } // InvalidHTTPFilter is a special filter for handling the case when configured filters are invalid. diff --git a/internal/mode/static/state/graph/backend_refs.go b/internal/mode/static/state/graph/backend_refs.go index 5e22743707..fcdd8a5977 100644 --- a/internal/mode/static/state/graph/backend_refs.go +++ b/internal/mode/static/state/graph/backend_refs.go @@ -8,7 +8,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation/field" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" - v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/apis/v1alpha2" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" @@ -16,7 +16,7 @@ import ( staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" ) -// BackendRef is an internal representation of a backendRef in an HTTPRoute. +// BackendRef is an internal representation of a backendRef in an HTTP/GRPCRoute. type BackendRef struct { // BackendTLSPolicy is the BackendTLSPolicy of the Service which is referenced by the backendRef. BackendTLSPolicy *BackendTLSPolicy @@ -40,7 +40,7 @@ func (b BackendRef) ServicePortReference() string { } func addBackendRefsToRouteRules( - routes map[types.NamespacedName]*Route, + routes map[RouteKey]*L7Route, refGrantResolver *referenceGrantResolver, services map[types.NamespacedName]*v1.Service, backendTLSPolicies map[types.NamespacedName]*BackendTLSPolicy, @@ -50,11 +50,10 @@ func addBackendRefsToRouteRules( } } -// addBackendRefsToRules iterates over the rules of a route and adds a list of BackendRef to each rule. -// The route is modified in place. +// addHTTPBackendRefsToRules iterates over the rules of a Route and adds a list of BackendRef to each rule. // If a reference in a rule is invalid, the function will add a condition to the rule. func addBackendRefsToRules( - route *Route, + route *L7Route, refGrantResolver *referenceGrantResolver, services map[types.NamespacedName]*v1.Service, backendTLSPolicies map[types.NamespacedName]*BackendTLSPolicy, @@ -63,27 +62,27 @@ func addBackendRefsToRules( return } - for idx, rule := range route.Source.Spec.Rules { - if !route.Rules[idx].ValidMatches { + for idx, rule := range route.Spec.Rules { + if !rule.ValidMatches { continue } - if !route.Rules[idx].ValidFilters { + if !rule.ValidFilters { continue } // zero backendRefs is OK. For example, a rule can include a redirect filter. - if len(rule.BackendRefs) == 0 { + if len(rule.RouteBackendRefs) == 0 { continue } - backendRefs := make([]BackendRef, 0, len(rule.BackendRefs)) + backendRefs := make([]BackendRef, 0, len(rule.RouteBackendRefs)) - for refIdx, ref := range rule.BackendRefs { + for refIdx, ref := range rule.RouteBackendRefs { refPath := field.NewPath("spec").Child("rules").Index(idx).Child("backendRefs").Index(refIdx) ref, cond := createBackendRef( ref, - route.Source.Namespace, + route.Source.GetNamespace(), refGrantResolver, services, refPath, @@ -106,13 +105,12 @@ func addBackendRefsToRules( } } } - - route.Rules[idx].BackendRefs = backendRefs + route.Spec.Rules[idx].BackendRefs = backendRefs } } func createBackendRef( - ref gatewayv1.HTTPBackendRef, + ref RouteBackendRef, sourceNamespace string, refGrantResolver *referenceGrantResolver, services map[types.NamespacedName]*v1.Service, @@ -134,7 +132,7 @@ func createBackendRef( var backendRef BackendRef - valid, cond := validateHTTPBackendRef(ref, sourceNamespace, refGrantResolver, refPath) + valid, cond := validateRouteBackendRef(ref, sourceNamespace, refGrantResolver, refPath) if !valid { backendRef = BackendRef{ Weight: weight, @@ -159,7 +157,8 @@ func createBackendRef( backendTLSPolicy, err := findBackendTLSPolicyForService( backendTLSPolicies, - ref, + ref.Namespace, + string(ref.Name), sourceNamespace, ) if err != nil { @@ -231,15 +230,16 @@ func validateBackendTLSPolicyMatchingAllBackends(backendRefs []BackendRef) *cond func findBackendTLSPolicyForService( backendTLSPolicies map[types.NamespacedName]*BackendTLSPolicy, - ref gatewayv1.HTTPBackendRef, + refNamespace *gatewayv1.Namespace, + refName, routeNamespace string, ) (*BackendTLSPolicy, error) { var beTLSPolicy *BackendTLSPolicy var err error refNs := routeNamespace - if ref.Namespace != nil { - refNs = string(*ref.Namespace) + if refNamespace != nil { + refNs = string(*refNamespace) } for _, btp := range backendTLSPolicies { @@ -247,7 +247,7 @@ func findBackendTLSPolicyForService( if btp.Source.Spec.TargetRef.Namespace != nil { btpNs = string(*btp.Source.Spec.TargetRef.Namespace) } - if btp.Source.Spec.TargetRef.Name == ref.Name && btpNs == refNs { + if string(btp.Source.Spec.TargetRef.Name) == refName && btpNs == refNs { if beTLSPolicy != nil { if sort.LessObjectMeta(&btp.Source.ObjectMeta, &beTLSPolicy.Source.ObjectMeta) { beTLSPolicy = btp @@ -261,7 +261,7 @@ func findBackendTLSPolicyForService( if beTLSPolicy != nil { beTLSPolicy.IsReferenced = true if !beTLSPolicy.Valid { - err = fmt.Errorf("The backend TLS policy is invalid: %s", beTLSPolicy.Conditions[0].Message) + err = fmt.Errorf("the backend TLS policy is invalid: %s", beTLSPolicy.Conditions[0].Message) } else { beTLSPolicy.Conditions = append(beTLSPolicy.Conditions, staticConds.NewBackendTLSPolicyAccepted()) } @@ -301,14 +301,13 @@ func getServiceAndPortFromRef( return svcNsName, svcPort, nil } -func validateHTTPBackendRef( - ref gatewayv1.HTTPBackendRef, +func validateRouteBackendRef( + ref RouteBackendRef, routeNs string, refGrantResolver *referenceGrantResolver, path *field.Path, ) (valid bool, cond conditions.Condition) { // Because all errors cause the same condition but different reasons, we return as soon as we find an error - if len(ref.Filters) > 0 { valErr := field.TooMany(path.Child("filters"), len(ref.Filters), 0) return false, staticConds.NewRouteBackendRefUnsupportedValue(valErr.Error()) diff --git a/internal/mode/static/state/graph/backend_refs_test.go b/internal/mode/static/state/graph/backend_refs_test.go index d4c0a5b24e..c6a4cc0f58 100644 --- a/internal/mode/static/state/graph/backend_refs_test.go +++ b/internal/mode/static/state/graph/backend_refs_test.go @@ -34,16 +34,16 @@ func getModifiedRef(mod func(ref gatewayv1.BackendRef) gatewayv1.BackendRef) gat return mod(getNormalRef()) } -func TestValidateHTTPBackendRef(t *testing.T) { +func TestValidateRouteBackendRef(t *testing.T) { tests := []struct { expectedCondition conditions.Condition name string - ref gatewayv1.HTTPBackendRef + ref RouteBackendRef expectedValid bool }{ { name: "normal case", - ref: gatewayv1.HTTPBackendRef{ + ref: RouteBackendRef{ BackendRef: getNormalRef(), Filters: nil, }, @@ -51,11 +51,13 @@ func TestValidateHTTPBackendRef(t *testing.T) { }, { name: "filters not supported", - ref: gatewayv1.HTTPBackendRef{ + ref: RouteBackendRef{ BackendRef: getNormalRef(), - Filters: []gatewayv1.HTTPRouteFilter{ - { - Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, + Filters: []any{ + []gatewayv1.HTTPRouteFilter{ + { + Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, + }, }, }, }, @@ -66,7 +68,7 @@ func TestValidateHTTPBackendRef(t *testing.T) { }, { name: "invalid base ref", - ref: gatewayv1.HTTPBackendRef{ + ref: RouteBackendRef{ BackendRef: getModifiedRef(func(backend gatewayv1.BackendRef) gatewayv1.BackendRef { backend.Kind = helpers.GetPointer[gatewayv1.Kind]("NotService") return backend @@ -84,7 +86,7 @@ func TestValidateHTTPBackendRef(t *testing.T) { g := NewWithT(t) resolver := newReferenceGrantResolver(nil) - valid, cond := validateHTTPBackendRef(test.ref, "test", resolver, field.NewPath("test")) + valid, cond := validateRouteBackendRef(test.ref, "test", resolver, field.NewPath("test")) g.Expect(valid).To(Equal(test.expectedValid)) g.Expect(cond).To(Equal(test.expectedCondition)) @@ -332,21 +334,34 @@ func TestGetServiceAndPortFromRef(t *testing.T) { } func TestAddBackendRefsToRulesTest(t *testing.T) { + sectionNameRefs := []ParentRef{ + { + Idx: 0, + Gateway: types.NamespacedName{Namespace: "test", Name: "gateway"}, + Attachment: &ParentRefAttachmentStatus{ + Attached: true, + }, + }, + } createRoute := func( name string, kind gatewayv1.Kind, refsPerBackend int, serviceNames ...string, - ) *gatewayv1.HTTPRoute { - hr := &gatewayv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - Name: name, + ) *L7Route { + hr := &L7Route{ + Source: &gatewayv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: name, + }, }, + ParentRefs: sectionNameRefs, + Valid: true, } - createHTTPBackendRef := func(svcName string, port gatewayv1.PortNumber, weight *int32) gatewayv1.HTTPBackendRef { - return gatewayv1.HTTPBackendRef{ + createRouteBackendRef := func(svcName string, port gatewayv1.PortNumber, weight *int32) RouteBackendRef { + return RouteBackendRef{ BackendRef: gatewayv1.BackendRef{ BackendObjectReference: gatewayv1.BackendObjectReference{ Kind: helpers.GetPointer(kind), @@ -359,57 +374,44 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { } } - hr.Spec.Rules = make([]gatewayv1.HTTPRouteRule, len(serviceNames)) + hr.Spec.Rules = make([]RouteRule, len(serviceNames)) for idx, svcName := range serviceNames { - refs := []gatewayv1.HTTPBackendRef{ - createHTTPBackendRef(svcName, 80, nil), + refs := []RouteBackendRef{ + createRouteBackendRef(svcName, 80, nil), } if refsPerBackend == 2 { - refs = append(refs, createHTTPBackendRef(svcName, 81, helpers.GetPointer[int32](5))) + refs = append(refs, createRouteBackendRef(svcName, 81, helpers.GetPointer[int32](5))) } if refsPerBackend != 1 && refsPerBackend != 2 { panic("invalid refsPerBackend") } - hr.Spec.Rules[idx] = gatewayv1.HTTPRouteRule{ - BackendRefs: refs, + hr.Spec.Rules[idx] = RouteRule{ + RouteBackendRefs: refs, + ValidMatches: true, + ValidFilters: true, } } return hr } - const ( - allValid = true - allInvalid = false - ) - - createRules := func(hr *gatewayv1.HTTPRoute, validMatches, validFilters bool) []Rule { - rules := make([]Rule, len(hr.Spec.Rules)) - for i := range rules { - rules[i].ValidMatches = validMatches - rules[i].ValidFilters = validFilters - } - return rules - } - - sectionNameRefs := []ParentRef{ - { - Idx: 0, - Gateway: types.NamespacedName{Namespace: "test", Name: "gateway"}, - Attachment: &ParentRefAttachmentStatus{ - Attached: true, - }, - }, - } - hrWithOneBackend := createRoute("hr1", "Service", 1, "svc1") hrWithTwoBackends := createRoute("hr2", "Service", 2, "svc1") hrWithTwoDiffBackends := createRoute("hr2", "Service", 2, "svc1") hrWithInvalidRule := createRoute("hr3", "NotService", 1, "svc1") hrWithZeroBackendRefs := createRoute("hr4", "Service", 1, "svc1") - hrWithZeroBackendRefs.Spec.Rules[0].BackendRefs = nil - hrWithTwoDiffBackends.Spec.Rules[0].BackendRefs[1].Name = "svc2" + hrWithZeroBackendRefs.Spec.Rules[0].RouteBackendRefs = nil + hrWithTwoDiffBackends.Spec.Rules[0].RouteBackendRefs[1].Name = "svc2" + + hrWithOneBackendInvalid := createRoute("hr1", "Service", 1, "svc1") + hrWithOneBackendInvalid.Valid = false + + hrWithOneBackendInvalidMatches := createRoute("hr1", "Service", 1, "svc1") + hrWithOneBackendInvalidMatches.Spec.Rules[0].ValidMatches = false + + hrWithOneBackendInvalidFilters := createRoute("hr1", "Service", 1, "svc1") + hrWithOneBackendInvalidFilters.Spec.Rules[0].ValidFilters = false getSvc := func(name string) *v1.Service { return &v1.Service{ @@ -538,19 +540,14 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { ) tests := []struct { - route *Route + route *L7Route policies map[types.NamespacedName]*BackendTLSPolicy name string expectedBackendRefs []BackendRef expectedConditions []conditions.Condition }{ { - route: &Route{ - Source: hrWithOneBackend, - ParentRefs: sectionNameRefs, - Valid: true, - Rules: createRules(hrWithOneBackend, allValid, allValid), - }, + route: hrWithOneBackend, expectedBackendRefs: []BackendRef{ { SvcNsName: svc1NsName, @@ -564,12 +561,7 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { name: "normal case with one rule with one backend", }, { - route: &Route{ - Source: hrWithTwoBackends, - ParentRefs: sectionNameRefs, - Valid: true, - Rules: createRules(hrWithTwoBackends, allValid, allValid), - }, + route: hrWithTwoBackends, expectedBackendRefs: []BackendRef{ { SvcNsName: svc1NsName, @@ -589,12 +581,7 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { name: "normal case with one rule with two backends", }, { - route: &Route{ - Source: hrWithTwoBackends, - ParentRefs: sectionNameRefs, - Valid: true, - Rules: createRules(hrWithTwoBackends, allValid, allValid), - }, + route: hrWithTwoBackends, expectedBackendRefs: []BackendRef{ { SvcNsName: svc1NsName, @@ -616,47 +603,28 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { name: "normal case with one rule with two backends and matching policies", }, { - route: &Route{ - Source: hrWithOneBackend, - ParentRefs: sectionNameRefs, - Valid: false, - }, + route: hrWithOneBackendInvalid, expectedBackendRefs: nil, expectedConditions: nil, policies: emptyPolicies, name: "invalid route", }, { - route: &Route{ - Source: hrWithOneBackend, - ParentRefs: sectionNameRefs, - Valid: true, - Rules: createRules(hrWithOneBackend, allInvalid, allValid), - }, + route: hrWithOneBackendInvalidMatches, expectedBackendRefs: nil, expectedConditions: nil, policies: emptyPolicies, name: "invalid matches", }, { - route: &Route{ - Source: hrWithOneBackend, - ParentRefs: sectionNameRefs, - Valid: true, - Rules: createRules(hrWithOneBackend, allValid, allInvalid), - }, + route: hrWithOneBackendInvalidFilters, expectedBackendRefs: nil, expectedConditions: nil, policies: emptyPolicies, name: "invalid filters", }, { - route: &Route{ - Source: hrWithInvalidRule, - ParentRefs: sectionNameRefs, - Valid: true, - Rules: createRules(hrWithInvalidRule, allValid, allValid), - }, + route: hrWithInvalidRule, expectedBackendRefs: []BackendRef{ { Weight: 1, @@ -671,12 +639,7 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { name: "invalid backendRef", }, { - route: &Route{ - Source: hrWithTwoDiffBackends, - ParentRefs: sectionNameRefs, - Valid: true, - Rules: createRules(hrWithTwoDiffBackends, allValid, allValid), - }, + route: hrWithTwoDiffBackends, expectedBackendRefs: []BackendRef{ { SvcNsName: svc1NsName, @@ -702,12 +665,7 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { name: "invalid backendRef - backend TLS policies do not match for all backends", }, { - route: &Route{ - Source: hrWithZeroBackendRefs, - ParentRefs: sectionNameRefs, - Valid: true, - Rules: createRules(hrWithZeroBackendRefs, allValid, allValid), - }, + route: hrWithZeroBackendRefs, expectedBackendRefs: nil, expectedConditions: nil, name: "zero backendRefs", @@ -721,8 +679,8 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { addBackendRefsToRules(test.route, resolver, services, test.policies) var actual []BackendRef - if test.route.Rules != nil { - actual = test.route.Rules[0].BackendRefs + if test.route.Spec.Rules != nil { + actual = test.route.Spec.Rules[0].BackendRefs } g.Expect(helpers.Diff(test.expectedBackendRefs, actual)).To(BeEmpty()) @@ -938,7 +896,7 @@ func TestCreateBackend(t *testing.T) { expectedServicePortReference: "", expectedCondition: helpers.GetPointer( staticConds.NewRouteBackendRefUnsupportedValue( - "The backend TLS policy is invalid: unsupported value", + "the backend TLS policy is invalid: unsupported value", ), ), name: "invalid policy", @@ -964,8 +922,13 @@ func TestCreateBackend(t *testing.T) { g := NewWithT(t) resolver := newReferenceGrantResolver(nil) + + rbr := RouteBackendRef{ + test.ref.BackendRef, + []any{}, + } backend, cond := createBackendRef( - test.ref, + rbr, sourceNamespace, resolver, services, @@ -1180,7 +1143,7 @@ func TestFindBackendTLSPolicyForService(t *testing.T) { t.Run(test.name, func(t *testing.T) { g := NewWithT(t) - btp, err := findBackendTLSPolicyForService(test.backendTLSPolicies, ref, "test") + btp, err := findBackendTLSPolicyForService(test.backendTLSPolicies, ref.Namespace, string(ref.Name), "test") g.Expect(btp.Source.Name).To(Equal(test.expectedBtpName)) g.Expect(err).ToNot(HaveOccurred()) diff --git a/internal/mode/static/state/graph/gateway_listener.go b/internal/mode/static/state/graph/gateway_listener.go index 8fe03d7383..432888ba88 100644 --- a/internal/mode/static/state/graph/gateway_listener.go +++ b/internal/mode/static/state/graph/gateway_listener.go @@ -20,9 +20,9 @@ type Listener struct { Name string // Source holds the source of the Listener from the Gateway resource. Source v1.Listener - // Routes holds the routes attached to the Listener. + // Routes holds the GRPC/HTTPRoutes attached to the Listener. // Only valid routes are attached. - Routes map[types.NamespacedName]*Route + Routes map[RouteKey]*L7Route // AllowedRouteLabelSelector is the label selector for this Listener's allowed routes, if defined. AllowedRouteLabelSelector labels.Selector // ResolvedSecret is the namespaced name of the Secret resolved for this listener. @@ -181,7 +181,7 @@ func (c *listenerConfigurator) configure(listener v1.Listener) *Listener { Source: listener, Conditions: conds, AllowedRouteLabelSelector: allowedRouteSelector, - Routes: make(map[types.NamespacedName]*Route), + Routes: make(map[RouteKey]*L7Route), Valid: valid, Attachable: attachable, SupportedKinds: supportedKinds, @@ -238,8 +238,8 @@ func getAndValidateListenerSupportedKinds(listener v1.Listener) ( supportedKinds := make([]v1.RouteGroupKind, 0, len(listener.AllowedRoutes.Kinds)) - validHTTPRouteKind := func(kind v1.RouteGroupKind) bool { - if kind.Kind != v1.Kind("HTTPRoute") { + validHTTPProtocolRouteKind := func(kind v1.RouteGroupKind) bool { + if kind.Kind != v1.Kind("HTTPRoute") && kind.Kind != v1.Kind("GRPCRoute") { return false } if kind.Group == nil || *kind.Group != v1.GroupName { @@ -251,7 +251,7 @@ func getAndValidateListenerSupportedKinds(listener v1.Listener) ( switch listener.Protocol { case v1.HTTPProtocolType, v1.HTTPSProtocolType: for _, kind := range listener.AllowedRoutes.Kinds { - if !validHTTPRouteKind(kind) { + if !validHTTPProtocolRouteKind(kind) { msg := fmt.Sprintf("Unsupported route kind \"%s/%s\"", *kind.Group, kind.Kind) conds = append(conds, staticConds.NewListenerInvalidRouteKinds(msg)...) continue diff --git a/internal/mode/static/state/graph/gateway_test.go b/internal/mode/static/state/graph/gateway_test.go index 80b2840b64..df10c2aaba 100644 --- a/internal/mode/static/state/graph/gateway_test.go +++ b/internal/mode/static/state/graph/gateway_test.go @@ -360,7 +360,7 @@ func TestBuildGateway(t *testing.T) { Source: foo80Listener1, Valid: true, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, }, @@ -370,7 +370,7 @@ func TestBuildGateway(t *testing.T) { Source: foo8080Listener, Valid: true, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, }, @@ -393,7 +393,7 @@ func TestBuildGateway(t *testing.T) { Source: foo443HTTPSListener1, Valid: true, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)), SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, @@ -404,7 +404,7 @@ func TestBuildGateway(t *testing.T) { Source: foo8443HTTPSListener, Valid: true, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)), SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, @@ -427,7 +427,7 @@ func TestBuildGateway(t *testing.T) { Valid: true, Attachable: true, AllowedRouteLabelSelector: labels.SelectorFromSet(labels.Set(labelSet)), - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute", Group: helpers.GetPointer[v1.Group](v1.GroupName)}, }, @@ -472,7 +472,7 @@ func TestBuildGateway(t *testing.T) { Source: crossNamespaceSecretListener, Valid: true, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretDiffNamespace)), SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, @@ -497,7 +497,7 @@ func TestBuildGateway(t *testing.T) { Conditions: staticConds.NewListenerRefNotPermitted( `Certificate ref to secret diff-ns/secret not permitted by any ReferenceGrant`, ), - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, }, @@ -521,7 +521,7 @@ func TestBuildGateway(t *testing.T) { Conditions: staticConds.NewListenerUnsupportedValue( `invalid label selector: "invalid" is not a valid label selector operator`, ), - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute", Group: helpers.GetPointer[v1.Group](v1.GroupName)}, }, @@ -545,7 +545,7 @@ func TestBuildGateway(t *testing.T) { Conditions: staticConds.NewListenerUnsupportedProtocol( `protocol: Unsupported value: "TCP": supported values: "HTTP", "HTTPS"`, ), - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, }, @@ -577,7 +577,7 @@ func TestBuildGateway(t *testing.T) { Conditions: staticConds.NewListenerUnsupportedValue( `port: Invalid value: 0: port must be between 1-65535`, ), - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, }, @@ -590,7 +590,7 @@ func TestBuildGateway(t *testing.T) { Conditions: staticConds.NewListenerUnsupportedValue( `port: Invalid value: 65536: port must be between 1-65535`, ), - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, }, @@ -603,7 +603,7 @@ func TestBuildGateway(t *testing.T) { Conditions: staticConds.NewListenerUnsupportedValue( `port: Invalid value: 9113: port is already in use as MetricsPort`, ), - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, }, @@ -626,7 +626,7 @@ func TestBuildGateway(t *testing.T) { Source: invalidHostnameListener, Valid: false, Conditions: staticConds.NewListenerUnsupportedValue(invalidHostnameMsg), - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, }, @@ -636,7 +636,7 @@ func TestBuildGateway(t *testing.T) { Source: invalidHTTPSHostnameListener, Valid: false, Conditions: staticConds.NewListenerUnsupportedValue(invalidHostnameMsg), - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, }, @@ -657,7 +657,7 @@ func TestBuildGateway(t *testing.T) { Source: invalidTLSConfigListener, Valid: false, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, Conditions: staticConds.NewListenerInvalidCertificateRef( `tls.certificateRefs[0]: Invalid value: test/does-not-exist: secret does not exist`, ), @@ -694,7 +694,7 @@ func TestBuildGateway(t *testing.T) { Source: foo80Listener1, Valid: true, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, }, @@ -704,7 +704,7 @@ func TestBuildGateway(t *testing.T) { Source: foo8080Listener, Valid: true, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, }, @@ -714,7 +714,7 @@ func TestBuildGateway(t *testing.T) { Source: foo8081Listener, Valid: true, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, }, @@ -724,7 +724,7 @@ func TestBuildGateway(t *testing.T) { Source: foo443HTTPSListener1, Valid: true, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)), SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, @@ -735,7 +735,7 @@ func TestBuildGateway(t *testing.T) { Source: foo8443HTTPSListener, Valid: true, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)), SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, @@ -746,7 +746,7 @@ func TestBuildGateway(t *testing.T) { Source: bar80Listener, Valid: true, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, }, @@ -756,7 +756,7 @@ func TestBuildGateway(t *testing.T) { Source: bar443HTTPSListener, Valid: true, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)), SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, @@ -767,7 +767,7 @@ func TestBuildGateway(t *testing.T) { Source: bar8443HTTPSListener, Valid: true, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)), SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, @@ -800,7 +800,7 @@ func TestBuildGateway(t *testing.T) { Source: foo80Listener1, Valid: false, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, Conditions: staticConds.NewListenerProtocolConflict(conflict80PortMsg), SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, @@ -811,7 +811,7 @@ func TestBuildGateway(t *testing.T) { Source: bar80Listener, Valid: false, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, Conditions: staticConds.NewListenerProtocolConflict(conflict80PortMsg), SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, @@ -822,7 +822,7 @@ func TestBuildGateway(t *testing.T) { Source: foo443Listener, Valid: false, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, Conditions: staticConds.NewListenerProtocolConflict(conflict443PortMsg), SupportedKinds: []v1.RouteGroupKind{ {Kind: "HTTPRoute"}, @@ -833,7 +833,7 @@ func TestBuildGateway(t *testing.T) { Source: foo80HTTPSListener, Valid: false, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, Conditions: staticConds.NewListenerProtocolConflict(conflict80PortMsg), ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)), SupportedKinds: []v1.RouteGroupKind{ @@ -845,7 +845,7 @@ func TestBuildGateway(t *testing.T) { Source: foo443HTTPSListener1, Valid: false, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, Conditions: staticConds.NewListenerProtocolConflict(conflict443PortMsg), ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)), SupportedKinds: []v1.RouteGroupKind{ @@ -857,7 +857,7 @@ func TestBuildGateway(t *testing.T) { Source: bar443HTTPSListener, Valid: false, Attachable: true, - Routes: map[types.NamespacedName]*Route{}, + Routes: map[RouteKey]*L7Route{}, Conditions: staticConds.NewListenerProtocolConflict(conflict443PortMsg), ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)), SupportedKinds: []v1.RouteGroupKind{ diff --git a/internal/mode/static/state/graph/graph.go b/internal/mode/static/state/graph/graph.go index af60f41a20..e121e90fab 100644 --- a/internal/mode/static/state/graph/graph.go +++ b/internal/mode/static/state/graph/graph.go @@ -28,6 +28,7 @@ type ClusterState struct { BackendTLSPolicies map[types.NamespacedName]*v1alpha2.BackendTLSPolicy ConfigMaps map[types.NamespacedName]*v1.ConfigMap NginxProxies map[types.NamespacedName]*ngfAPI.NginxProxy + GRPCRoutes map[types.NamespacedName]*v1alpha2.GRPCRoute } // Graph is a Graph-like representation of Gateway API resources. @@ -44,8 +45,8 @@ type Graph struct { // GatewayClassName field of the resource) but ignored. It doesn't hold the Gateway resources that do not belong to // the NGINX Gateway Fabric. IgnoredGateways map[types.NamespacedName]*gatewayv1.Gateway - // Routes holds Route resources. - Routes map[types.NamespacedName]*Route + // Routes hold Route resources. + Routes map[RouteKey]*L7Route // ReferencedSecrets includes Secrets referenced by Gateway Listeners, including invalid ones. // It is different from the other maps, because it includes entries for Secrets that do not exist // in the cluster. We need such entries so that we can query the Graph to determine if a Secret is referenced @@ -143,7 +144,12 @@ func BuildGraph( gw, ) - routes := buildRoutesForGateways(validators.HTTPFieldsValidator, state.HTTPRoutes, processedGws.GetAllNsNames()) + routes := buildRoutesForGateways( + validators.HTTPFieldsValidator, + state.HTTPRoutes, + state.GRPCRoutes, + processedGws.GetAllNsNames(), + ) bindRoutesToListeners(routes, gw, state.Namespaces) addBackendRefsToRouteRules(routes, refGrantResolver, state.Services, processedBackendTLSPolicies) diff --git a/internal/mode/static/state/graph/graph_test.go b/internal/mode/static/state/graph/graph_test.go index 12bfb4f3ca..5f5e81e132 100644 --- a/internal/mode/static/state/graph/graph_test.go +++ b/internal/mode/static/state/graph/graph_test.go @@ -35,65 +35,6 @@ func TestBuildGraph(t *testing.T) { 8081: "HealthPort", } - createValidRuleWithBackendRefs := func(refs []BackendRef) Rule { - return Rule{ - ValidMatches: true, - ValidFilters: true, - BackendRefs: refs, - } - } - - createRoute := func(name string, gatewayName string, listenerName string) *gatewayv1.HTTPRoute { - return &gatewayv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - Name: name, - }, - Spec: gatewayv1.HTTPRouteSpec{ - CommonRouteSpec: gatewayv1.CommonRouteSpec{ - ParentRefs: []gatewayv1.ParentReference{ - { - Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), - Name: gatewayv1.ObjectName(gatewayName), - SectionName: (*gatewayv1.SectionName)(helpers.GetPointer(listenerName)), - }, - }, - }, - Hostnames: []gatewayv1.Hostname{ - "foo.example.com", - }, - Rules: []gatewayv1.HTTPRouteRule{ - { - Matches: []gatewayv1.HTTPRouteMatch{ - { - Path: &gatewayv1.HTTPPathMatch{ - Type: helpers.GetPointer(gatewayv1.PathMatchPathPrefix), - Value: helpers.GetPointer("/"), - }, - }, - }, - BackendRefs: []gatewayv1.HTTPBackendRef{ - { - BackendRef: gatewayv1.BackendRef{ - BackendObjectReference: gatewayv1.BackendObjectReference{ - Kind: (*gatewayv1.Kind)(helpers.GetPointer("Service")), - Name: "foo", - Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("service")), - Port: (*gatewayv1.PortNumber)(helpers.GetPointer[int32](80)), - }, - }, - }, - }, - }, - }, - }, - } - } - - hr1 := createRoute("hr-1", "gateway-1", "listener-80-1") - hr2 := createRoute("hr-2", "wrong-gateway", "listener-80-1") - hr3 := createRoute("hr-3", "gateway-1", "listener-443-1") // https listener; should not conflict with hr1 - cm := &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "configmap", @@ -107,6 +48,7 @@ func TestBuildGraph(t *testing.T) { btpAcceptedConds := []conditions.Condition{ staticConds.NewBackendTLSPolicyAccepted(), staticConds.NewBackendTLSPolicyAccepted(), + staticConds.NewBackendTLSPolicyAccepted(), } btp := BackendTLSPolicy{ @@ -143,23 +85,112 @@ func TestBuildGraph(t *testing.T) { CaCertRef: types.NamespacedName{Namespace: "service", Name: "configmap"}, } - hr1Refs := []BackendRef{ - { - SvcNsName: types.NamespacedName{Namespace: "service", Name: "foo"}, - ServicePort: v1.ServicePort{Port: 80}, - Valid: true, - Weight: 1, - BackendTLSPolicy: &btp, + commonGWBackendRef := gatewayv1.BackendRef{ + BackendObjectReference: gatewayv1.BackendObjectReference{ + Kind: (*gatewayv1.Kind)(helpers.GetPointer("Service")), + Name: "foo", + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("service")), + Port: (*gatewayv1.PortNumber)(helpers.GetPointer[int32](80)), }, } - hr3Refs := []BackendRef{ + createValidRuleWithBackendRefs := func(matches []gatewayv1.HTTPRouteMatch) RouteRule { + refs := []BackendRef{ + { + SvcNsName: types.NamespacedName{Namespace: "service", Name: "foo"}, + ServicePort: v1.ServicePort{Port: 80}, + Valid: true, + Weight: 1, + BackendTLSPolicy: &btp, + }, + } + rbrs := []RouteBackendRef{ + { + BackendRef: commonGWBackendRef, + }, + } + return RouteRule{ + ValidMatches: true, + ValidFilters: true, + BackendRefs: refs, + Matches: matches, + RouteBackendRefs: rbrs, + } + } + + routeMatches := []gatewayv1.HTTPRouteMatch{ { - SvcNsName: types.NamespacedName{Namespace: "service", Name: "foo"}, - ServicePort: v1.ServicePort{Port: 80}, - Valid: true, - Weight: 1, - BackendTLSPolicy: &btp, + Path: &gatewayv1.HTTPPathMatch{ + Type: helpers.GetPointer(gatewayv1.PathMatchPathPrefix), + Value: helpers.GetPointer("/"), + }, + }, + } + + createRoute := func(name string, gatewayName string, listenerName string) *gatewayv1.HTTPRoute { + return &gatewayv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: name, + }, + Spec: gatewayv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayv1.CommonRouteSpec{ + ParentRefs: []gatewayv1.ParentReference{ + { + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), + Name: gatewayv1.ObjectName(gatewayName), + SectionName: (*gatewayv1.SectionName)(helpers.GetPointer(listenerName)), + }, + }, + }, + Hostnames: []gatewayv1.Hostname{ + "foo.example.com", + }, + Rules: []gatewayv1.HTTPRouteRule{ + { + Matches: routeMatches, + BackendRefs: []gatewayv1.HTTPBackendRef{ + { + BackendRef: commonGWBackendRef, + }, + }, + }, + }, + }, + } + } + + hr1 := createRoute("hr-1", "gateway-1", "listener-80-1") + hr2 := createRoute("hr-2", "wrong-gateway", "listener-80-1") + hr3 := createRoute("hr-3", "gateway-1", "listener-443-1") // https listener; should not conflict with hr1 + + gr := &v1alpha2.GRPCRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "gr", + }, + Spec: v1alpha2.GRPCRouteSpec{ + CommonRouteSpec: gatewayv1.CommonRouteSpec{ + ParentRefs: []gatewayv1.ParentReference{ + { + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), + Name: gatewayv1.ObjectName("gateway-1"), + SectionName: (*gatewayv1.SectionName)(helpers.GetPointer("listener-80-1")), + }, + }, + }, + Hostnames: []gatewayv1.Hostname{ + "foo.example.com", + }, + Rules: []v1alpha2.GRPCRouteRule{ + { + BackendRefs: []v1alpha2.GRPCBackendRef{ + { + BackendRef: commonGWBackendRef, + }, + }, + }, + }, }, } @@ -320,6 +351,9 @@ func TestBuildGraph(t *testing.T) { client.ObjectKeyFromObject(hr2): hr2, client.ObjectKeyFromObject(hr3): hr3, }, + GRPCRoutes: map[types.NamespacedName]*v1alpha2.GRPCRoute{ + client.ObjectKeyFromObject(gr): gr, + }, Services: map[types.NamespacedName]*v1.Service{ client.ObjectKeyFromObject(svc): svc, }, @@ -345,38 +379,72 @@ func TestBuildGraph(t *testing.T) { } } - routeHR1 := &Route{ + routeHR1 := &L7Route{ + RouteType: RouteTypeHTTP, Valid: true, Attachable: true, Source: hr1, ParentRefs: []ParentRef{ { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw1), + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw1), + SectionName: hr1.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + Attached: true, + AcceptedHostnames: map[string][]string{"listener-80-1": {"foo.example.com"}}, + }, + }, + }, + Spec: L7RouteSpec{ + Hostnames: hr1.Spec.Hostnames, + Rules: []RouteRule{createValidRuleWithBackendRefs(routeMatches)}, + }, + } + + routeGR := &L7Route{ + RouteType: RouteTypeGRPC, + Valid: true, + Attachable: true, + Source: gr, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw1), + SectionName: gr.Spec.ParentRefs[0].SectionName, Attachment: &ParentRefAttachmentStatus{ Attached: true, AcceptedHostnames: map[string][]string{"listener-80-1": {"foo.example.com"}}, }, }, }, - Rules: []Rule{createValidRuleWithBackendRefs(hr1Refs)}, + Spec: L7RouteSpec{ + Hostnames: gr.Spec.Hostnames, + Rules: []RouteRule{ + createValidRuleWithBackendRefs(routeMatches), + }, + }, } - routeHR3 := &Route{ + routeHR3 := &L7Route{ + RouteType: RouteTypeHTTP, Valid: true, Attachable: true, Source: hr3, ParentRefs: []ParentRef{ { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw1), + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw1), + SectionName: hr3.Spec.ParentRefs[0].SectionName, Attachment: &ParentRefAttachmentStatus{ Attached: true, AcceptedHostnames: map[string][]string{"listener-443-1": {"foo.example.com"}}, }, }, }, - Rules: []Rule{createValidRuleWithBackendRefs(hr3Refs)}, + Spec: L7RouteSpec{ + Hostnames: hr3.Spec.Hostnames, + Rules: []RouteRule{createValidRuleWithBackendRefs(routeMatches)}, + }, } createExpectedGraphWithGatewayClass := func(gc *gatewayv1.GatewayClass) *Graph { @@ -394,20 +462,19 @@ func TestBuildGraph(t *testing.T) { Source: gw1.Spec.Listeners[0], Valid: true, Attachable: true, - Routes: map[types.NamespacedName]*Route{ - {Namespace: "test", Name: "hr-1"}: routeHR1, + Routes: map[RouteKey]*L7Route{ + CreateRouteKey(hr1): routeHR1, + CreateRouteKey(gr): routeGR, }, SupportedKinds: []gatewayv1.RouteGroupKind{{Kind: "HTTPRoute"}}, AllowedRouteLabelSelector: labels.SelectorFromSet(map[string]string{"app": "allowed"}), }, { - Name: "listener-443-1", - Source: gw1.Spec.Listeners[1], - Valid: true, - Attachable: true, - Routes: map[types.NamespacedName]*Route{ - {Namespace: "test", Name: "hr-3"}: routeHR3, - }, + Name: "listener-443-1", + Source: gw1.Spec.Listeners[1], + Valid: true, + Attachable: true, + Routes: map[RouteKey]*L7Route{CreateRouteKey(hr3): routeHR3}, ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secret)), SupportedKinds: []gatewayv1.RouteGroupKind{{Kind: "HTTPRoute"}}, }, @@ -417,9 +484,10 @@ func TestBuildGraph(t *testing.T) { IgnoredGateways: map[types.NamespacedName]*gatewayv1.Gateway{ {Namespace: "test", Name: "gateway-2"}: gw2, }, - Routes: map[types.NamespacedName]*Route{ - {Namespace: "test", Name: "hr-1"}: routeHR1, - {Namespace: "test", Name: "hr-3"}: routeHR3, + Routes: map[RouteKey]*L7Route{ + CreateRouteKey(hr1): routeHR1, + CreateRouteKey(hr3): routeHR3, + CreateRouteKey(gr): routeGR, }, ReferencedSecrets: map[types.NamespacedName]*Secret{ client.ObjectKeyFromObject(secret): { diff --git a/internal/mode/static/state/graph/grpcroute.go b/internal/mode/static/state/graph/grpcroute.go new file mode 100644 index 0000000000..bffb929a9b --- /dev/null +++ b/internal/mode/static/state/graph/grpcroute.go @@ -0,0 +1,236 @@ +package graph + +import ( + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation/field" + v1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" + + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation" +) + +func buildGRPCRoute( + validator validation.HTTPFieldsValidator, + ghr *v1alpha2.GRPCRoute, + gatewayNsNames []types.NamespacedName, +) *L7Route { + r := &L7Route{ + Source: ghr, + RouteType: RouteTypeGRPC, + } + + sectionNameRefs, err := buildSectionNameRefs(ghr.Spec.ParentRefs, ghr.Namespace, gatewayNsNames) + if err != nil { + r.Valid = false + + return r + } + // route doesn't belong to any of the Gateways + if len(sectionNameRefs) == 0 { + return nil + } + r.ParentRefs = sectionNameRefs + + if err := validateHostnames( + ghr.Spec.Hostnames, + field.NewPath("spec").Child("hostnames"), + ); err != nil { + r.Valid = false + r.Conditions = append(r.Conditions, staticConds.NewRouteUnsupportedValue(err.Error())) + + return r + } + + r.Spec.Hostnames = ghr.Spec.Hostnames + + r.Valid = true + r.Attachable = true + + rules, atLeastOneValid, allRulesErrs := processGRPCRouteRules(ghr.Spec.Rules, validator) + + r.Spec.Rules = rules + + if len(allRulesErrs) > 0 { + msg := allRulesErrs.ToAggregate().Error() + + if atLeastOneValid { + r.Conditions = append(r.Conditions, staticConds.NewRoutePartiallyInvalid(msg)) + } else { + msg = "All rules are invalid: " + msg + r.Conditions = append(r.Conditions, staticConds.NewRouteUnsupportedValue(msg)) + + r.Valid = false + } + } + + return r +} + +func processGRPCRouteRules( + specRules []v1alpha2.GRPCRouteRule, + validator validation.HTTPFieldsValidator, +) (rules []RouteRule, atLeastOneValid bool, allRulesErrs field.ErrorList) { + rules = make([]RouteRule, len(specRules)) + validFilters := true + + for i, rule := range specRules { + rulePath := field.NewPath("spec").Child("rules").Index(i) + + var allErrs field.ErrorList + + var matchesErrs field.ErrorList + for j, match := range rule.Matches { + matchPath := rulePath.Child("matches").Index(j) + matchesErrs = append(matchesErrs, validateGRPCMatch(validator, match, matchPath)...) + } + + if len(rule.Filters) > 0 { + filterPath := rulePath.Child("filters") + allErrs = append( + allErrs, + field.NotSupported(filterPath, rule.Filters, []string{"gRPC filters are not yet supported"}), + ) + validFilters = false + } + + backendRefs := make([]RouteBackendRef, 0, len(rule.BackendRefs)) + + // rule.BackendRefs are validated separately because of their special requirements + for _, b := range rule.BackendRefs { + var interfaceFilters []interface{} + if len(b.Filters) > 0 { + interfaceFilters = make([]interface{}, 0, len(b.Filters)) + for i, v := range b.Filters { + interfaceFilters[i] = v + } + } + rbr := RouteBackendRef{ + BackendRef: b.BackendRef, + Filters: interfaceFilters, + } + backendRefs = append(backendRefs, rbr) + } + + allErrs = append(allErrs, matchesErrs...) + allRulesErrs = append(allRulesErrs, allErrs...) + + if len(allErrs) == 0 { + atLeastOneValid = true + } + + rules[i] = RouteRule{ + ValidMatches: len(matchesErrs) == 0, + ValidFilters: validFilters, + Matches: convertGRPCMatches(rule.Matches), + Filters: nil, + RouteBackendRefs: backendRefs, + } + } + return rules, atLeastOneValid, allRulesErrs +} + +func convertGRPCMatches(grpcMatches []v1alpha2.GRPCRouteMatch) []v1.HTTPRouteMatch { + pathValue := "/" + pathType := v1.PathMatchType("PathPrefix") + // If no matches are specified, the implementation MUST match every gRPC request. + if len(grpcMatches) == 0 { + return []v1.HTTPRouteMatch{ + { + Path: &v1.HTTPPathMatch{ + Type: &pathType, + Value: helpers.GetPointer(pathValue), + }, + }, + } + } + + hms := make([]v1.HTTPRouteMatch, 0, len(grpcMatches)) + + for _, gm := range grpcMatches { + var hm v1.HTTPRouteMatch + hmHeaders := make([]v1.HTTPHeaderMatch, 0, len(gm.Headers)) + for _, head := range gm.Headers { + hmHeaders = append(hmHeaders, v1.HTTPHeaderMatch{ + Name: v1.HTTPHeaderName(head.Name), + Value: head.Value, + }) + } + hm.Headers = hmHeaders + + if gm.Method != nil && gm.Method.Service != nil && gm.Method.Method != nil { + // if method match is provided, service and method are required + // as the only method type supported is exact. + // Validation has already been done at this point, and the condition will + // have been added there if required. + pathValue = "/" + *gm.Method.Service + "/" + *gm.Method.Method + pathType = v1.PathMatchType("Exact") + } + hm.Path = &v1.HTTPPathMatch{ + Type: &pathType, + Value: helpers.GetPointer(pathValue), + } + + hms = append(hms, hm) + } + return hms +} + +func validateGRPCMatch( + validator validation.HTTPFieldsValidator, + match v1alpha2.GRPCRouteMatch, + matchPath *field.Path, +) field.ErrorList { + var allErrs field.ErrorList + + methodPath := matchPath.Child("method") + allErrs = append(allErrs, validateGRPCMethodMatch(validator, match.Method, methodPath)...) + + for j, h := range match.Headers { + headerPath := matchPath.Child("headers").Index(j) + allErrs = append(allErrs, validateHeaderMatch(validator, h.Type, string(h.Name), h.Value, headerPath)...) + } + + return allErrs +} + +func validateGRPCMethodMatch( + validator validation.HTTPFieldsValidator, + method *v1alpha2.GRPCMethodMatch, + methodPath *field.Path, +) field.ErrorList { + var allErrs field.ErrorList + + if method != nil { + methodServicePath := methodPath.Child("service") + methodMethodPath := methodPath.Child("method") + if method.Type == nil { + allErrs = append(allErrs, field.Required(methodPath.Child("type"), "cannot be empty")) + } else if *method.Type != v1alpha2.GRPCMethodMatchExact { + allErrs = append( + allErrs, + field.NotSupported(methodPath.Child("type"), *method.Type, []string{string(v1alpha2.GRPCMethodMatchExact)}), + ) + } + if method.Service == nil || *method.Service == "" { + allErrs = append(allErrs, field.Required(methodServicePath, "service is required")) + } else { + pathValue := "/" + *method.Service + if err := validator.ValidatePathInMatch(pathValue); err != nil { + valErr := field.Invalid(methodServicePath, *method.Service, err.Error()) + allErrs = append(allErrs, valErr) + } + } + if method.Method == nil || *method.Method == "" { + allErrs = append(allErrs, field.Required(methodMethodPath, "method is required")) + } else { + pathValue := "/" + *method.Method + if err := validator.ValidatePathInMatch(pathValue); err != nil { + valErr := field.Invalid(methodMethodPath, *method.Method, err.Error()) + allErrs = append(allErrs, valErr) + } + } + } + return allErrs +} diff --git a/internal/mode/static/state/graph/grpcroute_test.go b/internal/mode/static/state/graph/grpcroute_test.go new file mode 100644 index 0000000000..574cbc6e60 --- /dev/null +++ b/internal/mode/static/state/graph/grpcroute_test.go @@ -0,0 +1,586 @@ +package graph + +import ( + "errors" + "testing" + + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + v1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" + + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation/validationfakes" +) + +func createGRPCMethodMatch(serviceName, methodName, methodType string) v1alpha2.GRPCRouteRule { + var mt *v1alpha2.GRPCMethodMatchType + if methodType != "nilType" { + mt = (*v1alpha2.GRPCMethodMatchType)(&methodType) + } + return v1alpha2.GRPCRouteRule{ + Matches: []v1alpha2.GRPCRouteMatch{ + { + Method: &v1alpha2.GRPCMethodMatch{ + Type: mt, + Service: &serviceName, + Method: &methodName, + }, + }, + }, + } +} + +func createGRPCHeadersMatch(headerType, headerName, headerValue string) v1alpha2.GRPCRouteRule { + return v1alpha2.GRPCRouteRule{ + Matches: []v1alpha2.GRPCRouteMatch{ + { + Headers: []v1alpha2.GRPCHeaderMatch{ + { + Type: (*v1.HeaderMatchType)(&headerType), + Name: v1alpha2.GRPCHeaderName(headerName), + Value: headerValue, + }, + }, + }, + }, + } +} + +func createGRPCRoute( + name string, + refName string, + hostname v1.Hostname, + rules []v1alpha2.GRPCRouteRule, +) *v1alpha2.GRPCRoute { + return &v1alpha2.GRPCRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: name, + }, + Spec: v1alpha2.GRPCRouteSpec{ + CommonRouteSpec: v1.CommonRouteSpec{ + ParentRefs: []v1.ParentReference{ + { + Namespace: helpers.GetPointer[v1.Namespace]("test"), + Name: v1.ObjectName(refName), + SectionName: helpers.GetPointer[v1.SectionName](v1.SectionName(sectionNameOfCreateHTTPRoute)), + }, + }, + }, + Hostnames: []v1.Hostname{hostname}, + Rules: rules, + }, + } +} + +func TestBuildGRPCRoutes(t *testing.T) { + gwNsName := types.NamespacedName{Namespace: "test", Name: "gateway"} + + gr := createGRPCRoute("gr-1", gwNsName.Name, "example.com", []v1alpha2.GRPCRouteRule{}) + + grWrongGateway := createGRPCRoute("gr-2", "some-gateway", "example.com", []v1alpha2.GRPCRouteRule{}) + + grRoutes := map[types.NamespacedName]*v1alpha2.GRPCRoute{ + client.ObjectKeyFromObject(gr): gr, + client.ObjectKeyFromObject(grWrongGateway): grWrongGateway, + } + + tests := []struct { + expected map[RouteKey]*L7Route + name string + gwNsNames []types.NamespacedName + }{ + { + gwNsNames: []types.NamespacedName{gwNsName}, + expected: map[RouteKey]*L7Route{ + CreateRouteKey(gr): { + RouteType: RouteTypeGRPC, + Source: gr, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: gwNsName, + SectionName: gr.Spec.ParentRefs[0].SectionName, + }, + }, + Valid: true, + Attachable: true, + Spec: L7RouteSpec{ + Hostnames: gr.Spec.Hostnames, + Rules: []RouteRule{}, + }, + }, + }, + name: "normal case", + }, + { + gwNsNames: []types.NamespacedName{}, + expected: nil, + name: "no gateways", + }, + } + + validator := &validationfakes.FakeHTTPFieldsValidator{} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + routes := buildRoutesForGateways(validator, map[types.NamespacedName]*v1.HTTPRoute{}, grRoutes, test.gwNsNames) + g.Expect(helpers.Diff(test.expected, routes)).To(BeEmpty()) + }) + } +} + +func TestBuildGRPCRoute(t *testing.T) { + gatewayNsName := types.NamespacedName{Namespace: "test", Name: "gateway"} + + methodMatchRule := createGRPCMethodMatch("myService", "myMethod", "Exact") + headersMatchRule := createGRPCHeadersMatch("Exact", "MyHeader", "SomeValue") + + methodMatchEmptyFields := createGRPCMethodMatch("", "", "") + methodMatchInvalidFields := createGRPCMethodMatch("service{}", "method{}", "Exact") + methodMatchNilType := createGRPCMethodMatch("myService", "myMethod", "nilType") + headersMatchInvalid := createGRPCHeadersMatch("", "MyHeader", "SomeValue") + + grBoth := createGRPCRoute( + "gr-1", + gatewayNsName.Name, + "example.com", + []v1alpha2.GRPCRouteRule{methodMatchRule, headersMatchRule}, + ) + + backendRef := v1.BackendRef{ + BackendObjectReference: v1.BackendObjectReference{ + Kind: helpers.GetPointer[v1.Kind]("Service"), + Name: "service1", + Namespace: helpers.GetPointer[v1.Namespace]("test"), + Port: helpers.GetPointer[v1.PortNumber](80), + }, + } + + grpcBackendRef := v1alpha2.GRPCBackendRef{ + BackendRef: backendRef, + } + + grEmptyMatch := createGRPCRoute( + "gr-1", + gatewayNsName.Name, + "example.com", + []v1alpha2.GRPCRouteRule{{BackendRefs: []v1alpha2.GRPCBackendRef{grpcBackendRef}}}, + ) + + grInvalidHostname := createGRPCRoute("gr-1", gatewayNsName.Name, "", []v1alpha2.GRPCRouteRule{methodMatchRule}) + grNotNGF := createGRPCRoute("gr", "some-gateway", "example.com", []v1alpha2.GRPCRouteRule{methodMatchRule}) + + grInvalidMatchesEmptyMethodFields := createGRPCRoute( + "gr-1", + gatewayNsName.Name, + "example.com", + []v1alpha2.GRPCRouteRule{methodMatchEmptyFields}, + ) + grInvalidMatchesInvalidMethodFields := createGRPCRoute( + "gr-1", + gatewayNsName.Name, + "example.com", + []v1alpha2.GRPCRouteRule{methodMatchInvalidFields}, + ) + grInvalidMatchesNilMethodType := createGRPCRoute( + "gr-1", + gatewayNsName.Name, + "example.com", + []v1alpha2.GRPCRouteRule{methodMatchNilType}, + ) + grInvalidHeadersEmptyType := createGRPCRoute( + "gr-1", + gatewayNsName.Name, + "example.com", + []v1alpha2.GRPCRouteRule{headersMatchInvalid}, + ) + grOneInvalid := createGRPCRoute( + "gr-1", + gatewayNsName.Name, + "example.com", + []v1alpha2.GRPCRouteRule{methodMatchRule, headersMatchInvalid}, + ) + + grDuplicateSectionName := createGRPCRoute( + "gr", + gatewayNsName.Name, + "example.com", + []v1alpha2.GRPCRouteRule{methodMatchRule}, + ) + grDuplicateSectionName.Spec.ParentRefs = append( + grDuplicateSectionName.Spec.ParentRefs, + grDuplicateSectionName.Spec.ParentRefs[0], + ) + + grInvalidFilterRule := createGRPCMethodMatch("myService", "myMethod", "Exact") + + grInvalidFilterRule.Filters = []v1alpha2.GRPCRouteFilter{ + { + Type: "RequestHeaderModifier", + }, + } + + grInvalidFilter := createGRPCRoute( + "gr", + gatewayNsName.Name, + "example.com", + []v1alpha2.GRPCRouteRule{grInvalidFilterRule}, + ) + + createAllValidValidator := func() *validationfakes.FakeHTTPFieldsValidator { + v := &validationfakes.FakeHTTPFieldsValidator{} + v.ValidateMethodInMatchReturns(true, nil) + return v + } + + tests := []struct { + validator *validationfakes.FakeHTTPFieldsValidator + gr *v1alpha2.GRPCRoute + expected *L7Route + name string + }{ + { + validator: createAllValidValidator(), + gr: grBoth, + expected: &L7Route{ + RouteType: RouteTypeGRPC, + Source: grBoth, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: gatewayNsName, + SectionName: grBoth.Spec.ParentRefs[0].SectionName, + }, + }, + Valid: true, + Attachable: true, + Spec: L7RouteSpec{ + Hostnames: grBoth.Spec.Hostnames, + Rules: []RouteRule{ + { + ValidMatches: true, + ValidFilters: true, + Matches: convertGRPCMatches(grBoth.Spec.Rules[0].Matches), + RouteBackendRefs: []RouteBackendRef{}, + }, + { + ValidMatches: true, + ValidFilters: true, + Matches: convertGRPCMatches(grBoth.Spec.Rules[1].Matches), + RouteBackendRefs: []RouteBackendRef{}, + }, + }, + }, + }, + name: "normal case with both", + }, + { + validator: createAllValidValidator(), + gr: grEmptyMatch, + expected: &L7Route{ + RouteType: RouteTypeGRPC, + Source: grEmptyMatch, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: gatewayNsName, + SectionName: grEmptyMatch.Spec.ParentRefs[0].SectionName, + }, + }, + Valid: true, + Attachable: true, + Spec: L7RouteSpec{ + Hostnames: grEmptyMatch.Spec.Hostnames, + Rules: []RouteRule{ + { + ValidMatches: true, + ValidFilters: true, + Matches: convertGRPCMatches(grEmptyMatch.Spec.Rules[0].Matches), + RouteBackendRefs: []RouteBackendRef{{BackendRef: backendRef}}, + }, + }, + }, + }, + name: "valid rule with empty match", + }, + { + validator: createAllValidValidator(), + gr: grInvalidMatchesEmptyMethodFields, + expected: &L7Route{ + RouteType: RouteTypeGRPC, + Source: grInvalidMatchesEmptyMethodFields, + Valid: false, + Attachable: true, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: gatewayNsName, + SectionName: grInvalidMatchesEmptyMethodFields.Spec.ParentRefs[0].SectionName, + }, + }, + Conditions: []conditions.Condition{ + staticConds.NewRouteUnsupportedValue( + `All rules are invalid: ` + + `[spec.rules[0].matches[0].method.type: Unsupported value: "": supported values: "Exact",` + + ` spec.rules[0].matches[0].method.service: Required value: service is required,` + + ` spec.rules[0].matches[0].method.method: Required value: method is required]`, + ), + }, + Spec: L7RouteSpec{ + Hostnames: grInvalidMatchesEmptyMethodFields.Spec.Hostnames, + Rules: []RouteRule{ + { + ValidMatches: false, + ValidFilters: true, + Matches: convertGRPCMatches(grInvalidMatchesEmptyMethodFields.Spec.Rules[0].Matches), + RouteBackendRefs: []RouteBackendRef{}, + }, + }, + }, + }, + name: "invalid matches with empty method fields", + }, + { + validator: func() *validationfakes.FakeHTTPFieldsValidator { + validator := createAllValidValidator() + validator.ValidatePathInMatchReturns(errors.New("invalid path value")) + return validator + }(), + gr: grInvalidMatchesInvalidMethodFields, + expected: &L7Route{ + RouteType: RouteTypeGRPC, + Source: grInvalidMatchesInvalidMethodFields, + Valid: false, + Attachable: true, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: gatewayNsName, + SectionName: grInvalidMatchesInvalidMethodFields.Spec.ParentRefs[0].SectionName, + }, + }, + Conditions: []conditions.Condition{ + staticConds.NewRouteUnsupportedValue( + `All rules are invalid: ` + + `[spec.rules[0].matches[0].method.service: Invalid value: "service{}": invalid path value,` + + ` spec.rules[0].matches[0].method.method: Invalid value: "method{}": invalid path value]`, + ), + }, + Spec: L7RouteSpec{ + Hostnames: grInvalidMatchesInvalidMethodFields.Spec.Hostnames, + Rules: []RouteRule{ + { + ValidMatches: false, + ValidFilters: true, + Matches: convertGRPCMatches(grInvalidMatchesInvalidMethodFields.Spec.Rules[0].Matches), + RouteBackendRefs: []RouteBackendRef{}, + }, + }, + }, + }, + name: "invalid matches with invalid method fields", + }, + { + validator: createAllValidValidator(), + gr: grDuplicateSectionName, + expected: &L7Route{ + RouteType: RouteTypeGRPC, + Source: grDuplicateSectionName, + }, + name: "invalid route with duplicate sectionName", + }, + { + validator: createAllValidValidator(), + gr: grOneInvalid, + expected: &L7Route{ + Source: grOneInvalid, + RouteType: RouteTypeGRPC, + Valid: true, + Attachable: true, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: gatewayNsName, + SectionName: grOneInvalid.Spec.ParentRefs[0].SectionName, + }, + }, + Conditions: []conditions.Condition{ + staticConds.NewRoutePartiallyInvalid( + `spec.rules[1].matches[0].headers[0].type: Unsupported value: "": supported values: "Exact"`, + ), + }, + Spec: L7RouteSpec{ + Hostnames: grOneInvalid.Spec.Hostnames, + Rules: []RouteRule{ + { + ValidMatches: true, + ValidFilters: true, + Matches: convertGRPCMatches(grOneInvalid.Spec.Rules[0].Matches), + RouteBackendRefs: []RouteBackendRef{}, + }, + { + ValidMatches: false, + ValidFilters: true, + Matches: convertGRPCMatches(grOneInvalid.Spec.Rules[1].Matches), + RouteBackendRefs: []RouteBackendRef{}, + }, + }, + }, + }, + name: "invalid headers and valid method", + }, + { + validator: createAllValidValidator(), + gr: grInvalidHeadersEmptyType, + expected: &L7Route{ + Source: grInvalidHeadersEmptyType, + RouteType: RouteTypeGRPC, + Valid: false, + Attachable: true, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: gatewayNsName, + SectionName: grInvalidHeadersEmptyType.Spec.ParentRefs[0].SectionName, + }, + }, + Conditions: []conditions.Condition{ + staticConds.NewRouteUnsupportedValue( + `All rules are invalid: spec.rules[0].matches[0].headers[0].type: ` + + `Unsupported value: "": supported values: "Exact"`, + ), + }, + Spec: L7RouteSpec{ + Hostnames: grInvalidHeadersEmptyType.Spec.Hostnames, + Rules: []RouteRule{ + { + ValidMatches: false, + ValidFilters: true, + Matches: convertGRPCMatches(grInvalidHeadersEmptyType.Spec.Rules[0].Matches), + RouteBackendRefs: []RouteBackendRef{}, + }, + }, + }, + }, + name: "invalid headers with empty type", + }, + { + validator: createAllValidValidator(), + gr: grInvalidMatchesNilMethodType, + expected: &L7Route{ + Source: grInvalidMatchesNilMethodType, + RouteType: RouteTypeGRPC, + Valid: false, + Attachable: true, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: gatewayNsName, + SectionName: grInvalidMatchesNilMethodType.Spec.ParentRefs[0].SectionName, + }, + }, + Conditions: []conditions.Condition{ + staticConds.NewRouteUnsupportedValue( + `All rules are invalid: spec.rules[0].matches[0].method.type: Required value: cannot be empty`, + ), + }, + Spec: L7RouteSpec{ + Hostnames: grInvalidMatchesNilMethodType.Spec.Hostnames, + Rules: []RouteRule{ + { + ValidMatches: false, + ValidFilters: true, + Matches: convertGRPCMatches(grInvalidMatchesNilMethodType.Spec.Rules[0].Matches), + RouteBackendRefs: []RouteBackendRef{}, + }, + }, + }, + }, + name: "invalid method with nil type", + }, + { + validator: createAllValidValidator(), + gr: grInvalidFilter, + expected: &L7Route{ + Source: grInvalidFilter, + RouteType: RouteTypeGRPC, + Valid: false, + Attachable: true, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: gatewayNsName, + SectionName: grInvalidFilter.Spec.ParentRefs[0].SectionName, + }, + }, + Conditions: []conditions.Condition{ + staticConds.NewRouteUnsupportedValue( + `All rules are invalid: spec.rules[0].filters: Unsupported value: []v1alpha2.GRPCRouteFilter{v1alpha2.` + + `GRPCRouteFilter{Type:"RequestHeaderModifier", RequestHeaderModifier:(*v1.HTTPHeaderFilter)(nil), ` + + `ResponseHeaderModifier:(*v1.HTTPHeaderFilter)(nil), RequestMirror:(*v1.HTTPRequestMirrorFilter)(nil), ` + + `ExtensionRef:(*v1.LocalObjectReference)(nil)}}: supported values: "gRPC filters are not yet supported"`, + ), + }, + Spec: L7RouteSpec{ + Hostnames: grInvalidFilter.Spec.Hostnames, + Rules: []RouteRule{ + { + ValidMatches: true, + ValidFilters: false, + Matches: convertGRPCMatches(grInvalidFilter.Spec.Rules[0].Matches), + RouteBackendRefs: []RouteBackendRef{}, + }, + }, + }, + }, + name: "invalid filter", + }, + { + validator: createAllValidValidator(), + gr: grNotNGF, + expected: nil, + name: "not NGF route", + }, + { + validator: createAllValidValidator(), + gr: grInvalidHostname, + expected: &L7Route{ + Source: grInvalidHostname, + RouteType: RouteTypeGRPC, + Valid: false, + Attachable: false, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: gatewayNsName, + SectionName: grInvalidHostname.Spec.ParentRefs[0].SectionName, + }, + }, + Conditions: []conditions.Condition{ + staticConds.NewRouteUnsupportedValue( + `spec.hostnames[0]: Invalid value: "": cannot be empty string`, + ), + }, + }, + name: "invalid hostname", + }, + } + + gatewayNsNames := []types.NamespacedName{gatewayNsName} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + route := buildGRPCRoute(test.validator, test.gr, gatewayNsNames) + g.Expect(helpers.Diff(test.expected, route)).To(BeEmpty()) + }) + } +} diff --git a/internal/mode/static/state/graph/httproute.go b/internal/mode/static/state/graph/httproute.go index d16e833f00..5a61d92185 100644 --- a/internal/mode/static/state/graph/httproute.go +++ b/internal/mode/static/state/graph/httproute.go @@ -4,174 +4,24 @@ import ( "fmt" "strings" - apiv1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation/field" - "sigs.k8s.io/controller-runtime/pkg/client" v1 "sigs.k8s.io/gateway-api/apis/v1" - "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation" ) -const wildcardHostname = "~^" - -// Rule represents a rule of an HTTPRoute. -type Rule struct { - // BackendRefs is a list of BackendRefs for the rule. - BackendRefs []BackendRef - // ValidMatches indicates whether the matches of the rule are valid. - // If the matches are invalid, NGF should not generate any configuration for the rule. - ValidMatches bool - // ValidFilters indicates whether the filters of the rule are valid. - // If the filters are invalid, the data-plane should return 500 error provided that the matches are valid. - ValidFilters bool -} - -// ParentRef describes a reference to a parent in an HTTPRoute. -type ParentRef struct { - // Attachment is the attachment status of the ParentRef. It could be nil. In that case, NGF didn't attempt to - // attach because of problems with the Route. - Attachment *ParentRefAttachmentStatus - // Gateway is the NamespacedName of the referenced Gateway - Gateway types.NamespacedName - // Idx is the index of the corresponding ParentReference in the HTTPRoute. - Idx int -} - -// ParentRefAttachmentStatus describes the attachment status of a ParentRef. -type ParentRefAttachmentStatus struct { - // AcceptedHostnames is an intersection between the hostnames supported by an attached Listener - // and the hostnames from this Route. Key is listener name, value is list of hostnames. - AcceptedHostnames map[string][]string - // FailedCondition is the condition that describes why the ParentRef is not attached to the Gateway. It is set - // when Attached is false. - FailedCondition conditions.Condition - // Attached indicates if the ParentRef is attached to the Gateway. - Attached bool -} - -// Route represents an HTTPRoute. -type Route struct { - // Source is the source resource of the Route. - Source *v1.HTTPRoute - // ParentRefs includes ParentRefs with NGF Gateways only. - ParentRefs []ParentRef - // Conditions include Conditions for the HTTPRoute. - Conditions []conditions.Condition - // Rules include Rules for the HTTPRoute. Each Rule[i] corresponds to the ith HTTPRouteRule. - // If the Route is invalid, this field is nil - Rules []Rule - // Valid tells if the Route is valid. - // If it is invalid, NGF should not generate any configuration for it. - Valid bool - // Attachable tells if the Route can be attached to any of the Gateways. - // Route can be invalid but still attachable. - Attachable bool -} - -// buildRoutesForGateways builds routes from HTTPRoutes that reference any of the specified Gateways. -func buildRoutesForGateways( - validator validation.HTTPFieldsValidator, - httpRoutes map[types.NamespacedName]*v1.HTTPRoute, - gatewayNsNames []types.NamespacedName, -) map[types.NamespacedName]*Route { - if len(gatewayNsNames) == 0 { - return nil - } - - routes := make(map[types.NamespacedName]*Route) - - for _, ghr := range httpRoutes { - r := buildRoute(validator, ghr, gatewayNsNames) - if r != nil { - routes[client.ObjectKeyFromObject(ghr)] = r - } - } - - return routes -} - -func buildSectionNameRefs( - parentRefs []v1.ParentReference, - routeNamespace string, - gatewayNsNames []types.NamespacedName, -) ([]ParentRef, error) { - sectionNameRefs := make([]ParentRef, 0, len(parentRefs)) - - type key struct { - gwNsName types.NamespacedName - sectionName string - } - uniqueSectionsPerGateway := make(map[key]struct{}) - - for i, p := range parentRefs { - gw, found := findGatewayForParentRef(p, routeNamespace, gatewayNsNames) - if !found { - continue - } - - var sectionName string - if p.SectionName != nil { - sectionName = string(*p.SectionName) - } - - k := key{ - gwNsName: gw, - sectionName: sectionName, - } - - if _, exist := uniqueSectionsPerGateway[k]; exist { - return nil, fmt.Errorf("duplicate section name %q for Gateway %s", sectionName, gw.String()) - } - uniqueSectionsPerGateway[k] = struct{}{} - - sectionNameRefs = append(sectionNameRefs, ParentRef{ - Idx: i, - Gateway: gw, - }) - } - - return sectionNameRefs, nil -} - -func findGatewayForParentRef( - ref v1.ParentReference, - routeNamespace string, - gatewayNsNames []types.NamespacedName, -) (gwNsName types.NamespacedName, found bool) { - if ref.Kind != nil && *ref.Kind != "Gateway" { - return types.NamespacedName{}, false - } - if ref.Group != nil && *ref.Group != v1.GroupName { - return types.NamespacedName{}, false - } - - // if the namespace is missing, assume the namespace of the HTTPRoute - ns := routeNamespace - if ref.Namespace != nil { - ns = string(*ref.Namespace) - } - - for _, gw := range gatewayNsNames { - if gw.Namespace == ns && gw.Name == string(ref.Name) { - return gw, true - } - } - - return types.NamespacedName{}, false -} - -func buildRoute( +func buildHTTPRoute( validator validation.HTTPFieldsValidator, ghr *v1.HTTPRoute, gatewayNsNames []types.NamespacedName, -) *Route { - r := &Route{ - Source: ghr, +) *L7Route { + r := &L7Route{ + Source: ghr, + RouteType: RouteTypeHTTP, } + sectionNameRefs, err := buildSectionNameRefs(ghr.Spec.ParentRefs, ghr.Namespace, gatewayNsNames) if err != nil { r.Valid = false @@ -194,45 +44,14 @@ func buildRoute( return r } + r.Spec.Hostnames = ghr.Spec.Hostnames + r.Valid = true r.Attachable = true - r.Rules = make([]Rule, len(ghr.Spec.Rules)) - - atLeastOneValid := false - var allRulesErrs field.ErrorList - - for i, rule := range ghr.Spec.Rules { - rulePath := field.NewPath("spec").Child("rules").Index(i) - - var matchesErrs field.ErrorList - for j, match := range rule.Matches { - matchPath := rulePath.Child("matches").Index(j) - matchesErrs = append(matchesErrs, validateMatch(validator, match, matchPath)...) - } - - var filtersErrs field.ErrorList - for j, filter := range rule.Filters { - filterPath := rulePath.Child("filters").Index(j) - filtersErrs = append(filtersErrs, validateFilter(validator, filter, filterPath)...) - } - - // rule.BackendRefs are validated separately because of their special requirements + rules, atLeastOneValid, allRulesErrs := processHTTPRouteRules(ghr.Spec.Rules, validator) - var allErrs field.ErrorList - allErrs = append(allErrs, matchesErrs...) - allErrs = append(allErrs, filtersErrs...) - allRulesErrs = append(allRulesErrs, allErrs...) - - if len(allErrs) == 0 { - atLeastOneValid = true - } - - r.Rules[i] = Rule{ - ValidMatches: len(matchesErrs) == 0, - ValidFilters: len(filtersErrs) == 0, - } - } + r.Spec.Rules = rules if len(allRulesErrs) > 0 { msg := allRulesErrs.ToAggregate().Error() @@ -250,307 +69,63 @@ func buildRoute( return r } -func bindRoutesToListeners( - routes map[types.NamespacedName]*Route, - gw *Gateway, - namespaces map[types.NamespacedName]*apiv1.Namespace, -) { - if gw == nil { - return - } - - for _, r := range routes { - bindRouteToListeners(r, gw, namespaces) - } -} - -func bindRouteToListeners(r *Route, gw *Gateway, namespaces map[types.NamespacedName]*apiv1.Namespace) { - if !r.Attachable { - return - } - - for i := 0; i < len(r.ParentRefs); i++ { - attachment := &ParentRefAttachmentStatus{ - AcceptedHostnames: make(map[string][]string), - } - ref := &r.ParentRefs[i] - ref.Attachment = attachment - - routeRef := r.Source.Spec.ParentRefs[ref.Idx] - - path := field.NewPath("spec").Child("parentRefs").Index(ref.Idx) - - attachableListeners, listenerExists := findAttachableListeners( - getSectionName(routeRef.SectionName), - gw.Listeners, - ) - - // Case 1: Attachment is not possible because the specified SectionName does not match any Listeners in the - // Gateway. - if !listenerExists { - attachment.FailedCondition = staticConds.NewRouteNoMatchingParent() - continue - } - - // Case 2: Attachment is not possible due to unsupported configuration - - if routeRef.Port != nil { - valErr := field.Forbidden(path.Child("port"), "cannot be set") - attachment.FailedCondition = staticConds.NewRouteUnsupportedValue(valErr.Error()) - continue - } - - // Case 3: the parentRef references an ignored Gateway resource. - - referencesWinningGw := ref.Gateway.Namespace == gw.Source.Namespace && ref.Gateway.Name == gw.Source.Name - - if !referencesWinningGw { - attachment.FailedCondition = staticConds.NewTODO("Gateway is ignored") - continue - } - - // Case 4: Attachment is not possible because Gateway is invalid - - if !gw.Valid { - attachment.FailedCondition = staticConds.NewRouteInvalidGateway() - continue - } - - // Case 5 - winning Gateway - - // Try to attach Route to all matching listeners - - cond, attached := tryToAttachRouteToListeners(ref.Attachment, attachableListeners, r, gw, namespaces) - if !attached { - attachment.FailedCondition = cond - continue - } - if cond != (conditions.Condition{}) { - r.Conditions = append(r.Conditions, cond) - } +func processHTTPRouteRules( + specRules []v1.HTTPRouteRule, + validator validation.HTTPFieldsValidator, +) (rules []RouteRule, atLeastOneValid bool, allRulesErrs field.ErrorList) { + rules = make([]RouteRule, len(specRules)) - attachment.Attached = true - } -} + for i, rule := range specRules { + rulePath := field.NewPath("spec").Child("rules").Index(i) -// tryToAttachRouteToListeners tries to attach the route to the listeners that match the parentRef and the hostnames. -// There are two cases: -// (1) If it succeeds in attaching at least one listener it will return true. The returned condition will be empty if -// at least one of the listeners is valid. Otherwise, it will return the failure condition. -// (2) If it fails to attach the route, it will return false and the failure condition. -func tryToAttachRouteToListeners( - refStatus *ParentRefAttachmentStatus, - attachableListeners []*Listener, - route *Route, - gw *Gateway, - namespaces map[types.NamespacedName]*apiv1.Namespace, -) (conditions.Condition, bool) { - if len(attachableListeners) == 0 { - return staticConds.NewRouteInvalidListener(), false - } - - bind := func(l *Listener) (allowed, attached bool) { - if !routeAllowedByListener(l, route.Source.Namespace, gw.Source.Namespace, namespaces) { - return false, false + var matchesErrs field.ErrorList + for j, match := range rule.Matches { + matchPath := rulePath.Child("matches").Index(j) + matchesErrs = append(matchesErrs, validateMatch(validator, match, matchPath)...) } - hostnames := findAcceptedHostnames(l.Source.Hostname, route.Source.Spec.Hostnames) - if len(hostnames) == 0 { - return true, false + var filtersErrs field.ErrorList + for j, filter := range rule.Filters { + filterPath := rulePath.Child("filters").Index(j) + filtersErrs = append(filtersErrs, validateFilter(validator, filter, filterPath)...) } - refStatus.AcceptedHostnames[string(l.Source.Name)] = hostnames - l.Routes[client.ObjectKeyFromObject(route.Source)] = route - - return true, true - } - - var attachedToAtLeastOneValidListener bool - - var allowed, attached bool - for _, l := range attachableListeners { - routeAllowed, routeAttached := bind(l) - allowed = allowed || routeAllowed - attached = attached || routeAttached - attachedToAtLeastOneValidListener = attachedToAtLeastOneValidListener || (routeAttached && l.Valid) - } + var allErrs field.ErrorList + allErrs = append(allErrs, matchesErrs...) + allErrs = append(allErrs, filtersErrs...) + allRulesErrs = append(allRulesErrs, allErrs...) - if !attached { - if !allowed { - return staticConds.NewRouteNotAllowedByListeners(), false + if len(allErrs) == 0 { + atLeastOneValid = true } - return staticConds.NewRouteNoMatchingListenerHostname(), false - } - - if !attachedToAtLeastOneValidListener { - return staticConds.NewRouteInvalidListener(), true - } - return conditions.Condition{}, true -} + backendRefs := make([]RouteBackendRef, 0, len(rule.BackendRefs)) -// findAttachableListeners returns a list of attachable listeners and whether the listener exists for a non-empty -// sectionName. -func findAttachableListeners(sectionName string, listeners []*Listener) ([]*Listener, bool) { - if sectionName != "" { - for _, l := range listeners { - if l.Name == sectionName { - if l.Attachable { - return []*Listener{l}, true + // rule.BackendRefs are validated separately because of their special requirements + for _, b := range rule.BackendRefs { + var interfaceFilters []interface{} + if len(b.Filters) > 0 { + interfaceFilters = make([]interface{}, 0, len(b.Filters)) + for i, v := range b.Filters { + interfaceFilters[i] = v } - return nil, true } - } - return nil, false - } - - attachableListeners := make([]*Listener, 0, len(listeners)) - for _, l := range listeners { - if !l.Attachable { - continue - } - - attachableListeners = append(attachableListeners, l) - } - - return attachableListeners, true -} - -func findAcceptedHostnames(listenerHostname *v1.Hostname, routeHostnames []v1.Hostname) []string { - hostname := getHostname(listenerHostname) - - if len(routeHostnames) == 0 { - if hostname == "" { - return []string{wildcardHostname} - } - return []string{hostname} - } - - var result []string - - for _, h := range routeHostnames { - routeHost := string(h) - if match(hostname, routeHost) { - result = append(result, GetMoreSpecificHostname(hostname, routeHost)) - } - } - - return result -} - -func match(listenerHost, routeHost string) bool { - if listenerHost == "" { - return true - } - - if routeHost == listenerHost { - return true - } - - wildcardMatch := func(host1, host2 string) bool { - return strings.HasPrefix(host1, "*.") && strings.HasSuffix(host2, strings.TrimPrefix(host1, "*")) - } - - // check if listenerHost is a wildcard and routeHost matches - if wildcardMatch(listenerHost, routeHost) { - return true - } - - // check if routeHost is a wildcard and listener matchess - return wildcardMatch(routeHost, listenerHost) -} - -// GetMoreSpecificHostname returns the more specific hostname between the two inputs. -// -// This function assumes that the two hostnames match each other, either: -// - Exactly -// - One as a substring of the other -func GetMoreSpecificHostname(hostname1, hostname2 string) string { - if hostname1 == hostname2 { - return hostname1 - } - if hostname1 == "" { - return hostname2 - } - if hostname2 == "" { - return hostname1 - } - - // Compare if wildcards are present - if strings.HasPrefix(hostname1, "*.") { - if strings.HasPrefix(hostname2, "*.") { - subdomains1 := strings.Split(hostname1, ".") - subdomains2 := strings.Split(hostname2, ".") - - // Compare number of subdomains - if len(subdomains1) > len(subdomains2) { - return hostname1 + rbr := RouteBackendRef{ + BackendRef: b.BackendRef, + Filters: interfaceFilters, } - - return hostname2 + backendRefs = append(backendRefs, rbr) } - return hostname2 - } - if strings.HasPrefix(hostname2, "*.") { - return hostname1 - } - - return "" -} - -func routeAllowedByListener( - listener *Listener, - routeNS, - gwNS string, - namespaces map[types.NamespacedName]*apiv1.Namespace, -) bool { - if listener.Source.AllowedRoutes != nil { - switch *listener.Source.AllowedRoutes.Namespaces.From { - case v1.NamespacesFromAll: - return true - case v1.NamespacesFromSame: - return routeNS == gwNS - case v1.NamespacesFromSelector: - if listener.AllowedRouteLabelSelector == nil { - return false - } - - ns, exists := namespaces[types.NamespacedName{Name: routeNS}] - if !exists { - panic(fmt.Errorf("route namespace %q not found in map", routeNS)) - } - return listener.AllowedRouteLabelSelector.Matches(labels.Set(ns.Labels)) + rules[i] = RouteRule{ + ValidMatches: len(matchesErrs) == 0, + ValidFilters: len(filtersErrs) == 0, + Matches: rule.Matches, + Filters: rule.Filters, + RouteBackendRefs: backendRefs, } } - return true -} - -func getHostname(h *v1.Hostname) string { - if h == nil { - return "" - } - return string(*h) -} - -func getSectionName(s *v1.SectionName) string { - if s == nil { - return "" - } - return string(*s) -} - -func validateHostnames(hostnames []v1.Hostname, path *field.Path) error { - var allErrs field.ErrorList - - for i := range hostnames { - if err := validateHostname(string(hostnames[i])); err != nil { - allErrs = append(allErrs, field.Invalid(path.Index(i), hostnames[i], err.Error())) - continue - } - } - - return allErrs.ToAggregate() + return rules, atLeastOneValid, allRulesErrs } func validateMatch( @@ -565,7 +140,7 @@ func validateMatch( for j, h := range match.Headers { headerPath := matchPath.Child("headers").Index(j) - allErrs = append(allErrs, validateHeaderMatch(validator, h, headerPath)...) + allErrs = append(allErrs, validateHeaderMatch(validator, h.Type, string(h.Name), h.Value, headerPath)...) } for j, q := range match.QueryParams { @@ -627,37 +202,6 @@ func validateQueryParamMatch( return allErrs } -func validateHeaderMatch( - validator validation.HTTPFieldsValidator, - header v1.HTTPHeaderMatch, - headerPath *field.Path, -) field.ErrorList { - var allErrs field.ErrorList - - if header.Type == nil { - allErrs = append(allErrs, field.Required(headerPath.Child("type"), "cannot be empty")) - } else if *header.Type != v1.HeaderMatchExact { - valErr := field.NotSupported( - headerPath.Child("type"), - *header.Type, - []string{string(v1.HeaderMatchExact)}, - ) - allErrs = append(allErrs, valErr) - } - - if err := validator.ValidateHeaderNameInMatch(string(header.Name)); err != nil { - valErr := field.Invalid(headerPath.Child("name"), header.Name, err.Error()) - allErrs = append(allErrs, valErr) - } - - if err := validator.ValidateHeaderValueInMatch(header.Value); err != nil { - valErr := field.Invalid(headerPath.Child("value"), header.Value, err.Error()) - allErrs = append(allErrs, valErr) - } - - return allErrs -} - func validatePathMatch( validator validation.HTTPFieldsValidator, path *v1.HTTPPathMatch, diff --git a/internal/mode/static/state/graph/httproute_test.go b/internal/mode/static/state/graph/httproute_test.go index d7580a37e2..36bddc430c 100644 --- a/internal/mode/static/state/graph/httproute_test.go +++ b/internal/mode/static/state/graph/httproute_test.go @@ -5,13 +5,12 @@ import ( "testing" . "github.com/onsi/gomega" - v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation/field" "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" @@ -88,10 +87,11 @@ func addFilterToPath(hr *gatewayv1.HTTPRoute, path string, filter gatewayv1.HTTP } } -func TestBuildRoutes(t *testing.T) { +func TestBuildHTTPRoutes(t *testing.T) { gwNsName := types.NamespacedName{Namespace: "test", Name: "gateway"} hr := createHTTPRoute("hr-1", gwNsName.Name, "example.com", "/") + hrWrongGateway := createHTTPRoute("hr-2", "some-gateway", "example.com", "/") hrRoutes := map[types.NamespacedName]*gatewayv1.HTTPRoute{ @@ -100,27 +100,34 @@ func TestBuildRoutes(t *testing.T) { } tests := []struct { - expected map[types.NamespacedName]*Route + expected map[RouteKey]*L7Route name string gwNsNames []types.NamespacedName }{ { gwNsNames: []types.NamespacedName{gwNsName}, - expected: map[types.NamespacedName]*Route{ - client.ObjectKeyFromObject(hr): { - Source: hr, + expected: map[RouteKey]*L7Route{ + CreateRouteKey(hr): { + Source: hr, + RouteType: RouteTypeHTTP, ParentRefs: []ParentRef{ { - Idx: 0, - Gateway: gwNsName, + Idx: 0, + Gateway: gwNsName, + SectionName: hr.Spec.ParentRefs[0].SectionName, }, }, Valid: true, Attachable: true, - Rules: []Rule{ - { - ValidMatches: true, - ValidFilters: true, + Spec: L7RouteSpec{ + Hostnames: hr.Spec.Hostnames, + Rules: []RouteRule{ + { + ValidMatches: true, + ValidFilters: true, + Matches: hr.Spec.Rules[0].Matches, + RouteBackendRefs: []RouteBackendRef{}, + }, }, }, }, @@ -139,209 +146,13 @@ func TestBuildRoutes(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { g := NewWithT(t) - routes := buildRoutesForGateways(validator, hrRoutes, test.gwNsNames) + routes := buildRoutesForGateways(validator, hrRoutes, map[types.NamespacedName]*v1alpha2.GRPCRoute{}, test.gwNsNames) g.Expect(helpers.Diff(test.expected, routes)).To(BeEmpty()) }) } } -func TestBuildSectionNameRefs(t *testing.T) { - const routeNamespace = "test" - - gwNsName1 := types.NamespacedName{Namespace: routeNamespace, Name: "gateway-1"} - gwNsName2 := types.NamespacedName{Namespace: routeNamespace, Name: "gateway-2"} - - parentRefs := []gatewayv1.ParentReference{ - { - Name: gatewayv1.ObjectName(gwNsName1.Name), - SectionName: helpers.GetPointer[gatewayv1.SectionName]("one"), - }, - { - Name: gatewayv1.ObjectName("some-other-gateway"), - SectionName: helpers.GetPointer[gatewayv1.SectionName]("two"), - }, - { - Name: gatewayv1.ObjectName(gwNsName2.Name), - SectionName: helpers.GetPointer[gatewayv1.SectionName]("three"), - }, - { - Name: gatewayv1.ObjectName(gwNsName1.Name), - SectionName: helpers.GetPointer[gatewayv1.SectionName]("same-name"), - }, - { - Name: gatewayv1.ObjectName(gwNsName2.Name), - SectionName: helpers.GetPointer[gatewayv1.SectionName]("same-name"), - }, - { - Name: gatewayv1.ObjectName("some-other-gateway"), - SectionName: helpers.GetPointer[gatewayv1.SectionName]("same-name"), - }, - } - - gwNsNames := []types.NamespacedName{gwNsName1, gwNsName2} - - expected := []ParentRef{ - { - Idx: 0, - Gateway: gwNsName1, - }, - { - Idx: 2, - Gateway: gwNsName2, - }, - { - Idx: 3, - Gateway: gwNsName1, - }, - { - Idx: 4, - Gateway: gwNsName2, - }, - } - - tests := []struct { - expectedError error - name string - parentRefs []gatewayv1.ParentReference - expectedRefs []ParentRef - }{ - { - name: "normal case", - parentRefs: parentRefs, - expectedRefs: expected, - expectedError: nil, - }, - { - parentRefs: []gatewayv1.ParentReference{ - { - Name: gatewayv1.ObjectName(gwNsName1.Name), - SectionName: helpers.GetPointer[gatewayv1.SectionName]("http"), - }, - { - Name: gatewayv1.ObjectName(gwNsName1.Name), - SectionName: helpers.GetPointer[gatewayv1.SectionName]("http"), - }, - }, - name: "duplicate sectionNames", - expectedError: errors.New("duplicate section name \"http\" for Gateway test/gateway-1"), - }, - { - parentRefs: []gatewayv1.ParentReference{ - { - Name: gatewayv1.ObjectName(gwNsName1.Name), - SectionName: nil, - }, - { - Name: gatewayv1.ObjectName(gwNsName1.Name), - SectionName: nil, - }, - }, - name: "nil sectionNames", - expectedError: errors.New("duplicate section name \"\" for Gateway test/gateway-1"), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - g := NewWithT(t) - - result, err := buildSectionNameRefs(test.parentRefs, routeNamespace, gwNsNames) - g.Expect(result).To(Equal(test.expectedRefs)) - if test.expectedError != nil { - g.Expect(err).To(Equal(test.expectedError)) - } else { - g.Expect(err).ToNot(HaveOccurred()) - } - }) - } -} - -func TestFindGatewayForParentRef(t *testing.T) { - gwNsName1 := types.NamespacedName{Namespace: "test-1", Name: "gateway-1"} - gwNsName2 := types.NamespacedName{Namespace: "test-2", Name: "gateway-2"} - - tests := []struct { - ref gatewayv1.ParentReference - expectedGwNsName types.NamespacedName - name string - expectedFound bool - }{ - { - ref: gatewayv1.ParentReference{ - Namespace: helpers.GetPointer(gatewayv1.Namespace(gwNsName1.Namespace)), - Name: gatewayv1.ObjectName(gwNsName1.Name), - }, - expectedFound: true, - expectedGwNsName: gwNsName1, - name: "found", - }, - { - ref: gatewayv1.ParentReference{ - Group: helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName), - Kind: helpers.GetPointer[gatewayv1.Kind]("Gateway"), - Namespace: helpers.GetPointer(gatewayv1.Namespace(gwNsName1.Namespace)), - Name: gatewayv1.ObjectName(gwNsName1.Name), - }, - expectedFound: true, - expectedGwNsName: gwNsName1, - name: "found with explicit group and kind", - }, - { - ref: gatewayv1.ParentReference{ - Name: gatewayv1.ObjectName(gwNsName2.Name), - }, - expectedFound: true, - expectedGwNsName: gwNsName2, - name: "found with implicit namespace", - }, - { - ref: gatewayv1.ParentReference{ - Kind: helpers.GetPointer[gatewayv1.Kind]("NotGateway"), - Name: gatewayv1.ObjectName(gwNsName2.Name), - }, - expectedFound: false, - expectedGwNsName: types.NamespacedName{}, - name: "wrong kind", - }, - { - ref: gatewayv1.ParentReference{ - Group: helpers.GetPointer[gatewayv1.Group]("wrong-group"), - Name: gatewayv1.ObjectName(gwNsName2.Name), - }, - expectedFound: false, - expectedGwNsName: types.NamespacedName{}, - name: "wrong group", - }, - { - ref: gatewayv1.ParentReference{ - Namespace: helpers.GetPointer(gatewayv1.Namespace(gwNsName1.Namespace)), - Name: "some-gateway", - }, - expectedFound: false, - expectedGwNsName: types.NamespacedName{}, - name: "not found", - }, - } - - routeNamespace := "test-2" - - gwNsNames := []types.NamespacedName{ - gwNsName1, - gwNsName2, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - g := NewWithT(t) - - gw, found := findGatewayForParentRef(test.ref, routeNamespace, gwNsNames) - g.Expect(found).To(Equal(test.expectedFound)) - g.Expect(gw).To(Equal(test.expectedGwNsName)) - }) - } -} - -func TestBuildRoute(t *testing.T) { +func TestBuildHTTPRoute(t *testing.T) { const ( invalidPath = "/invalid" invalidRedirectHostname = "invalid.example.com" @@ -410,30 +221,40 @@ func TestBuildRoute(t *testing.T) { tests := []struct { validator *validationfakes.FakeHTTPFieldsValidator hr *gatewayv1.HTTPRoute - expected *Route + expected *L7Route name string }{ { validator: &validationfakes.FakeHTTPFieldsValidator{}, hr: hr, - expected: &Route{ - Source: hr, + expected: &L7Route{ + RouteType: RouteTypeHTTP, + Source: hr, ParentRefs: []ParentRef{ { - Idx: 0, - Gateway: gatewayNsName, + Idx: 0, + Gateway: gatewayNsName, + SectionName: hr.Spec.ParentRefs[0].SectionName, }, }, Valid: true, Attachable: true, - Rules: []Rule{ - { - ValidMatches: true, - ValidFilters: true, - }, - { - ValidMatches: true, - ValidFilters: true, + Spec: L7RouteSpec{ + Hostnames: hr.Spec.Hostnames, + Rules: []RouteRule{ + { + ValidMatches: true, + ValidFilters: true, + Matches: hr.Spec.Rules[0].Matches, + RouteBackendRefs: []RouteBackendRef{}, + }, + { + ValidMatches: true, + ValidFilters: true, + Matches: hr.Spec.Rules[1].Matches, + Filters: hr.Spec.Rules[1].Filters, + RouteBackendRefs: []RouteBackendRef{}, + }, }, }, }, @@ -442,14 +263,16 @@ func TestBuildRoute(t *testing.T) { { validator: &validationfakes.FakeHTTPFieldsValidator{}, hr: hrInvalidMatchesEmptyPathType, - expected: &Route{ + expected: &L7Route{ + RouteType: RouteTypeHTTP, Source: hrInvalidMatchesEmptyPathType, Valid: false, Attachable: true, ParentRefs: []ParentRef{ { - Idx: 0, - Gateway: gatewayNsName, + Idx: 0, + Gateway: gatewayNsName, + SectionName: hrInvalidMatchesEmptyPathType.Spec.ParentRefs[0].SectionName, }, }, Conditions: []conditions.Condition{ @@ -457,10 +280,15 @@ func TestBuildRoute(t *testing.T) { `All rules are invalid: spec.rules[0].matches[0].path.type: Required value: path type cannot be nil`, ), }, - Rules: []Rule{ - { - ValidMatches: false, - ValidFilters: true, + Spec: L7RouteSpec{ + Hostnames: hrInvalidMatchesEmptyPathType.Spec.Hostnames, + Rules: []RouteRule{ + { + ValidMatches: false, + ValidFilters: true, + RouteBackendRefs: []RouteBackendRef{}, + Matches: hrInvalidMatchesEmptyPathType.Spec.Rules[0].Matches, + }, }, }, }, @@ -469,22 +297,25 @@ func TestBuildRoute(t *testing.T) { { validator: &validationfakes.FakeHTTPFieldsValidator{}, hr: hrDuplicateSectionName, - expected: &Route{ - Source: hrDuplicateSectionName, + expected: &L7Route{ + RouteType: RouteTypeHTTP, + Source: hrDuplicateSectionName, }, name: "invalid route with duplicate sectionName", }, { validator: &validationfakes.FakeHTTPFieldsValidator{}, hr: hrInvalidMatchesEmptyPathValue, - expected: &Route{ + expected: &L7Route{ + RouteType: RouteTypeHTTP, Source: hrInvalidMatchesEmptyPathValue, Valid: false, Attachable: true, ParentRefs: []ParentRef{ { - Idx: 0, - Gateway: gatewayNsName, + Idx: 0, + Gateway: gatewayNsName, + SectionName: hrInvalidMatchesEmptyPathValue.Spec.ParentRefs[0].SectionName, }, }, Conditions: []conditions.Condition{ @@ -492,10 +323,15 @@ func TestBuildRoute(t *testing.T) { `All rules are invalid: spec.rules[0].matches[0].path.value: Required value: path value cannot be nil`, ), }, - Rules: []Rule{ - { - ValidMatches: false, - ValidFilters: true, + Spec: L7RouteSpec{ + Hostnames: hr.Spec.Hostnames, + Rules: []RouteRule{ + { + ValidMatches: false, + ValidFilters: true, + RouteBackendRefs: []RouteBackendRef{}, + Matches: hrInvalidMatchesEmptyPathValue.Spec.Rules[0].Matches, + }, }, }, }, @@ -510,14 +346,16 @@ func TestBuildRoute(t *testing.T) { { validator: &validationfakes.FakeHTTPFieldsValidator{}, hr: hrInvalidHostname, - expected: &Route{ + expected: &L7Route{ + RouteType: RouteTypeHTTP, Source: hrInvalidHostname, Valid: false, Attachable: false, ParentRefs: []ParentRef{ { - Idx: 0, - Gateway: gatewayNsName, + Idx: 0, + Gateway: gatewayNsName, + SectionName: hrInvalidHostname.Spec.ParentRefs[0].SectionName, }, }, Conditions: []conditions.Condition{ @@ -531,14 +369,16 @@ func TestBuildRoute(t *testing.T) { { validator: validatorInvalidFieldsInRule, hr: hrInvalidMatches, - expected: &Route{ + expected: &L7Route{ + RouteType: RouteTypeHTTP, Source: hrInvalidMatches, Valid: false, Attachable: true, ParentRefs: []ParentRef{ { - Idx: 0, - Gateway: gatewayNsName, + Idx: 0, + Gateway: gatewayNsName, + SectionName: hrInvalidMatches.Spec.ParentRefs[0].SectionName, }, }, Conditions: []conditions.Condition{ @@ -546,10 +386,15 @@ func TestBuildRoute(t *testing.T) { `All rules are invalid: spec.rules[0].matches[0].path.value: Invalid value: "/invalid": invalid path`, ), }, - Rules: []Rule{ - { - ValidMatches: false, - ValidFilters: true, + Spec: L7RouteSpec{ + Hostnames: hr.Spec.Hostnames, + Rules: []RouteRule{ + { + ValidMatches: false, + ValidFilters: true, + Matches: hrInvalidMatches.Spec.Rules[0].Matches, + RouteBackendRefs: []RouteBackendRef{}, + }, }, }, }, @@ -558,14 +403,16 @@ func TestBuildRoute(t *testing.T) { { validator: validatorInvalidFieldsInRule, hr: hrInvalidFilters, - expected: &Route{ + expected: &L7Route{ + RouteType: RouteTypeHTTP, Source: hrInvalidFilters, Valid: false, Attachable: true, ParentRefs: []ParentRef{ { - Idx: 0, - Gateway: gatewayNsName, + Idx: 0, + Gateway: gatewayNsName, + SectionName: hrInvalidFilters.Spec.ParentRefs[0].SectionName, }, }, Conditions: []conditions.Condition{ @@ -574,10 +421,16 @@ func TestBuildRoute(t *testing.T) { `Invalid value: "invalid.example.com": invalid hostname`, ), }, - Rules: []Rule{ - { - ValidMatches: true, - ValidFilters: false, + Spec: L7RouteSpec{ + Hostnames: hr.Spec.Hostnames, + Rules: []RouteRule{ + { + ValidMatches: true, + ValidFilters: false, + Matches: hrInvalidFilters.Spec.Rules[0].Matches, + Filters: hrInvalidFilters.Spec.Rules[0].Filters, + RouteBackendRefs: []RouteBackendRef{}, + }, }, }, }, @@ -586,14 +439,16 @@ func TestBuildRoute(t *testing.T) { { validator: validatorInvalidFieldsInRule, hr: hrDroppedInvalidMatches, - expected: &Route{ + expected: &L7Route{ + RouteType: RouteTypeHTTP, Source: hrDroppedInvalidMatches, Valid: true, Attachable: true, ParentRefs: []ParentRef{ { - Idx: 0, - Gateway: gatewayNsName, + Idx: 0, + Gateway: gatewayNsName, + SectionName: hrDroppedInvalidMatches.Spec.ParentRefs[0].SectionName, }, }, Conditions: []conditions.Condition{ @@ -601,14 +456,21 @@ func TestBuildRoute(t *testing.T) { `spec.rules[0].matches[0].path.value: Invalid value: "/invalid": invalid path`, ), }, - Rules: []Rule{ - { - ValidMatches: false, - ValidFilters: true, - }, - { - ValidMatches: true, - ValidFilters: true, + Spec: L7RouteSpec{ + Hostnames: hr.Spec.Hostnames, + Rules: []RouteRule{ + { + ValidMatches: false, + ValidFilters: true, + Matches: hrDroppedInvalidMatches.Spec.Rules[0].Matches, + RouteBackendRefs: []RouteBackendRef{}, + }, + { + ValidMatches: true, + ValidFilters: true, + Matches: hrDroppedInvalidMatches.Spec.Rules[1].Matches, + RouteBackendRefs: []RouteBackendRef{}, + }, }, }, }, @@ -618,14 +480,16 @@ func TestBuildRoute(t *testing.T) { { validator: validatorInvalidFieldsInRule, hr: hrDroppedInvalidMatchesAndInvalidFilters, - expected: &Route{ + expected: &L7Route{ + RouteType: RouteTypeHTTP, Source: hrDroppedInvalidMatchesAndInvalidFilters, Valid: true, Attachable: true, ParentRefs: []ParentRef{ { - Idx: 0, - Gateway: gatewayNsName, + Idx: 0, + Gateway: gatewayNsName, + SectionName: hrDroppedInvalidMatchesAndInvalidFilters.Spec.ParentRefs[0].SectionName, }, }, Conditions: []conditions.Condition{ @@ -635,18 +499,28 @@ func TestBuildRoute(t *testing.T) { `"invalid.example.com": invalid hostname]`, ), }, - Rules: []Rule{ - { - ValidMatches: false, - ValidFilters: true, - }, - { - ValidMatches: true, - ValidFilters: false, - }, - { - ValidMatches: true, - ValidFilters: true, + Spec: L7RouteSpec{ + Hostnames: hr.Spec.Hostnames, + Rules: []RouteRule{ + { + ValidMatches: false, + ValidFilters: true, + Matches: hrDroppedInvalidMatchesAndInvalidFilters.Spec.Rules[0].Matches, + RouteBackendRefs: []RouteBackendRef{}, + }, + { + ValidMatches: true, + ValidFilters: false, + Matches: hrDroppedInvalidMatchesAndInvalidFilters.Spec.Rules[1].Matches, + Filters: hrDroppedInvalidMatchesAndInvalidFilters.Spec.Rules[1].Filters, + RouteBackendRefs: []RouteBackendRef{}, + }, + { + ValidMatches: true, + ValidFilters: true, + Matches: hrDroppedInvalidMatchesAndInvalidFilters.Spec.Rules[2].Matches, + RouteBackendRefs: []RouteBackendRef{}, + }, }, }, }, @@ -655,14 +529,16 @@ func TestBuildRoute(t *testing.T) { { validator: validatorInvalidFieldsInRule, hr: hrDroppedInvalidFilters, - expected: &Route{ + expected: &L7Route{ + RouteType: RouteTypeHTTP, Source: hrDroppedInvalidFilters, Valid: true, Attachable: true, ParentRefs: []ParentRef{ { - Idx: 0, - Gateway: gatewayNsName, + Idx: 0, + Gateway: gatewayNsName, + SectionName: hrDroppedInvalidFilters.Spec.ParentRefs[0].SectionName, }, }, Conditions: []conditions.Condition{ @@ -671,1007 +547,38 @@ func TestBuildRoute(t *testing.T) { `"invalid.example.com": invalid hostname`, ), }, - Rules: []Rule{ - { - ValidMatches: true, - ValidFilters: true, - }, - { - ValidMatches: true, - ValidFilters: false, - }, - }, - }, - name: "dropped invalid rule with invalid filters", - }, - } - - gatewayNsNames := []types.NamespacedName{gatewayNsName} - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - g := NewWithT(t) - - route := buildRoute(test.validator, test.hr, gatewayNsNames) - g.Expect(helpers.Diff(test.expected, route)).To(BeEmpty()) - }) - } -} - -func TestBindRouteToListeners(t *testing.T) { - // we create a new listener each time because the function under test can modify it - createListener := func(name string) *Listener { - return &Listener{ - Name: name, - Source: gatewayv1.Listener{ - Name: gatewayv1.SectionName(name), - Hostname: (*gatewayv1.Hostname)(helpers.GetPointer("foo.example.com")), - }, - Valid: true, - Attachable: true, - Routes: map[types.NamespacedName]*Route{}, - } - } - createModifiedListener := func(name string, m func(*Listener)) *Listener { - l := createListener(name) - m(l) - return l - } - - gw := &gatewayv1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - Name: "gateway", - }, - } - gwDiffNamespace := &gatewayv1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "diff-namespace", - Name: "gateway", - }, - } - - createHTTPRouteWithSectionNameAndPort := func( - sectionName *gatewayv1.SectionName, - port *gatewayv1.PortNumber, - ) *gatewayv1.HTTPRoute { - return &gatewayv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - Name: "hr", - }, - Spec: gatewayv1.HTTPRouteSpec{ - CommonRouteSpec: gatewayv1.CommonRouteSpec{ - ParentRefs: []gatewayv1.ParentReference{ + Spec: L7RouteSpec{ + Hostnames: hr.Spec.Hostnames, + Rules: []RouteRule{ { - Name: gatewayv1.ObjectName(gw.Name), - SectionName: sectionName, - Port: port, - }, - }, - }, - Hostnames: []gatewayv1.Hostname{ - "foo.example.com", - }, - }, - } - } - - hr := createHTTPRouteWithSectionNameAndPort(helpers.GetPointer[gatewayv1.SectionName]("listener-80-1"), nil) - hrWithNilSectionName := createHTTPRouteWithSectionNameAndPort(nil, nil) - hrWithEmptySectionName := createHTTPRouteWithSectionNameAndPort(helpers.GetPointer[gatewayv1.SectionName](""), nil) - hrWithPort := createHTTPRouteWithSectionNameAndPort( - helpers.GetPointer[gatewayv1.SectionName]("listener-80-1"), - helpers.GetPointer[gatewayv1.PortNumber](80), - ) - hrWithNonExistingListener := createHTTPRouteWithSectionNameAndPort( - helpers.GetPointer[gatewayv1.SectionName]("listener-80-2"), - nil, - ) - - var normalRoute *Route - createNormalRoute := func(gateway *gatewayv1.Gateway) *Route { - normalRoute = &Route{ - Source: hr, - Valid: true, - Attachable: true, - ParentRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gateway), - }, - }, - } - return normalRoute - } - getLastNormalRoute := func() *Route { - return normalRoute - } - - invalidAttachableRoute1 := &Route{ - Source: hr, - Valid: false, - Attachable: true, - ParentRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - }, - }, - } - invalidAttachableRoute2 := &Route{ - Source: hr, - Valid: false, - Attachable: true, - ParentRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - }, - }, - } - - routeWithMissingSectionName := &Route{ - Source: hrWithNilSectionName, - Valid: true, - Attachable: true, - ParentRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - }, - }, - } - routeWithEmptySectionName := &Route{ - Source: hrWithEmptySectionName, - Valid: true, - Attachable: true, - ParentRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - }, - }, - } - routeWithNonExistingListener := &Route{ - Source: hrWithNonExistingListener, - Valid: true, - Attachable: true, - ParentRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - }, - }, - } - routeWithPort := &Route{ - Source: hrWithPort, - Valid: true, - Attachable: true, - ParentRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - }, - }, - } - ignoredGwNsName := types.NamespacedName{Namespace: "test", Name: "ignored-gateway"} - routeWithIgnoredGateway := &Route{ - Source: hr, - Valid: true, - Attachable: true, - ParentRefs: []ParentRef{ - { - Idx: 0, - Gateway: ignoredGwNsName, - }, - }, - } - invalidRoute := &Route{ - Valid: false, - ParentRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - }, - }, - } - - invalidNotAttachableListener := createModifiedListener("listener-80-1", func(l *Listener) { - l.Valid = false - l.Attachable = false - }) - nonMatchingHostnameListener := createModifiedListener("listener-80-1", func(l *Listener) { - l.Source.Hostname = helpers.GetPointer[gatewayv1.Hostname]("bar.example.com") - }) - - tests := []struct { - route *Route - gateway *Gateway - expectedGatewayListeners []*Listener - name string - expectedSectionNameRefs []ParentRef - expectedConditions []conditions.Condition - }{ - { - route: createNormalRoute(gw), - gateway: &Gateway{ - Source: gw, - Valid: true, - Listeners: []*Listener{ - createListener("listener-80-1"), - }, - }, - expectedSectionNameRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - Attachment: &ParentRefAttachmentStatus{ - Attached: true, - AcceptedHostnames: map[string][]string{ - "listener-80-1": {"foo.example.com"}, - }, - }, - }, - }, - expectedGatewayListeners: []*Listener{ - createModifiedListener("listener-80-1", func(l *Listener) { - l.Routes = map[types.NamespacedName]*Route{ - client.ObjectKeyFromObject(hr): getLastNormalRoute(), - } - }), - }, - name: "normal case", - }, - { - route: routeWithMissingSectionName, - gateway: &Gateway{ - Source: gw, - Valid: true, - Listeners: []*Listener{ - createListener("listener-80-1"), - }, - }, - expectedSectionNameRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - Attachment: &ParentRefAttachmentStatus{ - Attached: true, - AcceptedHostnames: map[string][]string{ - "listener-80-1": {"foo.example.com"}, - }, - }, - }, - }, - expectedGatewayListeners: []*Listener{ - createModifiedListener("listener-80-1", func(l *Listener) { - l.Routes = map[types.NamespacedName]*Route{ - client.ObjectKeyFromObject(hr): routeWithMissingSectionName, - } - }), - }, - name: "section name is nil", - }, - { - route: routeWithEmptySectionName, - gateway: &Gateway{ - Source: gw, - Valid: true, - Listeners: []*Listener{ - createListener("listener-80"), - createListener("listener-8080"), - }, - }, - expectedSectionNameRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - Attachment: &ParentRefAttachmentStatus{ - Attached: true, - AcceptedHostnames: map[string][]string{ - "listener-80": {"foo.example.com"}, - "listener-8080": {"foo.example.com"}, + ValidMatches: true, + ValidFilters: true, + Matches: hrDroppedInvalidFilters.Spec.Rules[0].Matches, + Filters: hrDroppedInvalidFilters.Spec.Rules[0].Filters, + RouteBackendRefs: []RouteBackendRef{}, }, - }, - }, - }, - expectedGatewayListeners: []*Listener{ - createModifiedListener("listener-80", func(l *Listener) { - l.Routes = map[types.NamespacedName]*Route{ - client.ObjectKeyFromObject(hr): routeWithEmptySectionName, - } - }), - createModifiedListener("listener-8080", func(l *Listener) { - l.Routes = map[types.NamespacedName]*Route{ - client.ObjectKeyFromObject(hr): routeWithEmptySectionName, - } - }), - }, - name: "section name is empty; bind to multiple listeners", - }, - { - route: routeWithEmptySectionName, - gateway: &Gateway{ - Source: gw, - Valid: true, - Listeners: []*Listener{ - invalidNotAttachableListener, - }, - }, - expectedSectionNameRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - Attachment: &ParentRefAttachmentStatus{ - Attached: false, - FailedCondition: staticConds.NewRouteInvalidListener(), - AcceptedHostnames: map[string][]string{}, - }, - }, - }, - expectedGatewayListeners: []*Listener{ - invalidNotAttachableListener, - }, - name: "empty section name with no valid and attachable listeners", - }, - { - route: routeWithPort, - gateway: &Gateway{ - Source: gw, - Valid: true, - Listeners: []*Listener{ - createListener("listener-80-1"), - }, - }, - expectedSectionNameRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - Attachment: &ParentRefAttachmentStatus{ - Attached: false, - FailedCondition: staticConds.NewRouteUnsupportedValue( - `spec.parentRefs[0].port: Forbidden: cannot be set`, - ), - AcceptedHostnames: map[string][]string{}, - }, - }, - }, - expectedGatewayListeners: []*Listener{ - createListener("listener-80-1"), - }, - name: "port is configured", - }, - { - route: routeWithNonExistingListener, - gateway: &Gateway{ - Source: gw, - Valid: true, - Listeners: []*Listener{ - createListener("listener-80-1"), - }, - }, - expectedSectionNameRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - Attachment: &ParentRefAttachmentStatus{ - Attached: false, - FailedCondition: staticConds.NewRouteNoMatchingParent(), - AcceptedHostnames: map[string][]string{}, - }, - }, - }, - expectedGatewayListeners: []*Listener{ - createListener("listener-80-1"), - }, - name: "listener doesn't exist", - }, - { - route: createNormalRoute(gw), - gateway: &Gateway{ - Source: gw, - Valid: true, - Listeners: []*Listener{ - invalidNotAttachableListener, - }, - }, - expectedSectionNameRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - Attachment: &ParentRefAttachmentStatus{ - Attached: false, - FailedCondition: staticConds.NewRouteInvalidListener(), - AcceptedHostnames: map[string][]string{}, - }, - }, - }, - expectedGatewayListeners: []*Listener{ - invalidNotAttachableListener, - }, - name: "listener isn't valid and attachable", - }, - { - route: createNormalRoute(gw), - gateway: &Gateway{ - Source: gw, - Valid: true, - Listeners: []*Listener{ - nonMatchingHostnameListener, - }, - }, - expectedSectionNameRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - Attachment: &ParentRefAttachmentStatus{ - Attached: false, - FailedCondition: staticConds.NewRouteNoMatchingListenerHostname(), - AcceptedHostnames: map[string][]string{}, - }, - }, - }, - expectedGatewayListeners: []*Listener{ - nonMatchingHostnameListener, - }, - name: "no matching listener hostname", - }, - { - route: routeWithIgnoredGateway, - gateway: &Gateway{ - Source: gw, - Valid: true, - Listeners: []*Listener{ - createListener("listener-80-1"), - }, - }, - expectedSectionNameRefs: []ParentRef{ - { - Idx: 0, - Gateway: ignoredGwNsName, - Attachment: &ParentRefAttachmentStatus{ - Attached: false, - FailedCondition: staticConds.NewTODO("Gateway is ignored"), - AcceptedHostnames: map[string][]string{}, - }, - }, - }, - expectedGatewayListeners: []*Listener{ - createListener("listener-80-1"), - }, - name: "gateway is ignored", - }, - { - route: invalidRoute, - gateway: &Gateway{ - Source: gw, - Valid: true, - Listeners: []*Listener{ - createListener("listener-80-1"), - }, - }, - expectedSectionNameRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - Attachment: nil, - }, - }, - expectedGatewayListeners: []*Listener{ - createListener("listener-80-1"), - }, - name: "route isn't valid", - }, - { - route: createNormalRoute(gw), - gateway: &Gateway{ - Source: gw, - Valid: false, - Listeners: []*Listener{ - createListener("listener-80-1"), - }, - }, - expectedSectionNameRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - Attachment: &ParentRefAttachmentStatus{ - Attached: false, - FailedCondition: staticConds.NewRouteInvalidGateway(), - AcceptedHostnames: map[string][]string{}, - }, - }, - }, - expectedGatewayListeners: []*Listener{ - createListener("listener-80-1"), - }, - name: "invalid gateway", - }, - { - route: createNormalRoute(gw), - gateway: &Gateway{ - Source: gw, - Valid: true, - Listeners: []*Listener{ - createModifiedListener("listener-80-1", func(l *Listener) { - l.Valid = false - }), - }, - }, - expectedSectionNameRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - Attachment: &ParentRefAttachmentStatus{ - Attached: true, - AcceptedHostnames: map[string][]string{ - "listener-80-1": {"foo.example.com"}, - }, - }, - }, - }, - expectedGatewayListeners: []*Listener{ - createModifiedListener("listener-80-1", func(l *Listener) { - l.Valid = false - l.Routes = map[types.NamespacedName]*Route{ - client.ObjectKeyFromObject(hr): getLastNormalRoute(), - } - }), - }, - expectedConditions: []conditions.Condition{staticConds.NewRouteInvalidListener()}, - name: "invalid attachable listener", - }, - { - route: invalidAttachableRoute1, - gateway: &Gateway{ - Source: gw, - Valid: true, - Listeners: []*Listener{ - createListener("listener-80-1"), - }, - }, - expectedSectionNameRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - Attachment: &ParentRefAttachmentStatus{ - Attached: true, - AcceptedHostnames: map[string][]string{ - "listener-80-1": {"foo.example.com"}, - }, - }, - }, - }, - expectedGatewayListeners: []*Listener{ - createModifiedListener("listener-80-1", func(l *Listener) { - l.Routes = map[types.NamespacedName]*Route{ - client.ObjectKeyFromObject(hr): invalidAttachableRoute1, - } - }), - }, - name: "invalid attachable route", - }, - { - route: invalidAttachableRoute2, - gateway: &Gateway{ - Source: gw, - Valid: true, - Listeners: []*Listener{ - createModifiedListener("listener-80-1", func(l *Listener) { - l.Valid = false - }), - }, - }, - expectedSectionNameRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - Attachment: &ParentRefAttachmentStatus{ - Attached: true, - AcceptedHostnames: map[string][]string{ - "listener-80-1": {"foo.example.com"}, - }, - }, - }, - }, - expectedGatewayListeners: []*Listener{ - createModifiedListener("listener-80-1", func(l *Listener) { - l.Valid = false - l.Routes = map[types.NamespacedName]*Route{ - client.ObjectKeyFromObject(hr): invalidAttachableRoute2, - } - }), - }, - expectedConditions: []conditions.Condition{staticConds.NewRouteInvalidListener()}, - name: "invalid attachable listener with invalid attachable route", - }, - { - route: createNormalRoute(gw), - gateway: &Gateway{ - Source: gw, - Valid: true, - Listeners: []*Listener{ - createModifiedListener("listener-80-1", func(l *Listener) { - l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ - Namespaces: &gatewayv1.RouteNamespaces{ - From: helpers.GetPointer(gatewayv1.NamespacesFromSelector), - }, - } - allowedLabels := map[string]string{"app": "not-allowed"} - l.AllowedRouteLabelSelector = labels.SelectorFromSet(allowedLabels) - }), - }, - }, - expectedSectionNameRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - Attachment: &ParentRefAttachmentStatus{ - Attached: false, - FailedCondition: staticConds.NewRouteNotAllowedByListeners(), - AcceptedHostnames: map[string][]string{}, - }, - }, - }, - expectedGatewayListeners: []*Listener{ - createModifiedListener("listener-80-1", func(l *Listener) { - l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ - Namespaces: &gatewayv1.RouteNamespaces{ - From: helpers.GetPointer(gatewayv1.NamespacesFromSelector), - }, - } - allowedLabels := map[string]string{"app": "not-allowed"} - l.AllowedRouteLabelSelector = labels.SelectorFromSet(allowedLabels) - }), - }, - name: "route not allowed via labels", - }, - { - route: createNormalRoute(gw), - gateway: &Gateway{ - Source: gw, - Valid: true, - Listeners: []*Listener{ - createModifiedListener("listener-80-1", func(l *Listener) { - l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ - Namespaces: &gatewayv1.RouteNamespaces{ - From: helpers.GetPointer(gatewayv1.NamespacesFromSelector), - }, - } - allowedLabels := map[string]string{"app": "allowed"} - l.AllowedRouteLabelSelector = labels.SelectorFromSet(allowedLabels) - }), - }, - }, - expectedSectionNameRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - Attachment: &ParentRefAttachmentStatus{ - Attached: true, - AcceptedHostnames: map[string][]string{ - "listener-80-1": {"foo.example.com"}, - }, - }, - }, - }, - expectedGatewayListeners: []*Listener{ - createModifiedListener("listener-80-1", func(l *Listener) { - allowedLabels := map[string]string{"app": "allowed"} - l.AllowedRouteLabelSelector = labels.SelectorFromSet(allowedLabels) - l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ - Namespaces: &gatewayv1.RouteNamespaces{ - From: helpers.GetPointer(gatewayv1.NamespacesFromSelector), - }, - } - l.Routes = map[types.NamespacedName]*Route{ - client.ObjectKeyFromObject(hr): getLastNormalRoute(), - } - }), - }, - name: "route allowed via labels", - }, - { - route: createNormalRoute(gwDiffNamespace), - gateway: &Gateway{ - Source: gwDiffNamespace, - Valid: true, - Listeners: []*Listener{ - createModifiedListener("listener-80-1", func(l *Listener) { - l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ - Namespaces: &gatewayv1.RouteNamespaces{ - From: helpers.GetPointer(gatewayv1.NamespacesFromSame), - }, - } - }), - }, - }, - expectedSectionNameRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gwDiffNamespace), - Attachment: &ParentRefAttachmentStatus{ - Attached: false, - FailedCondition: staticConds.NewRouteNotAllowedByListeners(), - AcceptedHostnames: map[string][]string{}, - }, - }, - }, - expectedGatewayListeners: []*Listener{ - createModifiedListener("listener-80-1", func(l *Listener) { - l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ - Namespaces: &gatewayv1.RouteNamespaces{ - From: helpers.GetPointer(gatewayv1.NamespacesFromSame), - }, - } - }), - }, - name: "route not allowed via same namespace", - }, - { - route: createNormalRoute(gw), - gateway: &Gateway{ - Source: gw, - Valid: true, - Listeners: []*Listener{ - createModifiedListener("listener-80-1", func(l *Listener) { - l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ - Namespaces: &gatewayv1.RouteNamespaces{ - From: helpers.GetPointer(gatewayv1.NamespacesFromSame), - }, - } - }), - }, - }, - expectedSectionNameRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gw), - Attachment: &ParentRefAttachmentStatus{ - Attached: true, - AcceptedHostnames: map[string][]string{ - "listener-80-1": {"foo.example.com"}, - }, - }, - }, - }, - expectedGatewayListeners: []*Listener{ - createModifiedListener("listener-80-1", func(l *Listener) { - l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ - Namespaces: &gatewayv1.RouteNamespaces{ - From: helpers.GetPointer(gatewayv1.NamespacesFromSame), - }, - } - l.Routes = map[types.NamespacedName]*Route{ - client.ObjectKeyFromObject(hr): getLastNormalRoute(), - } - }), - }, - name: "route allowed via same namespace", - }, - { - route: createNormalRoute(gwDiffNamespace), - gateway: &Gateway{ - Source: gwDiffNamespace, - Valid: true, - Listeners: []*Listener{ - createModifiedListener("listener-80-1", func(l *Listener) { - l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ - Namespaces: &gatewayv1.RouteNamespaces{ - From: helpers.GetPointer(gatewayv1.NamespacesFromAll), - }, - } - }), - }, - }, - expectedSectionNameRefs: []ParentRef{ - { - Idx: 0, - Gateway: client.ObjectKeyFromObject(gwDiffNamespace), - Attachment: &ParentRefAttachmentStatus{ - Attached: true, - AcceptedHostnames: map[string][]string{ - "listener-80-1": {"foo.example.com"}, + { + ValidMatches: true, + ValidFilters: false, + Matches: hrDroppedInvalidFilters.Spec.Rules[1].Matches, + Filters: hrDroppedInvalidFilters.Spec.Rules[1].Filters, + RouteBackendRefs: []RouteBackendRef{}, }, }, }, }, - expectedGatewayListeners: []*Listener{ - createModifiedListener("listener-80-1", func(l *Listener) { - l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ - Namespaces: &gatewayv1.RouteNamespaces{ - From: helpers.GetPointer(gatewayv1.NamespacesFromAll), - }, - } - l.Routes = map[types.NamespacedName]*Route{ - client.ObjectKeyFromObject(hr): getLastNormalRoute(), - } - }), - }, - name: "route allowed via all namespaces", - }, - } - - namespaces := map[types.NamespacedName]*v1.Namespace{ - {Name: "test"}: { - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Labels: map[string]string{"app": "allowed"}, - }, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - g := NewWithT(t) - - bindRouteToListeners(test.route, test.gateway, namespaces) - - g.Expect(test.route.ParentRefs).To(Equal(test.expectedSectionNameRefs)) - g.Expect(helpers.Diff(test.gateway.Listeners, test.expectedGatewayListeners)).To(BeEmpty()) - g.Expect(helpers.Diff(test.route.Conditions, test.expectedConditions)).To(BeEmpty()) - }) - } -} - -func TestFindAcceptedHostnames(t *testing.T) { - var listenerHostnameFoo gatewayv1.Hostname = "foo.example.com" - var listenerHostnameCafe gatewayv1.Hostname = "cafe.example.com" - var listenerHostnameWildcard gatewayv1.Hostname = "*.example.com" - routeHostnames := []gatewayv1.Hostname{"foo.example.com", "bar.example.com"} - - tests := []struct { - listenerHostname *gatewayv1.Hostname - msg string - routeHostnames []gatewayv1.Hostname - expected []string - }{ - { - listenerHostname: &listenerHostnameFoo, - routeHostnames: routeHostnames, - expected: []string{"foo.example.com"}, - msg: "one match", - }, - { - listenerHostname: &listenerHostnameCafe, - routeHostnames: routeHostnames, - expected: nil, - msg: "no match", - }, - { - listenerHostname: nil, - routeHostnames: routeHostnames, - expected: []string{"foo.example.com", "bar.example.com"}, - msg: "nil listener hostname", - }, - { - listenerHostname: &listenerHostnameFoo, - routeHostnames: nil, - expected: []string{"foo.example.com"}, - msg: "route has empty hostnames", - }, - { - listenerHostname: nil, - routeHostnames: nil, - expected: []string{wildcardHostname}, - msg: "both listener and route have empty hostnames", - }, - { - listenerHostname: &listenerHostnameWildcard, - routeHostnames: routeHostnames, - expected: []string{"foo.example.com", "bar.example.com"}, - msg: "listener wildcard hostname", - }, - { - listenerHostname: &listenerHostnameFoo, - routeHostnames: []gatewayv1.Hostname{"*.example.com"}, - expected: []string{"foo.example.com"}, - msg: "route wildcard hostname; specific listener hostname", - }, - { - listenerHostname: &listenerHostnameWildcard, - routeHostnames: nil, - expected: []string{"*.example.com"}, - msg: "listener wildcard hostname; nil route hostname", - }, - { - listenerHostname: nil, - routeHostnames: []gatewayv1.Hostname{"*.example.com"}, - expected: []string{"*.example.com"}, - msg: "route wildcard hostname; nil listener hostname", - }, - { - listenerHostname: &listenerHostnameWildcard, - routeHostnames: []gatewayv1.Hostname{"*.bar.example.com"}, - expected: []string{"*.bar.example.com"}, - msg: "route and listener wildcard hostnames", - }, - } - - for _, test := range tests { - t.Run(test.msg, func(t *testing.T) { - g := NewWithT(t) - result := findAcceptedHostnames(test.listenerHostname, test.routeHostnames) - g.Expect(result).To(Equal(test.expected)) - }) - } -} - -func TestGetHostname(t *testing.T) { - var emptyHostname gatewayv1.Hostname - var hostname gatewayv1.Hostname = "example.com" - - tests := []struct { - h *gatewayv1.Hostname - expected string - msg string - }{ - { - h: nil, - expected: "", - msg: "nil hostname", - }, - { - h: &emptyHostname, - expected: "", - msg: "empty hostname", - }, - { - h: &hostname, - expected: string(hostname), - msg: "normal hostname", - }, - } - - for _, test := range tests { - t.Run(test.msg, func(t *testing.T) { - g := NewWithT(t) - result := getHostname(test.h) - g.Expect(result).To(Equal(test.expected)) - }) - } -} - -func TestValidateHostnames(t *testing.T) { - const validHostname = "example.com" - - tests := []struct { - name string - hostnames []gatewayv1.Hostname - expectErr bool - }{ - { - hostnames: []gatewayv1.Hostname{ - validHostname, - "example.org", - "foo.example.net", - }, - expectErr: false, - name: "multiple valid", - }, - { - hostnames: []gatewayv1.Hostname{ - validHostname, - "", - }, - expectErr: true, - name: "valid and invalid", + name: "dropped invalid rule with invalid filters", }, } - path := field.NewPath("test") + gatewayNsNames := []types.NamespacedName{gatewayNsName} for _, test := range tests { t.Run(test.name, func(t *testing.T) { g := NewWithT(t) - err := validateHostnames(test.hostnames, path) - - if test.expectErr { - g.Expect(err).To(HaveOccurred()) - } else { - g.Expect(err).ToNot(HaveOccurred()) - } + route := buildHTTPRoute(test.validator, test.hr, gatewayNsNames) + g.Expect(helpers.Diff(test.expected, route)).To(BeEmpty()) }) } } diff --git a/internal/mode/static/state/graph/route_common.go b/internal/mode/static/state/graph/route_common.go new file mode 100644 index 0000000000..dbfe8a541e --- /dev/null +++ b/internal/mode/static/state/graph/route_common.go @@ -0,0 +1,575 @@ +package graph + +import ( + "fmt" + "strings" + + apiv1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/controller-runtime/pkg/client" + v1 "sigs.k8s.io/gateway-api/apis/v1" + v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" + staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation" +) + +const wildcardHostname = "~^" + +// ParentRef describes a reference to a parent in a Route. +type ParentRef struct { + // Attachment is the attachment status of the ParentRef. It could be nil. In that case, NGF didn't attempt to + // attach because of problems with the Route. + Attachment *ParentRefAttachmentStatus + // SectionName is the name of a section within the target Gateway. + SectionName *v1.SectionName + // Port is the network port this Route targets. + Port *v1.PortNumber + // Gateway is the NamespacedName of the referenced Gateway + Gateway types.NamespacedName + // Idx is the index of the corresponding ParentReference in the Route. + Idx int +} + +// ParentRefAttachmentStatus describes the attachment status of a ParentRef. +type ParentRefAttachmentStatus struct { + // AcceptedHostnames is an intersection between the hostnames supported by an attached Listener + // and the hostnames from this Route. Key is listener name, value is list of hostnames. + AcceptedHostnames map[string][]string + // FailedCondition is the condition that describes why the ParentRef is not attached to the Gateway. It is set + // when Attached is false. + FailedCondition conditions.Condition + // Attached indicates if the ParentRef is attached to the Gateway. + Attached bool +} + +type RouteType string + +const ( + // RouteTypeHTTP indicates that the RouteType of the L7Route is HTTP + RouteTypeHTTP RouteType = "http" + // RouteTypeGRPC indicates that the RouteType of the L7Route is gRPC + RouteTypeGRPC RouteType = "grpc" +) + +// RouteKey is the unique identifier for a L7Route +type RouteKey struct { + NamespacedName types.NamespacedName + RouteType RouteType +} + +// L7Route is the generic type for the layer 7 routes, HTTPRoute and GRPCRoute +type L7Route struct { + // Source is the source Gateway API object of the Route. + Source client.Object + // RouteType is the type (http or grpc) of the Route. + RouteType RouteType + // Spec is the L7RouteSpec of the Route + Spec L7RouteSpec + // ParentRefs describe the references to the parents in a Route. + ParentRefs []ParentRef + // Conditions define the conditions to be reported in the status of the Route. + Conditions []conditions.Condition + // Valid indicates if the Route is valid. + Valid bool + // Attachable indicates if the Route is attachable to any Listener. + Attachable bool +} + +type L7RouteSpec struct { + // Hostnames defines a set of hostnames used to select a Route used to process the request. + Hostnames []v1.Hostname + // Rules are the list of HTTP matchers, filters and actions. + Rules []RouteRule +} + +type RouteRule struct { + // Matches define the predicate used to match requests to a given action. + Matches []v1.HTTPRouteMatch + // Filters define processing steps that must be completed during the request or response lifecycle. + Filters []v1.HTTPRouteFilter + // RouteBackendRefs are a wrapper for v1.BackendRef and any BackendRef filters from the HTTPRoute or GRPCRoute. + RouteBackendRefs []RouteBackendRef + // BackendRefs is an internal representation of a backendRef in a Route. + BackendRefs []BackendRef + // ValidMatches indicates if the matches are valid and accepted by the Route. + ValidMatches bool + // ValidFilters indicates if the filters are valid and accepted by the Route. + ValidFilters bool +} + +// RouteBackendRef is a wrapper for v1.BackendRef and any BackendRef filters from the HTTPRoute or GRPCRoute. +type RouteBackendRef struct { + v1.BackendRef + Filters []any +} + +// CreateRouteKey takes a client.Object and creates a RouteKey +func CreateRouteKey(obj client.Object) RouteKey { + nsName := types.NamespacedName{ + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + } + var routeType RouteType + switch obj.(type) { + case *v1.HTTPRoute: + routeType = RouteTypeHTTP + case *v1alpha2.GRPCRoute: + routeType = RouteTypeGRPC + default: + panic(fmt.Sprintf("Unknown type: %T", obj)) + } + return RouteKey{ + NamespacedName: nsName, + RouteType: routeType, + } +} + +// buildGRPCRoutesForGateways builds routes from HTTP/GRPCRoutes that reference any of the specified Gateways. +func buildRoutesForGateways( + validator validation.HTTPFieldsValidator, + httpRoutes map[types.NamespacedName]*v1.HTTPRoute, + grpcRoutes map[types.NamespacedName]*v1alpha2.GRPCRoute, + gatewayNsNames []types.NamespacedName, +) map[RouteKey]*L7Route { + if len(gatewayNsNames) == 0 { + return nil + } + + routes := make(map[RouteKey]*L7Route) + + for _, route := range httpRoutes { + r := buildHTTPRoute(validator, route, gatewayNsNames) + if r != nil { + routes[CreateRouteKey(route)] = r + } + } + + for _, route := range grpcRoutes { + r := buildGRPCRoute(validator, route, gatewayNsNames) + if r != nil { + routes[CreateRouteKey(route)] = r + } + } + + return routes +} + +func buildSectionNameRefs( + parentRefs []v1.ParentReference, + routeNamespace string, + gatewayNsNames []types.NamespacedName, +) ([]ParentRef, error) { + sectionNameRefs := make([]ParentRef, 0, len(parentRefs)) + + type key struct { + gwNsName types.NamespacedName + sectionName string + } + uniqueSectionsPerGateway := make(map[key]struct{}) + + for i, p := range parentRefs { + gw, found := findGatewayForParentRef(p, routeNamespace, gatewayNsNames) + if !found { + continue + } + + var sectionName string + if p.SectionName != nil { + sectionName = string(*p.SectionName) + } + + k := key{ + gwNsName: gw, + sectionName: sectionName, + } + + if _, exist := uniqueSectionsPerGateway[k]; exist { + return nil, fmt.Errorf("duplicate section name %q for Gateway %s", sectionName, gw.String()) + } + uniqueSectionsPerGateway[k] = struct{}{} + + sectionNameRefs = append(sectionNameRefs, ParentRef{ + Idx: i, + Gateway: gw, + SectionName: p.SectionName, + Port: p.Port, + }) + } + + return sectionNameRefs, nil +} + +func findGatewayForParentRef( + ref v1.ParentReference, + routeNamespace string, + gatewayNsNames []types.NamespacedName, +) (gwNsName types.NamespacedName, found bool) { + if ref.Kind != nil && *ref.Kind != "Gateway" { + return types.NamespacedName{}, false + } + if ref.Group != nil && *ref.Group != v1.GroupName { + return types.NamespacedName{}, false + } + + // if the namespace is missing, assume the namespace of the HTTPRoute + ns := routeNamespace + if ref.Namespace != nil { + ns = string(*ref.Namespace) + } + + for _, gw := range gatewayNsNames { + if gw.Namespace == ns && gw.Name == string(ref.Name) { + return gw, true + } + } + + return types.NamespacedName{}, false +} + +func bindRoutesToListeners( + routes map[RouteKey]*L7Route, + gw *Gateway, + namespaces map[types.NamespacedName]*apiv1.Namespace, +) { + if gw == nil { + return + } + + for _, r := range routes { + bindRouteToListeners(r, gw, namespaces) + } +} + +func bindRouteToListeners( + route *L7Route, + gw *Gateway, + namespaces map[types.NamespacedName]*apiv1.Namespace, +) { + if !route.Attachable { + return + } + + for i := 0; i < len(route.ParentRefs); i++ { + attachment := &ParentRefAttachmentStatus{ + AcceptedHostnames: make(map[string][]string), + } + ref := &route.ParentRefs[i] + ref.Attachment = attachment + + path := field.NewPath("spec").Child("parentRefs").Index(ref.Idx) + + attachableListeners, listenerExists := findAttachableListeners( + getSectionName(ref.SectionName), + gw.Listeners, + ) + + // Case 1: Attachment is not possible because the specified SectionName does not match any Listeners in the + // Gateway. + if !listenerExists { + attachment.FailedCondition = staticConds.NewRouteNoMatchingParent() + continue + } + + // Case 2: Attachment is not possible due to unsupported configuration + + if ref.Port != nil { + valErr := field.Forbidden(path.Child("port"), "cannot be set") + attachment.FailedCondition = staticConds.NewRouteUnsupportedValue(valErr.Error()) + continue + } + + // Case 3: the parentRef references an ignored Gateway resource. + + referencesWinningGw := ref.Gateway.Namespace == gw.Source.Namespace && ref.Gateway.Name == gw.Source.Name + + if !referencesWinningGw { + attachment.FailedCondition = staticConds.NewTODO("Gateway is ignored") + continue + } + + // Case 4: Attachment is not possible because Gateway is invalid + + if !gw.Valid { + attachment.FailedCondition = staticConds.NewRouteInvalidGateway() + continue + } + + // Case 5 - winning Gateway + + // Try to attach Route to all matching listeners + + cond, attached := tryToAttachRouteToListeners( + ref.Attachment, + attachableListeners, + route, + gw, + namespaces, + ) + if !attached { + attachment.FailedCondition = cond + continue + } + if cond != (conditions.Condition{}) { + route.Conditions = append(route.Conditions, cond) + } + + attachment.Attached = true + } +} + +// tryToAttachRouteToListeners tries to attach the route to the listeners that match the parentRef and the hostnames. +// There are two cases: +// (1) If it succeeds in attaching at least one listener it will return true. The returned condition will be empty if +// at least one of the listeners is valid. Otherwise, it will return the failure condition. +// (2) If it fails to attach the route, it will return false and the failure condition. +func tryToAttachRouteToListeners( + refStatus *ParentRefAttachmentStatus, + attachableListeners []*Listener, + route *L7Route, + gw *Gateway, + namespaces map[types.NamespacedName]*apiv1.Namespace, +) (conditions.Condition, bool) { + if len(attachableListeners) == 0 { + return staticConds.NewRouteInvalidListener(), false + } + + rk := CreateRouteKey(route.Source) + + bind := func(l *Listener) (allowed, attached bool) { + if !routeAllowedByListener(l, route.Source.GetNamespace(), gw.Source.Namespace, namespaces) { + return false, false + } + + hostnames := findAcceptedHostnames(l.Source.Hostname, route.Spec.Hostnames) + if len(hostnames) == 0 { + return true, false + } + refStatus.AcceptedHostnames[string(l.Source.Name)] = hostnames + + l.Routes[rk] = route + return true, true + } + + var attachedToAtLeastOneValidListener bool + + var allowed, attached bool + for _, l := range attachableListeners { + routeAllowed, routeAttached := bind(l) + allowed = allowed || routeAllowed + attached = attached || routeAttached + attachedToAtLeastOneValidListener = attachedToAtLeastOneValidListener || (routeAttached && l.Valid) + } + + if !attached { + if !allowed { + return staticConds.NewRouteNotAllowedByListeners(), false + } + return staticConds.NewRouteNoMatchingListenerHostname(), false + } + + if !attachedToAtLeastOneValidListener { + return staticConds.NewRouteInvalidListener(), true + } + + return conditions.Condition{}, true +} + +// findAttachableListeners returns a list of attachable listeners and whether the listener exists for a non-empty +// sectionName. +func findAttachableListeners(sectionName string, listeners []*Listener) ([]*Listener, bool) { + if sectionName != "" { + for _, l := range listeners { + if l.Name == sectionName { + if l.Attachable { + return []*Listener{l}, true + } + return nil, true + } + } + return nil, false + } + + attachableListeners := make([]*Listener, 0, len(listeners)) + for _, l := range listeners { + if !l.Attachable { + continue + } + + attachableListeners = append(attachableListeners, l) + } + + return attachableListeners, true +} + +func findAcceptedHostnames(listenerHostname *v1.Hostname, routeHostnames []v1.Hostname) []string { + hostname := getHostname(listenerHostname) + + if len(routeHostnames) == 0 { + if hostname == "" { + return []string{wildcardHostname} + } + return []string{hostname} + } + + var result []string + + for _, h := range routeHostnames { + routeHost := string(h) + if match(hostname, routeHost) { + result = append(result, GetMoreSpecificHostname(hostname, routeHost)) + } + } + + return result +} + +func match(listenerHost, routeHost string) bool { + if listenerHost == "" { + return true + } + + if routeHost == listenerHost { + return true + } + + wildcardMatch := func(host1, host2 string) bool { + return strings.HasPrefix(host1, "*.") && strings.HasSuffix(host2, strings.TrimPrefix(host1, "*")) + } + + // check if listenerHost is a wildcard and routeHost matches + if wildcardMatch(listenerHost, routeHost) { + return true + } + + // check if routeHost is a wildcard and listener matchess + return wildcardMatch(routeHost, listenerHost) +} + +// GetMoreSpecificHostname returns the more specific hostname between the two inputs. +// +// This function assumes that the two hostnames match each other, either: +// - Exactly +// - One as a substring of the other +func GetMoreSpecificHostname(hostname1, hostname2 string) string { + if hostname1 == hostname2 { + return hostname1 + } + if hostname1 == "" { + return hostname2 + } + if hostname2 == "" { + return hostname1 + } + + // Compare if wildcards are present + if strings.HasPrefix(hostname1, "*.") { + if strings.HasPrefix(hostname2, "*.") { + subdomains1 := strings.Split(hostname1, ".") + subdomains2 := strings.Split(hostname2, ".") + + // Compare number of subdomains + if len(subdomains1) > len(subdomains2) { + return hostname1 + } + + return hostname2 + } + + return hostname2 + } + if strings.HasPrefix(hostname2, "*.") { + return hostname1 + } + + return "" +} + +func routeAllowedByListener( + listener *Listener, + routeNS, + gwNS string, + namespaces map[types.NamespacedName]*apiv1.Namespace, +) bool { + if listener.Source.AllowedRoutes != nil { + switch *listener.Source.AllowedRoutes.Namespaces.From { + case v1.NamespacesFromAll: + return true + case v1.NamespacesFromSame: + return routeNS == gwNS + case v1.NamespacesFromSelector: + if listener.AllowedRouteLabelSelector == nil { + return false + } + + ns, exists := namespaces[types.NamespacedName{Name: routeNS}] + if !exists { + panic(fmt.Errorf("route namespace %q not found in map", routeNS)) + } + return listener.AllowedRouteLabelSelector.Matches(labels.Set(ns.Labels)) + } + } + return true +} + +func getHostname(h *v1.Hostname) string { + if h == nil { + return "" + } + return string(*h) +} + +func getSectionName(s *v1.SectionName) string { + if s == nil { + return "" + } + return string(*s) +} + +func validateHostnames(hostnames []v1.Hostname, path *field.Path) error { + var allErrs field.ErrorList + + for i := range hostnames { + if err := validateHostname(string(hostnames[i])); err != nil { + allErrs = append(allErrs, field.Invalid(path.Index(i), hostnames[i], err.Error())) + continue + } + } + + return allErrs.ToAggregate() +} + +func validateHeaderMatch( + validator validation.HTTPFieldsValidator, + headerType *v1.HeaderMatchType, + headerName, headerValue string, + headerPath *field.Path, +) field.ErrorList { + var allErrs field.ErrorList + + if headerType == nil { + allErrs = append(allErrs, field.Required(headerPath.Child("type"), "cannot be empty")) + } else if *headerType != v1.HeaderMatchExact { + valErr := field.NotSupported( + headerPath.Child("type"), + *headerType, + []string{string(v1.HeaderMatchExact)}, + ) + allErrs = append(allErrs, valErr) + } + + if err := validator.ValidateHeaderNameInMatch(headerName); err != nil { + valErr := field.Invalid(headerPath.Child("name"), headerName, err.Error()) + allErrs = append(allErrs, valErr) + } + + if err := validator.ValidateHeaderValueInMatch(headerValue); err != nil { + valErr := field.Invalid(headerPath.Child("value"), headerValue, err.Error()) + allErrs = append(allErrs, valErr) + } + + return allErrs +} diff --git a/internal/mode/static/state/graph/route_common_test.go b/internal/mode/static/state/graph/route_common_test.go new file mode 100644 index 0000000000..36a7031783 --- /dev/null +++ b/internal/mode/static/state/graph/route_common_test.go @@ -0,0 +1,1244 @@ +package graph + +import ( + "errors" + "testing" + + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/controller-runtime/pkg/client" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" +) + +func TestBuildSectionNameRefs(t *testing.T) { + const routeNamespace = "test" + + gwNsName1 := types.NamespacedName{Namespace: routeNamespace, Name: "gateway-1"} + gwNsName2 := types.NamespacedName{Namespace: routeNamespace, Name: "gateway-2"} + + parentRefs := []gatewayv1.ParentReference{ + { + Name: gatewayv1.ObjectName(gwNsName1.Name), + SectionName: helpers.GetPointer[gatewayv1.SectionName]("one"), + }, + { + Name: gatewayv1.ObjectName("some-other-gateway"), + SectionName: helpers.GetPointer[gatewayv1.SectionName]("two"), + }, + { + Name: gatewayv1.ObjectName(gwNsName2.Name), + SectionName: helpers.GetPointer[gatewayv1.SectionName]("three"), + }, + { + Name: gatewayv1.ObjectName(gwNsName1.Name), + SectionName: helpers.GetPointer[gatewayv1.SectionName]("same-name"), + }, + { + Name: gatewayv1.ObjectName(gwNsName2.Name), + SectionName: helpers.GetPointer[gatewayv1.SectionName]("same-name"), + }, + { + Name: gatewayv1.ObjectName("some-other-gateway"), + SectionName: helpers.GetPointer[gatewayv1.SectionName]("same-name"), + }, + } + + gwNsNames := []types.NamespacedName{gwNsName1, gwNsName2} + + expected := []ParentRef{ + { + Idx: 0, + Gateway: gwNsName1, + SectionName: parentRefs[0].SectionName, + }, + { + Idx: 2, + Gateway: gwNsName2, + SectionName: parentRefs[2].SectionName, + }, + { + Idx: 3, + Gateway: gwNsName1, + SectionName: parentRefs[3].SectionName, + }, + { + Idx: 4, + Gateway: gwNsName2, + SectionName: parentRefs[4].SectionName, + }, + } + + tests := []struct { + expectedError error + name string + parentRefs []gatewayv1.ParentReference + expectedRefs []ParentRef + }{ + { + name: "normal case", + parentRefs: parentRefs, + expectedRefs: expected, + expectedError: nil, + }, + { + parentRefs: []gatewayv1.ParentReference{ + { + Name: gatewayv1.ObjectName(gwNsName1.Name), + SectionName: helpers.GetPointer[gatewayv1.SectionName]("http"), + }, + { + Name: gatewayv1.ObjectName(gwNsName1.Name), + SectionName: helpers.GetPointer[gatewayv1.SectionName]("http"), + }, + }, + name: "duplicate sectionNames", + expectedError: errors.New("duplicate section name \"http\" for Gateway test/gateway-1"), + }, + { + parentRefs: []gatewayv1.ParentReference{ + { + Name: gatewayv1.ObjectName(gwNsName1.Name), + SectionName: nil, + }, + { + Name: gatewayv1.ObjectName(gwNsName1.Name), + SectionName: nil, + }, + }, + name: "nil sectionNames", + expectedError: errors.New("duplicate section name \"\" for Gateway test/gateway-1"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + result, err := buildSectionNameRefs(test.parentRefs, routeNamespace, gwNsNames) + g.Expect(result).To(Equal(test.expectedRefs)) + if test.expectedError != nil { + g.Expect(err).To(Equal(test.expectedError)) + } else { + g.Expect(err).ToNot(HaveOccurred()) + } + }) + } +} + +func TestFindGatewayForParentRef(t *testing.T) { + gwNsName1 := types.NamespacedName{Namespace: "test-1", Name: "gateway-1"} + gwNsName2 := types.NamespacedName{Namespace: "test-2", Name: "gateway-2"} + + tests := []struct { + ref gatewayv1.ParentReference + expectedGwNsName types.NamespacedName + name string + expectedFound bool + }{ + { + ref: gatewayv1.ParentReference{ + Namespace: helpers.GetPointer(gatewayv1.Namespace(gwNsName1.Namespace)), + Name: gatewayv1.ObjectName(gwNsName1.Name), + }, + expectedFound: true, + expectedGwNsName: gwNsName1, + name: "found", + }, + { + ref: gatewayv1.ParentReference{ + Group: helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName), + Kind: helpers.GetPointer[gatewayv1.Kind]("Gateway"), + Namespace: helpers.GetPointer(gatewayv1.Namespace(gwNsName1.Namespace)), + Name: gatewayv1.ObjectName(gwNsName1.Name), + }, + expectedFound: true, + expectedGwNsName: gwNsName1, + name: "found with explicit group and kind", + }, + { + ref: gatewayv1.ParentReference{ + Name: gatewayv1.ObjectName(gwNsName2.Name), + }, + expectedFound: true, + expectedGwNsName: gwNsName2, + name: "found with implicit namespace", + }, + { + ref: gatewayv1.ParentReference{ + Kind: helpers.GetPointer[gatewayv1.Kind]("NotGateway"), + Name: gatewayv1.ObjectName(gwNsName2.Name), + }, + expectedFound: false, + expectedGwNsName: types.NamespacedName{}, + name: "wrong kind", + }, + { + ref: gatewayv1.ParentReference{ + Group: helpers.GetPointer[gatewayv1.Group]("wrong-group"), + Name: gatewayv1.ObjectName(gwNsName2.Name), + }, + expectedFound: false, + expectedGwNsName: types.NamespacedName{}, + name: "wrong group", + }, + { + ref: gatewayv1.ParentReference{ + Namespace: helpers.GetPointer(gatewayv1.Namespace(gwNsName1.Namespace)), + Name: "some-gateway", + }, + expectedFound: false, + expectedGwNsName: types.NamespacedName{}, + name: "not found", + }, + } + + routeNamespace := "test-2" + + gwNsNames := []types.NamespacedName{ + gwNsName1, + gwNsName2, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + gw, found := findGatewayForParentRef(test.ref, routeNamespace, gwNsNames) + g.Expect(found).To(Equal(test.expectedFound)) + g.Expect(gw).To(Equal(test.expectedGwNsName)) + }) + } +} + +func TestBindRouteToListeners(t *testing.T) { + // we create a new listener each time because the function under test can modify it + createListener := func(name string) *Listener { + return &Listener{ + Name: name, + Source: gatewayv1.Listener{ + Name: gatewayv1.SectionName(name), + Hostname: (*gatewayv1.Hostname)(helpers.GetPointer("foo.example.com")), + }, + Valid: true, + Attachable: true, + Routes: map[RouteKey]*L7Route{}, + } + } + createModifiedListener := func(name string, m func(*Listener)) *Listener { + l := createListener(name) + m(l) + return l + } + + gw := &gatewayv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "gateway", + }, + } + gwDiffNamespace := &gatewayv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "diff-namespace", + Name: "gateway", + }, + } + + createHTTPRouteWithSectionNameAndPort := func( + sectionName *gatewayv1.SectionName, + port *gatewayv1.PortNumber, + ) *gatewayv1.HTTPRoute { + return &gatewayv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "hr", + }, + Spec: gatewayv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayv1.CommonRouteSpec{ + ParentRefs: []gatewayv1.ParentReference{ + { + Name: gatewayv1.ObjectName(gw.Name), + SectionName: sectionName, + Port: port, + }, + }, + }, + Hostnames: []gatewayv1.Hostname{ + "foo.example.com", + }, + }, + } + } + + hr := createHTTPRouteWithSectionNameAndPort(helpers.GetPointer[gatewayv1.SectionName]("listener-80-1"), nil) + hrWithNilSectionName := createHTTPRouteWithSectionNameAndPort(nil, nil) + hrWithEmptySectionName := createHTTPRouteWithSectionNameAndPort(helpers.GetPointer[gatewayv1.SectionName](""), nil) + hrWithPort := createHTTPRouteWithSectionNameAndPort( + helpers.GetPointer[gatewayv1.SectionName]("listener-80-1"), + helpers.GetPointer[gatewayv1.PortNumber](80), + ) + hrWithNonExistingListener := createHTTPRouteWithSectionNameAndPort( + helpers.GetPointer[gatewayv1.SectionName]("listener-80-2"), + nil, + ) + + var normalRoute *L7Route + createNormalRoute := func(gateway *gatewayv1.Gateway) *L7Route { + normalRoute = &L7Route{ + RouteType: RouteTypeHTTP, + Source: hr, + Spec: L7RouteSpec{ + Hostnames: hr.Spec.Hostnames, + }, + Valid: true, + Attachable: true, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gateway), + SectionName: hr.Spec.ParentRefs[0].SectionName, + }, + }, + } + return normalRoute + } + getLastNormalRoute := func() *L7Route { + return normalRoute + } + + invalidAttachableRoute1 := &L7Route{ + RouteType: RouteTypeHTTP, + Source: hr, + Valid: false, + Attachable: true, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hr.Spec.ParentRefs[0].SectionName, + }, + }, + } + invalidAttachableRoute2 := &L7Route{ + RouteType: RouteTypeHTTP, + Source: hr, + Valid: false, + Attachable: true, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hr.Spec.ParentRefs[0].SectionName, + }, + }, + } + + routeWithMissingSectionName := &L7Route{ + RouteType: RouteTypeHTTP, + Source: hrWithNilSectionName, + Valid: true, + Attachable: true, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hrWithNilSectionName.Spec.ParentRefs[0].SectionName, + }, + }, + } + routeWithEmptySectionName := &L7Route{ + RouteType: RouteTypeHTTP, + Source: hrWithEmptySectionName, + Valid: true, + Attachable: true, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hrWithEmptySectionName.Spec.ParentRefs[0].SectionName, + }, + }, + } + routeWithNonExistingListener := &L7Route{ + RouteType: RouteTypeHTTP, + Source: hrWithNonExistingListener, + Valid: true, + Attachable: true, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hrWithNonExistingListener.Spec.ParentRefs[0].SectionName, + }, + }, + } + routeWithPort := &L7Route{ + RouteType: RouteTypeHTTP, + Source: hrWithPort, + Valid: true, + Attachable: true, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hrWithPort.Spec.ParentRefs[0].SectionName, + Port: hrWithPort.Spec.ParentRefs[0].Port, + }, + }, + } + ignoredGwNsName := types.NamespacedName{Namespace: "test", Name: "ignored-gateway"} + routeWithIgnoredGateway := &L7Route{ + RouteType: RouteTypeHTTP, + Source: hr, + Valid: true, + Attachable: true, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: ignoredGwNsName, + SectionName: hr.Spec.ParentRefs[0].SectionName, + }, + }, + } + invalidRoute := &L7Route{ + RouteType: RouteTypeHTTP, + Valid: false, + Source: hr, + ParentRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hr.Spec.ParentRefs[0].SectionName, + }, + }, + } + + invalidNotAttachableListener := createModifiedListener("listener-80-1", func(l *Listener) { + l.Valid = false + l.Attachable = false + }) + nonMatchingHostnameListener := createModifiedListener("listener-80-1", func(l *Listener) { + l.Source.Hostname = helpers.GetPointer[gatewayv1.Hostname]("bar.example.com") + }) + + tests := []struct { + route *L7Route + gateway *Gateway + expectedGatewayListeners []*Listener + name string + expectedSectionNameRefs []ParentRef + expectedConditions []conditions.Condition + }{ + { + route: createNormalRoute(gw), + gateway: &Gateway{ + Source: gw, + Valid: true, + Listeners: []*Listener{ + createListener("listener-80-1"), + }, + }, + expectedSectionNameRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hr.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + Attached: true, + AcceptedHostnames: map[string][]string{ + "listener-80-1": {"foo.example.com"}, + }, + }, + }, + }, + expectedGatewayListeners: []*Listener{ + createModifiedListener("listener-80-1", func(l *Listener) { + l.Routes = map[RouteKey]*L7Route{ + CreateRouteKey(hr): getLastNormalRoute(), + } + }), + }, + name: "normal case", + }, + { + route: routeWithMissingSectionName, + gateway: &Gateway{ + Source: gw, + Valid: true, + Listeners: []*Listener{ + createListener("listener-80-1"), + }, + }, + expectedSectionNameRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hrWithNilSectionName.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + Attached: true, + AcceptedHostnames: map[string][]string{ + "listener-80-1": {"foo.example.com"}, + }, + }, + }, + }, + expectedGatewayListeners: []*Listener{ + createModifiedListener("listener-80-1", func(l *Listener) { + l.Routes = map[RouteKey]*L7Route{ + CreateRouteKey(hr): routeWithMissingSectionName, + } + }), + }, + name: "section name is nil", + }, + { + route: routeWithEmptySectionName, + gateway: &Gateway{ + Source: gw, + Valid: true, + Listeners: []*Listener{ + createListener("listener-80"), + createListener("listener-8080"), + }, + }, + expectedSectionNameRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hrWithEmptySectionName.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + Attached: true, + AcceptedHostnames: map[string][]string{ + "listener-80": {"foo.example.com"}, + "listener-8080": {"foo.example.com"}, + }, + }, + }, + }, + expectedGatewayListeners: []*Listener{ + createModifiedListener("listener-80", func(l *Listener) { + l.Routes = map[RouteKey]*L7Route{ + CreateRouteKey(hr): routeWithEmptySectionName, + } + }), + createModifiedListener("listener-8080", func(l *Listener) { + l.Routes = map[RouteKey]*L7Route{ + CreateRouteKey(hr): routeWithEmptySectionName, + } + }), + }, + name: "section name is empty; bind to multiple listeners", + }, + { + route: routeWithEmptySectionName, + gateway: &Gateway{ + Source: gw, + Valid: true, + Listeners: []*Listener{ + invalidNotAttachableListener, + }, + }, + expectedSectionNameRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hrWithEmptySectionName.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + Attached: false, + FailedCondition: staticConds.NewRouteInvalidListener(), + AcceptedHostnames: map[string][]string{}, + }, + }, + }, + expectedGatewayListeners: []*Listener{ + invalidNotAttachableListener, + }, + name: "empty section name with no valid and attachable listeners", + }, + { + route: routeWithPort, + gateway: &Gateway{ + Source: gw, + Valid: true, + Listeners: []*Listener{ + createListener("listener-80-1"), + }, + }, + expectedSectionNameRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hrWithPort.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + Attached: false, + FailedCondition: staticConds.NewRouteUnsupportedValue( + `spec.parentRefs[0].port: Forbidden: cannot be set`, + ), + AcceptedHostnames: map[string][]string{}, + }, + Port: hrWithPort.Spec.ParentRefs[0].Port, + }, + }, + expectedGatewayListeners: []*Listener{ + createListener("listener-80-1"), + }, + name: "port is configured", + }, + { + route: routeWithNonExistingListener, + gateway: &Gateway{ + Source: gw, + Valid: true, + Listeners: []*Listener{ + createListener("listener-80-1"), + }, + }, + expectedSectionNameRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hrWithNonExistingListener.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + Attached: false, + FailedCondition: staticConds.NewRouteNoMatchingParent(), + AcceptedHostnames: map[string][]string{}, + }, + }, + }, + expectedGatewayListeners: []*Listener{ + createListener("listener-80-1"), + }, + name: "listener doesn't exist", + }, + { + route: createNormalRoute(gw), + gateway: &Gateway{ + Source: gw, + Valid: true, + Listeners: []*Listener{ + invalidNotAttachableListener, + }, + }, + expectedSectionNameRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hr.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + Attached: false, + FailedCondition: staticConds.NewRouteInvalidListener(), + AcceptedHostnames: map[string][]string{}, + }, + }, + }, + expectedGatewayListeners: []*Listener{ + invalidNotAttachableListener, + }, + name: "listener isn't valid and attachable", + }, + { + route: createNormalRoute(gw), + gateway: &Gateway{ + Source: gw, + Valid: true, + Listeners: []*Listener{ + nonMatchingHostnameListener, + }, + }, + expectedSectionNameRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hr.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + Attached: false, + FailedCondition: staticConds.NewRouteNoMatchingListenerHostname(), + AcceptedHostnames: map[string][]string{}, + }, + }, + }, + expectedGatewayListeners: []*Listener{ + nonMatchingHostnameListener, + }, + name: "no matching listener hostname", + }, + { + route: routeWithIgnoredGateway, + gateway: &Gateway{ + Source: gw, + Valid: true, + Listeners: []*Listener{ + createListener("listener-80-1"), + }, + }, + expectedSectionNameRefs: []ParentRef{ + { + Idx: 0, + Gateway: ignoredGwNsName, + SectionName: hr.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + Attached: false, + FailedCondition: staticConds.NewTODO("Gateway is ignored"), + AcceptedHostnames: map[string][]string{}, + }, + }, + }, + expectedGatewayListeners: []*Listener{ + createListener("listener-80-1"), + }, + name: "gateway is ignored", + }, + { + route: invalidRoute, + gateway: &Gateway{ + Source: gw, + Valid: true, + Listeners: []*Listener{ + createListener("listener-80-1"), + }, + }, + expectedSectionNameRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + Attachment: nil, + SectionName: hr.Spec.ParentRefs[0].SectionName, + }, + }, + expectedGatewayListeners: []*Listener{ + createListener("listener-80-1"), + }, + name: "route isn't valid", + }, + { + route: createNormalRoute(gw), + gateway: &Gateway{ + Source: gw, + Valid: false, + Listeners: []*Listener{ + createListener("listener-80-1"), + }, + }, + expectedSectionNameRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hr.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + Attached: false, + FailedCondition: staticConds.NewRouteInvalidGateway(), + AcceptedHostnames: map[string][]string{}, + }, + }, + }, + expectedGatewayListeners: []*Listener{ + createListener("listener-80-1"), + }, + name: "invalid gateway", + }, + { + route: createNormalRoute(gw), + gateway: &Gateway{ + Source: gw, + Valid: true, + Listeners: []*Listener{ + createModifiedListener("listener-80-1", func(l *Listener) { + l.Valid = false + }), + }, + }, + expectedSectionNameRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hr.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + Attached: true, + AcceptedHostnames: map[string][]string{ + "listener-80-1": {"foo.example.com"}, + }, + }, + }, + }, + expectedGatewayListeners: []*Listener{ + createModifiedListener("listener-80-1", func(l *Listener) { + l.Valid = false + l.Routes = map[RouteKey]*L7Route{ + CreateRouteKey(hr): getLastNormalRoute(), + } + }), + }, + expectedConditions: []conditions.Condition{staticConds.NewRouteInvalidListener()}, + name: "invalid attachable listener", + }, + { + route: invalidAttachableRoute1, + gateway: &Gateway{ + Source: gw, + Valid: true, + Listeners: []*Listener{ + createListener("listener-80-1"), + }, + }, + expectedSectionNameRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hr.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + Attached: true, + AcceptedHostnames: map[string][]string{ + "listener-80-1": {"foo.example.com"}, + }, + }, + }, + }, + expectedGatewayListeners: []*Listener{ + createModifiedListener("listener-80-1", func(l *Listener) { + l.Routes = map[RouteKey]*L7Route{ + CreateRouteKey(hr): invalidAttachableRoute1, + } + }), + }, + name: "invalid attachable route", + }, + { + route: invalidAttachableRoute2, + gateway: &Gateway{ + Source: gw, + Valid: true, + Listeners: []*Listener{ + createModifiedListener("listener-80-1", func(l *Listener) { + l.Valid = false + }), + }, + }, + expectedSectionNameRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hr.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + Attached: true, + AcceptedHostnames: map[string][]string{ + "listener-80-1": {"foo.example.com"}, + }, + }, + }, + }, + expectedGatewayListeners: []*Listener{ + createModifiedListener("listener-80-1", func(l *Listener) { + l.Valid = false + l.Routes = map[RouteKey]*L7Route{ + CreateRouteKey(hr): invalidAttachableRoute2, + } + }), + }, + expectedConditions: []conditions.Condition{staticConds.NewRouteInvalidListener()}, + name: "invalid attachable listener with invalid attachable route", + }, + { + route: createNormalRoute(gw), + gateway: &Gateway{ + Source: gw, + Valid: true, + Listeners: []*Listener{ + createModifiedListener("listener-80-1", func(l *Listener) { + l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ + Namespaces: &gatewayv1.RouteNamespaces{ + From: helpers.GetPointer(gatewayv1.NamespacesFromSelector), + }, + } + allowedLabels := map[string]string{"app": "not-allowed"} + l.AllowedRouteLabelSelector = labels.SelectorFromSet(allowedLabels) + }), + }, + }, + expectedSectionNameRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hr.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + Attached: false, + FailedCondition: staticConds.NewRouteNotAllowedByListeners(), + AcceptedHostnames: map[string][]string{}, + }, + }, + }, + expectedGatewayListeners: []*Listener{ + createModifiedListener("listener-80-1", func(l *Listener) { + l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ + Namespaces: &gatewayv1.RouteNamespaces{ + From: helpers.GetPointer(gatewayv1.NamespacesFromSelector), + }, + } + allowedLabels := map[string]string{"app": "not-allowed"} + l.AllowedRouteLabelSelector = labels.SelectorFromSet(allowedLabels) + }), + }, + name: "route not allowed via labels", + }, + { + route: createNormalRoute(gw), + gateway: &Gateway{ + Source: gw, + Valid: true, + Listeners: []*Listener{ + createModifiedListener("listener-80-1", func(l *Listener) { + l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ + Namespaces: &gatewayv1.RouteNamespaces{ + From: helpers.GetPointer(gatewayv1.NamespacesFromSelector), + }, + } + allowedLabels := map[string]string{"app": "allowed"} + l.AllowedRouteLabelSelector = labels.SelectorFromSet(allowedLabels) + }), + }, + }, + expectedSectionNameRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hr.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + Attached: true, + AcceptedHostnames: map[string][]string{ + "listener-80-1": {"foo.example.com"}, + }, + }, + }, + }, + expectedGatewayListeners: []*Listener{ + createModifiedListener("listener-80-1", func(l *Listener) { + allowedLabels := map[string]string{"app": "allowed"} + l.AllowedRouteLabelSelector = labels.SelectorFromSet(allowedLabels) + l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ + Namespaces: &gatewayv1.RouteNamespaces{ + From: helpers.GetPointer(gatewayv1.NamespacesFromSelector), + }, + } + l.Routes = map[RouteKey]*L7Route{ + CreateRouteKey(hr): getLastNormalRoute(), + } + }), + }, + name: "route allowed via labels", + }, + { + route: createNormalRoute(gwDiffNamespace), + gateway: &Gateway{ + Source: gwDiffNamespace, + Valid: true, + Listeners: []*Listener{ + createModifiedListener("listener-80-1", func(l *Listener) { + l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ + Namespaces: &gatewayv1.RouteNamespaces{ + From: helpers.GetPointer(gatewayv1.NamespacesFromSame), + }, + } + }), + }, + }, + expectedSectionNameRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gwDiffNamespace), + SectionName: hr.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + Attached: false, + FailedCondition: staticConds.NewRouteNotAllowedByListeners(), + AcceptedHostnames: map[string][]string{}, + }, + }, + }, + expectedGatewayListeners: []*Listener{ + createModifiedListener("listener-80-1", func(l *Listener) { + l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ + Namespaces: &gatewayv1.RouteNamespaces{ + From: helpers.GetPointer(gatewayv1.NamespacesFromSame), + }, + } + }), + }, + name: "route not allowed via same namespace", + }, + { + route: createNormalRoute(gw), + gateway: &Gateway{ + Source: gw, + Valid: true, + Listeners: []*Listener{ + createModifiedListener("listener-80-1", func(l *Listener) { + l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ + Namespaces: &gatewayv1.RouteNamespaces{ + From: helpers.GetPointer(gatewayv1.NamespacesFromSame), + }, + } + }), + }, + }, + expectedSectionNameRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gw), + SectionName: hr.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + Attached: true, + AcceptedHostnames: map[string][]string{ + "listener-80-1": {"foo.example.com"}, + }, + }, + }, + }, + expectedGatewayListeners: []*Listener{ + createModifiedListener("listener-80-1", func(l *Listener) { + l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ + Namespaces: &gatewayv1.RouteNamespaces{ + From: helpers.GetPointer(gatewayv1.NamespacesFromSame), + }, + } + l.Routes = map[RouteKey]*L7Route{ + CreateRouteKey(hr): getLastNormalRoute(), + } + }), + }, + name: "route allowed via same namespace", + }, + { + route: createNormalRoute(gwDiffNamespace), + gateway: &Gateway{ + Source: gwDiffNamespace, + Valid: true, + Listeners: []*Listener{ + createModifiedListener("listener-80-1", func(l *Listener) { + l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ + Namespaces: &gatewayv1.RouteNamespaces{ + From: helpers.GetPointer(gatewayv1.NamespacesFromAll), + }, + } + }), + }, + }, + expectedSectionNameRefs: []ParentRef{ + { + Idx: 0, + Gateway: client.ObjectKeyFromObject(gwDiffNamespace), + SectionName: hr.Spec.ParentRefs[0].SectionName, + Attachment: &ParentRefAttachmentStatus{ + Attached: true, + AcceptedHostnames: map[string][]string{ + "listener-80-1": {"foo.example.com"}, + }, + }, + }, + }, + expectedGatewayListeners: []*Listener{ + createModifiedListener("listener-80-1", func(l *Listener) { + l.Source.AllowedRoutes = &gatewayv1.AllowedRoutes{ + Namespaces: &gatewayv1.RouteNamespaces{ + From: helpers.GetPointer(gatewayv1.NamespacesFromAll), + }, + } + l.Routes = map[RouteKey]*L7Route{ + CreateRouteKey(hr): getLastNormalRoute(), + } + }), + }, + name: "route allowed via all namespaces", + }, + } + + namespaces := map[types.NamespacedName]*v1.Namespace{ + {Name: "test"}: { + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Labels: map[string]string{"app": "allowed"}, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + bindRouteToListeners( + test.route, + test.gateway, + namespaces, + ) + + g.Expect(test.route.ParentRefs).To(Equal(test.expectedSectionNameRefs)) + g.Expect(helpers.Diff(test.gateway.Listeners, test.expectedGatewayListeners)).To(BeEmpty()) + g.Expect(helpers.Diff(test.route.Conditions, test.expectedConditions)).To(BeEmpty()) + }) + } +} + +func TestFindAcceptedHostnames(t *testing.T) { + var listenerHostnameFoo gatewayv1.Hostname = "foo.example.com" + var listenerHostnameCafe gatewayv1.Hostname = "cafe.example.com" + var listenerHostnameWildcard gatewayv1.Hostname = "*.example.com" + routeHostnames := []gatewayv1.Hostname{"foo.example.com", "bar.example.com"} + + tests := []struct { + listenerHostname *gatewayv1.Hostname + msg string + routeHostnames []gatewayv1.Hostname + expected []string + }{ + { + listenerHostname: &listenerHostnameFoo, + routeHostnames: routeHostnames, + expected: []string{"foo.example.com"}, + msg: "one match", + }, + { + listenerHostname: &listenerHostnameCafe, + routeHostnames: routeHostnames, + expected: nil, + msg: "no match", + }, + { + listenerHostname: nil, + routeHostnames: routeHostnames, + expected: []string{"foo.example.com", "bar.example.com"}, + msg: "nil listener hostname", + }, + { + listenerHostname: &listenerHostnameFoo, + routeHostnames: nil, + expected: []string{"foo.example.com"}, + msg: "route has empty hostnames", + }, + { + listenerHostname: nil, + routeHostnames: nil, + expected: []string{wildcardHostname}, + msg: "both listener and route have empty hostnames", + }, + { + listenerHostname: &listenerHostnameWildcard, + routeHostnames: routeHostnames, + expected: []string{"foo.example.com", "bar.example.com"}, + msg: "listener wildcard hostname", + }, + { + listenerHostname: &listenerHostnameFoo, + routeHostnames: []gatewayv1.Hostname{"*.example.com"}, + expected: []string{"foo.example.com"}, + msg: "route wildcard hostname; specific listener hostname", + }, + { + listenerHostname: &listenerHostnameWildcard, + routeHostnames: nil, + expected: []string{"*.example.com"}, + msg: "listener wildcard hostname; nil route hostname", + }, + { + listenerHostname: nil, + routeHostnames: []gatewayv1.Hostname{"*.example.com"}, + expected: []string{"*.example.com"}, + msg: "route wildcard hostname; nil listener hostname", + }, + { + listenerHostname: &listenerHostnameWildcard, + routeHostnames: []gatewayv1.Hostname{"*.bar.example.com"}, + expected: []string{"*.bar.example.com"}, + msg: "route and listener wildcard hostnames", + }, + } + + for _, test := range tests { + t.Run(test.msg, func(t *testing.T) { + g := NewWithT(t) + result := findAcceptedHostnames(test.listenerHostname, test.routeHostnames) + g.Expect(result).To(Equal(test.expected)) + }) + } +} + +func TestGetHostname(t *testing.T) { + var emptyHostname gatewayv1.Hostname + var hostname gatewayv1.Hostname = "example.com" + + tests := []struct { + h *gatewayv1.Hostname + expected string + msg string + }{ + { + h: nil, + expected: "", + msg: "nil hostname", + }, + { + h: &emptyHostname, + expected: "", + msg: "empty hostname", + }, + { + h: &hostname, + expected: string(hostname), + msg: "normal hostname", + }, + } + + for _, test := range tests { + t.Run(test.msg, func(t *testing.T) { + g := NewWithT(t) + result := getHostname(test.h) + g.Expect(result).To(Equal(test.expected)) + }) + } +} + +func TestValidateHostnames(t *testing.T) { + const validHostname = "example.com" + + tests := []struct { + name string + hostnames []gatewayv1.Hostname + expectErr bool + }{ + { + hostnames: []gatewayv1.Hostname{ + validHostname, + "example.org", + "foo.example.net", + }, + expectErr: false, + name: "multiple valid", + }, + { + hostnames: []gatewayv1.Hostname{ + validHostname, + "", + }, + expectErr: true, + name: "valid and invalid", + }, + } + + path := field.NewPath("test") + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + err := validateHostnames(test.hostnames, path) + + if test.expectErr { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).ToNot(HaveOccurred()) + } + }) + } +} diff --git a/internal/mode/static/state/graph/service.go b/internal/mode/static/state/graph/service.go index 9568b59df1..08a5fe497c 100644 --- a/internal/mode/static/state/graph/service.go +++ b/internal/mode/static/state/graph/service.go @@ -5,31 +5,24 @@ import ( ) func buildReferencedServices( - routes map[types.NamespacedName]*Route, + routes map[RouteKey]*L7Route, ) map[types.NamespacedName]struct{} { svcNames := make(map[types.NamespacedName]struct{}) - // routes all have populated ParentRefs from when they were created. - // - // Get all the service names referenced from all the HTTPRoutes. - for _, route := range routes { - if !route.Valid { - continue - } - + getServiceNamesFromRoute := func(parentRefs []ParentRef, routeRules []RouteRule) { // If none of the ParentRefs are attached to the Gateway, we want to skip the route. attached := false - for _, ref := range route.ParentRefs { + for _, ref := range parentRefs { if ref.Attachment.Attached { attached = true break } } if !attached { - continue + return } - for _, rule := range route.Rules { + for _, rule := range routeRules { for _, ref := range rule.BackendRefs { // Processes both valid and invalid BackendRefs as invalid ones still have referenced services // we may want to track. @@ -40,6 +33,17 @@ func buildReferencedServices( } } + // routes all have populated ParentRefs from when they were created. + // + // Get all the service names referenced from all the Routes. + for _, route := range routes { + if !route.Valid { + continue + } + + getServiceNamesFromRoute(route.ParentRefs, route.Spec.Rules) + } + if len(svcNames) == 0 { return nil } diff --git a/internal/mode/static/state/graph/service_test.go b/internal/mode/static/state/graph/service_test.go index 2eb0aaf646..73e4851947 100644 --- a/internal/mode/static/state/graph/service_test.go +++ b/internal/mode/static/state/graph/service_test.go @@ -8,7 +8,7 @@ import ( ) func TestBuildReferencedServices(t *testing.T) { - normalRoute := &Route{ + normalRoute := &L7Route{ ParentRefs: []ParentRef{ { Attachment: &ParentRefAttachmentStatus{ @@ -17,21 +17,24 @@ func TestBuildReferencedServices(t *testing.T) { }, }, Valid: true, - Rules: []Rule{ - { - BackendRefs: []BackendRef{ - { - SvcNsName: types.NamespacedName{Namespace: "banana-ns", Name: "service"}, - Weight: 1, + Spec: L7RouteSpec{ + Rules: []RouteRule{ + { + BackendRefs: []BackendRef{ + { + SvcNsName: types.NamespacedName{Namespace: "banana-ns", Name: "service"}, + Weight: 1, + }, }, + ValidMatches: true, + ValidFilters: true, }, - ValidMatches: true, - ValidFilters: true, }, }, + RouteType: RouteTypeHTTP, } - validRouteTwoServicesOneRule := &Route{ + validRouteTwoServicesOneRule := &L7Route{ ParentRefs: []ParentRef{ { Attachment: &ParentRefAttachmentStatus{ @@ -40,25 +43,27 @@ func TestBuildReferencedServices(t *testing.T) { }, }, Valid: true, - Rules: []Rule{ - { - BackendRefs: []BackendRef{ - { - SvcNsName: types.NamespacedName{Namespace: "service-ns", Name: "service"}, - Weight: 1, - }, - { - SvcNsName: types.NamespacedName{Namespace: "service-ns2", Name: "service2"}, - Weight: 1, + Spec: L7RouteSpec{ + Rules: []RouteRule{ + { + BackendRefs: []BackendRef{ + { + SvcNsName: types.NamespacedName{Namespace: "service-ns", Name: "service"}, + Weight: 1, + }, + { + SvcNsName: types.NamespacedName{Namespace: "service-ns2", Name: "service2"}, + Weight: 1, + }, }, + ValidMatches: true, + ValidFilters: true, }, - ValidMatches: true, - ValidFilters: true, }, }, } - validRouteTwoServicesTwoRules := &Route{ + validRouteTwoServicesTwoRules := &L7Route{ ParentRefs: []ParentRef{ { Attachment: &ParentRefAttachmentStatus{ @@ -67,31 +72,33 @@ func TestBuildReferencedServices(t *testing.T) { }, }, Valid: true, - Rules: []Rule{ - { - BackendRefs: []BackendRef{ - { - SvcNsName: types.NamespacedName{Namespace: "service-ns", Name: "service"}, - Weight: 1, + Spec: L7RouteSpec{ + Rules: []RouteRule{ + { + BackendRefs: []BackendRef{ + { + SvcNsName: types.NamespacedName{Namespace: "service-ns", Name: "service"}, + Weight: 1, + }, }, + ValidMatches: true, + ValidFilters: true, }, - ValidMatches: true, - ValidFilters: true, - }, - { - BackendRefs: []BackendRef{ - { - SvcNsName: types.NamespacedName{Namespace: "service-ns2", Name: "service2"}, - Weight: 1, + { + BackendRefs: []BackendRef{ + { + SvcNsName: types.NamespacedName{Namespace: "service-ns2", Name: "service2"}, + Weight: 1, + }, }, + ValidMatches: true, + ValidFilters: true, }, - ValidMatches: true, - ValidFilters: true, }, }, } - invalidRoute := &Route{ + invalidRoute := &L7Route{ ParentRefs: []ParentRef{ { Attachment: &ParentRefAttachmentStatus{ @@ -100,21 +107,23 @@ func TestBuildReferencedServices(t *testing.T) { }, }, Valid: false, - Rules: []Rule{ - { - BackendRefs: []BackendRef{ - { - SvcNsName: types.NamespacedName{Namespace: "service-ns", Name: "service"}, - Weight: 1, + Spec: L7RouteSpec{ + Rules: []RouteRule{ + { + BackendRefs: []BackendRef{ + { + SvcNsName: types.NamespacedName{Namespace: "service-ns", Name: "service"}, + Weight: 1, + }, }, + ValidMatches: true, + ValidFilters: true, }, - ValidMatches: true, - ValidFilters: true, }, }, } - unattachedRoute := &Route{ + unattachedRoute := &L7Route{ ParentRefs: []ParentRef{ { Attachment: &ParentRefAttachmentStatus{ @@ -123,21 +132,23 @@ func TestBuildReferencedServices(t *testing.T) { }, }, Valid: true, - Rules: []Rule{ - { - BackendRefs: []BackendRef{ - { - SvcNsName: types.NamespacedName{Namespace: "service-ns", Name: "service"}, - Weight: 1, + Spec: L7RouteSpec{ + Rules: []RouteRule{ + { + BackendRefs: []BackendRef{ + { + SvcNsName: types.NamespacedName{Namespace: "service-ns", Name: "service"}, + Weight: 1, + }, }, + ValidMatches: true, + ValidFilters: true, }, - ValidMatches: true, - ValidFilters: true, }, }, } - attachedRouteWithManyParentRefs := &Route{ + attachedRouteWithManyParentRefs := &L7Route{ ParentRefs: []ParentRef{ { Attachment: &ParentRefAttachmentStatus{ @@ -156,20 +167,22 @@ func TestBuildReferencedServices(t *testing.T) { }, }, Valid: true, - Rules: []Rule{ - { - BackendRefs: []BackendRef{ - { - SvcNsName: types.NamespacedName{Namespace: "service-ns", Name: "service"}, - Weight: 1, + Spec: L7RouteSpec{ + Rules: []RouteRule{ + { + BackendRefs: []BackendRef{ + { + SvcNsName: types.NamespacedName{Namespace: "service-ns", Name: "service"}, + Weight: 1, + }, }, + ValidMatches: true, + ValidFilters: true, }, - ValidMatches: true, - ValidFilters: true, }, }, } - validRouteNoServiceNsName := &Route{ + validRouteNoServiceNsName := &L7Route{ ParentRefs: []ParentRef{ { Attachment: &ParentRefAttachmentStatus{ @@ -178,28 +191,30 @@ func TestBuildReferencedServices(t *testing.T) { }, }, Valid: true, - Rules: []Rule{ - { - BackendRefs: []BackendRef{ - { - Weight: 1, + Spec: L7RouteSpec{ + Rules: []RouteRule{ + { + BackendRefs: []BackendRef{ + { + Weight: 1, + }, }, + ValidMatches: true, + ValidFilters: true, }, - ValidMatches: true, - ValidFilters: true, }, }, } tests := []struct { - routes map[types.NamespacedName]*Route + routes map[RouteKey]*L7Route exp map[types.NamespacedName]struct{} name string }{ { name: "normal route", - routes: map[types.NamespacedName]*Route{ - {Name: "normal-route"}: normalRoute, + routes: map[RouteKey]*L7Route{ + {NamespacedName: types.NamespacedName{Name: "normal-route"}}: normalRoute, }, exp: map[types.NamespacedName]struct{}{ {Namespace: "banana-ns", Name: "service"}: {}, @@ -207,8 +222,8 @@ func TestBuildReferencedServices(t *testing.T) { }, { name: "route with two services in one Rule", - routes: map[types.NamespacedName]*Route{ - {Name: "two-svc-one-rule"}: validRouteTwoServicesOneRule, + routes: map[RouteKey]*L7Route{ + {NamespacedName: types.NamespacedName{Name: "two-svc-one-rule"}}: validRouteTwoServicesOneRule, }, exp: map[types.NamespacedName]struct{}{ {Namespace: "service-ns", Name: "service"}: {}, @@ -217,8 +232,8 @@ func TestBuildReferencedServices(t *testing.T) { }, { name: "route with one service per rule", - routes: map[types.NamespacedName]*Route{ - {Name: "one-svc-per-rule"}: validRouteTwoServicesTwoRules, + routes: map[RouteKey]*L7Route{ + {NamespacedName: types.NamespacedName{Name: "one-svc-per-rule"}}: validRouteTwoServicesTwoRules, }, exp: map[types.NamespacedName]struct{}{ {Namespace: "service-ns", Name: "service"}: {}, @@ -227,9 +242,9 @@ func TestBuildReferencedServices(t *testing.T) { }, { name: "two valid routes with same services", - routes: map[types.NamespacedName]*Route{ - {Name: "one-svc-per-rule"}: validRouteTwoServicesTwoRules, - {Name: "two-svc-one-rule"}: validRouteTwoServicesOneRule, + routes: map[RouteKey]*L7Route{ + {NamespacedName: types.NamespacedName{Name: "one-svc-per-rule"}}: validRouteTwoServicesTwoRules, + {NamespacedName: types.NamespacedName{Name: "two-svc-one-rule"}}: validRouteTwoServicesOneRule, }, exp: map[types.NamespacedName]struct{}{ {Namespace: "service-ns", Name: "service"}: {}, @@ -238,9 +253,9 @@ func TestBuildReferencedServices(t *testing.T) { }, { name: "two valid routes with different services", - routes: map[types.NamespacedName]*Route{ - {Name: "one-svc-per-rule"}: validRouteTwoServicesTwoRules, - {Name: "normal-route"}: normalRoute, + routes: map[RouteKey]*L7Route{ + {NamespacedName: types.NamespacedName{Name: "one-svc-per-rule"}}: validRouteTwoServicesTwoRules, + {NamespacedName: types.NamespacedName{Name: "normal-route"}}: normalRoute, }, exp: map[types.NamespacedName]struct{}{ {Namespace: "service-ns", Name: "service"}: {}, @@ -249,24 +264,24 @@ func TestBuildReferencedServices(t *testing.T) { }, }, { - name: "invalid route", - routes: map[types.NamespacedName]*Route{ - {Name: "invalid-route"}: invalidRoute, + name: "invalid routes", + routes: map[RouteKey]*L7Route{ + {NamespacedName: types.NamespacedName{Name: "invalid-route"}}: invalidRoute, }, exp: nil, }, { name: "unattached route", - routes: map[types.NamespacedName]*Route{ - {Name: "unattached-route"}: unattachedRoute, + routes: map[RouteKey]*L7Route{ + {NamespacedName: types.NamespacedName{Name: "unattached-route"}}: unattachedRoute, }, exp: nil, }, { name: "combination of valid and invalid routes", - routes: map[types.NamespacedName]*Route{ - {Name: "normal-route"}: normalRoute, - {Name: "invalid-route"}: invalidRoute, + routes: map[RouteKey]*L7Route{ + {NamespacedName: types.NamespacedName{Name: "normal-route"}}: normalRoute, + {NamespacedName: types.NamespacedName{Name: "invalid-route"}}: invalidRoute, }, exp: map[types.NamespacedName]struct{}{ {Namespace: "banana-ns", Name: "service"}: {}, @@ -274,8 +289,8 @@ func TestBuildReferencedServices(t *testing.T) { }, { name: "route with many parentRefs and one is attached", - routes: map[types.NamespacedName]*Route{ - {Name: "multiple-parent-ref-route"}: attachedRouteWithManyParentRefs, + routes: map[RouteKey]*L7Route{ + {NamespacedName: types.NamespacedName{Name: "multiple-parent-ref-route"}}: attachedRouteWithManyParentRefs, }, exp: map[types.NamespacedName]struct{}{ {Namespace: "service-ns", Name: "service"}: {}, @@ -283,8 +298,8 @@ func TestBuildReferencedServices(t *testing.T) { }, { name: "valid route no service nsname", - routes: map[types.NamespacedName]*Route{ - {Name: "no-service-nsname"}: validRouteNoServiceNsName, + routes: map[RouteKey]*L7Route{ + {NamespacedName: types.NamespacedName{Name: "no-service-nsname"}}: validRouteNoServiceNsName, }, exp: nil, }, diff --git a/internal/mode/static/status/prepare_requests.go b/internal/mode/static/status/prepare_requests.go index 216b3d86d5..70c2c3611c 100644 --- a/internal/mode/static/status/prepare_requests.go +++ b/internal/mode/static/status/prepare_requests.go @@ -25,74 +25,108 @@ type NginxReloadResult struct { // PrepareRouteRequests prepares status UpdateRequests for the given Routes. func PrepareRouteRequests( - routes map[types.NamespacedName]*graph.Route, + routes map[graph.RouteKey]*graph.L7Route, transitionTime metav1.Time, nginxReloadRes NginxReloadResult, gatewayCtlrName string, ) []frameworkStatus.UpdateRequest { reqs := make([]frameworkStatus.UpdateRequest, 0, len(routes)) - for nsname, r := range routes { - parents := make([]v1.RouteParentStatus, 0, len(r.ParentRefs)) + for routeKey, r := range routes { - defaultConds := staticConds.NewDefaultRouteConditions() + routeStatus := prepareRouteStatus( + gatewayCtlrName, + r.ParentRefs, + r.Conditions, + nginxReloadRes, + transitionTime, + r.Source.GetGeneration(), + ) - for _, ref := range r.ParentRefs { - failedAttachmentCondCount := 0 - if ref.Attachment != nil && !ref.Attachment.Attached { - failedAttachmentCondCount = 1 + if r.RouteType == graph.RouteTypeHTTP { + status := v1.HTTPRouteStatus{ + RouteStatus: routeStatus, } - allConds := make([]conditions.Condition, 0, len(r.Conditions)+len(defaultConds)+failedAttachmentCondCount) - - // We add defaultConds first, so that any additional conditions will override them, which is - // ensured by DeduplicateConditions. - allConds = append(allConds, defaultConds...) - allConds = append(allConds, r.Conditions...) - if failedAttachmentCondCount == 1 { - allConds = append(allConds, ref.Attachment.FailedCondition) + + req := frameworkStatus.UpdateRequest{ + NsName: routeKey.NamespacedName, + ResourceType: &v1.HTTPRoute{}, + Setter: newHTTPRouteStatusSetter(status, gatewayCtlrName), } - if nginxReloadRes.Error != nil { - allConds = append( - allConds, - staticConds.NewRouteGatewayNotProgrammed(staticConds.RouteMessageFailedNginxReload), - ) + reqs = append(reqs, req) + } else if r.RouteType == graph.RouteTypeGRPC { + status := v1alpha2.GRPCRouteStatus{ + RouteStatus: routeStatus, } - routeRef := r.Source.Spec.ParentRefs[ref.Idx] + req := frameworkStatus.UpdateRequest{ + NsName: routeKey.NamespacedName, + ResourceType: &v1alpha2.GRPCRoute{}, + Setter: newGRPCRouteStatusSetter(status, gatewayCtlrName), + } - conds := conditions.DeduplicateConditions(allConds) - apiConds := conditions.ConvertConditions(conds, r.Source.Generation, transitionTime) + reqs = append(reqs, req) + } else { + panic(fmt.Sprintf("Unknown route type: %s", r.RouteType)) + } - ps := v1.RouteParentStatus{ - ParentRef: v1.ParentReference{ - Namespace: helpers.GetPointer(v1.Namespace(ref.Gateway.Namespace)), - Name: v1.ObjectName(ref.Gateway.Name), - SectionName: routeRef.SectionName, - }, - ControllerName: v1.GatewayController(gatewayCtlrName), - Conditions: apiConds, - } + } + + return reqs +} + +func prepareRouteStatus( + gatewayCtlrName string, + parentRefs []graph.ParentRef, + conds []conditions.Condition, + nginxReloadRes NginxReloadResult, + transitionTime metav1.Time, + srcGeneration int64, +) v1.RouteStatus { + parents := make([]v1.RouteParentStatus, 0, len(parentRefs)) + + defaultConds := staticConds.NewDefaultRouteConditions() - parents = append(parents, ps) + for _, ref := range parentRefs { + failedAttachmentCondCount := 0 + if ref.Attachment != nil && !ref.Attachment.Attached { + failedAttachmentCondCount = 1 } + allConds := make([]conditions.Condition, 0, len(conds)+len(defaultConds)+failedAttachmentCondCount) - status := v1.HTTPRouteStatus{ - RouteStatus: v1.RouteStatus{ - Parents: parents, - }, + // We add defaultConds first, so that any additional conditions will override them, which is + // ensured by DeduplicateConditions. + allConds = append(allConds, defaultConds...) + allConds = append(allConds, conds...) + if failedAttachmentCondCount == 1 { + allConds = append(allConds, ref.Attachment.FailedCondition) } - req := frameworkStatus.UpdateRequest{ - NsName: nsname, - ResourceType: &v1.HTTPRoute{}, - Setter: newHTTPRouteStatusSetter(status, gatewayCtlrName), + if nginxReloadRes.Error != nil { + allConds = append( + allConds, + staticConds.NewRouteGatewayNotProgrammed(staticConds.RouteMessageFailedNginxReload), + ) } - reqs = append(reqs, req) + conds := conditions.DeduplicateConditions(allConds) + apiConds := conditions.ConvertConditions(conds, srcGeneration, transitionTime) + + ps := v1.RouteParentStatus{ + ParentRef: v1.ParentReference{ + Namespace: helpers.GetPointer(v1.Namespace(ref.Gateway.Namespace)), + Name: v1.ObjectName(ref.Gateway.Name), + SectionName: ref.SectionName, + }, + ControllerName: v1.GatewayController(gatewayCtlrName), + Conditions: apiConds, + } + + parents = append(parents, ps) } - return reqs + return v1.RouteStatus{Parents: parents} } // PrepareGatewayClassRequests prepares status UpdateRequests for the given GatewayClasses. diff --git a/internal/mode/static/status/prepare_requests_test.go b/internal/mode/static/status/prepare_requests_test.go index 9ad3a07df7..cc34054f7c 100644 --- a/internal/mode/static/status/prepare_requests_test.go +++ b/internal/mode/static/status/prepare_requests_test.go @@ -42,193 +42,213 @@ func createK8sClientFor(resourceType client.Object) client.Client { return k8sClient } -func TestBuildRouteStatuses(t *testing.T) { - const gatewayCtlrName = "controller" +const gatewayCtlrName = "controller" - gwNsName := types.NamespacedName{Namespace: "test", Name: "gateway"} +var ( + gwNsName = types.NamespacedName{Namespace: "test", Name: "gateway"} + transitionTime = helpers.PrepareTimeForFakeClient(metav1.Now()) - invalidRouteCondition := conditions.Condition{ + invalidRouteCondition = conditions.Condition{ Type: "TestInvalidRoute", Status: metav1.ConditionTrue, } - invalidAttachmentCondition := conditions.Condition{ + invalidAttachmentCondition = conditions.Condition{ Type: "TestInvalidAttachment", Status: metav1.ConditionTrue, } - routes := map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-valid"}: { - Valid: true, - Source: &v1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - Name: "hr-valid", - Generation: 3, + commonRouteSpecValid = v1.CommonRouteSpec{ + ParentRefs: []v1.ParentReference{ + { + SectionName: helpers.GetPointer[v1.SectionName]("listener-80-1"), + }, + { + SectionName: helpers.GetPointer[v1.SectionName]("listener-80-2"), + }, + }, + } + + commonRouteSpecInvalid = v1.CommonRouteSpec{ + ParentRefs: []v1.ParentReference{ + { + SectionName: helpers.GetPointer[v1.SectionName]("listener-80-1"), + }, + }, + } + + parentRefsValid = []graph.ParentRef{ + { + Idx: 0, + Gateway: gwNsName, + SectionName: commonRouteSpecValid.ParentRefs[0].SectionName, + Attachment: &graph.ParentRefAttachmentStatus{ + Attached: true, + }, + }, + { + Idx: 1, + Gateway: gwNsName, + SectionName: commonRouteSpecValid.ParentRefs[1].SectionName, + Attachment: &graph.ParentRefAttachmentStatus{ + Attached: false, + FailedCondition: invalidAttachmentCondition, + }, + }, + } + + parentRefsInvalid = []graph.ParentRef{ + { + Idx: 0, + Gateway: gwNsName, + Attachment: nil, + SectionName: commonRouteSpecInvalid.ParentRefs[0].SectionName, + }, + } + + routeStatusValid = v1.RouteStatus{ + Parents: []v1.RouteParentStatus{ + { + ParentRef: v1.ParentReference{ + Namespace: helpers.GetPointer(v1.Namespace(gwNsName.Namespace)), + Name: v1.ObjectName(gwNsName.Name), + SectionName: helpers.GetPointer[v1.SectionName]("listener-80-1"), }, - Spec: v1.HTTPRouteSpec{ - CommonRouteSpec: v1.CommonRouteSpec{ - ParentRefs: []v1.ParentReference{ - { - SectionName: helpers.GetPointer[v1.SectionName]("listener-80-1"), - }, - { - SectionName: helpers.GetPointer[v1.SectionName]("listener-80-2"), - }, - }, + ControllerName: gatewayCtlrName, + Conditions: []metav1.Condition{ + { + Type: string(v1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + ObservedGeneration: 3, + LastTransitionTime: transitionTime, + Reason: string(v1.RouteReasonAccepted), + Message: "The route is accepted", + }, + { + Type: string(v1.RouteConditionResolvedRefs), + Status: metav1.ConditionTrue, + ObservedGeneration: 3, + LastTransitionTime: transitionTime, + Reason: string(v1.RouteReasonResolvedRefs), + Message: "All references are resolved", }, }, }, - ParentRefs: []graph.ParentRef{ - { - Idx: 0, - Gateway: gwNsName, - Attachment: &graph.ParentRefAttachmentStatus{ - Attached: true, - }, + { + ParentRef: v1.ParentReference{ + Namespace: helpers.GetPointer(v1.Namespace(gwNsName.Namespace)), + Name: v1.ObjectName(gwNsName.Name), + SectionName: helpers.GetPointer[v1.SectionName]("listener-80-2"), }, - { - Idx: 1, - Gateway: gwNsName, - Attachment: &graph.ParentRefAttachmentStatus{ - Attached: false, - FailedCondition: invalidAttachmentCondition, + ControllerName: gatewayCtlrName, + Conditions: []metav1.Condition{ + { + Type: string(v1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + ObservedGeneration: 3, + LastTransitionTime: transitionTime, + Reason: string(v1.RouteReasonAccepted), + Message: "The route is accepted", + }, + { + Type: string(v1.RouteConditionResolvedRefs), + Status: metav1.ConditionTrue, + ObservedGeneration: 3, + LastTransitionTime: transitionTime, + Reason: string(v1.RouteReasonResolvedRefs), + Message: "All references are resolved", + }, + { + Type: invalidAttachmentCondition.Type, + Status: metav1.ConditionTrue, + ObservedGeneration: 3, + LastTransitionTime: transitionTime, }, }, }, }, - {Namespace: "test", Name: "hr-invalid"}: { - Valid: false, - Conditions: []conditions.Condition{invalidRouteCondition}, - Source: &v1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - Name: "hr-invalid", - Generation: 3, + } + + routeStatusInvalid = v1.RouteStatus{ + Parents: []v1.RouteParentStatus{ + { + ParentRef: v1.ParentReference{ + Namespace: helpers.GetPointer(v1.Namespace(gwNsName.Namespace)), + Name: v1.ObjectName(gwNsName.Name), + SectionName: helpers.GetPointer[v1.SectionName]("listener-80-1"), }, - Spec: v1.HTTPRouteSpec{ - CommonRouteSpec: v1.CommonRouteSpec{ - ParentRefs: []v1.ParentReference{ - { - SectionName: helpers.GetPointer[v1.SectionName]("listener-80-1"), - }, - }, + ControllerName: gatewayCtlrName, + Conditions: []metav1.Condition{ + { + Type: string(v1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + ObservedGeneration: 3, + LastTransitionTime: transitionTime, + Reason: string(v1.RouteReasonAccepted), + Message: "The route is accepted", + }, + { + Type: string(v1.RouteConditionResolvedRefs), + Status: metav1.ConditionTrue, + ObservedGeneration: 3, + LastTransitionTime: transitionTime, + Reason: string(v1.RouteReasonResolvedRefs), + Message: "All references are resolved", + }, + { + Type: invalidRouteCondition.Type, + Status: metav1.ConditionTrue, + ObservedGeneration: 3, + LastTransitionTime: transitionTime, }, - }, - }, - ParentRefs: []graph.ParentRef{ - { - Idx: 0, - Gateway: gwNsName, - Attachment: nil, }, }, }, } +) - transitionTime := helpers.PrepareTimeForFakeClient(metav1.Now()) +func TestBuildHTTPRouteStatuses(t *testing.T) { + hrValid := &v1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "hr-valid", + Generation: 3, + }, + Spec: v1.HTTPRouteSpec{ + CommonRouteSpec: commonRouteSpecValid, + }, + } + hrInvalid := &v1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "hr-invalid", + Generation: 3, + }, + Spec: v1.HTTPRouteSpec{ + CommonRouteSpec: commonRouteSpecInvalid, + }, + } + routes := map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hrValid): { + Valid: true, + Source: hrValid, + ParentRefs: parentRefsValid, + RouteType: graph.RouteTypeHTTP, + }, + graph.CreateRouteKey(hrInvalid): { + Valid: false, + Conditions: []conditions.Condition{invalidRouteCondition}, + Source: hrInvalid, + ParentRefs: parentRefsInvalid, + RouteType: graph.RouteTypeHTTP, + }, + } expectedStatuses := map[types.NamespacedName]v1.HTTPRouteStatus{ {Namespace: "test", Name: "hr-valid"}: { - RouteStatus: v1.RouteStatus{ - Parents: []v1.RouteParentStatus{ - { - ParentRef: v1.ParentReference{ - Namespace: helpers.GetPointer(v1.Namespace(gwNsName.Namespace)), - Name: v1.ObjectName(gwNsName.Name), - SectionName: helpers.GetPointer[v1.SectionName]("listener-80-1"), - }, - ControllerName: gatewayCtlrName, - Conditions: []metav1.Condition{ - { - Type: string(v1.RouteConditionAccepted), - Status: metav1.ConditionTrue, - ObservedGeneration: 3, - LastTransitionTime: transitionTime, - Reason: string(v1.RouteReasonAccepted), - Message: "The route is accepted", - }, - { - Type: string(v1.RouteConditionResolvedRefs), - Status: metav1.ConditionTrue, - ObservedGeneration: 3, - LastTransitionTime: transitionTime, - Reason: string(v1.RouteReasonResolvedRefs), - Message: "All references are resolved", - }, - }, - }, - { - ParentRef: v1.ParentReference{ - Namespace: helpers.GetPointer(v1.Namespace(gwNsName.Namespace)), - Name: v1.ObjectName(gwNsName.Name), - SectionName: helpers.GetPointer[v1.SectionName]("listener-80-2"), - }, - ControllerName: gatewayCtlrName, - Conditions: []metav1.Condition{ - { - Type: string(v1.RouteConditionAccepted), - Status: metav1.ConditionTrue, - ObservedGeneration: 3, - LastTransitionTime: transitionTime, - Reason: string(v1.RouteReasonAccepted), - Message: "The route is accepted", - }, - { - Type: string(v1.RouteConditionResolvedRefs), - Status: metav1.ConditionTrue, - ObservedGeneration: 3, - LastTransitionTime: transitionTime, - Reason: string(v1.RouteReasonResolvedRefs), - Message: "All references are resolved", - }, - { - Type: invalidAttachmentCondition.Type, - Status: metav1.ConditionTrue, - ObservedGeneration: 3, - LastTransitionTime: transitionTime, - }, - }, - }, - }, - }, + RouteStatus: routeStatusValid, }, {Namespace: "test", Name: "hr-invalid"}: { - RouteStatus: v1.RouteStatus{ - Parents: []v1.RouteParentStatus{ - { - ParentRef: v1.ParentReference{ - Namespace: helpers.GetPointer(v1.Namespace(gwNsName.Namespace)), - Name: v1.ObjectName(gwNsName.Name), - SectionName: helpers.GetPointer[v1.SectionName]("listener-80-1"), - }, - ControllerName: gatewayCtlrName, - Conditions: []metav1.Condition{ - { - Type: string(v1.RouteConditionAccepted), - Status: metav1.ConditionTrue, - ObservedGeneration: 3, - LastTransitionTime: transitionTime, - Reason: string(v1.RouteReasonAccepted), - Message: "The route is accepted", - }, - { - Type: string(v1.RouteConditionResolvedRefs), - Status: metav1.ConditionTrue, - ObservedGeneration: 3, - LastTransitionTime: transitionTime, - Reason: string(v1.RouteReasonResolvedRefs), - Message: "All references are resolved", - }, - { - Type: invalidRouteCondition.Type, - Status: metav1.ConditionTrue, - ObservedGeneration: 3, - LastTransitionTime: transitionTime, - }, - }, - }, - }, - }, + RouteStatus: routeStatusInvalid, }, } @@ -258,34 +278,101 @@ func TestBuildRouteStatuses(t *testing.T) { } } +func TestBuildGRPCRouteStatuses(t *testing.T) { + grValid := &v1alpha2.GRPCRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "gr-valid", + Generation: 3, + }, + Spec: v1alpha2.GRPCRouteSpec{ + CommonRouteSpec: commonRouteSpecValid, + }, + } + grInvalid := &v1alpha2.GRPCRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "gr-invalid", + Generation: 3, + }, + Spec: v1alpha2.GRPCRouteSpec{ + CommonRouteSpec: commonRouteSpecInvalid, + }, + } + routes := map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(grValid): { + Valid: true, + Source: grValid, + ParentRefs: parentRefsValid, + RouteType: graph.RouteTypeGRPC, + }, + graph.CreateRouteKey(grInvalid): { + Valid: false, + Conditions: []conditions.Condition{invalidRouteCondition}, + Source: grInvalid, + ParentRefs: parentRefsInvalid, + RouteType: graph.RouteTypeGRPC, + }, + } + + expectedStatuses := map[types.NamespacedName]v1alpha2.GRPCRouteStatus{ + {Namespace: "test", Name: "gr-valid"}: { + RouteStatus: routeStatusValid, + }, + {Namespace: "test", Name: "gr-invalid"}: { + RouteStatus: routeStatusInvalid, + }, + } + + g := NewWithT(t) + + k8sClient := createK8sClientFor(&v1alpha2.GRPCRoute{}) + + for _, r := range routes { + err := k8sClient.Create(context.Background(), r.Source) + g.Expect(err).ToNot(HaveOccurred()) + } + + updater := statusFramework.NewUpdater(k8sClient, zap.New()) + + reqs := PrepareRouteRequests(routes, transitionTime, NginxReloadResult{}, gatewayCtlrName) + + updater.Update(context.Background(), reqs...) + + g.Expect(reqs).To(HaveLen(len(expectedStatuses))) + + for nsname, expected := range expectedStatuses { + var hr v1alpha2.GRPCRoute + + err := k8sClient.Get(context.Background(), nsname, &hr) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(helpers.Diff(expected, hr.Status)).To(BeEmpty()) + } +} + func TestBuildRouteStatusesNginxErr(t *testing.T) { const gatewayCtlrName = "controller" + hr1 := &v1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "hr-valid", + Generation: 3, + }, + Spec: v1.HTTPRouteSpec{ + CommonRouteSpec: commonRouteSpecValid, + }, + } + + routeKey := graph.CreateRouteKey(hr1) + gwNsName := types.NamespacedName{Namespace: "test", Name: "gateway"} - routeNsName := types.NamespacedName{Namespace: "test", Name: "hr-valid"} - routes := map[types.NamespacedName]*graph.Route{ - routeNsName: { - Valid: true, - Source: &v1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: routeNsName.Namespace, - Name: routeNsName.Name, - Generation: 3, - }, - Spec: v1.HTTPRouteSpec{ - CommonRouteSpec: v1.CommonRouteSpec{ - ParentRefs: []v1.ParentReference{ - { - SectionName: helpers.GetPointer[v1.SectionName]("listener-80-1"), - }, - { - SectionName: helpers.GetPointer[v1.SectionName]("listener-80-2"), - }, - }, - }, - }, - }, + routes := map[graph.RouteKey]*graph.L7Route{ + routeKey: { + Valid: true, + RouteType: graph.RouteTypeHTTP, + Source: hr1, ParentRefs: []graph.ParentRef{ { Idx: 0, @@ -293,6 +380,7 @@ func TestBuildRouteStatusesNginxErr(t *testing.T) { Attachment: &graph.ParentRefAttachmentStatus{ Attached: true, }, + SectionName: commonRouteSpecValid.ParentRefs[0].SectionName, }, }, }, @@ -357,7 +445,7 @@ func TestBuildRouteStatusesNginxErr(t *testing.T) { var hr v1.HTTPRoute - err := k8sClient.Get(context.Background(), routeNsName, &hr) + err := k8sClient.Get(context.Background(), routeKey.NamespacedName, &hr) g.Expect(err).ToNot(HaveOccurred()) g.Expect(helpers.Diff(expectedStatus, hr.Status)).To(BeEmpty()) } @@ -547,6 +635,8 @@ func TestBuildGatewayStatuses(t *testing.T) { }, } + routeKey := graph.RouteKey{NamespacedName: types.NamespacedName{Namespace: "test", Name: "hr-1"}} + tests := []struct { nginxReloadRes NginxReloadResult gateway *graph.Gateway @@ -625,18 +715,14 @@ func TestBuildGatewayStatuses(t *testing.T) { Source: createGateway(), Listeners: []*graph.Listener{ { - Name: "listener-valid-1", - Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-1"}: {}, - }, + Name: "listener-valid-1", + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{routeKey: {}}, }, { - Name: "listener-valid-2", - Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-1"}: {}, - }, + Name: "listener-valid-2", + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{routeKey: {}}, }, }, Valid: true, @@ -683,11 +769,9 @@ func TestBuildGatewayStatuses(t *testing.T) { Source: createGateway(), Listeners: []*graph.Listener{ { - Name: "listener-valid", - Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-1"}: {}, - }, + Name: "listener-valid", + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{routeKey: {}}, }, { Name: "listener-invalid", @@ -877,11 +961,9 @@ func TestBuildGatewayStatuses(t *testing.T) { Conditions: staticConds.NewDefaultGatewayConditions(), Listeners: []*graph.Listener{ { - Name: "listener-valid", - Valid: true, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-1"}: {}, - }, + Name: "listener-valid", + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{routeKey: {}}, }, }, }, diff --git a/internal/mode/static/status/status_setters.go b/internal/mode/static/status/status_setters.go index 90fab63d04..d745de6214 100644 --- a/internal/mode/static/status/status_setters.go +++ b/internal/mode/static/status/status_setters.go @@ -5,7 +5,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/apis/v1alpha2" ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" @@ -89,7 +89,7 @@ func newHTTPRouteStatusSetter(status gatewayv1.HTTPRouteStatus, gatewayCtlrName } } - if hrStatusEqual(gatewayCtlrName, hr.Status, status) { + if routeStatusEqual(gatewayCtlrName, hr.Status.Parents, status.Parents) { return false } @@ -99,18 +99,39 @@ func newHTTPRouteStatusSetter(status gatewayv1.HTTPRouteStatus, gatewayCtlrName } } -func hrStatusEqual(gatewayCtlrName string, prev, cur gatewayv1.HTTPRouteStatus) bool { +func newGRPCRouteStatusSetter(status v1alpha2.GRPCRouteStatus, gatewayCtlrName string) frameworkStatus.Setter { + return func(object client.Object) (wasSet bool) { + gr := object.(*v1alpha2.GRPCRoute) + + // keep all the parent statuses that belong to other controllers + for _, os := range gr.Status.Parents { + if string(os.ControllerName) != gatewayCtlrName { + status.Parents = append(status.Parents, os) + } + } + + if routeStatusEqual(gatewayCtlrName, gr.Status.Parents, status.Parents) { + return false + } + + gr.Status = status + + return true + } +} + +func routeStatusEqual(gatewayCtlrName string, prevParents, curParents []gatewayv1.RouteParentStatus) bool { // Since other controllers may update HTTPRoute status we can't assume anything about the order of the statuses, // and we have to ignore statuses written by other controllers when checking for equality. // Therefore, we can't use slices.EqualFunc here because it cares about the order. // First, we check if the prev status has any RouteParentStatuses that are no longer present in the cur status. - for _, prevParent := range prev.Parents { + for _, prevParent := range prevParents { if prevParent.ControllerName != gatewayv1.GatewayController(gatewayCtlrName) { continue } - exists := slices.ContainsFunc(cur.Parents, func(curParent gatewayv1.RouteParentStatus) bool { + exists := slices.ContainsFunc(curParents, func(curParent gatewayv1.RouteParentStatus) bool { return routeParentStatusEqual(prevParent, curParent) }) @@ -120,8 +141,8 @@ func hrStatusEqual(gatewayCtlrName string, prev, cur gatewayv1.HTTPRouteStatus) } // Then, we check if the cur status has any RouteParentStatuses that are no longer present in the prev status. - for _, curParent := range cur.Parents { - exists := slices.ContainsFunc(prev.Parents, func(prevParent gatewayv1.RouteParentStatus) bool { + for _, curParent := range curParents { + exists := slices.ContainsFunc(prevParents, func(prevParent gatewayv1.RouteParentStatus) bool { return routeParentStatusEqual(curParent, prevParent) }) @@ -169,16 +190,16 @@ func newGatewayClassStatusSetter(status gatewayv1.GatewayClassStatus) frameworkS } func newBackendTLSPolicyStatusSetter( - status gatewayv1alpha2.PolicyStatus, + status v1alpha2.PolicyStatus, gatewayCtlrName string, ) frameworkStatus.Setter { return func(object client.Object) (wasSet bool) { - btp := helpers.MustCastObject[*gatewayv1alpha2.BackendTLSPolicy](object) + btp := helpers.MustCastObject[*v1alpha2.BackendTLSPolicy](object) // maxAncestors is the max number of ancestor statuses which is the sum of all new ancestor statuses and all old // ancestor statuses. maxAncestors := 1 + len(btp.Status.Ancestors) - ancestors := make([]gatewayv1alpha2.PolicyAncestorStatus, 0, maxAncestors) + ancestors := make([]v1alpha2.PolicyAncestorStatus, 0, maxAncestors) // keep all the ancestor statuses that belong to other controllers for _, os := range btp.Status.Ancestors { @@ -199,7 +220,7 @@ func newBackendTLSPolicyStatusSetter( } } -func btpStatusEqual(gatewayCtlrName string, prev, cur gatewayv1alpha2.PolicyStatus) bool { +func btpStatusEqual(gatewayCtlrName string, prev, cur v1alpha2.PolicyStatus) bool { // Since other controllers may update BackendTLSPolicy status we can't assume anything about the order of the // statuses, and we have to ignore statuses written by other controllers when checking for equality. // Therefore, we can't use slices.EqualFunc here because it cares about the order. @@ -210,7 +231,7 @@ func btpStatusEqual(gatewayCtlrName string, prev, cur gatewayv1alpha2.PolicyStat continue } - exists := slices.ContainsFunc(cur.Ancestors, func(curAncestor gatewayv1alpha2.PolicyAncestorStatus) bool { + exists := slices.ContainsFunc(cur.Ancestors, func(curAncestor v1alpha2.PolicyAncestorStatus) bool { return btpAncestorStatusEqual(prevAncestor, curAncestor) }) @@ -221,7 +242,7 @@ func btpStatusEqual(gatewayCtlrName string, prev, cur gatewayv1alpha2.PolicyStat // Then, we check if the cur status has any PolicyAncestorStatuses that are no longer present in the prev status. for _, curParent := range cur.Ancestors { - exists := slices.ContainsFunc(prev.Ancestors, func(prevAncestor gatewayv1alpha2.PolicyAncestorStatus) bool { + exists := slices.ContainsFunc(prev.Ancestors, func(prevAncestor v1alpha2.PolicyAncestorStatus) bool { return btpAncestorStatusEqual(curParent, prevAncestor) }) @@ -233,7 +254,7 @@ func btpStatusEqual(gatewayCtlrName string, prev, cur gatewayv1alpha2.PolicyStat return true } -func btpAncestorStatusEqual(p1, p2 gatewayv1alpha2.PolicyAncestorStatus) bool { +func btpAncestorStatusEqual(p1, p2 v1alpha2.PolicyAncestorStatus) bool { if p1.ControllerName != p2.ControllerName { return false } diff --git a/internal/mode/static/status/status_setters_test.go b/internal/mode/static/status/status_setters_test.go index 9078f420e7..4c060fe930 100644 --- a/internal/mode/static/status/status_setters_test.go +++ b/internal/mode/static/status/status_setters_test.go @@ -6,7 +6,7 @@ import ( . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/apis/v1alpha2" ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" @@ -299,6 +299,181 @@ func TestNewHTTPRouteStatusSetter(t *testing.T) { } } +func TestNewGRPCRouteStatusSetter(t *testing.T) { + const ( + controllerName = "controller" + otherControllerName = "different" + ) + + tests := []struct { + name string + status, newStatus, expStatus v1alpha2.GRPCRouteStatus + expStatusSet bool + }{ + { + name: "GRPCRoute has no status", + newStatus: v1alpha2.GRPCRouteStatus{ + RouteStatus: gatewayv1.RouteStatus{ + Parents: []gatewayv1.RouteParentStatus{ + { + ParentRef: gatewayv1.ParentReference{}, + ControllerName: gatewayv1.GatewayController(controllerName), + Conditions: []metav1.Condition{{Message: "new condition"}}, + }, + }, + }, + }, + expStatus: v1alpha2.GRPCRouteStatus{ + RouteStatus: gatewayv1.RouteStatus{ + Parents: []gatewayv1.RouteParentStatus{ + { + ParentRef: gatewayv1.ParentReference{}, + ControllerName: gatewayv1.GatewayController(controllerName), + Conditions: []metav1.Condition{{Message: "new condition"}}, + }, + }, + }, + }, + expStatusSet: true, + }, + { + name: "GRPCRoute has old status", + newStatus: v1alpha2.GRPCRouteStatus{ + RouteStatus: gatewayv1.RouteStatus{ + Parents: []gatewayv1.RouteParentStatus{ + { + ParentRef: gatewayv1.ParentReference{}, + ControllerName: gatewayv1.GatewayController(controllerName), + Conditions: []metav1.Condition{{Message: "new condition"}}, + }, + }, + }, + }, + status: v1alpha2.GRPCRouteStatus{ + RouteStatus: gatewayv1.RouteStatus{ + Parents: []gatewayv1.RouteParentStatus{ + { + ParentRef: gatewayv1.ParentReference{}, + ControllerName: gatewayv1.GatewayController(controllerName), + Conditions: []metav1.Condition{{Message: "old condition"}}, + }, + }, + }, + }, + expStatus: v1alpha2.GRPCRouteStatus{ + RouteStatus: gatewayv1.RouteStatus{ + Parents: []gatewayv1.RouteParentStatus{ + { + ParentRef: gatewayv1.ParentReference{}, + ControllerName: gatewayv1.GatewayController(controllerName), + Conditions: []metav1.Condition{{Message: "new condition"}}, + }, + }, + }, + }, + expStatusSet: true, + }, + { + name: "GRPCRoute has old status, keep other controller statuses", + newStatus: v1alpha2.GRPCRouteStatus{ + RouteStatus: gatewayv1.RouteStatus{ + Parents: []gatewayv1.RouteParentStatus{ + { + ParentRef: gatewayv1.ParentReference{}, + ControllerName: gatewayv1.GatewayController(controllerName), + Conditions: []metav1.Condition{{Message: "new condition"}}, + }, + }, + }, + }, + status: v1alpha2.GRPCRouteStatus{ + RouteStatus: gatewayv1.RouteStatus{ + Parents: []gatewayv1.RouteParentStatus{ + { + ParentRef: gatewayv1.ParentReference{}, + ControllerName: gatewayv1.GatewayController(otherControllerName), + Conditions: []metav1.Condition{{Message: "some condition"}}, + }, + { + ParentRef: gatewayv1.ParentReference{}, + ControllerName: gatewayv1.GatewayController(controllerName), + Conditions: []metav1.Condition{{Message: "old condition"}}, + }, + }, + }, + }, + expStatus: v1alpha2.GRPCRouteStatus{ + RouteStatus: gatewayv1.RouteStatus{ + Parents: []gatewayv1.RouteParentStatus{ + { + ParentRef: gatewayv1.ParentReference{}, + ControllerName: gatewayv1.GatewayController(controllerName), + Conditions: []metav1.Condition{{Message: "new condition"}}, + }, + { + ParentRef: gatewayv1.ParentReference{}, + ControllerName: gatewayv1.GatewayController(otherControllerName), + Conditions: []metav1.Condition{{Message: "some condition"}}, + }, + }, + }, + }, + expStatusSet: true, + }, + { + name: "GRPCRoute has same status", + newStatus: v1alpha2.GRPCRouteStatus{ + RouteStatus: gatewayv1.RouteStatus{ + Parents: []gatewayv1.RouteParentStatus{ + { + ParentRef: gatewayv1.ParentReference{}, + ControllerName: gatewayv1.GatewayController(controllerName), + Conditions: []metav1.Condition{{Message: "same condition"}}, + }, + }, + }, + }, + status: v1alpha2.GRPCRouteStatus{ + RouteStatus: gatewayv1.RouteStatus{ + Parents: []gatewayv1.RouteParentStatus{ + { + ParentRef: gatewayv1.ParentReference{}, + ControllerName: gatewayv1.GatewayController(controllerName), + Conditions: []metav1.Condition{{Message: "same condition"}}, + }, + }, + }, + }, + expStatus: v1alpha2.GRPCRouteStatus{ + RouteStatus: gatewayv1.RouteStatus{ + Parents: []gatewayv1.RouteParentStatus{ + { + ParentRef: gatewayv1.ParentReference{}, + ControllerName: gatewayv1.GatewayController(controllerName), + Conditions: []metav1.Condition{{Message: "same condition"}}, + }, + }, + }, + }, + expStatusSet: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + setter := newGRPCRouteStatusSetter(test.newStatus, controllerName) + obj := &v1alpha2.GRPCRoute{Status: test.status} + + statusSet := setter(obj) + + g.Expect(statusSet).To(Equal(test.expStatusSet)) + g.Expect(obj.Status).To(Equal(test.expStatus)) + }) + } +} + func TestNewGatewayClassStatusSetter(t *testing.T) { tests := []struct { name string @@ -357,21 +532,21 @@ func TestNewBackendTLSPolicyStatusSetter(t *testing.T) { tests := []struct { name string - status, newStatus, expStatus gatewayv1alpha2.PolicyStatus + status, newStatus, expStatus v1alpha2.PolicyStatus expStatusSet bool }{ { name: "BackendTLSPolicy has no status", - newStatus: gatewayv1alpha2.PolicyStatus{ - Ancestors: []gatewayv1alpha2.PolicyAncestorStatus{ + newStatus: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ { ControllerName: controllerName, Conditions: []metav1.Condition{{Message: "new condition"}}, }, }, }, - expStatus: gatewayv1alpha2.PolicyStatus{ - Ancestors: []gatewayv1alpha2.PolicyAncestorStatus{ + expStatus: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ { ControllerName: controllerName, Conditions: []metav1.Condition{{Message: "new condition"}}, @@ -382,24 +557,24 @@ func TestNewBackendTLSPolicyStatusSetter(t *testing.T) { }, { name: "BackendTLSPolicy has old status", - newStatus: gatewayv1alpha2.PolicyStatus{ - Ancestors: []gatewayv1alpha2.PolicyAncestorStatus{ + newStatus: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ { ControllerName: controllerName, Conditions: []metav1.Condition{{Message: "new condition"}}, }, }, }, - status: gatewayv1alpha2.PolicyStatus{ - Ancestors: []gatewayv1alpha2.PolicyAncestorStatus{ + status: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ { ControllerName: controllerName, Conditions: []metav1.Condition{{Message: "old condition"}}, }, }, }, - expStatus: gatewayv1alpha2.PolicyStatus{ - Ancestors: []gatewayv1alpha2.PolicyAncestorStatus{ + expStatus: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ { ControllerName: controllerName, Conditions: []metav1.Condition{{Message: "new condition"}}, @@ -410,16 +585,16 @@ func TestNewBackendTLSPolicyStatusSetter(t *testing.T) { }, { name: "BackendTLSPolicy has old status and other controller status", - newStatus: gatewayv1alpha2.PolicyStatus{ - Ancestors: []gatewayv1alpha2.PolicyAncestorStatus{ + newStatus: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ { ControllerName: controllerName, Conditions: []metav1.Condition{{Message: "new condition"}}, }, }, }, - status: gatewayv1alpha2.PolicyStatus{ - Ancestors: []gatewayv1alpha2.PolicyAncestorStatus{ + status: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ { ControllerName: controllerName, Conditions: []metav1.Condition{{Message: "old condition"}}, @@ -430,8 +605,8 @@ func TestNewBackendTLSPolicyStatusSetter(t *testing.T) { }, }, }, - expStatus: gatewayv1alpha2.PolicyStatus{ - Ancestors: []gatewayv1alpha2.PolicyAncestorStatus{ + expStatus: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ { ControllerName: otherControllerName, Conditions: []metav1.Condition{{Message: "some condition"}}, @@ -446,24 +621,24 @@ func TestNewBackendTLSPolicyStatusSetter(t *testing.T) { }, { name: "BackendTLSPolicy has same status", - newStatus: gatewayv1alpha2.PolicyStatus{ - Ancestors: []gatewayv1alpha2.PolicyAncestorStatus{ + newStatus: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ { ControllerName: controllerName, Conditions: []metav1.Condition{{Message: "same condition"}}, }, }, }, - status: gatewayv1alpha2.PolicyStatus{ - Ancestors: []gatewayv1alpha2.PolicyAncestorStatus{ + status: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ { ControllerName: controllerName, Conditions: []metav1.Condition{{Message: "same condition"}}, }, }, }, - expStatus: gatewayv1alpha2.PolicyStatus{ - Ancestors: []gatewayv1alpha2.PolicyAncestorStatus{ + expStatus: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ { ControllerName: controllerName, Conditions: []metav1.Condition{{Message: "same condition"}}, @@ -479,7 +654,7 @@ func TestNewBackendTLSPolicyStatusSetter(t *testing.T) { g := NewWithT(t) setter := newBackendTLSPolicyStatusSetter(test.newStatus, controllerName) - obj := &gatewayv1alpha2.BackendTLSPolicy{Status: test.status} + obj := &v1alpha2.BackendTLSPolicy{Status: test.status} statusSet := setter(obj) @@ -818,7 +993,7 @@ func TestHRStatusEqual(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { g := NewWithT(t) - equal := hrStatusEqual("ours", test.prevStatus, test.curStatus) + equal := routeStatusEqual("ours", test.prevStatus.Parents, test.curStatus.Parents) g.Expect(equal).To(Equal(test.expEqual)) }) } @@ -977,15 +1152,15 @@ func TestEqualPointers(t *testing.T) { } func TestBtpStatusEqual(t *testing.T) { - getPolicyStatus := func(ancestorName, ancestorNs, ctlrName string) gatewayv1alpha2.PolicyStatus { - return gatewayv1alpha2.PolicyStatus{ - Ancestors: []gatewayv1alpha2.PolicyAncestorStatus{ + getPolicyStatus := func(ancestorName, ancestorNs, ctlrName string) v1alpha2.PolicyStatus { + return v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ { AncestorRef: gatewayv1.ParentReference{ Namespace: helpers.GetPointer[gatewayv1.Namespace]((gatewayv1.Namespace)(ancestorNs)), - Name: gatewayv1alpha2.ObjectName(ancestorName), + Name: v1alpha2.ObjectName(ancestorName), }, - ControllerName: gatewayv1alpha2.GatewayController(ctlrName), + ControllerName: v1alpha2.GatewayController(ctlrName), Conditions: []metav1.Condition{{Type: "otherType", Status: "otherStatus"}}, }, }, @@ -1000,8 +1175,8 @@ func TestBtpStatusEqual(t *testing.T) { tests := []struct { name string controllerName string - previous gatewayv1alpha2.PolicyStatus - current gatewayv1alpha2.PolicyStatus + previous v1alpha2.PolicyStatus + current v1alpha2.PolicyStatus expEqual bool }{ { diff --git a/internal/mode/static/telemetry/collector_test.go b/internal/mode/static/telemetry/collector_test.go index 22bb40c39b..f367b06933 100644 --- a/internal/mode/static/telemetry/collector_test.go +++ b/internal/mode/static/telemetry/collector_test.go @@ -278,10 +278,10 @@ var _ = Describe("Collector", Ordered, func() { {Name: "ignoredGw1"}: {}, {Name: "ignoredGw2"}: {}, }, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-1"}: {}, - {Namespace: "test", Name: "hr-2"}: {}, - {Namespace: "test", Name: "hr-3"}: {}, + Routes: map[graph.RouteKey]*graph.L7Route{ + {NamespacedName: types.NamespacedName{Namespace: "test", Name: "hr-1"}, RouteType: graph.RouteTypeHTTP}: {}, + {NamespacedName: types.NamespacedName{Namespace: "test", Name: "hr-2"}, RouteType: graph.RouteTypeHTTP}: {}, + {NamespacedName: types.NamespacedName{Namespace: "test", Name: "hr-3"}, RouteType: graph.RouteTypeHTTP}: {}, }, ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ client.ObjectKeyFromObject(secret1): { @@ -475,8 +475,8 @@ var _ = Describe("Collector", Ordered, func() { graph1 = &graph.Graph{ GatewayClass: &graph.GatewayClass{}, Gateway: &graph.Gateway{}, - Routes: map[types.NamespacedName]*graph.Route{ - {Namespace: "test", Name: "hr-1"}: {}, + Routes: map[graph.RouteKey]*graph.L7Route{ + {NamespacedName: types.NamespacedName{Namespace: "test", Name: "hr-1"}, RouteType: graph.RouteTypeHTTP}: {}, }, ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ client.ObjectKeyFromObject(secret): { diff --git a/site/content/overview/gateway-api-compatibility.md b/site/content/overview/gateway-api-compatibility.md index 2b411b7b2f..82e25523f1 100644 --- a/site/content/overview/gateway-api-compatibility.md +++ b/site/content/overview/gateway-api-compatibility.md @@ -9,17 +9,18 @@ docs: "DOCS-1412" ## Summary {{< bootstrap-table "table table-striped table-bordered" >}} -| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | -| ------------------------------------- | ------------------ | ---------------------- | ------------------------------------- | ----------- | -| [GatewayClass](#gatewayclass) | Supported | Not supported | Not supported | v1 | -| [Gateway](#gateway) | Supported | Not supported | Not supported | v1 | -| [HTTPRoute](#httproute) | Supported | Partially supported | Not supported | v1 | -| [ReferenceGrant](#referencegrant) | Supported | N/A | Not supported | v1beta1 | -| [TLSRoute](#tlsroute) | Not supported | Not supported | Not supported | N/A | -| [TCPRoute](#tcproute) | Not supported | Not supported | Not supported | N/A | -| [UDPRoute](#udproute) | Not supported | Not supported | Not supported | N/A | -| [BackendTLSPolicy](#backendtlspolicy) | Supported | Supported | Not supported | v1alpha2 | -| [Custom policies](#custom-policies) | Not supported | N/A | Not supported | N/A | +| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | +| ------------------------------------- | ------------------- | ---------------------- | ------------------------------------- | ----------- | +| [GatewayClass](#gatewayclass) | Supported | Not supported | Not supported | v1 | +| [Gateway](#gateway) | Supported | Not supported | Not supported | v1 | +| [HTTPRoute](#httproute) | Supported | Partially supported | Not supported | v1 | +| [ReferenceGrant](#referencegrant) | Supported | N/A | Not supported | v1beta1 | +| [GRPCRoute](#grpcroute) | Partially Supported | Not supported | Not supported | v1alpha2 | +| [TLSRoute](#tlsroute) | Not supported | Not supported | Not supported | N/A | +| [TCPRoute](#tcproute) | Not supported | Not supported | Not supported | N/A | +| [UDPRoute](#udproute) | Not supported | Not supported | Not supported | N/A | +| [BackendTLSPolicy](#backendtlspolicy) | Supported | Supported | Not supported | v1alpha2 | +| [Custom policies](#custom-policies) | Not supported | N/A | Not supported | N/A | {{< /bootstrap-table >}} --- @@ -180,6 +181,46 @@ See the [static-mode]({{< relref "/reference/cli-help.md#static-mode">}}) comman --- +### GRPCRoute + +{{< bootstrap-table "table table-striped table-bordered" >}} +| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | +| --------- | ------------------- | ---------------------- | ------------------------------------- | ----------- | +| GRPCRoute | Partially Supported | Not supported | Not supported | v1alpha2 | +{{< /bootstrap-table >}} + +**Fields**: + +- `spec` + - `parentRefs`: Partially supported. Port not supported. + - `hostnames`: Supported. + - `rules` + - `matches` + - `method`: Partially supported. Only `Exact` type with both `method.service` and `method.method` specified. + - `headers`: Partially supported. Only `Exact` type. + - `filters`: Not supported + - `backendRefs`: Partially supported. Backend ref `filters` are not supported. +- `status` + - `parents` + - `parentRef`: Supported. + - `controllerName`: Supported. + - `conditions`: Partially supported. Supported (Condition/Status/Reason): + - `Accepted/True/Accepted` + - `Accepted/False/NoMatchingListenerHostname` + - `Accepted/False/NoMatchingParent` + - `Accepted/False/NotAllowedByListeners` + - `Accepted/False/UnsupportedValue`: Custom reason for when the GRPCRoute includes an invalid or unsupported value. + - `Accepted/False/InvalidListener`: Custom reason for when the GRPCRoute references an invalid listener. + - `Accepted/False/GatewayNotProgrammed`: Custom reason for when the Gateway is not Programmed. GRPCRoute can be valid and configured, but will maintain this status as long as the Gateway is not Programmed. + - `ResolvedRefs/True/ResolvedRefs` + - `ResolvedRefs/False/InvalidKind` + - `ResolvedRefs/False/RefNotPermitted` + - `ResolvedRefs/False/BackendNotFound` + - `ResolvedRefs/False/UnsupportedValue`: Custom reason for when one of the GRPCRoute rules has a backendRef with an unsupported value. + - `PartiallyInvalid/True/UnsupportedValue` + +--- + ### ReferenceGrant {{< bootstrap-table "table table-striped table-bordered" >}}