From dd59e6e8f51a9cbee11bcee0bdc299ea96ab378e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 10:09:20 -0700 Subject: [PATCH 1/2] Update Helm release opentelemetry-collector to v0.111.1 (#2982) | datasource | package | from | to | | ---------- | ----------------------- | ------- | ------- | | helm | opentelemetry-collector | 0.111.0 | 0.111.1 | Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- tests/framework/collector.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/framework/collector.go b/tests/framework/collector.go index 07eebf8398..8933957e7d 100644 --- a/tests/framework/collector.go +++ b/tests/framework/collector.go @@ -12,7 +12,7 @@ const ( collectorChartReleaseName = "otel-collector" //nolint:lll // renovate: datasource=helm depName=opentelemetry-collector registryUrl=https://open-telemetry.github.io/opentelemetry-helm-charts - collectorChartVersion = "0.111.0" + collectorChartVersion = "0.111.1" ) // InstallCollector installs the otel-collector. From 10db5e4471b4ad204f790e2ff04b2efc0691ddfc Mon Sep 17 00:00:00 2001 From: salonichf5 <146118978+salonichf5@users.noreply.github.com> Date: Tue, 7 Jan 2025 11:44:10 -0700 Subject: [PATCH 2/2] Add path field for RequestRedirect Filter (#2979) Add path field for RequestRedirect Filter Problem: Users want to be able to provide path with the RequestRedirect filter. Solution: Added functionality for path in RequestRedirect filter. --- internal/mode/static/nginx/config/servers.go | 93 +++-- .../mode/static/nginx/config/servers_test.go | 297 ++++++++++++---- .../static/nginx/config/validation/common.go | 17 + .../nginx/config/validation/common_test.go | 21 ++ .../nginx/config/validation/http_filters.go | 29 +- .../config/validation/http_filters_test.go | 8 +- .../nginx/config/validation/http_validator.go | 1 + .../mode/static/state/dataplane/convert.go | 1 + .../static/state/dataplane/convert_test.go | 46 +++ internal/mode/static/state/dataplane/types.go | 2 + internal/mode/static/state/graph/httproute.go | 22 +- .../mode/static/state/graph/httproute_test.go | 56 ++- .../fake_httpfields_validator.go | 148 ++++---- .../mode/static/state/validation/validator.go | 2 +- .../redirects-and-rewrites.md | 331 ++++++++++++++++-- .../overview/gateway-api-compatibility.md | 2 +- tests/Makefile | 2 +- 17 files changed, 844 insertions(+), 234 deletions(-) diff --git a/internal/mode/static/nginx/config/servers.go b/internal/mode/static/nginx/config/servers.go index 5f236817c9..101c2cbd0a 100644 --- a/internal/mode/static/nginx/config/servers.go +++ b/internal/mode/static/nginx/config/servers.go @@ -435,7 +435,10 @@ func updateLocation( location.Includes = append(location.Includes, createIncludesFromLocationSnippetsFilters(filters.SnippetsFilters)...) if filters.RequestRedirect != nil { - ret := createReturnValForRedirectFilter(filters.RequestRedirect, listenerPort) + ret, rewrite := createReturnAndRewriteConfigForRedirectFilter(filters.RequestRedirect, listenerPort, path) + if rewrite.MainRewrite != "" { + location.Rewrites = append(location.Rewrites, rewrite.MainRewrite) + } location.Return = ret return location } @@ -543,9 +546,13 @@ func createProxySSLVerify(v *dataplane.VerifyTLS) *http.ProxySSLVerify { } } -func createReturnValForRedirectFilter(filter *dataplane.HTTPRequestRedirectFilter, listenerPort int32) *http.Return { +func createReturnAndRewriteConfigForRedirectFilter( + filter *dataplane.HTTPRequestRedirectFilter, + listenerPort int32, + path string, +) (*http.Return, *rewriteConfig) { if filter == nil { - return nil + return nil, nil } hostname := "$host" @@ -582,10 +589,55 @@ func createReturnValForRedirectFilter(filter *dataplane.HTTPRequestRedirectFilte } } + body := fmt.Sprintf("%s://%s$request_uri", scheme, hostnamePort) + + rewrites := &rewriteConfig{} + if filter.Path != nil { + rewrites.MainRewrite = createMainRewriteForFilters(filter.Path, path) + body = fmt.Sprintf("%s://%s$uri$is_args$args", scheme, hostnamePort) + } + return &http.Return{ Code: code, - Body: fmt.Sprintf("%s://%s$request_uri", scheme, hostnamePort), + Body: body, + }, rewrites +} + +func createMainRewriteForFilters(pathModifier *dataplane.HTTPPathModifier, path string) string { + var mainRewrite string + switch pathModifier.Type { + case dataplane.ReplaceFullPath: + mainRewrite = fmt.Sprintf("^ %s", pathModifier.Replacement) + case dataplane.ReplacePrefixMatch: + filterPrefix := pathModifier.Replacement + if filterPrefix == "" { + filterPrefix = "/" + } + + // capture everything following the configured prefix up to the first ?, if present. + regex := fmt.Sprintf("^%s([^?]*)?", path) + // replace the configured prefix with the filter prefix, append the captured segment, + // and include the request arguments stored in nginx variable $args. + // https://nginx.org/en/docs/http/ngx_http_core_module.html#var_args + replacement := fmt.Sprintf("%s$1?$args?", filterPrefix) + + // if configured prefix does not end in /, but replacement prefix does end in /, + // then make sure that we *require* but *don't capture* a trailing slash in the request, + // otherwise we'll get duplicate slashes in the full replacement + if strings.HasSuffix(filterPrefix, "/") && !strings.HasSuffix(path, "/") { + regex = fmt.Sprintf("^%s(?:/([^?]*))?", path) + } + + // if configured prefix ends in / we won't capture it for a request (since it's not in the regex), + // so append it to the replacement prefix if the replacement prefix doesn't already end in / + if strings.HasSuffix(path, "/") && !strings.HasSuffix(filterPrefix, "/") { + replacement = fmt.Sprintf("%s/$1?$args?", filterPrefix) + } + + mainRewrite = fmt.Sprintf("%s %s", regex, replacement) } + + return mainRewrite } func createRewritesValForRewriteFilter(filter *dataplane.HTTPURLRewriteFilter, path string) *rewriteConfig { @@ -594,40 +646,11 @@ func createRewritesValForRewriteFilter(filter *dataplane.HTTPURLRewriteFilter, p } rewrites := &rewriteConfig{} - if filter.Path != nil { rewrites.InternalRewrite = "^ $request_uri" - switch filter.Path.Type { - case dataplane.ReplaceFullPath: - rewrites.MainRewrite = fmt.Sprintf("^ %s break", filter.Path.Replacement) - case dataplane.ReplacePrefixMatch: - filterPrefix := filter.Path.Replacement - if filterPrefix == "" { - filterPrefix = "/" - } - // capture everything following the configured prefix up to the first ?, if present. - regex := fmt.Sprintf("^%s([^?]*)?", path) - // replace the configured prefix with the filter prefix, append the captured segment, - // and include the request arguments stored in nginx variable $args. - // https://nginx.org/en/docs/http/ngx_http_core_module.html#var_args - replacement := fmt.Sprintf("%s$1?$args?", filterPrefix) - - // if configured prefix does not end in /, but replacement prefix does end in /, - // then make sure that we *require* but *don't capture* a trailing slash in the request, - // otherwise we'll get duplicate slashes in the full replacement - if strings.HasSuffix(filterPrefix, "/") && !strings.HasSuffix(path, "/") { - regex = fmt.Sprintf("^%s(?:/([^?]*))?", path) - } - - // if configured prefix ends in / we won't capture it for a request (since it's not in the regex), - // so append it to the replacement prefix if the replacement prefix doesn't already end in / - if strings.HasSuffix(path, "/") && !strings.HasSuffix(filterPrefix, "/") { - replacement = fmt.Sprintf("%s/$1?$args?", filterPrefix) - } - - rewrites.MainRewrite = fmt.Sprintf("%s %s break", regex, replacement) - } + // for URLRewriteFilter, we add a break to the rewrite to prevent further processing of the request. + rewrites.MainRewrite = fmt.Sprintf("%s break", createMainRewriteForFilters(filter.Path, path)) } return rewrites diff --git a/internal/mode/static/nginx/config/servers_test.go b/internal/mode/static/nginx/config/servers_test.go index 3fd5266f4c..86919091a6 100644 --- a/internal/mode/static/nginx/config/servers_test.go +++ b/internal/mode/static/nginx/config/servers_test.go @@ -998,6 +998,27 @@ func TestCreateServers(t *testing.T) { }, }, }, + { + Path: "/redirect-with-path", + PathType: dataplane.PathTypePrefix, + MatchRules: []dataplane.MatchRule{ + { + Match: dataplane.Match{}, + Filters: dataplane.HTTPFilters{ + RequestRedirect: &dataplane.HTTPRequestRedirectFilter{ + Hostname: helpers.GetPointer("redirect.example.com"), + StatusCode: helpers.GetPointer[int](301), + Port: helpers.GetPointer[int32](8080), + Path: &dataplane.HTTPPathModifier{ + Type: dataplane.ReplaceFullPath, + Replacement: "/replacement", + }, + }, + }, + BackendGroup: fooGroup, + }, + }, + }, } conf := dataplane.Configuration{ @@ -1461,6 +1482,26 @@ func TestCreateServers(t *testing.T) { Type: http.ExternalLocationType, Includes: externalIncludes, }, + { + Path: "/redirect-with-path/", + Type: http.ExternalLocationType, + Return: &http.Return{ + Code: 301, + Body: "$scheme://redirect.example.com:8080$uri$is_args$args", + }, + Rewrites: []string{"^ /replacement"}, + Includes: externalIncludes, + }, + { + Path: "= /redirect-with-path", + Type: http.ExternalLocationType, + Return: &http.Return{ + Code: 301, + Body: "$scheme://redirect.example.com:8080$uri$is_args$args", + }, + Rewrites: []string{"^ /replacement"}, + Includes: externalIncludes, + }, } } @@ -2259,53 +2300,82 @@ func TestCreateReturnValForRedirectFilter(t *testing.T) { const listenerPortHTTP = 80 const listenerPortHTTPS = 443 + createBasicHTTPRequestRedirectFilter := func() *dataplane.HTTPRequestRedirectFilter { + return &dataplane.HTTPRequestRedirectFilter{ + Scheme: helpers.GetPointer("http"), + Hostname: helpers.GetPointer("foo.example.com"), + StatusCode: helpers.GetPointer(301), + } + } + + modifiedHTTPRequestRedirectFilter := func( + mod func( + filter *dataplane.HTTPRequestRedirectFilter, + ) *dataplane.HTTPRequestRedirectFilter, + ) *dataplane.HTTPRequestRedirectFilter { + return mod(createBasicHTTPRequestRedirectFilter()) + } + tests := []struct { - filter *dataplane.HTTPRequestRedirectFilter - expected *http.Return - msg string - listenerPort int32 + filter *dataplane.HTTPRequestRedirectFilter + expectedReturn *http.Return + expectedRewrite *rewriteConfig + msg string + path string + listenerPort int32 }{ { - filter: nil, - expected: nil, - listenerPort: listenerPortCustom, - msg: "filter is nil", + filter: nil, + expectedReturn: nil, + listenerPort: listenerPortCustom, + msg: "filter is nil", }, { filter: &dataplane.HTTPRequestRedirectFilter{}, listenerPort: listenerPortCustom, - expected: &http.Return{ + expectedReturn: &http.Return{ Code: http.StatusFound, Body: "$scheme://$host:123$request_uri", }, - msg: "all fields are empty", + expectedRewrite: &rewriteConfig{}, + msg: "all fields are empty", }, { - filter: &dataplane.HTTPRequestRedirectFilter{ - Scheme: helpers.GetPointer("https"), - Hostname: helpers.GetPointer("foo.example.com"), - Port: helpers.GetPointer[int32](2022), - StatusCode: helpers.GetPointer(301), - }, + filter: modifiedHTTPRequestRedirectFilter(func( + filter *dataplane.HTTPRequestRedirectFilter, + ) *dataplane.HTTPRequestRedirectFilter { + filter.Scheme = helpers.GetPointer("https") + filter.Port = helpers.GetPointer[int32](2022) + filter.Path = &dataplane.HTTPPathModifier{ + Type: dataplane.ReplaceFullPath, + Replacement: "/full-path", + } + return filter + }), listenerPort: listenerPortCustom, - expected: &http.Return{ + expectedReturn: &http.Return{ Code: 301, - Body: "https://foo.example.com:2022$request_uri", + Body: "https://foo.example.com:2022$uri$is_args$args", + }, + expectedRewrite: &rewriteConfig{ + MainRewrite: "^ /full-path", }, msg: "all fields are set", }, { - filter: &dataplane.HTTPRequestRedirectFilter{ - Scheme: helpers.GetPointer("https"), - Hostname: helpers.GetPointer("foo.example.com"), - StatusCode: helpers.GetPointer(301), - }, + filter: modifiedHTTPRequestRedirectFilter(func( + filter *dataplane.HTTPRequestRedirectFilter, + ) *dataplane.HTTPRequestRedirectFilter { + filter.Scheme = helpers.GetPointer("https") + return filter + }), listenerPort: listenerPortCustom, - expected: &http.Return{ + expectedReturn: &http.Return{ Code: 301, Body: "https://foo.example.com$request_uri", }, - msg: "listenerPort is custom, scheme is set, no port", + expectedRewrite: &rewriteConfig{}, + msg: "listenerPort is custom, scheme is set, no port", }, { filter: &dataplane.HTTPRequestRedirectFilter{ @@ -2313,65 +2383,173 @@ func TestCreateReturnValForRedirectFilter(t *testing.T) { StatusCode: helpers.GetPointer(301), }, listenerPort: listenerPortHTTPS, - expected: &http.Return{ + expectedReturn: &http.Return{ Code: 301, Body: "$scheme://foo.example.com:443$request_uri", }, - msg: "no scheme, listenerPort https, no port is set", + expectedRewrite: &rewriteConfig{}, + msg: "no scheme, listenerPort https, no port is set", }, { - filter: &dataplane.HTTPRequestRedirectFilter{ - Scheme: helpers.GetPointer("https"), - Hostname: helpers.GetPointer("foo.example.com"), - StatusCode: helpers.GetPointer(301), - }, + filter: modifiedHTTPRequestRedirectFilter(func( + filter *dataplane.HTTPRequestRedirectFilter, + ) *dataplane.HTTPRequestRedirectFilter { + filter.Scheme = helpers.GetPointer("https") + return filter + }), listenerPort: listenerPortHTTPS, - expected: &http.Return{ + expectedReturn: &http.Return{ Code: 301, Body: "https://foo.example.com$request_uri", }, - msg: "scheme is https, listenerPort https, no port is set", + expectedRewrite: &rewriteConfig{}, + msg: "scheme is https, listenerPort https, no port is set", }, { - filter: &dataplane.HTTPRequestRedirectFilter{ - Scheme: helpers.GetPointer("http"), - Hostname: helpers.GetPointer("foo.example.com"), - StatusCode: helpers.GetPointer(301), - }, + filter: createBasicHTTPRequestRedirectFilter(), listenerPort: listenerPortHTTP, - expected: &http.Return{ + expectedReturn: &http.Return{ Code: 301, Body: "http://foo.example.com$request_uri", }, - msg: "scheme is http, listenerPort http, no port is set", + expectedRewrite: &rewriteConfig{}, + msg: "scheme is http, listenerPort http, no port is set", }, { - filter: &dataplane.HTTPRequestRedirectFilter{ - Scheme: helpers.GetPointer("http"), - Hostname: helpers.GetPointer("foo.example.com"), - Port: helpers.GetPointer[int32](80), - StatusCode: helpers.GetPointer(301), + filter: modifiedHTTPRequestRedirectFilter(func( + filter *dataplane.HTTPRequestRedirectFilter, + ) *dataplane.HTTPRequestRedirectFilter { + filter.Port = helpers.GetPointer[int32](80) + filter.Path = &dataplane.HTTPPathModifier{ + Type: dataplane.ReplacePrefixMatch, + Replacement: "", + } + return filter + }), + path: "/original", + listenerPort: listenerPortCustom, + expectedReturn: &http.Return{ + Code: 301, + Body: "http://foo.example.com$uri$is_args$args", }, + expectedRewrite: &rewriteConfig{ + MainRewrite: "^/original(?:/([^?]*))? /$1?$args?", + }, + msg: "scheme is http, port http, prefix path is empty", + }, + { + filter: modifiedHTTPRequestRedirectFilter(func( + filter *dataplane.HTTPRequestRedirectFilter, + ) *dataplane.HTTPRequestRedirectFilter { + filter.Port = helpers.GetPointer[int32](443) + filter.Scheme = helpers.GetPointer("https") + filter.Path = &dataplane.HTTPPathModifier{ + Type: dataplane.ReplacePrefixMatch, + Replacement: "/", + } + return filter + }), + path: "/original", listenerPort: listenerPortCustom, - expected: &http.Return{ + expectedReturn: &http.Return{ Code: 301, - Body: "http://foo.example.com$request_uri", + Body: "https://foo.example.com$uri$is_args$args", }, - msg: "scheme is http, port http", + expectedRewrite: &rewriteConfig{ + MainRewrite: "^/original(?:/([^?]*))? /$1?$args?", + }, + msg: "scheme is https, port https and prefix path is /", }, { - filter: &dataplane.HTTPRequestRedirectFilter{ - Scheme: helpers.GetPointer("https"), - Hostname: helpers.GetPointer("foo.example.com"), - Port: helpers.GetPointer[int32](443), - StatusCode: helpers.GetPointer(301), + filter: modifiedHTTPRequestRedirectFilter(func( + filter *dataplane.HTTPRequestRedirectFilter, + ) *dataplane.HTTPRequestRedirectFilter { + filter.Port = helpers.GetPointer[int32](80) + filter.Path = &dataplane.HTTPPathModifier{ + Type: dataplane.ReplacePrefixMatch, + Replacement: "/prefix-path", + } + return filter + }), + path: "/original", + listenerPort: listenerPortCustom, + expectedReturn: &http.Return{ + Code: 301, + Body: "http://foo.example.com$uri$is_args$args", + }, + expectedRewrite: &rewriteConfig{ + MainRewrite: "^/original([^?]*)? /prefix-path$1?$args?", + }, + msg: "scheme is http, port http, prefix path with no trailing slashes", + }, + { + filter: modifiedHTTPRequestRedirectFilter(func( + filter *dataplane.HTTPRequestRedirectFilter, + ) *dataplane.HTTPRequestRedirectFilter { + filter.Port = helpers.GetPointer[int32](80) + filter.StatusCode = helpers.GetPointer(302) + filter.Path = &dataplane.HTTPPathModifier{ + Type: dataplane.ReplacePrefixMatch, + Replacement: "/trailing/", + } + return filter + }), + path: "/original", + listenerPort: listenerPortCustom, + expectedReturn: &http.Return{ + Code: 302, + Body: "http://foo.example.com$uri$is_args$args", + }, + expectedRewrite: &rewriteConfig{ + MainRewrite: "^/original(?:/([^?]*))? /trailing/$1?$args?", + }, + msg: "scheme is http, port http, prefix path replacement with trailing /", + }, + { + filter: modifiedHTTPRequestRedirectFilter(func( + filter *dataplane.HTTPRequestRedirectFilter, + ) *dataplane.HTTPRequestRedirectFilter { + filter.Port = helpers.GetPointer[int32](80) + filter.StatusCode = helpers.GetPointer(301) + filter.Path = &dataplane.HTTPPathModifier{ + Type: dataplane.ReplacePrefixMatch, + Replacement: "/trailing", + } + return filter + }), + path: "/original/", + listenerPort: listenerPortCustom, + expectedReturn: &http.Return{ + Code: 301, + Body: "http://foo.example.com$uri$is_args$args", + }, + expectedRewrite: &rewriteConfig{ + MainRewrite: "^/original/([^?]*)? /trailing/$1?$args?", }, + msg: "scheme is http, port http, prefix path original with trailing /", + }, + { + filter: modifiedHTTPRequestRedirectFilter(func( + filter *dataplane.HTTPRequestRedirectFilter, + ) *dataplane.HTTPRequestRedirectFilter { + filter.Port = helpers.GetPointer[int32](80) + filter.StatusCode = helpers.GetPointer(301) + filter.Path = &dataplane.HTTPPathModifier{ + Type: dataplane.ReplacePrefixMatch, + Replacement: "/trailing/", + } + return filter + }), + path: "/original/", listenerPort: listenerPortCustom, - expected: &http.Return{ + expectedReturn: &http.Return{ Code: 301, - Body: "https://foo.example.com$request_uri", + Body: "http://foo.example.com$uri$is_args$args", + }, + expectedRewrite: &rewriteConfig{ + MainRewrite: "^/original/([^?]*)? /trailing/$1?$args?", }, - msg: "scheme is https, port https", + msg: "scheme is http, port http, prefix path both with trailing slashes", }, } @@ -2380,8 +2558,9 @@ func TestCreateReturnValForRedirectFilter(t *testing.T) { t.Parallel() g := NewWithT(t) - result := createReturnValForRedirectFilter(test.filter, test.listenerPort) - g.Expect(helpers.Diff(test.expected, result)).To(BeEmpty()) + result, rewriteConfig := createReturnAndRewriteConfigForRedirectFilter(test.filter, test.listenerPort, test.path) + g.Expect(helpers.Diff(test.expectedReturn, result)).To(BeEmpty()) + g.Expect(helpers.Diff(test.expectedRewrite, rewriteConfig)).To(BeEmpty()) }) } } diff --git a/internal/mode/static/nginx/config/validation/common.go b/internal/mode/static/nginx/config/validation/common.go index bca6a97095..ec2eb2d1e2 100644 --- a/internal/mode/static/nginx/config/validation/common.go +++ b/internal/mode/static/nginx/config/validation/common.go @@ -84,3 +84,20 @@ func validateHeaderName(name string) error { } return nil } + +func validatePath(path string) error { + if path == "" { + return nil + } + + if !pathRegexp.MatchString(path) { + msg := k8svalidation.RegexError(pathErrMsg, pathFmt, pathExamples...) + return errors.New(msg) + } + + if strings.Contains(path, "$") { + return errors.New("cannot contain $") + } + + return nil +} diff --git a/internal/mode/static/nginx/config/validation/common_test.go b/internal/mode/static/nginx/config/validation/common_test.go index 33cdd0ee5e..042a82f6a3 100644 --- a/internal/mode/static/nginx/config/validation/common_test.go +++ b/internal/mode/static/nginx/config/validation/common_test.go @@ -74,3 +74,24 @@ func TestValidateValidHeaderName(t *testing.T) { strings.Repeat("very-long-header", 16)+"1", ) } + +func TestValidatePathForFilters(t *testing.T) { + t.Parallel() + validator := validatePath + + testValidValuesForSimpleValidator( + t, + validator, + `/path`, + `/longer/path`, + `/trailing/`, + ) + + testInvalidValuesForSimpleValidator( + t, + validator, + `path`, + `$path`, + "/path$", + ) +} diff --git a/internal/mode/static/nginx/config/validation/http_filters.go b/internal/mode/static/nginx/config/validation/http_filters.go index 0a8de17957..5a92a01be9 100644 --- a/internal/mode/static/nginx/config/validation/http_filters.go +++ b/internal/mode/static/nginx/config/validation/http_filters.go @@ -1,12 +1,5 @@ package validation -import ( - "errors" - "strings" - - k8svalidation "k8s.io/apimachinery/pkg/util/validation" -) - // HTTPRedirectValidator validates values for a redirect, which in NGINX is done with the return directive. // For example, return 302 "https://example.com:8080"; type HTTPRedirectValidator struct{} @@ -18,6 +11,9 @@ type HTTPURLRewriteValidator struct{} // which in NGINX is done with the proxy_set_header directive. type HTTPHeaderValidator struct{} +// HTTPPathValidator validates values for path used in filters. +type HTTPPathValidator struct{} + var supportedRedirectSchemes = map[string]struct{}{ "http": {}, "https": {}, @@ -54,22 +50,9 @@ func (HTTPRedirectValidator) ValidateHostname(hostname string) error { return validateEscapedStringNoVarExpansion(hostname, hostnameExamples) } -// ValidateRewritePath validates a path used in a URL Rewrite filter. -func (HTTPURLRewriteValidator) ValidateRewritePath(path string) error { - if path == "" { - return nil - } - - if !pathRegexp.MatchString(path) { - msg := k8svalidation.RegexError(pathErrMsg, pathFmt, pathExamples...) - return errors.New(msg) - } - - if strings.Contains(path, "$") { - return errors.New("cannot contain $") - } - - return nil +// ValidatePath validates a path used in filters. +func (HTTPPathValidator) ValidatePath(path string) error { + return validatePath(path) } func (HTTPHeaderValidator) ValidateFilterHeaderName(name string) error { diff --git a/internal/mode/static/nginx/config/validation/http_filters_test.go b/internal/mode/static/nginx/config/validation/http_filters_test.go index bd06ed2b41..103df6751d 100644 --- a/internal/mode/static/nginx/config/validation/http_filters_test.go +++ b/internal/mode/static/nginx/config/validation/http_filters_test.go @@ -71,13 +71,13 @@ func TestValidateHostname(t *testing.T) { ) } -func TestValidateRewritePath(t *testing.T) { +func TestValidatePath(t *testing.T) { t.Parallel() - validator := HTTPURLRewriteValidator{} + validator := HTTPPathValidator{} testValidValuesForSimpleValidator( t, - validator.ValidateRewritePath, + validator.ValidatePath, "", "/path", "/longer/path", @@ -86,7 +86,7 @@ func TestValidateRewritePath(t *testing.T) { testInvalidValuesForSimpleValidator( t, - validator.ValidateRewritePath, + validator.ValidatePath, "path", "$path", "/path$", diff --git a/internal/mode/static/nginx/config/validation/http_validator.go b/internal/mode/static/nginx/config/validation/http_validator.go index 82a6d55429..29da381ad6 100644 --- a/internal/mode/static/nginx/config/validation/http_validator.go +++ b/internal/mode/static/nginx/config/validation/http_validator.go @@ -12,6 +12,7 @@ type HTTPValidator struct { HTTPRedirectValidator HTTPURLRewriteValidator HTTPHeaderValidator + HTTPPathValidator } var _ validation.HTTPFieldsValidator = HTTPValidator{} diff --git a/internal/mode/static/state/dataplane/convert.go b/internal/mode/static/state/dataplane/convert.go index 61e5761b7f..4bc03635de 100644 --- a/internal/mode/static/state/dataplane/convert.go +++ b/internal/mode/static/state/dataplane/convert.go @@ -47,6 +47,7 @@ func convertHTTPRequestRedirectFilter(filter *v1.HTTPRequestRedirectFilter) *HTT Hostname: (*string)(filter.Hostname), Port: (*int32)(filter.Port), StatusCode: filter.StatusCode, + Path: convertPathModifier(filter.Path), } } diff --git a/internal/mode/static/state/dataplane/convert_test.go b/internal/mode/static/state/dataplane/convert_test.go index 5ffa900073..cc1a9e1293 100644 --- a/internal/mode/static/state/dataplane/convert_test.go +++ b/internal/mode/static/state/dataplane/convert_test.go @@ -137,6 +137,52 @@ func TestConvertHTTPRequestRedirectFilter(t *testing.T) { expected: &HTTPRequestRedirectFilter{}, name: "empty", }, + { + filter: &v1.HTTPRequestRedirectFilter{ + Scheme: helpers.GetPointer("http"), + Hostname: helpers.GetPointer[v1.PreciseHostname]("example.com"), + Port: helpers.GetPointer[v1.PortNumber](8080), + StatusCode: helpers.GetPointer(302), + Path: &v1.HTTPPathModifier{ + Type: v1.FullPathHTTPPathModifier, + ReplaceFullPath: helpers.GetPointer("/path"), + }, + }, + expected: &HTTPRequestRedirectFilter{ + Scheme: helpers.GetPointer("http"), + Hostname: helpers.GetPointer("example.com"), + Port: helpers.GetPointer[int32](8080), + StatusCode: helpers.GetPointer(302), + Path: &HTTPPathModifier{ + Type: ReplaceFullPath, + Replacement: "/path", + }, + }, + name: "request redirect with ReplaceFullPath modifier", + }, + { + filter: &v1.HTTPRequestRedirectFilter{ + Scheme: helpers.GetPointer("https"), + Hostname: helpers.GetPointer[v1.PreciseHostname]("example.com"), + Port: helpers.GetPointer[v1.PortNumber](8443), + StatusCode: helpers.GetPointer(302), + Path: &v1.HTTPPathModifier{ + Type: v1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: helpers.GetPointer("/prefix"), + }, + }, + expected: &HTTPRequestRedirectFilter{ + Scheme: helpers.GetPointer("https"), + Hostname: helpers.GetPointer("example.com"), + Port: helpers.GetPointer[int32](8443), + StatusCode: helpers.GetPointer(302), + Path: &HTTPPathModifier{ + Type: ReplacePrefixMatch, + Replacement: "/prefix", + }, + }, + name: "request redirect with ReplacePrefixMatch modifier", + }, { filter: &v1.HTTPRequestRedirectFilter{ Scheme: helpers.GetPointer("https"), diff --git a/internal/mode/static/state/dataplane/types.go b/internal/mode/static/state/dataplane/types.go index 6cbc6192f4..8a46dbf126 100644 --- a/internal/mode/static/state/dataplane/types.go +++ b/internal/mode/static/state/dataplane/types.go @@ -193,6 +193,8 @@ type HTTPRequestRedirectFilter struct { Port *int32 // StatusCode is the HTTP status code of the redirect. StatusCode *int + // Path is the path of the redirect. + Path *HTTPPathModifier } // HTTPURLRewriteFilter rewrites HTTP requests. diff --git a/internal/mode/static/state/graph/httproute.go b/internal/mode/static/state/graph/httproute.go index 090039ebf7..c8278280e0 100644 --- a/internal/mode/static/state/graph/httproute.go +++ b/internal/mode/static/state/graph/httproute.go @@ -328,8 +328,22 @@ func validateFilterRedirect( } if redirect.Path != nil { - valErr := field.Forbidden(redirectPath.Child("path"), "path is not supported") - allErrs = append(allErrs, valErr) + var path string + switch redirect.Path.Type { + case v1.FullPathHTTPPathModifier: + path = *redirect.Path.ReplaceFullPath + case v1.PrefixMatchHTTPPathModifier: + path = *redirect.Path.ReplacePrefixMatch + default: + msg := fmt.Sprintf("requestRedirect path type %s not supported", redirect.Path.Type) + valErr := field.Invalid(redirectPath.Child("path"), *redirect.Path, msg) + return append(allErrs, valErr) + } + + if err := validator.ValidatePath(path); err != nil { + valErr := field.Invalid(redirectPath.Child("path"), *redirect.Path, err.Error()) + allErrs = append(allErrs, valErr) + } } if redirect.StatusCode != nil { @@ -372,10 +386,10 @@ func validateFilterRewrite( default: msg := fmt.Sprintf("urlRewrite path type %s not supported", rewrite.Path.Type) valErr := field.Invalid(rewritePath.Child("path"), *rewrite.Path, msg) - return append(allErrs, valErr) + allErrs = append(allErrs, valErr) } - if err := validator.ValidateRewritePath(path); err != nil { + if err := validator.ValidatePath(path); err != nil { valErr := field.Invalid(rewritePath.Child("path"), *rewrite.Path, err.Error()) allErrs = append(allErrs, valErr) } diff --git a/internal/mode/static/state/graph/httproute_test.go b/internal/mode/static/state/graph/httproute_test.go index 825007d1ab..0f44f358b9 100644 --- a/internal/mode/static/state/graph/httproute_test.go +++ b/internal/mode/static/state/graph/httproute_test.go @@ -1207,6 +1207,10 @@ func TestValidateFilterRedirect(t *testing.T) { Hostname: helpers.GetPointer[gatewayv1.PreciseHostname]("example.com"), Port: helpers.GetPointer[gatewayv1.PortNumber](80), StatusCode: helpers.GetPointer(301), + Path: &gatewayv1.HTTPPathModifier{ + Type: gatewayv1.FullPathHTTPPathModifier, + ReplaceFullPath: helpers.GetPointer("/path"), + }, }, expectErrCount: 0, name: "valid redirect filter", @@ -1256,24 +1260,56 @@ func TestValidateFilterRedirect(t *testing.T) { name: "redirect filter with invalid port", }, { - validator: createAllValidValidator(), + validator: func() *validationfakes.FakeHTTPFieldsValidator { + validator := createAllValidValidator() + validator.ValidateRedirectStatusCodeReturns(false, []string{"200"}) + return validator + }(), requestRedirect: &gatewayv1.HTTPRequestRedirectFilter{ - Path: &gatewayv1.HTTPPathModifier{}, + StatusCode: helpers.GetPointer(301), // any value is invalid by the validator }, expectErrCount: 1, - name: "redirect filter with unsupported path modifier", + name: "redirect filter with invalid status code", }, { validator: func() *validationfakes.FakeHTTPFieldsValidator { - validator := createAllValidValidator() - validator.ValidateRedirectStatusCodeReturns(false, []string{"200"}) + validator := &validationfakes.FakeHTTPFieldsValidator{} + validator.ValidatePathReturns(errors.New("invalid path value")) return validator }(), requestRedirect: &gatewayv1.HTTPRequestRedirectFilter{ - StatusCode: helpers.GetPointer(301), // any value is invalid by the validator + Path: &gatewayv1.HTTPPathModifier{ + Type: gatewayv1.FullPathHTTPPathModifier, + ReplaceFullPath: helpers.GetPointer("/path"), + }, // any value is invalid by the validator }, expectErrCount: 1, - name: "redirect filter with invalid status code", + name: "redirect filter with invalid full path", + }, + { + validator: func() *validationfakes.FakeHTTPFieldsValidator { + validator := &validationfakes.FakeHTTPFieldsValidator{} + validator.ValidatePathReturns(errors.New("invalid path")) + return validator + }(), + requestRedirect: &gatewayv1.HTTPRequestRedirectFilter{ + Path: &gatewayv1.HTTPPathModifier{ + Type: gatewayv1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: helpers.GetPointer("/path"), + }, // any value is invalid by the validator + }, + expectErrCount: 1, + name: "redirect filter with invalid prefix path", + }, + { + validator: &validationfakes.FakeHTTPFieldsValidator{}, + requestRedirect: &gatewayv1.HTTPRequestRedirectFilter{ + Path: &gatewayv1.HTTPPathModifier{ + Type: "invalid-type", + }, + }, + expectErrCount: 1, + name: "redirect filter with invalid path type", }, { validator: func() *validationfakes.FakeHTTPFieldsValidator { @@ -1367,7 +1403,7 @@ func TestValidateFilterRewrite(t *testing.T) { { validator: func() *validationfakes.FakeHTTPFieldsValidator { validator := &validationfakes.FakeHTTPFieldsValidator{} - validator.ValidateRewritePathReturns(errors.New("invalid path value")) + validator.ValidatePathReturns(errors.New("invalid path value")) return validator }(), urlRewrite: &gatewayv1.HTTPURLRewriteFilter{ @@ -1382,7 +1418,7 @@ func TestValidateFilterRewrite(t *testing.T) { { validator: func() *validationfakes.FakeHTTPFieldsValidator { validator := &validationfakes.FakeHTTPFieldsValidator{} - validator.ValidateRewritePathReturns(errors.New("invalid path")) + validator.ValidatePathReturns(errors.New("invalid path")) return validator }(), urlRewrite: &gatewayv1.HTTPURLRewriteFilter{ @@ -1398,7 +1434,7 @@ func TestValidateFilterRewrite(t *testing.T) { validator: func() *validationfakes.FakeHTTPFieldsValidator { validator := &validationfakes.FakeHTTPFieldsValidator{} validator.ValidateHostnameReturns(errors.New("invalid hostname")) - validator.ValidateRewritePathReturns(errors.New("invalid path")) + validator.ValidatePathReturns(errors.New("invalid path")) return validator }(), urlRewrite: &gatewayv1.HTTPURLRewriteFilter{ diff --git a/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go b/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go index ffdd4df892..490018358f 100644 --- a/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go +++ b/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go @@ -76,6 +76,17 @@ type FakeHTTPFieldsValidator struct { result1 bool result2 []string } + ValidatePathStub func(string) error + validatePathMutex sync.RWMutex + validatePathArgsForCall []struct { + arg1 string + } + validatePathReturns struct { + result1 error + } + validatePathReturnsOnCall map[int]struct { + result1 error + } ValidatePathInMatchStub func(string) error validatePathInMatchMutex sync.RWMutex validatePathInMatchArgsForCall []struct { @@ -146,17 +157,6 @@ type FakeHTTPFieldsValidator struct { result1 bool result2 []string } - ValidateRewritePathStub func(string) error - validateRewritePathMutex sync.RWMutex - validateRewritePathArgsForCall []struct { - arg1 string - } - validateRewritePathReturns struct { - result1 error - } - validateRewritePathReturnsOnCall map[int]struct { - result1 error - } invocations map[string][][]interface{} invocationsMutex sync.RWMutex } @@ -530,6 +530,67 @@ func (fake *FakeHTTPFieldsValidator) ValidateMethodInMatchReturnsOnCall(i int, r }{result1, result2} } +func (fake *FakeHTTPFieldsValidator) ValidatePath(arg1 string) error { + fake.validatePathMutex.Lock() + ret, specificReturn := fake.validatePathReturnsOnCall[len(fake.validatePathArgsForCall)] + fake.validatePathArgsForCall = append(fake.validatePathArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.ValidatePathStub + fakeReturns := fake.validatePathReturns + fake.recordInvocation("ValidatePath", []interface{}{arg1}) + fake.validatePathMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeHTTPFieldsValidator) ValidatePathCallCount() int { + fake.validatePathMutex.RLock() + defer fake.validatePathMutex.RUnlock() + return len(fake.validatePathArgsForCall) +} + +func (fake *FakeHTTPFieldsValidator) ValidatePathCalls(stub func(string) error) { + fake.validatePathMutex.Lock() + defer fake.validatePathMutex.Unlock() + fake.ValidatePathStub = stub +} + +func (fake *FakeHTTPFieldsValidator) ValidatePathArgsForCall(i int) string { + fake.validatePathMutex.RLock() + defer fake.validatePathMutex.RUnlock() + argsForCall := fake.validatePathArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeHTTPFieldsValidator) ValidatePathReturns(result1 error) { + fake.validatePathMutex.Lock() + defer fake.validatePathMutex.Unlock() + fake.ValidatePathStub = nil + fake.validatePathReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeHTTPFieldsValidator) ValidatePathReturnsOnCall(i int, result1 error) { + fake.validatePathMutex.Lock() + defer fake.validatePathMutex.Unlock() + fake.ValidatePathStub = nil + if fake.validatePathReturnsOnCall == nil { + fake.validatePathReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.validatePathReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FakeHTTPFieldsValidator) ValidatePathInMatch(arg1 string) error { fake.validatePathInMatchMutex.Lock() ret, specificReturn := fake.validatePathInMatchReturnsOnCall[len(fake.validatePathInMatchArgsForCall)] @@ -902,67 +963,6 @@ func (fake *FakeHTTPFieldsValidator) ValidateRedirectStatusCodeReturnsOnCall(i i }{result1, result2} } -func (fake *FakeHTTPFieldsValidator) ValidateRewritePath(arg1 string) error { - fake.validateRewritePathMutex.Lock() - ret, specificReturn := fake.validateRewritePathReturnsOnCall[len(fake.validateRewritePathArgsForCall)] - fake.validateRewritePathArgsForCall = append(fake.validateRewritePathArgsForCall, struct { - arg1 string - }{arg1}) - stub := fake.ValidateRewritePathStub - fakeReturns := fake.validateRewritePathReturns - fake.recordInvocation("ValidateRewritePath", []interface{}{arg1}) - fake.validateRewritePathMutex.Unlock() - if stub != nil { - return stub(arg1) - } - if specificReturn { - return ret.result1 - } - return fakeReturns.result1 -} - -func (fake *FakeHTTPFieldsValidator) ValidateRewritePathCallCount() int { - fake.validateRewritePathMutex.RLock() - defer fake.validateRewritePathMutex.RUnlock() - return len(fake.validateRewritePathArgsForCall) -} - -func (fake *FakeHTTPFieldsValidator) ValidateRewritePathCalls(stub func(string) error) { - fake.validateRewritePathMutex.Lock() - defer fake.validateRewritePathMutex.Unlock() - fake.ValidateRewritePathStub = stub -} - -func (fake *FakeHTTPFieldsValidator) ValidateRewritePathArgsForCall(i int) string { - fake.validateRewritePathMutex.RLock() - defer fake.validateRewritePathMutex.RUnlock() - argsForCall := fake.validateRewritePathArgsForCall[i] - return argsForCall.arg1 -} - -func (fake *FakeHTTPFieldsValidator) ValidateRewritePathReturns(result1 error) { - fake.validateRewritePathMutex.Lock() - defer fake.validateRewritePathMutex.Unlock() - fake.ValidateRewritePathStub = nil - fake.validateRewritePathReturns = struct { - result1 error - }{result1} -} - -func (fake *FakeHTTPFieldsValidator) ValidateRewritePathReturnsOnCall(i int, result1 error) { - fake.validateRewritePathMutex.Lock() - defer fake.validateRewritePathMutex.Unlock() - fake.ValidateRewritePathStub = nil - if fake.validateRewritePathReturnsOnCall == nil { - fake.validateRewritePathReturnsOnCall = make(map[int]struct { - result1 error - }) - } - fake.validateRewritePathReturnsOnCall[i] = struct { - result1 error - }{result1} -} - func (fake *FakeHTTPFieldsValidator) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() @@ -978,6 +978,8 @@ func (fake *FakeHTTPFieldsValidator) Invocations() map[string][][]interface{} { defer fake.validateHostnameMutex.RUnlock() fake.validateMethodInMatchMutex.RLock() defer fake.validateMethodInMatchMutex.RUnlock() + fake.validatePathMutex.RLock() + defer fake.validatePathMutex.RUnlock() fake.validatePathInMatchMutex.RLock() defer fake.validatePathInMatchMutex.RUnlock() fake.validateQueryParamNameInMatchMutex.RLock() @@ -990,8 +992,6 @@ func (fake *FakeHTTPFieldsValidator) Invocations() map[string][][]interface{} { defer fake.validateRedirectSchemeMutex.RUnlock() fake.validateRedirectStatusCodeMutex.RLock() defer fake.validateRedirectStatusCodeMutex.RUnlock() - fake.validateRewritePathMutex.RLock() - defer fake.validateRewritePathMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} for key, value := range fake.invocations { copiedInvocations[key] = value diff --git a/internal/mode/static/state/validation/validator.go b/internal/mode/static/state/validation/validator.go index 4006fbf4a8..eba86b0e57 100644 --- a/internal/mode/static/state/validation/validator.go +++ b/internal/mode/static/state/validation/validator.go @@ -32,9 +32,9 @@ type HTTPFieldsValidator interface { ValidateRedirectPort(port int32) error ValidateRedirectStatusCode(statusCode int) (valid bool, supportedValues []string) ValidateHostname(hostname string) error - ValidateRewritePath(path string) error ValidateFilterHeaderName(name string) error ValidateFilterHeaderValue(value string) error + ValidatePath(path string) error } // GenericValidator validates any generic values from NGF API resources from the perspective of a data-plane. diff --git a/site/content/how-to/traffic-management/redirects-and-rewrites.md b/site/content/how-to/traffic-management/redirects-and-rewrites.md index 8785da32aa..f8eb293130 100644 --- a/site/content/how-to/traffic-management/redirects-and-rewrites.md +++ b/site/content/how-to/traffic-management/redirects-and-rewrites.md @@ -11,11 +11,11 @@ Learn how to redirect or rewrite your HTTP traffic using NGINX Gateway Fabric. [HTTPRoute](https://gateway-api.sigs.k8s.io/api-types/httproute/) filters can be used to configure HTTP redirects or rewrites. Redirects return HTTP 3XX responses to a client, instructing it to retrieve a different resource. Rewrites modify components of a client request (such as hostname and/or path) before proxying it upstream. -{{< note >}}NGINX Gateway Fabric currently does not support path-based redirects.{{< /note >}} +In this guide, we will set up the coffee application to demonstrate path URL rewriting, and the tea and soda applications to showcase path-based request redirection. For an introduction to exposing your application, we recommend that you follow the [basic guide]({{< relref "/how-to/traffic-management/routing-traffic-to-your-app.md" >}}) first. To see an example of a redirect using scheme and port, see the [HTTPS Termination]({{< relref "/how-to/traffic-management/https-termination.md" >}}) guide. -In this guide, we will be configuring a path URL rewrite. +--- ## Before you begin @@ -29,7 +29,42 @@ In this guide, we will be configuring a path URL rewrite. {{< note >}}In a production environment, you should have a DNS record for the external IP address that is exposed, and it should refer to the hostname that the gateway will forward for.{{< /note >}} -## Set up +--- + +## HTTP rewrites and redirects examples + +We will configure a common gateway for the `URLRewrite` and `RequestRedirect` filter examples mentioned below. + +--- + +### Deploy the Gateway resource for the applications + +The [Gateway](https://gateway-api.sigs.k8s.io/api-types/gateway/) resource is typically deployed by the [Cluster Operator](https://gateway-api.sigs.k8s.io/concepts/roles-and-personas/#roles-and-personas_1). This Gateway defines a single listener on port 80. Since no hostname is specified, this listener matches on all hostnames. To deploy the Gateway: + +```yaml +kubectl apply -f - < 80/TCP 40s ``` -## Configure a path rewrite - -To create the **cafe** gateway, copy and paste the following into your terminal: +--- -```yaml -kubectl apply -f - <}}If you have a DNS record allocated for `cafe.example.com`, you can send the request directly to that hostname, without needing to resolve.{{< /note >}} +This example demonstrates a rewrite from `http://cafe.example.com/coffee/flavors` to `http://cafe.example.com/beans`. + ```shell curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee/flavors ``` @@ -177,6 +201,8 @@ URI: /beans Other examples: +This example demonstrates a rewrite from `http://cafe.example.com/coffee` to `http://cafe.example.com/beans`. + ```shell curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee ``` @@ -188,6 +214,8 @@ Server name: coffee-6b8b6d6486-7fc78 URI: /beans ``` +This example demonstrates a rewrite from `http://cafe.example.com/coffee/mocha` to `http://cafe.example.com/beans` and specifies query parameters `test=v1&test=v2`. + ```shell curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee/mocha\?test\=v1\&test\=v2 ``` @@ -199,6 +227,8 @@ Server name: coffee-6db967495b-twn6x URI: /beans?test=v1&test=v2 ``` +This example demonstrates a rewrite from `http://cafe.example.com/latte/prices` to `http://cafe.example.com/prices`. + ```shell curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/latte/prices ``` @@ -210,6 +240,8 @@ Server name: coffee-6b8b6d6486-7fc78 URI: /prices ``` +This example demonstrates a rewrite from `http://cafe.example.com/coffee/latte/prices` to `http://cafe.example.com/prices` and specifies query parameters `test=v1&test=v2`. + ```shell curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/latte/prices\?test\=v1\&test\=v2 ``` @@ -222,6 +254,261 @@ Server name: coffee-6db967495b-twn6x URI: /prices?test=v1&test=v2 ``` +--- + +## RequestRedirect example + +This example demonstrates how to redirect the traffic to a new location for the tea and soda applications, using `RequestRedirect` filters. + +--- + +### Setup + +Create the **tea** and **soda** application in Kubernetes by copying and pasting the following block into your terminal: + +```yaml +kubectl apply -f - < 80/TCP 89m +service/tea ClusterIP 10.96.151.194 80/TCP 120m +``` + +--- + +### Configure a path redirect + +In this section, we'll define two HTTPRoutes for the tea and soda applications to demonstrate different types of request redirection using the `RequestRedirect` filter. + +1. The `tea-redirect` route uses the `ReplacePrefixMatch` type for path modification. This configuration matches the prefix of the original path and updates it to a new path, preserving the rest of the original URL structure. It will redirect request as follows: + + - `http://cafe.example.com/tea` to `http://cafe.example.com/organic` + - `http://cafe.example.com/tea/origin` to `http://cafe.example.com/organic/origin` + +2. The `soda-redirect` route uses the `ReplaceFullPath` type for path modification. This configuration updates the entire original path to the new location, effectively overwriting it. It will redirect request as follows: + + - `http://cafe.example.com/soda` to `http://cafe.example.com/flavors` + - `http://cafe.example.com/soda/pepsi` to `http://cafe.example.com/flavors` + +To create the httproute resource, copy and paste the following into your terminal: + +```yaml +kubectl apply -f - <}}If you have a DNS record allocated for `cafe.example.com`, you can send the request directly to that hostname, without needing to resolve.{{< /note >}} + +This example demonstrates a redirect from `http://cafe.example.com/tea` to `http://cafe.example.com/organic`. + +```shell +curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/tea --include +``` + +Notice in the output that the URI has been redirected to the new location: + +```text +HTTP/1.1 302 Moved Temporarily +.. +Location: http://cafe.example.com:8080/organic +``` + +Other examples: + +This example demonstrates a redirect from `http://cafe.example.com/tea/type` to `http://cafe.example.com/organic/type`. + +```shell +curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/tea/type --include +``` + +```text +HTTP/1.1 302 Moved Temporarily +.. +Location: http://cafe.example.com:8080/organic/type +``` + +This example demonstrates a redirect from `http://cafe.example.com/tea/type` to `http://cafe.example.com/organic/type` and specifies query params `test=v1`. + +```shell +curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/tea/type\?test\=v1 --include +``` + +```text +HTTP/1.1 302 Moved Temporarily +.. +Location: http://cafe.example.com:8080/organic/type?test=v1 +``` + +This example demonstrates a redirect from `http://cafe.example.com/soda` to `http://cafe.example.com/flavors`. + +```shell +curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/soda --include +``` + +```text +HTTP/1.1 302 Moved Temporarily +.. +Location: http://cafe.example.com:8080/flavors +``` + +This example demonstrates a redirect from `http://cafe.example.com/soda/pepsi` to `http://cafe.example.com/flavors/pepsi`. + +```shell +curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/soda/pepsi --include +``` + +```text +HTTP/1.1 302 Moved Temporarily +.. +Location: http://cafe.example.com:8080/flavors +``` + +This example demonstrates a redirect from `http://cafe.example.com/soda/pepsi` to `http://cafe.example.com/flavors/pepsi` and specifies query params `test=v1`. + +```shell +curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/soda/pepsi\?test\=v1 --include +``` + +```text +HTTP/1.1 302 Moved Temporarily +.. +Location: http://cafe.example.com:8080/flavors?test=v1 +``` + +--- + ## Further reading To learn more about redirects and rewrites using the Gateway API, see the following resource: diff --git a/site/content/overview/gateway-api-compatibility.md b/site/content/overview/gateway-api-compatibility.md index b0a344e0a6..7573597983 100644 --- a/site/content/overview/gateway-api-compatibility.md +++ b/site/content/overview/gateway-api-compatibility.md @@ -166,7 +166,7 @@ See the [static-mode]({{< relref "/reference/cli-help.md#static-mode">}}) comman - `method`: Supported. - `filters` - `type`: Supported. - - `requestRedirect`: Supported except for the experimental `path` field. If multiple filters are configured, NGINX Gateway Fabric will choose the first and ignore the rest. Incompatible with `urlRewrite`. + - `requestRedirect`: Supported. If multiple filters are configured, NGINX Gateway Fabric will choose the first and ignore the rest. Incompatible with `urlRewrite`. - `requestHeaderModifier`: Supported. If multiple filters are configured, NGINX Gateway Fabric will choose the first and ignore the rest. - `urlRewrite`: Supported. If multiple filters are configured, NGINX Gateway Fabric will choose the first and ignore the rest. Incompatible with `requestRedirect`. - `responseHeaderModifier`: Supported. If multiple filters are configured, NGINX Gateway Fabric will choose the first and ignore the rest. diff --git a/tests/Makefile b/tests/Makefile index b4e34787ee..8db42ea777 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -14,7 +14,7 @@ NGF_VERSION ?= edge## NGF version to be tested PULL_POLICY = Never## Pull policy for the images NGINX_CONF_DIR = internal/mode/static/nginx/conf PROVISIONER_MANIFEST = conformance/provisioner/provisioner.yaml -SUPPORTED_EXTENDED_FEATURES = HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteSchemeRedirect,HTTPRouteHostRewrite,HTTPRoutePathRewrite,GatewayPort8080,HTTPRouteResponseHeaderModification +SUPPORTED_EXTENDED_FEATURES = HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteSchemeRedirect,HTTPRouteHostRewrite,HTTPRoutePathRewrite,GatewayPort8080,HTTPRouteResponseHeaderModification,HTTPRoutePathRedirect STANDARD_CONFORMANCE_PROFILES = GATEWAY-HTTP,GATEWAY-GRPC EXPERIMENTAL_CONFORMANCE_PROFILES = GATEWAY-TLS CONFORMANCE_PROFILES = $(STANDARD_CONFORMANCE_PROFILES) # by default we use the standard conformance profiles. If experimental is enabled we override this and add the experimental profiles.