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..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. +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 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..2aabd65043 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,41 @@ 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. + + // 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()) + } + + loc.ProxyPass = generateProxyPass(address) + } + + locs = append(locs, loc) } if len(matches) > 0 { @@ -150,6 +169,41 @@ func generateProxyPass(address string) string { return "http://" + address } +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 +237,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..381539dbae 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,32 @@ 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, - }, - { - MatchIdx: 2, - RuleIdx: 0, - Source: hr, - }, - }, - }, - { - Path: "/test", - MatchRules: []state.MatchRule{ - { - MatchIdx: 0, - RuleIdx: 1, - Source: hr, + { + // A match with a redirect with implicit port + Matches: []v1beta1.HTTPRouteMatch{ + { + Path: &v1beta1.HTTPPathMatch{ + Value: helpers.GetStringPointer("/redirect-implicit-port"), + }, + }, }, + // redirect is set in the corresponding state.MatchRule }, - }, - { - Path: "/path-only", - MatchRules: []state.MatchRule{ - { - MatchIdx: 0, - RuleIdx: 2, - Source: hr, + { + // A match with a redirect with explicit port + Matches: []v1beta1.HTTPRouteMatch{ + { + Path: &v1beta1.HTTPPathMatch{ + Value: helpers.GetStringPointer("/redirect-explicit-port"), + }, + }, }, + // redirect is set in the corresponding state.MatchRule }, }, }, } - httpsHost := httpHost - httpsHost.SSL = &state.SSL{CertificatePath: certPath} - fakeServiceStore := &statefakes.FakeServiceStore{} fakeServiceStore.ResolveReturns("10.0.0.1", nil) @@ -288,48 +263,162 @@ 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, + Filters: state.Filters{ + RequestRedirect: &v1beta1.HTTPRequestRedirectFilter{ + Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("foo.example.com")), + }, + }, + }, + }, + }, + { + Path: "/redirect-explicit-port", + MatchRules: []state.MatchRule{ + { + 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)), + }, + }, + }, + }, + }, }, - }, + } } - 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 +431,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 +448,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 +472,50 @@ func TestGenerateProxyPass(t *testing.T) { } } +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 +717,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..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 . @@ -153,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) @@ -166,6 +177,7 @@ func (b *virtualServerBuilder) upsertListener(l *listener) { MatchIdx: j, RuleIdx: i, Source: r.Source, + Filters: filters, }) b.rulesPerHost[h][path] = rule @@ -241,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 95b158847d..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{