From 4b4fd5383294f94293e687fac2661cce23cbb11e Mon Sep 17 00:00:00 2001 From: Michael Pleshakov Date: Tue, 23 Aug 2022 20:12:54 -0600 Subject: [PATCH 1/3] Implement request redirect filter in HTTPRoute rule This commit implements the request redirect filter as part of the routing rule in the HTTPRoute. A common use-case for a request redirect is redirecting HTTP requests to HTTPS. The commit updates the HTTPS termination example to include HTTPS redirect configuration. Notes: - The experimental 'path' field of 'requestRedirect' is out of scope. - The validation of the fields of `requestRedirect` is not implemented. It is left to be done in a separate component responsible for validation with FIXMEs added to the relevant code locations. - If multiple redirect filters are configured, NGINX Kubernetes Gateway will choose the first one and ignore the rest. - NGINX will always redirect a request even if the request has already been redirected. Thus, any backendRefs defined in the routing rule will be ignored. However, that "always redirect" behavior is not specified by the Gateway API. As a result, we might need to change our implementation if different behavior becomes specified by the Gateway API in the future. --- docs/gateway-api-compatibility.md.md | 5 +- examples/https-termination/README.md | 51 ++- examples/https-termination/cafe-routes.yaml | 17 + examples/https-termination/gateway.yaml | 3 + internal/helpers/helpers.go | 5 + internal/nginx/config/generator.go | 107 ++++- internal/nginx/config/generator_test.go | 411 +++++++++++++++----- internal/nginx/config/http.go | 6 +- internal/nginx/config/template.go | 2 +- internal/state/configuration.go | 5 + internal/state/configuration_test.go | 64 +++ 11 files changed, 557 insertions(+), 119 deletions(-) diff --git a/docs/gateway-api-compatibility.md.md b/docs/gateway-api-compatibility.md.md index 66ef575b96..3c7fd43a9e 100644 --- a/docs/gateway-api-compatibility.md.md +++ b/docs/gateway-api-compatibility.md.md @@ -87,7 +87,10 @@ Fields: * `headers` - partially supported. Only `Exact` type. * `queryParams` - partially supported. Only `Exact` type. * `method` - supported. - * `filters` - not supported. + * `filters` + * `type` - supported. + * `requestRedirect` - supported except for the experimental `path` field. If multiple filters with `requestRedirect` are configured, NGINX Kubernetes Gateway will choose the first one and ignore the rest. + * `requestHeaderModifier`, `requestMirror`, `urlRewrite`, `extensionRef` - not supported. * `backendRefs` - partially supported. Only a single backend ref without support for `weight`. Backend ref `filters` are not supported. NGINX Kubernetes Gateway will use the IP of the Service as a backend, not the IPs of the corresponding Pods. Watching for Service updates is not supported. * `status` * `parents` diff --git a/examples/https-termination/README.md b/examples/https-termination/README.md index 0c53bc15e1..4265d42f9b 100644 --- a/examples/https-termination/README.md +++ b/examples/https-termination/README.md @@ -1,6 +1,6 @@ # HTTPS Termination Example -In this example we expand on the simple [cafe-example](../cafe-example) by adding HTTPS termination to our routes. +In this example we expand on the simple [cafe-example](../cafe-example) by adding HTTPS termination to our routes and an HTTPS redirect from port 80 to 443. ## Running the Example @@ -14,10 +14,11 @@ In this example we expand on the simple [cafe-example](../cafe-example) by addin GW_IP=XXX.YYY.ZZZ.III ``` -1. Save the HTTPS port of NGINX Kubernetes Gateway: +1. Save the ports of NGINX Kubernetes Gateway: ``` - GW_HTTPS_PORT=port + GW_HTTP_PORT= + GW_HTTPS_PORT= ``` ## 2. Deploy the Cafe Application @@ -52,26 +53,60 @@ In this example we expand on the simple [cafe-example](../cafe-example) by addin kubectl apply -f gateway.yaml ``` - This [gateway](./gateway.yaml) configures an `https` listener is to terminate TLS connections using the `cafe-secret` we created in the step 1. + This [Gateway](./gateway.yaml) configures: + * `http` listener for HTTP traffic + * `https` listener for HTTPS traffic. It terminates TLS connections using the `cafe-secret` we created in the step 1. 1. Create the `HTTPRoute` resources: ``` kubectl apply -f cafe-routes.yaml ``` - To configure HTTPS termination for our cafe application, we will bind the `https` listener to our `HTTPRoutes` in [cafe-routes.yaml](./cafe-routes.yaml) using the [`parentReference`](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.ParentReference) field: + To configure HTTPS termination for our cafe application, we will bind our `coffee` and `tea` HTTPRoutes to the `https` listener in [cafe-routes.yaml](./cafe-routes.yaml) using the [`parentReference`](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.ParentReference) field: ```yaml parentRefs: - name: gateway - namespace: default sectionName: https ``` + To configure an HTTPS redirect from port 80 to 443, we will bind the special `cafe-tls-redirect` HTTPRoute with a [`HTTPRequestRedirectFilter`](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRequestRedirectFilter) to the `http` listener: + + ```yaml + parentRefs: + - name: gateway + sectionName: http + ``` + ## 4. Test the Application -To access the application, we will use `curl` to send requests to the `coffee` and `tea` Services. -Since our certificate is self-signed, we'll use curl's `--insecure` option to turn off certificate verification. +To access the application, we will use `curl` to send requests to the `coffee` and `tea` Services. First, we will access the application over HTTP to test that the HTTPS redirect works. Then we will use HTTPS. + +### 4.1 Test HTTPS Redirect + +To test that NGINX sends an HTTPS redirect, we will send requests to the `coffee` and `tea` Services on HTTP port. We will use curl's `--include` option to print the response headers (we are interested in the `Location` header). + +To get a redirect for coffee: +``` +curl --resolve cafe.example.com:$GW_HTTP_PORT:$GW_IP http://cafe.example.com:$GW_HTTP_PORT/coffee --include +HTTP/1.1 302 Moved Temporarily +... +Location: https://cafe.example.com:443/coffee +... +``` + +To get a redirect for tea: +``` +curl --resolve cafe.example.com:$GW_HTTP_PORT:$GW_IP http://cafe.example.com:$GW_HTTP_PORT/tea --include +HTTP/1.1 302 Moved Temporarily +... +Location: https://cafe.example.com:443/tea +... +``` + +### 4.2 Access Coffee and Tea + +Now we will access the application over HTTPS. Since our certificate is self-signed, we will use curl's `--insecure` option to turn off certificate verification. To get coffee: diff --git a/examples/https-termination/cafe-routes.yaml b/examples/https-termination/cafe-routes.yaml index a91a469275..1940c76bf3 100644 --- a/examples/https-termination/cafe-routes.yaml +++ b/examples/https-termination/cafe-routes.yaml @@ -1,5 +1,22 @@ apiVersion: gateway.networking.k8s.io/v1beta1 kind: HTTPRoute +metadata: + name: cafe-tls-redirect +spec: + parentRefs: + - name: gateway + sectionName: http + hostnames: + - "cafe.example.com" + rules: + - filters: + - type: RequestRedirect + requestRedirect: + scheme: https + port: 443 +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: HTTPRoute metadata: name: coffee spec: diff --git a/examples/https-termination/gateway.yaml b/examples/https-termination/gateway.yaml index 1a815686f3..75861a43ae 100644 --- a/examples/https-termination/gateway.yaml +++ b/examples/https-termination/gateway.yaml @@ -7,6 +7,9 @@ metadata: spec: gatewayClassName: nginx listeners: + - name: http + port: 80 + protocol: HTTP - name: https port: 443 protocol: HTTPS diff --git a/internal/helpers/helpers.go b/internal/helpers/helpers.go index 054968a3b9..565c6e1ff7 100644 --- a/internal/helpers/helpers.go +++ b/internal/helpers/helpers.go @@ -22,6 +22,11 @@ func GetStringPointer(s string) *string { return &s } +// GetIntPointer takes an int and returns a pointer to it. Useful in unit tests when initializing structs. +func GetIntPointer(i int) *int { + return &i +} + // GetInt32Pointer takes an int32 and returns a pointer to it. Useful in unit tests when initializing structs. func GetInt32Pointer(i int32) *int32 { return &i diff --git a/internal/nginx/config/generator.go b/internal/nginx/config/generator.go index 7130886a9e..6ed36be0b2 100644 --- a/internal/nginx/config/generator.go +++ b/internal/nginx/config/generator.go @@ -82,11 +82,15 @@ func generate(virtualServer state.VirtualServer, serviceStore state.ServiceStore s := server{ServerName: virtualServer.Hostname} + listenerPort := 80 + if virtualServer.SSL != nil { s.SSL = &ssl{ Certificate: virtualServer.SSL.CertificatePath, CertificateKey: virtualServer.SSL.CertificatePath, } + + listenerPort = 443 } if len(virtualServer.PathRules) == 0 { @@ -100,26 +104,46 @@ func generate(virtualServer state.VirtualServer, serviceStore state.ServiceStore matches := make([]httpMatch, 0, len(rule.MatchRules)) for ruleIdx, r := range rule.MatchRules { - - address, err := getBackendAddress(r.Source.Spec.Rules[r.RuleIdx].BackendRefs, r.Source.Namespace, serviceStore) - if err != nil { - warnings.AddWarning(r.Source, err.Error()) - } - m := r.GetMatch() + var loc location + // handle case where the only route is a path-only match // generate a standard location block without http_matches. if len(rule.MatchRules) == 1 && isPathOnlyMatch(m) { - locs = append(locs, location{ - Path: rule.Path, - ProxyPass: generateProxyPass(address), - }) + loc = location{ + Path: rule.Path, + } } else { path := createPathForMatch(rule.Path, ruleIdx) - locs = append(locs, generateMatchLocation(path, address)) + loc = generateMatchLocation(path) matches = append(matches, createHTTPMatch(m, path)) } + + // FIXME(pleshakov): There could be a case when the filter has the type set but not the corresponding field. + // For example, type is v1beta1.HTTPRouteFilterRequestRedirect, but RequestRedirect field is nil. + // The validation webhook catches that. + // If it doesn't work as expected, such situation is silently handled below in findFirstFilters. + // Consider reporting an error. But that should be done in a separate validation layer. + + firstFilters := findFirstFilters(r.GetFilters(), supportedFilters) + + loc.Return = generateReturnValForRedirectFilter( + firstFilters[v1beta1.HTTPRouteFilterRequestRedirect].RequestRedirect, + listenerPort, + ) + + // ProxyPass is mutually exclusive with Return + if loc.Return == nil { + address, err := getBackendAddress(r.Source.Spec.Rules[r.RuleIdx].BackendRefs, r.Source.Namespace, serviceStore) + if err != nil { + warnings.AddWarning(r.Source, err.Error()) + } + + loc.ProxyPass = generateProxyPass(address) + } + + locs = append(locs, loc) } if len(matches) > 0 { @@ -150,6 +174,60 @@ func generateProxyPass(address string) string { return "http://" + address } +var supportedFilters = map[v1beta1.HTTPRouteFilterType]struct{}{ + v1beta1.HTTPRouteFilterRequestRedirect: {}, +} + +func findFirstFilters( + filters []v1beta1.HTTPRouteFilter, + filterTypes map[v1beta1.HTTPRouteFilterType]struct{}, +) map[v1beta1.HTTPRouteFilterType]v1beta1.HTTPRouteFilter { + + result := make(map[v1beta1.HTTPRouteFilterType]v1beta1.HTTPRouteFilter) + for i := len(filters) - 1; i >= 0; i-- { + if _, exist := filterTypes[filters[i].Type]; exist { + result[filters[i].Type] = filters[i] + } + } + + return result +} + +func generateReturnValForRedirectFilter(filter *v1beta1.HTTPRequestRedirectFilter, listenerPort int) *returnVal { + if filter == nil { + return nil + } + + hostname := "$host" + if filter.Hostname != nil { + hostname = string(*filter.Hostname) + } + + // FIXME(pleshakov): Unknown values here must result in the implementation setting the Attached Condition for + // the Route to `status: False`, with a Reason of `UnsupportedValue`. In that case, all routes of the Route will be + // ignored. NGINX will return 500. This should be implemented in the validation layer. + code := statusFound + if filter.StatusCode != nil { + code = statusCode(*filter.StatusCode) + } + + port := listenerPort + if filter.Port != nil { + port = int(*filter.Port) + } + + // FIXME(pleshakov): Same as the FIXME about StatusCode above. + scheme := "$scheme" + if filter.Scheme != nil { + scheme = *filter.Scheme + } + + return &returnVal{ + Code: code, + URL: fmt.Sprintf("%s://%s:%d$request_uri", scheme, hostname, port), + } +} + func getBackendAddress( refs []v1beta1.HTTPBackendRef, parentNS string, @@ -183,11 +261,10 @@ func getBackendAddress( return fmt.Sprintf("%s:%d", address, *ref.Port), nil } -func generateMatchLocation(path, address string) location { +func generateMatchLocation(path string) location { return location{ - Path: path, - ProxyPass: generateProxyPass(address), - Internal: true, + Path: path, + Internal: true, } } diff --git a/internal/nginx/config/generator_test.go b/internal/nginx/config/generator_test.go index 1f4d93ed19..802a20777c 100644 --- a/internal/nginx/config/generator_test.go +++ b/internal/nginx/config/generator_test.go @@ -3,6 +3,7 @@ package config import ( "encoding/json" "errors" + "fmt" "strings" "testing" @@ -117,6 +118,7 @@ func TestGenerate(t *testing.T) { }, Rules: []v1beta1.HTTPRouteRule{ { + // matches with path and methods Matches: []v1beta1.HTTPRouteMatch{ { Path: &v1beta1.HTTPPathMatch{ @@ -149,6 +151,7 @@ func TestGenerate(t *testing.T) { }, }, { + // A match with all possible fields set Matches: []v1beta1.HTTPRouteMatch{ { Path: &v1beta1.HTTPPathMatch{ @@ -189,6 +192,7 @@ func TestGenerate(t *testing.T) { BackendRefs: nil, // no backend refs will cause warnings }, { + // A match with just path Matches: []v1beta1.HTTPRouteMatch{ { Path: &v1beta1.HTTPPathMatch{ @@ -208,61 +212,47 @@ func TestGenerate(t *testing.T) { }, }, }, - }, - }, - } - - certPath := "/etc/nginx/secrets/cert" - - httpHost := state.VirtualServer{ - Hostname: "example.com", - PathRules: []state.PathRule{ - { - Path: "/", - MatchRules: []state.MatchRule{ - { - MatchIdx: 0, - RuleIdx: 0, - Source: hr, - }, - { - MatchIdx: 1, - RuleIdx: 0, - Source: hr, + { + // A match with a redirect with implicit port + Matches: []v1beta1.HTTPRouteMatch{ + { + Path: &v1beta1.HTTPPathMatch{ + Value: helpers.GetStringPointer("/redirect-implicit-port"), + }, + }, }, - { - MatchIdx: 2, - RuleIdx: 0, - Source: hr, + Filters: []v1beta1.HTTPRouteFilter{ + { + Type: v1beta1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &v1beta1.HTTPRequestRedirectFilter{ + Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("foo.example.com")), + }, + }, }, }, - }, - { - Path: "/test", - MatchRules: []state.MatchRule{ - { - MatchIdx: 0, - RuleIdx: 1, - Source: hr, + { + // A match with a redirect with explicit port + Matches: []v1beta1.HTTPRouteMatch{ + { + Path: &v1beta1.HTTPPathMatch{ + Value: helpers.GetStringPointer("/redirect-explicit-port"), + }, + }, }, - }, - }, - { - Path: "/path-only", - MatchRules: []state.MatchRule{ - { - MatchIdx: 0, - RuleIdx: 2, - Source: hr, + Filters: []v1beta1.HTTPRouteFilter{ + { + Type: v1beta1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &v1beta1.HTTPRequestRedirectFilter{ + Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("bar.example.com")), + Port: (*v1beta1.PortNumber)(helpers.GetInt32Pointer(8080)), + }, + }, }, }, }, }, } - httpsHost := httpHost - httpsHost.SSL = &state.SSL{CertificatePath: certPath} - fakeServiceStore := &statefakes.FakeServiceStore{} fakeServiceStore.ResolveReturns("10.0.0.1", nil) @@ -288,48 +278,151 @@ func TestGenerate(t *testing.T) { }, } - const backendAddr = "http://10.0.0.1:80" + const ( + backendAddr = "http://10.0.0.1:80" + certPath = "/etc/nginx/secrets/cert" + http = false + https = true + ) - expectedHTTPServer := server{ - ServerName: "example.com", - Locations: []location{ - { - Path: "/_route0", - Internal: true, - ProxyPass: backendAddr, - }, - { - Path: "/_route1", - Internal: true, - ProxyPass: backendAddr, - }, - { - Path: "/_route2", - Internal: true, - ProxyPass: backendAddr, - }, - { - Path: "/", - HTTPMatchVar: expectedMatchString(slashMatches), - }, - { - Path: "/test_route0", - Internal: true, - ProxyPass: "http://" + nginx502Server, - }, - { - Path: "/test", - HTTPMatchVar: expectedMatchString(testMatches), - }, - { - Path: "/path-only", - ProxyPass: backendAddr, + getExpectedHost := func(isHTTPS bool) state.VirtualServer { + var ssl *state.SSL + if isHTTPS { + ssl = &state.SSL{CertificatePath: certPath} + } + + return state.VirtualServer{ + Hostname: "example.com", + SSL: ssl, + PathRules: []state.PathRule{ + { + Path: "/", + MatchRules: []state.MatchRule{ + { + MatchIdx: 0, + RuleIdx: 0, + Source: hr, + }, + { + MatchIdx: 1, + RuleIdx: 0, + Source: hr, + }, + { + MatchIdx: 2, + RuleIdx: 0, + Source: hr, + }, + }, + }, + { + Path: "/test", + MatchRules: []state.MatchRule{ + { + MatchIdx: 0, + RuleIdx: 1, + Source: hr, + }, + }, + }, + { + Path: "/path-only", + MatchRules: []state.MatchRule{ + { + MatchIdx: 0, + RuleIdx: 2, + Source: hr, + }, + }, + }, + { + Path: "/redirect-implicit-port", + MatchRules: []state.MatchRule{ + { + MatchIdx: 0, + RuleIdx: 3, + Source: hr, + }, + }, + }, + { + Path: "/redirect-explicit-port", + MatchRules: []state.MatchRule{ + { + MatchIdx: 0, + RuleIdx: 4, + Source: hr, + }, + }, + }, }, - }, + } } - expectedHTTPSServer := expectedHTTPServer - expectedHTTPSServer.SSL = &ssl{Certificate: certPath, CertificateKey: certPath} + getExpectedHTTPServer := func(isHTTPS bool) server { + var sslCfg *ssl + port := 80 + if isHTTPS { + sslCfg = &ssl{ + Certificate: certPath, + CertificateKey: certPath, + } + port = 443 + } + + return server{ + ServerName: "example.com", + SSL: sslCfg, + Locations: []location{ + { + Path: "/_route0", + Internal: true, + ProxyPass: backendAddr, + }, + { + Path: "/_route1", + Internal: true, + ProxyPass: backendAddr, + }, + { + Path: "/_route2", + Internal: true, + ProxyPass: backendAddr, + }, + { + Path: "/", + HTTPMatchVar: expectedMatchString(slashMatches), + }, + { + Path: "/test_route0", + Internal: true, + ProxyPass: "http://" + nginx502Server, + }, + { + Path: "/test", + HTTPMatchVar: expectedMatchString(testMatches), + }, + { + Path: "/path-only", + ProxyPass: backendAddr, + }, + { + Path: "/redirect-implicit-port", + Return: &returnVal{ + Code: 302, + URL: fmt.Sprintf("$scheme://foo.example.com:%d$request_uri", port), + }, + }, + { + Path: "/redirect-explicit-port", + Return: &returnVal{ + Code: 302, + URL: "$scheme://bar.example.com:8080$request_uri", + }, + }, + }, + } + } expectedWarnings := Warnings{ hr: []string{"empty backend refs"}, @@ -342,15 +435,15 @@ func TestGenerate(t *testing.T) { msg string }{ { - host: httpHost, + host: getExpectedHost(http), expWarnings: expectedWarnings, - expResult: expectedHTTPServer, + expResult: getExpectedHTTPServer(http), msg: "http server", }, { - host: httpsHost, + host: getExpectedHost(https), expWarnings: expectedWarnings, - expResult: expectedHTTPSServer, + expResult: getExpectedHTTPServer(https), msg: "https server", }, } @@ -359,10 +452,10 @@ func TestGenerate(t *testing.T) { result, warnings := generate(tc.host, fakeServiceStore) if diff := cmp.Diff(tc.expResult, result); diff != "" { - t.Errorf("generate() mismatch (-want +got):\n%s", diff) + t.Errorf("generate() '%s' mismatch (-want +got):\n%s", tc.msg, diff) } if diff := cmp.Diff(tc.expWarnings, warnings); diff != "" { - t.Errorf("generate() mismatch on warnings (-want +got):\n%s", diff) + t.Errorf("generate() '%s' mismatch on warnings (-want +got):\n%s", tc.msg, diff) } } } @@ -383,6 +476,139 @@ func TestGenerateProxyPass(t *testing.T) { } } +func TestFindFirstFilters(t *testing.T) { + oneType := map[v1beta1.HTTPRouteFilterType]struct{}{ + v1beta1.HTTPRouteFilterRequestRedirect: {}, + } + + twoTypes := map[v1beta1.HTTPRouteFilterType]struct{}{ + v1beta1.HTTPRouteFilterRequestRedirect: {}, + v1beta1.HTTPRouteFilterURLRewrite: {}, + } + + redirect1 := v1beta1.HTTPRouteFilter{ + Type: v1beta1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &v1beta1.HTTPRequestRedirectFilter{ + Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("foo.example.com")), + }, + } + redirect2 := v1beta1.HTTPRouteFilter{ + Type: v1beta1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &v1beta1.HTTPRequestRedirectFilter{ + Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("bar.example.com")), + }, + } + rewrite1 := v1beta1.HTTPRouteFilter{ + Type: v1beta1.HTTPRouteFilterURLRewrite, + URLRewrite: &v1beta1.HTTPURLRewriteFilter{ + Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("foo.example.com")), + }, + } + rewrite2 := v1beta1.HTTPRouteFilter{ + Type: v1beta1.HTTPRouteFilterURLRewrite, + URLRewrite: &v1beta1.HTTPURLRewriteFilter{ + Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("bar.example.com")), + }, + } + + oneTypeFilters := []v1beta1.HTTPRouteFilter{redirect1, redirect2} + + twoTypesFilters := []v1beta1.HTTPRouteFilter{ + redirect1, + rewrite1, + rewrite2, + redirect2, + } + + tests := []struct { + filters []v1beta1.HTTPRouteFilter + filterTypes map[v1beta1.HTTPRouteFilterType]struct{} + expected map[v1beta1.HTTPRouteFilterType]v1beta1.HTTPRouteFilter + msg string + }{ + { + filters: []v1beta1.HTTPRouteFilter{}, + filterTypes: twoTypes, + expected: map[v1beta1.HTTPRouteFilterType]v1beta1.HTTPRouteFilter{}, + msg: "no filters", + }, + { + filters: oneTypeFilters, + filterTypes: oneType, + expected: map[v1beta1.HTTPRouteFilterType]v1beta1.HTTPRouteFilter{ + v1beta1.HTTPRouteFilterRequestRedirect: redirect1, + }, + msg: "only one type", + }, + { + filters: twoTypesFilters, + filterTypes: map[v1beta1.HTTPRouteFilterType]struct{}{}, + expected: map[v1beta1.HTTPRouteFilterType]v1beta1.HTTPRouteFilter{}, + msg: "no supported type", + }, + { + filters: twoTypesFilters, + filterTypes: twoTypes, + expected: map[v1beta1.HTTPRouteFilterType]v1beta1.HTTPRouteFilter{ + v1beta1.HTTPRouteFilterRequestRedirect: redirect1, + v1beta1.HTTPRouteFilterURLRewrite: rewrite1, + }, + msg: "two types two filters", + }, + } + + for _, test := range tests { + result := findFirstFilters(test.filters, test.filterTypes) + if diff := cmp.Diff(test.expected, result); diff != "" { + t.Errorf("findFirstFilters() mismatch '%s' (-want +got):\n%s", test.msg, diff) + } + } +} + +func TestGenerateReturnValForRedirectFilter(t *testing.T) { + const listenerPort = 123 + + tests := []struct { + filter *v1beta1.HTTPRequestRedirectFilter + expected *returnVal + msg string + }{ + { + filter: nil, + expected: nil, + msg: "filter is nil", + }, + { + filter: &v1beta1.HTTPRequestRedirectFilter{}, + expected: &returnVal{ + Code: statusFound, + URL: "$scheme://$host:123$request_uri", + }, + msg: "all fields are empty", + }, + { + filter: &v1beta1.HTTPRequestRedirectFilter{ + Scheme: helpers.GetStringPointer("https"), + Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("foo.example.com")), + Port: (*v1beta1.PortNumber)(helpers.GetInt32Pointer(2022)), + StatusCode: helpers.GetIntPointer(101), + }, + expected: &returnVal{ + Code: 101, + URL: "https://foo.example.com:2022$request_uri", + }, + msg: "all fields are set", + }, + } + + for _, test := range tests { + result := generateReturnValForRedirectFilter(test.filter, listenerPort) + if diff := cmp.Diff(test.expected, result); diff != "" { + t.Errorf("generateReturnValForRedirectFilter() mismatch '%s' (-want +got):\n%s", test.msg, diff) + } + } +} + func TestGetBackendAddress(t *testing.T) { getNormalRefs := func() []v1beta1.HTTPBackendRef { return []v1beta1.HTTPBackendRef{ @@ -584,12 +810,11 @@ func TestGetBackendAddress(t *testing.T) { func TestGenerateMatchLocation(t *testing.T) { expected := location{ - Path: "/path", - Internal: true, - ProxyPass: "http://10.0.0.1:80", + Path: "/path", + Internal: true, } - result := generateMatchLocation("/path", "10.0.0.1:80") + result := generateMatchLocation("/path") if result != expected { t.Errorf("generateMatchLocation() returned %v but expected %v", result, expected) } diff --git a/internal/nginx/config/http.go b/internal/nginx/config/http.go index 11d2a1762e..3f74d5e56e 100644 --- a/internal/nginx/config/http.go +++ b/internal/nginx/config/http.go @@ -22,6 +22,7 @@ type location struct { type returnVal struct { Code statusCode + URL string } type ssl struct { @@ -31,4 +32,7 @@ type ssl struct { type statusCode int -const statusNotFound statusCode = 404 +const ( + statusFound statusCode = 302 + statusNotFound statusCode = 404 +) diff --git a/internal/nginx/config/template.go b/internal/nginx/config/template.go index b67a162318..8e86008603 100644 --- a/internal/nginx/config/template.go +++ b/internal/nginx/config/template.go @@ -41,7 +41,7 @@ server { {{ end }} {{ if $l.Return }} - return {{ $l.Return.Code }}; + return {{ $l.Return.Code }} {{ $l.Return.URL }}; {{ end }} {{ if $l.HTTPMatchVar }} diff --git a/internal/state/configuration.go b/internal/state/configuration.go index a65888ae17..9225994808 100644 --- a/internal/state/configuration.go +++ b/internal/state/configuration.go @@ -61,6 +61,11 @@ func (r *MatchRule) GetMatch() v1beta1.HTTPRouteMatch { return r.Source.Spec.Rules[r.RuleIdx].Matches[r.MatchIdx] } +// GetFilters returns the filters for the MatchRule. +func (r *MatchRule) GetFilters() []v1beta1.HTTPRouteFilter { + return r.Source.Spec.Rules[r.RuleIdx].Filters +} + // buildConfiguration builds the Configuration from the graph. // FIXME(pleshakov) For now we only handle paths with prefix matches. Handle exact and regex matches func buildConfiguration(graph *graph) Configuration { diff --git a/internal/state/configuration_test.go b/internal/state/configuration_test.go index 95b158847d..5420a2c9fb 100644 --- a/internal/state/configuration_test.go +++ b/internal/state/configuration_test.go @@ -801,6 +801,70 @@ func TestMatchRuleGetMatch(t *testing.T) { } } +func TestGetFilters(t *testing.T) { + hr := &v1beta1.HTTPRoute{ + Spec: v1beta1.HTTPRouteSpec{ + Rules: []v1beta1.HTTPRouteRule{ + { + Filters: []v1beta1.HTTPRouteFilter{ + { + Type: v1beta1.HTTPRouteFilterURLRewrite, + }, + }, + Matches: []v1beta1.HTTPRouteMatch{ + {}, + {}, + }, + }, + { + // No filters + Matches: []v1beta1.HTTPRouteMatch{ + {}, + {}, + }, + }, + }, + }, + } + + tests := []struct { + name string + expected []v1beta1.HTTPRouteFilter + rule MatchRule + }{ + { + name: "filters for first match in first rule", + expected: []v1beta1.HTTPRouteFilter{ + { + Type: v1beta1.HTTPRouteFilterURLRewrite, + }, + }, + rule: MatchRule{MatchIdx: 0, RuleIdx: 0, Source: hr}, + }, + { + name: "filters for second match in first rule", + expected: []v1beta1.HTTPRouteFilter{ + { + Type: v1beta1.HTTPRouteFilterURLRewrite, + }, + }, + rule: MatchRule{MatchIdx: 1, RuleIdx: 0, Source: hr}, + }, + { + name: "filters for second match in second rule", + expected: nil, + rule: MatchRule{MatchIdx: 1, RuleIdx: 1, Source: hr}, + }, + } + + for _, tc := range tests { + actual := tc.rule.GetFilters() + if diff := cmp.Diff(tc.expected, actual); diff != "" { + t.Errorf("MatchRule.GetFilters() returned incorrect filters for test case: %q, diff (-want, +got):\n%s", tc.name, diff) + } + } +} + func TestGetListenerHostname(t *testing.T) { var emptyHostname v1beta1.Hostname var hostname v1beta1.Hostname = "example.com" From a30aa71893706b5124492aa4769e88526c29d7f3 Mon Sep 17 00:00:00 2001 From: Michael Pleshakov Date: Wed, 7 Sep 2022 14:07:51 -0500 Subject: [PATCH 2/3] Docs fixes Co-authored-by: Kate Osborn <50597707+kate-osborn@users.noreply.github.com> --- examples/https-termination/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/https-termination/README.md b/examples/https-termination/README.md index 4265d42f9b..085192176d 100644 --- a/examples/https-termination/README.md +++ b/examples/https-termination/README.md @@ -1,6 +1,6 @@ # HTTPS Termination Example -In this example we expand on the simple [cafe-example](../cafe-example) by adding HTTPS termination to our routes and an HTTPS redirect from port 80 to 443. +In this example, we expand on the simple [cafe-example](../cafe-example) by adding HTTPS termination to our routes and an HTTPS redirect from port 80 to 443. ## Running the Example @@ -55,7 +55,7 @@ In this example we expand on the simple [cafe-example](../cafe-example) by addin This [Gateway](./gateway.yaml) configures: * `http` listener for HTTP traffic - * `https` listener for HTTPS traffic. It terminates TLS connections using the `cafe-secret` we created in the step 1. + * `https` listener for HTTPS traffic. It terminates TLS connections using the `cafe-secret` we created in step 1. 1. Create the `HTTPRoute` resources: ``` From 875b213265cfdc12f0a16b0b3bdd04dace9417a2 Mon Sep 17 00:00:00 2001 From: Michael Pleshakov Date: Thu, 8 Sep 2022 13:28:00 -0500 Subject: [PATCH 3/3] Make filters part of MatchRule --- internal/nginx/config/generator.go | 32 +--- internal/nginx/config/generator_test.go | 119 ++------------- internal/state/configuration.go | 32 +++- internal/state/configuration_test.go | 194 ++++++++++++++++-------- 4 files changed, 174 insertions(+), 203 deletions(-) diff --git a/internal/nginx/config/generator.go b/internal/nginx/config/generator.go index 6ed36be0b2..2aabd65043 100644 --- a/internal/nginx/config/generator.go +++ b/internal/nginx/config/generator.go @@ -126,15 +126,10 @@ func generate(virtualServer state.VirtualServer, serviceStore state.ServiceStore // If it doesn't work as expected, such situation is silently handled below in findFirstFilters. // Consider reporting an error. But that should be done in a separate validation layer. - firstFilters := findFirstFilters(r.GetFilters(), supportedFilters) - - loc.Return = generateReturnValForRedirectFilter( - firstFilters[v1beta1.HTTPRouteFilterRequestRedirect].RequestRedirect, - listenerPort, - ) - - // ProxyPass is mutually exclusive with Return - if loc.Return == nil { + // RequestRedirect and proxying are mutually exclusive. + if r.Filters.RequestRedirect != nil { + loc.Return = generateReturnValForRedirectFilter(r.Filters.RequestRedirect, listenerPort) + } else { address, err := getBackendAddress(r.Source.Spec.Rules[r.RuleIdx].BackendRefs, r.Source.Namespace, serviceStore) if err != nil { warnings.AddWarning(r.Source, err.Error()) @@ -174,25 +169,6 @@ func generateProxyPass(address string) string { return "http://" + address } -var supportedFilters = map[v1beta1.HTTPRouteFilterType]struct{}{ - v1beta1.HTTPRouteFilterRequestRedirect: {}, -} - -func findFirstFilters( - filters []v1beta1.HTTPRouteFilter, - filterTypes map[v1beta1.HTTPRouteFilterType]struct{}, -) map[v1beta1.HTTPRouteFilterType]v1beta1.HTTPRouteFilter { - - result := make(map[v1beta1.HTTPRouteFilterType]v1beta1.HTTPRouteFilter) - for i := len(filters) - 1; i >= 0; i-- { - if _, exist := filterTypes[filters[i].Type]; exist { - result[filters[i].Type] = filters[i] - } - } - - return result -} - func generateReturnValForRedirectFilter(filter *v1beta1.HTTPRequestRedirectFilter, listenerPort int) *returnVal { if filter == nil { return nil diff --git a/internal/nginx/config/generator_test.go b/internal/nginx/config/generator_test.go index 802a20777c..381539dbae 100644 --- a/internal/nginx/config/generator_test.go +++ b/internal/nginx/config/generator_test.go @@ -221,14 +221,7 @@ func TestGenerate(t *testing.T) { }, }, }, - Filters: []v1beta1.HTTPRouteFilter{ - { - Type: v1beta1.HTTPRouteFilterRequestRedirect, - RequestRedirect: &v1beta1.HTTPRequestRedirectFilter{ - Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("foo.example.com")), - }, - }, - }, + // redirect is set in the corresponding state.MatchRule }, { // A match with a redirect with explicit port @@ -239,15 +232,7 @@ func TestGenerate(t *testing.T) { }, }, }, - Filters: []v1beta1.HTTPRouteFilter{ - { - Type: v1beta1.HTTPRouteFilterRequestRedirect, - RequestRedirect: &v1beta1.HTTPRequestRedirectFilter{ - Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("bar.example.com")), - Port: (*v1beta1.PortNumber)(helpers.GetInt32Pointer(8080)), - }, - }, - }, + // redirect is set in the corresponding state.MatchRule }, }, }, @@ -342,6 +327,11 @@ func TestGenerate(t *testing.T) { MatchIdx: 0, RuleIdx: 3, Source: hr, + Filters: state.Filters{ + RequestRedirect: &v1beta1.HTTPRequestRedirectFilter{ + Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("foo.example.com")), + }, + }, }, }, }, @@ -352,6 +342,12 @@ func TestGenerate(t *testing.T) { MatchIdx: 0, RuleIdx: 4, Source: hr, + Filters: state.Filters{ + RequestRedirect: &v1beta1.HTTPRequestRedirectFilter{ + Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("bar.example.com")), + Port: (*v1beta1.PortNumber)(helpers.GetInt32Pointer(8080)), + }, + }, }, }, }, @@ -476,95 +472,6 @@ func TestGenerateProxyPass(t *testing.T) { } } -func TestFindFirstFilters(t *testing.T) { - oneType := map[v1beta1.HTTPRouteFilterType]struct{}{ - v1beta1.HTTPRouteFilterRequestRedirect: {}, - } - - twoTypes := map[v1beta1.HTTPRouteFilterType]struct{}{ - v1beta1.HTTPRouteFilterRequestRedirect: {}, - v1beta1.HTTPRouteFilterURLRewrite: {}, - } - - redirect1 := v1beta1.HTTPRouteFilter{ - Type: v1beta1.HTTPRouteFilterRequestRedirect, - RequestRedirect: &v1beta1.HTTPRequestRedirectFilter{ - Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("foo.example.com")), - }, - } - redirect2 := v1beta1.HTTPRouteFilter{ - Type: v1beta1.HTTPRouteFilterRequestRedirect, - RequestRedirect: &v1beta1.HTTPRequestRedirectFilter{ - Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("bar.example.com")), - }, - } - rewrite1 := v1beta1.HTTPRouteFilter{ - Type: v1beta1.HTTPRouteFilterURLRewrite, - URLRewrite: &v1beta1.HTTPURLRewriteFilter{ - Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("foo.example.com")), - }, - } - rewrite2 := v1beta1.HTTPRouteFilter{ - Type: v1beta1.HTTPRouteFilterURLRewrite, - URLRewrite: &v1beta1.HTTPURLRewriteFilter{ - Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("bar.example.com")), - }, - } - - oneTypeFilters := []v1beta1.HTTPRouteFilter{redirect1, redirect2} - - twoTypesFilters := []v1beta1.HTTPRouteFilter{ - redirect1, - rewrite1, - rewrite2, - redirect2, - } - - tests := []struct { - filters []v1beta1.HTTPRouteFilter - filterTypes map[v1beta1.HTTPRouteFilterType]struct{} - expected map[v1beta1.HTTPRouteFilterType]v1beta1.HTTPRouteFilter - msg string - }{ - { - filters: []v1beta1.HTTPRouteFilter{}, - filterTypes: twoTypes, - expected: map[v1beta1.HTTPRouteFilterType]v1beta1.HTTPRouteFilter{}, - msg: "no filters", - }, - { - filters: oneTypeFilters, - filterTypes: oneType, - expected: map[v1beta1.HTTPRouteFilterType]v1beta1.HTTPRouteFilter{ - v1beta1.HTTPRouteFilterRequestRedirect: redirect1, - }, - msg: "only one type", - }, - { - filters: twoTypesFilters, - filterTypes: map[v1beta1.HTTPRouteFilterType]struct{}{}, - expected: map[v1beta1.HTTPRouteFilterType]v1beta1.HTTPRouteFilter{}, - msg: "no supported type", - }, - { - filters: twoTypesFilters, - filterTypes: twoTypes, - expected: map[v1beta1.HTTPRouteFilterType]v1beta1.HTTPRouteFilter{ - v1beta1.HTTPRouteFilterRequestRedirect: redirect1, - v1beta1.HTTPRouteFilterURLRewrite: rewrite1, - }, - msg: "two types two filters", - }, - } - - for _, test := range tests { - result := findFirstFilters(test.filters, test.filterTypes) - if diff := cmp.Diff(test.expected, result); diff != "" { - t.Errorf("findFirstFilters() mismatch '%s' (-want +got):\n%s", test.msg, diff) - } - } -} - func TestGenerateReturnValForRedirectFilter(t *testing.T) { const listenerPort = 123 diff --git a/internal/state/configuration.go b/internal/state/configuration.go index 9225994808..aec875c8d4 100644 --- a/internal/state/configuration.go +++ b/internal/state/configuration.go @@ -44,6 +44,11 @@ type PathRule struct { MatchRules []MatchRule } +// Filters hold the filters for a MatchRule. +type Filters struct { + RequestRedirect *v1beta1.HTTPRequestRedirectFilter +} + // MatchRule represents a routing rule. It corresponds directly to a Match in the HTTPRoute resource. // An HTTPRoute is guaranteed to have at least one rule with one match. // If no rule or match is specified by the user, the default rule {{path:{ type: "PathPrefix", value: "/"}}} is set by the schema. @@ -53,7 +58,11 @@ type MatchRule struct { // RuleIdx is the index of the corresponding rule in the HTTPRoute. RuleIdx int // Source is the corresponding HTTPRoute resource. + // FIXME(pleshakov): Consider referencing only the parts neeeded for the config generation rather than + // the entire resource. Source *v1beta1.HTTPRoute + // Filters holds the filters for the MatchRule. + Filters Filters } // GetMatch returns the HTTPRouteMatch of the Route . @@ -61,11 +70,6 @@ func (r *MatchRule) GetMatch() v1beta1.HTTPRouteMatch { return r.Source.Spec.Rules[r.RuleIdx].Matches[r.MatchIdx] } -// GetFilters returns the filters for the MatchRule. -func (r *MatchRule) GetFilters() []v1beta1.HTTPRouteFilter { - return r.Source.Spec.Rules[r.RuleIdx].Filters -} - // buildConfiguration builds the Configuration from the graph. // FIXME(pleshakov) For now we only handle paths with prefix matches. Handle exact and regex matches func buildConfiguration(graph *graph) Configuration { @@ -158,6 +162,8 @@ func (b *virtualServerBuilder) upsertListener(l *listener) { } for i, rule := range r.Source.Spec.Rules { + filters := createFilters(rule.Filters) + for _, h := range hostnames { for j, m := range rule.Matches { path := getPath(m.Path) @@ -171,6 +177,7 @@ func (b *virtualServerBuilder) upsertListener(l *listener) { MatchIdx: j, RuleIdx: i, Source: r.Source, + Filters: filters, }) b.rulesPerHost[h][path] = rule @@ -246,3 +253,18 @@ func getPath(path *v1beta1.HTTPPathMatch) string { } return *path.Value } + +func createFilters(filters []v1beta1.HTTPRouteFilter) Filters { + var result Filters + + for _, f := range filters { + switch f.Type { + case v1beta1.HTTPRouteFilterRequestRedirect: + result.RequestRedirect = f.RequestRedirect + // using the first filter + return result + } + } + + return result +} diff --git a/internal/state/configuration_test.go b/internal/state/configuration_test.go index 5420a2c9fb..5b4ab510ca 100644 --- a/internal/state/configuration_test.go +++ b/internal/state/configuration_test.go @@ -48,6 +48,13 @@ func TestBuildConfiguration(t *testing.T) { } } + addFilters := func(hr *v1beta1.HTTPRoute, filters []v1beta1.HTTPRouteFilter) *v1beta1.HTTPRoute { + for i := range hr.Spec.Rules { + hr.Spec.Rules[i].Filters = filters + } + return hr + } + hr1 := createRoute("hr-1", "foo.example.com", "listener-80-1", "/") routeHR1 := &route{ @@ -138,6 +145,26 @@ func TestBuildConfiguration(t *testing.T) { InvalidSectionNameRefs: map[string]struct{}{}, } + redirect := v1beta1.HTTPRouteFilter{ + Type: v1beta1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &v1beta1.HTTPRequestRedirectFilter{ + Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("foo.example.com")), + }, + } + + hr6 := addFilters( + createRoute("hr-6", "foo.example.com", "listener-80-1", "/"), + []v1beta1.HTTPRouteFilter{redirect}, + ) + + routeHR6 := &route{ + Source: hr6, + ValidSectionNameRefs: map[string]struct{}{ + "listener-80-1": {}, + }, + InvalidSectionNameRefs: map[string]struct{}{}, + } + listener80 := v1beta1.Listener{ Name: "listener-80-1", Hostname: nil, @@ -689,6 +716,56 @@ func TestBuildConfiguration(t *testing.T) { expected: Configuration{}, msg: "missing gateway", }, + { + graph: &graph{ + GatewayClass: &gatewayClass{ + Source: &v1beta1.GatewayClass{}, + Valid: true, + }, + Gateway: &gateway{ + Source: &v1beta1.Gateway{}, + Listeners: map[string]*listener{ + "listener-80-1": { + Source: listener80, + Valid: true, + Routes: map[types.NamespacedName]*route{ + {Namespace: "test", Name: "hr-6"}: routeHR6, + }, + AcceptedHostnames: map[string]struct{}{ + "foo.example.com": {}, + }, + }, + }, + }, + Routes: map[types.NamespacedName]*route{ + {Namespace: "test", Name: "hr-6"}: routeHR6, + }, + }, + expected: Configuration{ + HTTPServers: []VirtualServer{ + { + Hostname: "foo.example.com", + PathRules: []PathRule{ + { + Path: "/", + MatchRules: []MatchRule{ + { + MatchIdx: 0, + RuleIdx: 0, + Source: hr6, + Filters: Filters{ + RequestRedirect: redirect.RequestRedirect, + }, + }, + }, + }, + }, + }, + }, + SSLServers: []VirtualServer{}, + }, + msg: "one http listener with one route with filters", + }, } for _, test := range tests { @@ -735,6 +812,59 @@ func TestGetPath(t *testing.T) { } } +func TestCreateFilters(t *testing.T) { + redirect1 := v1beta1.HTTPRouteFilter{ + Type: v1beta1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &v1beta1.HTTPRequestRedirectFilter{ + Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("foo.example.com")), + }, + } + redirect2 := v1beta1.HTTPRouteFilter{ + Type: v1beta1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &v1beta1.HTTPRequestRedirectFilter{ + Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("bar.example.com")), + }, + } + + tests := []struct { + filters []v1beta1.HTTPRouteFilter + expected Filters + msg string + }{ + { + filters: []v1beta1.HTTPRouteFilter{}, + expected: Filters{}, + msg: "no filters", + }, + { + filters: []v1beta1.HTTPRouteFilter{ + redirect1, + }, + expected: Filters{ + RequestRedirect: redirect1.RequestRedirect, + }, + msg: "one filter", + }, + { + filters: []v1beta1.HTTPRouteFilter{ + redirect1, + redirect2, + }, + expected: Filters{ + RequestRedirect: redirect1.RequestRedirect, + }, + msg: "two filters, first wins", + }, + } + + for _, test := range tests { + result := createFilters(test.filters) + if diff := cmp.Diff(test.expected, result); diff != "" { + t.Errorf("createFilters() %q mismatch (-want +got):\n%s", test.msg, diff) + } + } +} + func TestMatchRuleGetMatch(t *testing.T) { hr := &v1beta1.HTTPRoute{ Spec: v1beta1.HTTPRouteSpec{ @@ -801,70 +931,6 @@ func TestMatchRuleGetMatch(t *testing.T) { } } -func TestGetFilters(t *testing.T) { - hr := &v1beta1.HTTPRoute{ - Spec: v1beta1.HTTPRouteSpec{ - Rules: []v1beta1.HTTPRouteRule{ - { - Filters: []v1beta1.HTTPRouteFilter{ - { - Type: v1beta1.HTTPRouteFilterURLRewrite, - }, - }, - Matches: []v1beta1.HTTPRouteMatch{ - {}, - {}, - }, - }, - { - // No filters - Matches: []v1beta1.HTTPRouteMatch{ - {}, - {}, - }, - }, - }, - }, - } - - tests := []struct { - name string - expected []v1beta1.HTTPRouteFilter - rule MatchRule - }{ - { - name: "filters for first match in first rule", - expected: []v1beta1.HTTPRouteFilter{ - { - Type: v1beta1.HTTPRouteFilterURLRewrite, - }, - }, - rule: MatchRule{MatchIdx: 0, RuleIdx: 0, Source: hr}, - }, - { - name: "filters for second match in first rule", - expected: []v1beta1.HTTPRouteFilter{ - { - Type: v1beta1.HTTPRouteFilterURLRewrite, - }, - }, - rule: MatchRule{MatchIdx: 1, RuleIdx: 0, Source: hr}, - }, - { - name: "filters for second match in second rule", - expected: nil, - rule: MatchRule{MatchIdx: 1, RuleIdx: 1, Source: hr}, - }, - } - - for _, tc := range tests { - actual := tc.rule.GetFilters() - if diff := cmp.Diff(tc.expected, actual); diff != "" { - t.Errorf("MatchRule.GetFilters() returned incorrect filters for test case: %q, diff (-want, +got):\n%s", tc.name, diff) - } - } -} - func TestGetListenerHostname(t *testing.T) { var emptyHostname v1beta1.Hostname var hostname v1beta1.Hostname = "example.com"