From 1063affd4ed47a39c74bde38bb17dd87ebccbb14 Mon Sep 17 00:00:00 2001 From: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:43:49 +0000 Subject: [PATCH 1/5] Add nginx.org/http-redirect-code annotation and ConfigMap support - Add ConfigMap option to set default HTTP redirect code globally - Add nginx.org/http-redirect-code annotation for per-Ingress override - Support redirect codes 301, 302, 307, 308 with validation - Annotation requires ssl-redirect or redirect-to-https to be enabled - Add k8s validation for annotation format checking - Update NGINX templates to use configurable redirect code Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> --- .../k8s.nginx.org_virtualserverroutes.yaml | 19 +- .../bases/k8s.nginx.org_virtualservers.yaml | 19 +- deploy/crds.yaml | 38 ++-- docs/crd/k8s.nginx.org_virtualserverroutes.md | 10 +- docs/crd/k8s.nginx.org_virtualservers.md | 10 +- internal/configs/annotations.go | 26 ++- internal/configs/annotations_test.go | 56 ++++++ internal/configs/config_params.go | 2 + internal/configs/configmaps.go | 34 ++++ internal/configs/configmaps_test.go | 162 ++++++++++++++++++ internal/configs/ingress.go | 1 + internal/configs/ingress_test.go | 6 + .../version1/__snapshots__/template_test.snap | 50 ++++++ internal/configs/version1/config.go | 1 + .../configs/version1/nginx-plus.ingress.tmpl | 4 +- internal/configs/version1/nginx.ingress.tmpl | 4 +- internal/configs/version1/template_test.go | 66 +++++++ internal/configs/virtualserver.go | 6 +- internal/configs/virtualserver_test.go | 45 +++-- internal/k8s/validation.go | 12 ++ internal/k8s/validation_test.go | 104 ++++++++++- internal/telemetry/cluster.go | 1 + pkg/apis/configuration/v1/types.go | 2 +- 23 files changed, 621 insertions(+), 57 deletions(-) diff --git a/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml b/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml index f8105b7dcd..6303fcd55d 100644 --- a/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml +++ b/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml @@ -178,7 +178,9 @@ spec: properties: code: description: 'The status code of a redirect. The allowed - values are: 301, 302, 307 or 308. The default is 301.' + values are: 301, 302, 307 or 308. The default follows + the ConfigMap http-redirect-code setting, which defaults + to 301.' type: integer url: description: 'The URL to redirect the request to. Supported @@ -243,8 +245,9 @@ spec: properties: code: description: 'The status code of a redirect. The allowed - values are: 301, 302, 307 or 308. The default is - 301.' + values are: 301, 302, 307 or 308. The default follows + the ConfigMap http-redirect-code setting, which + defaults to 301.' type: integer url: description: 'The URL to redirect the request to. @@ -400,7 +403,8 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The - default is 301.' + default follows the ConfigMap http-redirect-code + setting, which defaults to 301.' type: integer url: description: 'The URL to redirect the request @@ -585,7 +589,9 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 - or 308. The default is 301.' + or 308. The default follows the ConfigMap + http-redirect-code setting, which defaults + to 301.' type: integer url: description: 'The URL to redirect the request @@ -783,7 +789,8 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The - default is 301.' + default follows the ConfigMap http-redirect-code + setting, which defaults to 301.' type: integer url: description: 'The URL to redirect the request diff --git a/config/crd/bases/k8s.nginx.org_virtualservers.yaml b/config/crd/bases/k8s.nginx.org_virtualservers.yaml index 677beb84aa..272fa93e8e 100644 --- a/config/crd/bases/k8s.nginx.org_virtualservers.yaml +++ b/config/crd/bases/k8s.nginx.org_virtualservers.yaml @@ -265,7 +265,9 @@ spec: properties: code: description: 'The status code of a redirect. The allowed - values are: 301, 302, 307 or 308. The default is 301.' + values are: 301, 302, 307 or 308. The default follows + the ConfigMap http-redirect-code setting, which defaults + to 301.' type: integer url: description: 'The URL to redirect the request to. Supported @@ -330,8 +332,9 @@ spec: properties: code: description: 'The status code of a redirect. The allowed - values are: 301, 302, 307 or 308. The default is - 301.' + values are: 301, 302, 307 or 308. The default follows + the ConfigMap http-redirect-code setting, which + defaults to 301.' type: integer url: description: 'The URL to redirect the request to. @@ -487,7 +490,8 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The - default is 301.' + default follows the ConfigMap http-redirect-code + setting, which defaults to 301.' type: integer url: description: 'The URL to redirect the request @@ -672,7 +676,9 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 - or 308. The default is 301.' + or 308. The default follows the ConfigMap + http-redirect-code setting, which defaults + to 301.' type: integer url: description: 'The URL to redirect the request @@ -870,7 +876,8 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The - default is 301.' + default follows the ConfigMap http-redirect-code + setting, which defaults to 301.' type: integer url: description: 'The URL to redirect the request diff --git a/deploy/crds.yaml b/deploy/crds.yaml index 93fc92efaa..cfc4b92c7f 100644 --- a/deploy/crds.yaml +++ b/deploy/crds.yaml @@ -1368,7 +1368,9 @@ spec: properties: code: description: 'The status code of a redirect. The allowed - values are: 301, 302, 307 or 308. The default is 301.' + values are: 301, 302, 307 or 308. The default follows + the ConfigMap http-redirect-code setting, which defaults + to 301.' type: integer url: description: 'The URL to redirect the request to. Supported @@ -1433,8 +1435,9 @@ spec: properties: code: description: 'The status code of a redirect. The allowed - values are: 301, 302, 307 or 308. The default is - 301.' + values are: 301, 302, 307 or 308. The default follows + the ConfigMap http-redirect-code setting, which + defaults to 301.' type: integer url: description: 'The URL to redirect the request to. @@ -1590,7 +1593,8 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The - default is 301.' + default follows the ConfigMap http-redirect-code + setting, which defaults to 301.' type: integer url: description: 'The URL to redirect the request @@ -1775,7 +1779,9 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 - or 308. The default is 301.' + or 308. The default follows the ConfigMap + http-redirect-code setting, which defaults + to 301.' type: integer url: description: 'The URL to redirect the request @@ -1973,7 +1979,8 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The - default is 301.' + default follows the ConfigMap http-redirect-code + setting, which defaults to 301.' type: integer url: description: 'The URL to redirect the request @@ -2692,7 +2699,9 @@ spec: properties: code: description: 'The status code of a redirect. The allowed - values are: 301, 302, 307 or 308. The default is 301.' + values are: 301, 302, 307 or 308. The default follows + the ConfigMap http-redirect-code setting, which defaults + to 301.' type: integer url: description: 'The URL to redirect the request to. Supported @@ -2757,8 +2766,9 @@ spec: properties: code: description: 'The status code of a redirect. The allowed - values are: 301, 302, 307 or 308. The default is - 301.' + values are: 301, 302, 307 or 308. The default follows + the ConfigMap http-redirect-code setting, which + defaults to 301.' type: integer url: description: 'The URL to redirect the request to. @@ -2914,7 +2924,8 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The - default is 301.' + default follows the ConfigMap http-redirect-code + setting, which defaults to 301.' type: integer url: description: 'The URL to redirect the request @@ -3099,7 +3110,9 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 - or 308. The default is 301.' + or 308. The default follows the ConfigMap + http-redirect-code setting, which defaults + to 301.' type: integer url: description: 'The URL to redirect the request @@ -3297,7 +3310,8 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The - default is 301.' + default follows the ConfigMap http-redirect-code + setting, which defaults to 301.' type: integer url: description: 'The URL to redirect the request diff --git a/docs/crd/k8s.nginx.org_virtualserverroutes.md b/docs/crd/k8s.nginx.org_virtualserverroutes.md index 3874555213..dc39cde5a8 100644 --- a/docs/crd/k8s.nginx.org_virtualserverroutes.md +++ b/docs/crd/k8s.nginx.org_virtualserverroutes.md @@ -37,7 +37,7 @@ The `.spec` object supports the following fields: | `subroutes[].action.proxy.rewritePath` | `string` | The rewritten URI. If the route path is a regular expression – starts with ~ – the rewritePath can include capture groups with $1-9. For example $1 for the first group, and so on. For more information, check the rewrite example. | | `subroutes[].action.proxy.upstream` | `string` | The name of the upstream which the requests will be proxied to. The upstream with that name must be defined in the resource. | | `subroutes[].action.redirect` | `object` | Redirects requests to a provided URL. | -| `subroutes[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | +| `subroutes[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | | `subroutes[].action.redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `subroutes[].action.return` | `object` | Returns a preconfigured response. | | `subroutes[].action.return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | @@ -50,7 +50,7 @@ The `.spec` object supports the following fields: | `subroutes[].errorPages` | `array` | The custom responses for error codes. NGINX will use those responses instead of returning the error responses from the upstream servers or the default responses generated by NGINX. A custom response can be a redirect or a canned response. For example, a redirect to another URL if an upstream server responded with a 404 status code. | | `subroutes[].errorPages[].codes` | `array[integer]` | A list of error status codes. | | `subroutes[].errorPages[].redirect` | `object` | The canned response action for the given status codes. | -| `subroutes[].errorPages[].redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | +| `subroutes[].errorPages[].redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | | `subroutes[].errorPages[].redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `subroutes[].errorPages[].return` | `object` | The redirect action for the given status codes. | | `subroutes[].errorPages[].return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | @@ -80,7 +80,7 @@ The `.spec` object supports the following fields: | `subroutes[].matches[].action.proxy.rewritePath` | `string` | The rewritten URI. If the route path is a regular expression – starts with ~ – the rewritePath can include capture groups with $1-9. For example $1 for the first group, and so on. For more information, check the rewrite example. | | `subroutes[].matches[].action.proxy.upstream` | `string` | The name of the upstream which the requests will be proxied to. The upstream with that name must be defined in the resource. | | `subroutes[].matches[].action.redirect` | `object` | Redirects requests to a provided URL. | -| `subroutes[].matches[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | +| `subroutes[].matches[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | | `subroutes[].matches[].action.redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `subroutes[].matches[].action.return` | `object` | Returns a preconfigured response. | | `subroutes[].matches[].action.return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | @@ -115,7 +115,7 @@ The `.spec` object supports the following fields: | `subroutes[].matches[].splits[].action.proxy.rewritePath` | `string` | The rewritten URI. If the route path is a regular expression – starts with ~ – the rewritePath can include capture groups with $1-9. For example $1 for the first group, and so on. For more information, check the rewrite example. | | `subroutes[].matches[].splits[].action.proxy.upstream` | `string` | The name of the upstream which the requests will be proxied to. The upstream with that name must be defined in the resource. | | `subroutes[].matches[].splits[].action.redirect` | `object` | Redirects requests to a provided URL. | -| `subroutes[].matches[].splits[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | +| `subroutes[].matches[].splits[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | | `subroutes[].matches[].splits[].action.redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `subroutes[].matches[].splits[].action.return` | `object` | Returns a preconfigured response. | | `subroutes[].matches[].splits[].action.return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | @@ -150,7 +150,7 @@ The `.spec` object supports the following fields: | `subroutes[].splits[].action.proxy.rewritePath` | `string` | The rewritten URI. If the route path is a regular expression – starts with ~ – the rewritePath can include capture groups with $1-9. For example $1 for the first group, and so on. For more information, check the rewrite example. | | `subroutes[].splits[].action.proxy.upstream` | `string` | The name of the upstream which the requests will be proxied to. The upstream with that name must be defined in the resource. | | `subroutes[].splits[].action.redirect` | `object` | Redirects requests to a provided URL. | -| `subroutes[].splits[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | +| `subroutes[].splits[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | | `subroutes[].splits[].action.redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `subroutes[].splits[].action.return` | `object` | Returns a preconfigured response. | | `subroutes[].splits[].action.return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | diff --git a/docs/crd/k8s.nginx.org_virtualservers.md b/docs/crd/k8s.nginx.org_virtualservers.md index cffd599ffd..10a4eadd52 100644 --- a/docs/crd/k8s.nginx.org_virtualservers.md +++ b/docs/crd/k8s.nginx.org_virtualservers.md @@ -55,7 +55,7 @@ The `.spec` object supports the following fields: | `routes[].action.proxy.rewritePath` | `string` | The rewritten URI. If the route path is a regular expression – starts with ~ – the rewritePath can include capture groups with $1-9. For example $1 for the first group, and so on. For more information, check the rewrite example. | | `routes[].action.proxy.upstream` | `string` | The name of the upstream which the requests will be proxied to. The upstream with that name must be defined in the resource. | | `routes[].action.redirect` | `object` | Redirects requests to a provided URL. | -| `routes[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | +| `routes[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | | `routes[].action.redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `routes[].action.return` | `object` | Returns a preconfigured response. | | `routes[].action.return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | @@ -68,7 +68,7 @@ The `.spec` object supports the following fields: | `routes[].errorPages` | `array` | The custom responses for error codes. NGINX will use those responses instead of returning the error responses from the upstream servers or the default responses generated by NGINX. A custom response can be a redirect or a canned response. For example, a redirect to another URL if an upstream server responded with a 404 status code. | | `routes[].errorPages[].codes` | `array[integer]` | A list of error status codes. | | `routes[].errorPages[].redirect` | `object` | The canned response action for the given status codes. | -| `routes[].errorPages[].redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | +| `routes[].errorPages[].redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | | `routes[].errorPages[].redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `routes[].errorPages[].return` | `object` | The redirect action for the given status codes. | | `routes[].errorPages[].return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | @@ -98,7 +98,7 @@ The `.spec` object supports the following fields: | `routes[].matches[].action.proxy.rewritePath` | `string` | The rewritten URI. If the route path is a regular expression – starts with ~ – the rewritePath can include capture groups with $1-9. For example $1 for the first group, and so on. For more information, check the rewrite example. | | `routes[].matches[].action.proxy.upstream` | `string` | The name of the upstream which the requests will be proxied to. The upstream with that name must be defined in the resource. | | `routes[].matches[].action.redirect` | `object` | Redirects requests to a provided URL. | -| `routes[].matches[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | +| `routes[].matches[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | | `routes[].matches[].action.redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `routes[].matches[].action.return` | `object` | Returns a preconfigured response. | | `routes[].matches[].action.return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | @@ -133,7 +133,7 @@ The `.spec` object supports the following fields: | `routes[].matches[].splits[].action.proxy.rewritePath` | `string` | The rewritten URI. If the route path is a regular expression – starts with ~ – the rewritePath can include capture groups with $1-9. For example $1 for the first group, and so on. For more information, check the rewrite example. | | `routes[].matches[].splits[].action.proxy.upstream` | `string` | The name of the upstream which the requests will be proxied to. The upstream with that name must be defined in the resource. | | `routes[].matches[].splits[].action.redirect` | `object` | Redirects requests to a provided URL. | -| `routes[].matches[].splits[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | +| `routes[].matches[].splits[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | | `routes[].matches[].splits[].action.redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `routes[].matches[].splits[].action.return` | `object` | Returns a preconfigured response. | | `routes[].matches[].splits[].action.return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | @@ -168,7 +168,7 @@ The `.spec` object supports the following fields: | `routes[].splits[].action.proxy.rewritePath` | `string` | The rewritten URI. If the route path is a regular expression – starts with ~ – the rewritePath can include capture groups with $1-9. For example $1 for the first group, and so on. For more information, check the rewrite example. | | `routes[].splits[].action.proxy.upstream` | `string` | The name of the upstream which the requests will be proxied to. The upstream with that name must be defined in the resource. | | `routes[].splits[].action.redirect` | `object` | Redirects requests to a provided URL. | -| `routes[].splits[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | +| `routes[].splits[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | | `routes[].splits[].action.redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `routes[].splits[].action.return` | `object` | Returns a preconfigured response. | | `routes[].splits[].action.return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | diff --git a/internal/configs/annotations.go b/internal/configs/annotations.go index 8e1f58ef85..00c112d6ef 100644 --- a/internal/configs/annotations.go +++ b/internal/configs/annotations.go @@ -33,6 +33,12 @@ const UseClusterIPAnnotation = "nginx.org/use-cluster-ip" // SSLRedirectAnnotation is the annotation where the SSL redirect boolean is specified. const SSLRedirectAnnotation = "nginx.org/ssl-redirect" +// HTTPRedirectCodeAnnotation is the annotation where the HTTP redirect code is specified. +const HTTPRedirectCodeAnnotation = "nginx.org/http-redirect-code" + +// RedirectToHTTPSAnnotation is the annotation where the redirect-to-https boolean is specified. +const RedirectToHTTPSAnnotation = "nginx.org/redirect-to-https" + // AppProtectPolicyAnnotation is where the NGINX App Protect policy is specified const AppProtectPolicyAnnotation = "appprotect.f5.com/app-protect-policy" @@ -63,9 +69,10 @@ var masterDenylist = map[string]bool{ var minionDenylist = map[string]bool{ "nginx.org/proxy-hide-headers": true, "nginx.org/proxy-pass-headers": true, - "nginx.org/redirect-to-https": true, + RedirectToHTTPSAnnotation: true, "ingress.kubernetes.io/ssl-redirect": true, SSLRedirectAnnotation: true, + HTTPRedirectCodeAnnotation: true, "nginx.org/hsts": true, "nginx.org/hsts-max-age": true, "nginx.org/hsts-include-subdomains": true, @@ -259,7 +266,7 @@ func parseAnnotations(ingEx *IngressEx, baseCfgParams *ConfigParams, isPlus bool cfgParams.ClientBodyBufferSize = size } - if redirectToHTTPS, exists, err := GetMapKeyAsBool(ingEx.Ingress.Annotations, "nginx.org/redirect-to-https", ingEx.Ingress); exists { + if redirectToHTTPS, exists, err := GetMapKeyAsBool(ingEx.Ingress.Annotations, RedirectToHTTPSAnnotation, ingEx.Ingress); exists { if err != nil { nl.Error(l, err) } else { @@ -281,6 +288,21 @@ func parseAnnotations(ingEx *IngressEx, baseCfgParams *ConfigParams, isPlus bool } } + if httpRedirectCode, exists := ingEx.Ingress.Annotations[HTTPRedirectCodeAnnotation]; exists { + // Check if SSL redirect or redirect-to-https is enabled + sslRedirectEnabled := cfgParams.SSLRedirect + redirectToHTTPSEnabled := cfgParams.RedirectToHTTPS + + // HTTP redirect code annotation only applies when SSL redirect or redirect-to-https is enabled + if sslRedirectEnabled || redirectToHTTPSEnabled { + if code, err := ParseHTTPRedirectCode(httpRedirectCode); err != nil { + nl.Errorf(l, "Ingress %s/%s: Invalid value for nginx.org/http-redirect-code: %q: %v", ingEx.Ingress.GetNamespace(), ingEx.Ingress.GetName(), httpRedirectCode, err) + } else { + cfgParams.HTTPRedirectCode = code + } + } + } + if sslCiphers, exists := ingEx.Ingress.Annotations[SSLCiphersAnnotation]; exists { cfgParams.ServerSSLCiphers = sslCiphers } diff --git a/internal/configs/annotations_test.go b/internal/configs/annotations_test.go index 1d612f5515..4afd0da131 100644 --- a/internal/configs/annotations_test.go +++ b/internal/configs/annotations_test.go @@ -1011,3 +1011,59 @@ func TestSSLRedirectAnnotations(t *testing.T) { }) } } + +func TestHTTPRedirectCodeAnnotationBehavior(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + annotations map[string]string + expectedCode int + }{ + { + name: "redirect code applied when SSL redirect enabled", + annotations: map[string]string{ + "nginx.org/ssl-redirect": "true", + "nginx.org/http-redirect-code": "307", + }, + expectedCode: 307, + }, + { + name: "redirect code ignored when SSL redirect disabled", + annotations: map[string]string{ + "nginx.org/ssl-redirect": "false", + "nginx.org/http-redirect-code": "307", + }, + expectedCode: 301, // default + }, + { + name: "redirect code applied with redirect-to-https", + annotations: map[string]string{ + "nginx.org/redirect-to-https": "true", + "nginx.org/http-redirect-code": "302", + }, + expectedCode: 302, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ingEx := &IngressEx{ + Ingress: &networking.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ingress", + Namespace: "default", + Annotations: tt.annotations, + }, + }, + } + + baseCfgParams := NewDefaultConfigParams(context.Background(), false) + result := parseAnnotations(ingEx, baseCfgParams, false, false, false, false, false) + + if result.HTTPRedirectCode != tt.expectedCode { + t.Errorf("Test %q: expected HTTPRedirectCode %d, got %d", tt.name, tt.expectedCode, result.HTTPRedirectCode) + } + }) + } +} diff --git a/internal/configs/config_params.go b/internal/configs/config_params.go index 82e32944da..f5fc4c8c35 100644 --- a/internal/configs/config_params.go +++ b/internal/configs/config_params.go @@ -84,6 +84,7 @@ type ConfigParams struct { ProxyReadTimeout string ProxySendTimeout string RedirectToHTTPS bool + HTTPRedirectCode int ResolverAddresses []string ResolverIPV6 bool ResolverTimeout string @@ -245,6 +246,7 @@ func NewDefaultConfigParams(ctx context.Context, isPlus bool) *ConfigParams { ProxySendTimeout: "60s", ClientMaxBodySize: "1m", SSLRedirect: true, + HTTPRedirectCode: 301, MainAccessLog: "/dev/stdout main", MainServerNamesHashBucketSize: "256", MainServerNamesHashMaxSize: "1024", diff --git a/internal/configs/configmaps.go b/internal/configs/configmaps.go index 7137e0d6c3..be8ad9c871 100644 --- a/internal/configs/configmaps.go +++ b/internal/configs/configmaps.go @@ -6,6 +6,7 @@ import ( "fmt" "log/slog" "os" + "strconv" "strings" "time" @@ -160,6 +161,17 @@ func ParseConfigMap(ctx context.Context, cfgm *v1.ConfigMap, nginxPlus bool, has } } + if httpRedirectCode, exists := cfgm.Data["http-redirect-code"]; exists { + if code, err := ParseHTTPRedirectCode(httpRedirectCode); err != nil { + errorText := fmt.Sprintf("ConfigMap %s/%s: Invalid value for 'http-redirect-code': %q: %v, ignoring", cfgm.GetNamespace(), cfgm.GetName(), httpRedirectCode, err) + nl.Error(l, errorText) + eventLog.Event(cfgm, v1.EventTypeWarning, nl.EventReasonInvalidValue, errorText) + configOk = false + } else { + cfgParams.HTTPRedirectCode = code + } + } + if hsts, exists, err := GetMapKeyAsBool(cfgm.Data, "hsts", cfgm); exists { if err != nil { nl.Error(l, err) @@ -955,6 +967,28 @@ func parseConfigMapOpenTelemetry(l *slog.Logger, cfgm *v1.ConfigMap, cfgParams * return cfgParams, nil } +// ParseHTTPRedirectCode parses and validates an HTTP redirect code. +func ParseHTTPRedirectCode(code string) (int, error) { + redirectCode, err := strconv.Atoi(code) + if err != nil { + return 0, fmt.Errorf("invalid redirect code: %w", err) + } + + // Validate that the code is one of the allowed redirect codes: 301, 302, 307, 308 + validCodes := map[int]bool{ + 301: true, + 302: true, + 307: true, + 308: true, + } + + if _, valid := validCodes[redirectCode]; !valid { + return 0, fmt.Errorf("status code out of accepted range. accepted values are '301', '302', '307', '308'") + } + + return redirectCode, nil +} + // ParseMGMTConfigMap parses the mgmt block ConfigMap into MGMTConfigParams. // //nolint:gocyclo diff --git a/internal/configs/configmaps_test.go b/internal/configs/configmaps_test.go index 6d73204040..4831fd8877 100644 --- a/internal/configs/configmaps_test.go +++ b/internal/configs/configmaps_test.go @@ -2712,6 +2712,168 @@ func TestParseErrorLogLevelToVirtualServer(t *testing.T) { } } +func TestParseHTTPRedirectCode(t *testing.T) { + t.Parallel() + tests := []struct { + code string + expect int + isValid bool + msg string + }{ + { + code: "301", + expect: 301, + isValid: true, + msg: "valid code 301", + }, + { + code: "302", + expect: 302, + isValid: true, + msg: "valid code 302", + }, + { + code: "307", + expect: 307, + isValid: true, + msg: "valid code 307", + }, + { + code: "308", + expect: 308, + isValid: true, + msg: "valid code 308", + }, + { + code: "200", + expect: 0, + isValid: false, + msg: "invalid code 200", + }, + { + code: "404", + expect: 0, + isValid: false, + msg: "invalid code 404", + }, + { + code: "invalid", + expect: 0, + isValid: false, + msg: "non-numeric code", + }, + { + code: "", + expect: 0, + isValid: false, + msg: "empty code", + }, + } + + for _, test := range tests { + result, err := ParseHTTPRedirectCode(test.code) + if test.isValid { + assert.NoError(t, err, test.msg) + assert.Equal(t, test.expect, result, test.msg) + } else { + assert.Error(t, err, test.msg) + } + } +} + +func TestParseConfigMapWithHTTPRedirectCode(t *testing.T) { + t.Parallel() + nginxPlus := false + hasAppProtect := false + hasAppProtectDos := false + hasTLSPassthrough := false + directiveAutoadjustEnabled := false + + tests := []struct { + configMap map[string]string + expected int + expectError bool + msg string + }{ + { + configMap: map[string]string{ + "http-redirect-code": "301", + }, + expected: 301, + expectError: false, + msg: "valid redirect code 301", + }, + { + configMap: map[string]string{ + "http-redirect-code": "302", + }, + expected: 302, + expectError: false, + msg: "valid redirect code 302", + }, + { + configMap: map[string]string{ + "http-redirect-code": "307", + }, + expected: 307, + expectError: false, + msg: "valid redirect code 307", + }, + { + configMap: map[string]string{ + "http-redirect-code": "308", + }, + expected: 308, + expectError: false, + msg: "valid redirect code 308", + }, + { + configMap: map[string]string{ + "http-redirect-code": "200", + }, + expected: 301, // should fallback to default + expectError: true, + msg: "invalid redirect code 200", + }, + { + configMap: map[string]string{ + "http-redirect-code": "invalid", + }, + expected: 301, // should fallback to default + expectError: true, + msg: "non-numeric redirect code", + }, + { + configMap: map[string]string{}, + expected: 301, // default value + expectError: false, + msg: "no redirect code specified", + }, + } + + for _, test := range tests { + t.Run(test.msg, func(t *testing.T) { + configMap := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nginx-config", + Namespace: "nginx-ingress", + }, + Data: test.configMap, + } + + result, configOK := ParseConfigMap(context.Background(), configMap, nginxPlus, hasAppProtect, hasAppProtectDos, hasTLSPassthrough, directiveAutoadjustEnabled, makeEventLogger()) + + if test.expectError { + assert.False(t, configOK, test.msg) + } else { + assert.True(t, configOK, test.msg) + } + + assert.Equal(t, test.expected, result.HTTPRedirectCode, test.msg) + }) + } +} + func makeEventLogger() record.EventRecorder { return record.NewFakeRecorder(1024) } diff --git a/internal/configs/ingress.go b/internal/configs/ingress.go index 0b4fc2d12b..19e0fb4e74 100644 --- a/internal/configs/ingress.go +++ b/internal/configs/ingress.go @@ -163,6 +163,7 @@ func generateNginxCfg(p NginxCfgParams) (version1.IngressNginxConfig, Warnings) HTTP2: cfgParams.HTTP2, RedirectToHTTPS: cfgParams.RedirectToHTTPS, SSLRedirect: cfgParams.SSLRedirect, + HTTPRedirectCode: cfgParams.HTTPRedirectCode, SSLCiphers: cfgParams.ServerSSLCiphers, SSLPreferServerCiphers: cfgParams.ServerSSLPreferServerCiphers, ProxyProtocol: cfgParams.ProxyProtocol, diff --git a/internal/configs/ingress_test.go b/internal/configs/ingress_test.go index 2e5ddb5f2e..75c99c4227 100644 --- a/internal/configs/ingress_test.go +++ b/internal/configs/ingress_test.go @@ -379,6 +379,7 @@ func createExpectedConfigForCafeIngressEx(isPlus bool) version1.IngressNginxConf Ports: []int{80}, SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, HealthChecks: make(map[string]version1.HealthCheck), }, }, @@ -780,6 +781,7 @@ func createExpectedConfigForMergeableCafeIngressWithUseClusterIP() version1.Ingr Ports: []int{80}, SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, HealthChecks: make(map[string]version1.HealthCheck), }, }, @@ -868,6 +870,7 @@ func createExpectedConfigForCafeIngressWithUseClusterIPNamedPorts() version1.Ing Ports: []int{80}, SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, HealthChecks: make(map[string]version1.HealthCheck), }, }, @@ -955,6 +958,7 @@ func createExpectedConfigForCafeIngressWithUseClusterIP() version1.IngressNginxC Ports: []int{80}, SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, HealthChecks: make(map[string]version1.HealthCheck), }, }, @@ -1693,6 +1697,7 @@ func createExpectedConfigForMergeableCafeIngress(isPlus bool) version1.IngressNg Ports: []int{80}, SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, HealthChecks: make(map[string]version1.HealthCheck), }, }, @@ -1793,6 +1798,7 @@ func createExpectedConfigForCrossNamespaceMergeableCafeIngress() version1.Ingres Ports: []int{80}, SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, HealthChecks: make(map[string]version1.HealthCheck), }, }, diff --git a/internal/configs/version1/__snapshots__/template_test.snap b/internal/configs/version1/__snapshots__/template_test.snap index a8e907f669..b7e9f3d93a 100644 --- a/internal/configs/version1/__snapshots__/template_test.snap +++ b/internal/configs/version1/__snapshots__/template_test.snap @@ -2402,6 +2402,56 @@ server { --- +[TestExecuteTemplate_ForIngressForNGINXWithHTTPRedirectCode - 1] +# configuration for default/cafe-ingress +upstream test {zone test 256k; + server 127.0.0.1:8181 max_fails=0 fail_timeout=1s max_conns=0;keepalive 16; +} + + + +server { + listen 443 ssl;listen [::]:443 ssl; + ssl_certificate secret.pem; + ssl_certificate_key secret.pem; + + server_tokens off; + + server_name test.example.com; + + set $resource_type "ingress"; + set $resource_name "cafe-ingress"; + set $resource_namespace "default"; + if ($scheme = http) { + return 308 https://$host:443$request_uri; + } + if ($http_x_forwarded_proto = 'http') { + return 308 https://$host$request_uri; + } + location /tea { + set $service ""; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_connect_timeout 10s; + proxy_read_timeout 10s; + proxy_send_timeout 10s; + client_max_body_size 2m; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Proto https; + proxy_buffering off; + proxy_pass http://test; + + + } + +} + +--- + [TestExecuteTemplate_ForIngressForNGINXWithProxySetHeadersAnnotationWithDefaultValue - 1] # configuration for default/cafe-ingress-master diff --git a/internal/configs/version1/config.go b/internal/configs/version1/config.go index 317157db97..abea167e6c 100644 --- a/internal/configs/version1/config.go +++ b/internal/configs/version1/config.go @@ -94,6 +94,7 @@ type Server struct { HTTP2 bool RedirectToHTTPS bool SSLRedirect bool + HTTPRedirectCode int ProxyProtocol bool HSTS bool HSTSMaxAge int64 diff --git a/internal/configs/version1/nginx-plus.ingress.tmpl b/internal/configs/version1/nginx-plus.ingress.tmpl index 25b2049e5d..81cd4031bb 100644 --- a/internal/configs/version1/nginx-plus.ingress.tmpl +++ b/internal/configs/version1/nginx-plus.ingress.tmpl @@ -141,7 +141,7 @@ server { {{- if not $server.GRPCOnly}} {{- if $server.SSLRedirect}} if ($scheme = http) { - return 301 https://$host:{{index $server.SSLPorts 0}}$request_uri; + return {{$server.HTTPRedirectCode}} https://$host:{{index $server.SSLPorts 0}}$request_uri; } {{- end}} {{- end}} @@ -149,7 +149,7 @@ server { {{- if $server.RedirectToHTTPS}} if ($http_x_forwarded_proto = 'http') { - return 301 https://$host$request_uri; + return {{$server.HTTPRedirectCode}} https://$host$request_uri; } {{- end}} diff --git a/internal/configs/version1/nginx.ingress.tmpl b/internal/configs/version1/nginx.ingress.tmpl index 70d8f2cfa5..8d989313f4 100644 --- a/internal/configs/version1/nginx.ingress.tmpl +++ b/internal/configs/version1/nginx.ingress.tmpl @@ -96,7 +96,7 @@ server { {{- if not $server.GRPCOnly}} {{- if $server.SSLRedirect}} if ($scheme = http) { - return 301 https://$host:{{index $server.SSLPorts 0}}$request_uri; + return {{$server.HTTPRedirectCode}} https://$host:{{index $server.SSLPorts 0}}$request_uri; } {{- end}} {{- end}} @@ -104,7 +104,7 @@ server { {{- if $server.RedirectToHTTPS}} if ($http_x_forwarded_proto = 'http') { - return 301 https://$host$request_uri; + return {{$server.HTTPRedirectCode}} https://$host$request_uri; } {{- end}} diff --git a/internal/configs/version1/template_test.go b/internal/configs/version1/template_test.go index 62178b9de4..4bd6d2a1a0 100644 --- a/internal/configs/version1/template_test.go +++ b/internal/configs/version1/template_test.go @@ -159,6 +159,20 @@ func TestExecuteTemplate_ForIngressForNGINX(t *testing.T) { snaps.MatchSnapshot(t, buf.String()) } +func TestExecuteTemplate_ForIngressForNGINXWithHTTPRedirectCode(t *testing.T) { + t.Parallel() + + tmpl := newNGINXIngressTmpl(t) + buf := &bytes.Buffer{} + + err := tmpl.Execute(buf, ingressCfgWithHTTPRedirectCode) + t.Log(buf.String()) + if err != nil { + t.Fatal(err) + } + snaps.MatchSnapshot(t, buf.String()) +} + func TestExecuteTemplate_ForIngressForNGINXPlusWithRegexAnnotationCaseSensitiveModifier(t *testing.T) { t.Parallel() @@ -2100,6 +2114,7 @@ var ( SSLCertificateKey: "secret.pem", SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, Locations: []Location{ { Path: "/tea", @@ -2170,6 +2185,7 @@ var ( SSLCertificateKey: "secret.pem", SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, Locations: []Location{ { Path: "/tea", @@ -2224,6 +2240,41 @@ var ( }, } + // Ingress Config example with ssl-redirect and redirect-to-https enabled with custom http-redirect-ccode + ingressCfgWithHTTPRedirectCode = IngressNginxConfig{ + Servers: []Server{ + { + Name: "test.example.com", + ServerTokens: "off", + StatusZone: "test.example.com", + SSL: true, + SSLCertificate: "secret.pem", + SSLCertificateKey: "secret.pem", + SSLPorts: []int{443}, + SSLRedirect: true, + RedirectToHTTPS: true, + HTTPRedirectCode: 308, + Locations: []Location{ + { + Path: "/tea", + Upstream: testUpstream, + ProxyConnectTimeout: "10s", + ProxyReadTimeout: "10s", + ProxySendTimeout: "10s", + ClientMaxBodySize: "2m", + }, + }, + HealthChecks: map[string]HealthCheck{"test": healthCheck}, + }, + }, + Upstreams: []Upstream{testUpstream}, + Keepalive: "16", + Ingress: Ingress{ + Name: "cafe-ingress", + Namespace: "default", + }, + } + // Ingress Config example with path-regex annotation value "case_sensitive" ingressCfgWithRegExAnnotationCaseSensitive = IngressNginxConfig{ Servers: []Server{ @@ -2242,6 +2293,7 @@ var ( SSLCertificateKey: "secret.pem", SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, Locations: []Location{ { Path: "/tea/[A-Z0-9]{3}", @@ -2297,6 +2349,7 @@ var ( SSLCertificateKey: "secret.pem", SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, Locations: []Location{ { Path: "/tea/[A-Z0-9]{3}", @@ -2352,6 +2405,7 @@ var ( SSLCertificateKey: "secret.pem", SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, Locations: []Location{ { Path: "/tea", @@ -2407,6 +2461,7 @@ var ( SSLCertificateKey: "secret.pem", SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, Locations: []Location{ { Path: "/tea", @@ -2899,6 +2954,7 @@ var ( Ports: []int{80}, SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, HealthChecks: make(map[string]HealthCheck), }, }, @@ -2972,6 +3028,7 @@ var ( Ports: []int{80}, SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, HealthChecks: make(map[string]HealthCheck), }, }, @@ -3047,6 +3104,7 @@ var ( Ports: []int{80}, SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, HealthChecks: make(map[string]HealthCheck), }, }, @@ -3121,6 +3179,7 @@ var ( Ports: []int{80}, SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, HealthChecks: make(map[string]HealthCheck), }, }, @@ -3195,6 +3254,7 @@ var ( Ports: []int{80}, SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, HealthChecks: make(map[string]HealthCheck), }, }, @@ -3272,6 +3332,7 @@ var ( Ports: []int{80}, SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, HealthChecks: make(map[string]HealthCheck), }, }, @@ -3348,6 +3409,7 @@ var ( Ports: []int{80}, SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, HealthChecks: make(map[string]HealthCheck), }, }, @@ -3424,6 +3486,7 @@ var ( Ports: []int{80}, SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, HealthChecks: make(map[string]HealthCheck), }, }, @@ -3449,6 +3512,7 @@ var ( SSLCertificateKey: "secret.pem", SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, Locations: []Location{ { Path: "/tea", @@ -3502,6 +3566,7 @@ var ( SSLCertificateKey: "secret.pem", SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, Locations: []Location{ { Path: "/tea", @@ -3586,6 +3651,7 @@ var ( SSLCertificateKey: "secret.pem", SSLPorts: []int{443}, SSLRedirect: true, + HTTPRedirectCode: 301, Locations: []Location{ { Path: "/tea", diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index be16b1f3fb..502da00ad6 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -425,7 +425,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig( } sslConfig := vsc.generateSSLConfig(vsEx.VirtualServer, vsEx.VirtualServer.Spec.TLS, vsEx.VirtualServer.Namespace, vsEx.SecretRefs, vsc.cfgParams) - tlsRedirectConfig := generateTLSRedirectConfig(vsEx.VirtualServer.Spec.TLS) + tlsRedirectConfig := generateTLSRedirectConfig(vsEx.VirtualServer.Spec.TLS, vsc.cfgParams) policyOpts := policyOptions{ tls: sslConfig != nil, @@ -3346,13 +3346,13 @@ func (vsc *virtualServerConfigurator) generateSSLConfig(owner runtime.Object, tl return &ssl } -func generateTLSRedirectConfig(tls *conf_v1.TLS) *version2.TLSRedirect { +func generateTLSRedirectConfig(tls *conf_v1.TLS, cfgParams *ConfigParams) *version2.TLSRedirect { if tls == nil || tls.Redirect == nil || !tls.Redirect.Enable { return nil } redirect := &version2.TLSRedirect{ - Code: generateIntFromPointer(tls.Redirect.Code, 301), + Code: generateIntFromPointer(tls.Redirect.Code, cfgParams.HTTPRedirectCode), BasedOn: generateTLSRedirectBasedOn(tls.Redirect.BasedOn), } diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index fd08b3dd92..d04891723f 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -16852,30 +16852,34 @@ func TestGenerateSSLConfig(t *testing.T) { func TestGenerateRedirectConfig(t *testing.T) { t.Parallel() tests := []struct { - inputTLS *conf_v1.TLS - expected *version2.TLSRedirect - msg string + inputTLS *conf_v1.TLS + defaultRedirectCode int + expected *version2.TLSRedirect + msg string }{ { - inputTLS: nil, - expected: nil, - msg: "no TLS field", + inputTLS: nil, + defaultRedirectCode: 301, + expected: nil, + msg: "no TLS field", }, { inputTLS: &conf_v1.TLS{ Secret: "secret", Redirect: nil, }, - expected: nil, - msg: "no redirect field", + defaultRedirectCode: 301, + expected: nil, + msg: "no redirect field", }, { inputTLS: &conf_v1.TLS{ Secret: "secret", Redirect: &conf_v1.TLSRedirect{Enable: false}, }, - expected: nil, - msg: "redirect disabled", + defaultRedirectCode: 301, + expected: nil, + msg: "redirect disabled", }, { inputTLS: &conf_v1.TLS{ @@ -16884,6 +16888,7 @@ func TestGenerateRedirectConfig(t *testing.T) { Enable: true, }, }, + defaultRedirectCode: 301, expected: &version2.TLSRedirect{ Code: 301, BasedOn: "$scheme", @@ -16898,16 +16903,34 @@ func TestGenerateRedirectConfig(t *testing.T) { BasedOn: "x-forwarded-proto", }, }, + defaultRedirectCode: 301, expected: &version2.TLSRedirect{ Code: 301, BasedOn: "$http_x_forwarded_proto", }, msg: "normal case with BasedOn set", }, + { + inputTLS: &conf_v1.TLS{ + Secret: "secret", + Redirect: &conf_v1.TLSRedirect{ + Enable: true, + }, + }, + defaultRedirectCode: 308, + expected: &version2.TLSRedirect{ + Code: 308, + BasedOn: "$scheme", + }, + msg: "normal case with non 301 default redirect code", + }, } for _, test := range tests { - result := generateTLSRedirectConfig(test.inputTLS) + cfgParams := &ConfigParams{ + HTTPRedirectCode: test.defaultRedirectCode, + } + result := generateTLSRedirectConfig(test.inputTLS, cfgParams) if !reflect.DeepEqual(result, test.expected) { t.Errorf("generateTLSRedirectConfig() returned %v but expected %v for the case of %s", result, test.expected, test.msg) } diff --git a/internal/k8s/validation.go b/internal/k8s/validation.go index b0baec9992..4ac12939e8 100644 --- a/internal/k8s/validation.go +++ b/internal/k8s/validation.go @@ -75,6 +75,7 @@ const ( stickyCookieServicesAnnotation = "nginx.com/sticky-cookie-services" pathRegexAnnotation = "nginx.org/path-regex" useClusterIPAnnotation = "nginx.org/use-cluster-ip" + httpRedirectCodeAnnotation = "nginx.org/http-redirect-code" ) const ( @@ -360,6 +361,10 @@ var ( useClusterIPAnnotation: { validateBoolAnnotation, }, + httpRedirectCodeAnnotation: { + validateRequiredAnnotation, + validateHTTPRedirectCodeAnnotation, + }, } annotationNames = sortedAnnotationNames(annotationValidations) ) @@ -373,6 +378,13 @@ func validatePathRegex(context *annotationValidationContext) field.ErrorList { } } +func validateHTTPRedirectCodeAnnotation(context *annotationValidationContext) field.ErrorList { + if _, err := configs.ParseHTTPRedirectCode(context.value); err != nil { + return field.ErrorList{field.Invalid(context.fieldPath, context.value, err.Error())} + } + return nil +} + func validateJWTLoginURLAnnotation(context *annotationValidationContext) field.ErrorList { allErrs := field.ErrorList{} diff --git a/internal/k8s/validation_test.go b/internal/k8s/validation_test.go index 72a1e9c9e1..7819594999 100644 --- a/internal/k8s/validation_test.go +++ b/internal/k8s/validation_test.go @@ -1480,6 +1480,104 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { msg: "invalid ingress.kubernetes.io/ssl-redirect annotation", }, + { + annotations: map[string]string{ + "nginx.org/http-redirect-code": "301", + }, + specServices: map[string]bool{}, + isPlus: false, + appProtectEnabled: false, + appProtectDosEnabled: false, + internalRoutesEnabled: false, + directiveAutoAdjust: false, + expectedErrors: nil, + msg: "valid nginx.org/http-redirect-code annotation", + }, + { + annotations: map[string]string{ + "nginx.org/http-redirect-code": "302", + }, + specServices: map[string]bool{}, + isPlus: false, + appProtectEnabled: false, + appProtectDosEnabled: false, + internalRoutesEnabled: false, + directiveAutoAdjust: false, + expectedErrors: nil, + msg: "valid nginx.org/http-redirect-code annotation with 302", + }, + { + annotations: map[string]string{ + "nginx.org/http-redirect-code": "307", + }, + specServices: map[string]bool{}, + isPlus: false, + appProtectEnabled: false, + appProtectDosEnabled: false, + internalRoutesEnabled: false, + directiveAutoAdjust: false, + expectedErrors: nil, + msg: "valid nginx.org/http-redirect-code annotation with 307", + }, + { + annotations: map[string]string{ + "nginx.org/http-redirect-code": "308", + }, + specServices: map[string]bool{}, + isPlus: false, + appProtectEnabled: false, + appProtectDosEnabled: false, + internalRoutesEnabled: false, + directiveAutoAdjust: false, + expectedErrors: nil, + msg: "valid nginx.org/http-redirect-code annotation with 308", + }, + { + annotations: map[string]string{ + "nginx.org/http-redirect-code": "", + }, + specServices: map[string]bool{}, + isPlus: false, + appProtectEnabled: false, + appProtectDosEnabled: false, + internalRoutesEnabled: false, + directiveAutoAdjust: false, + expectedErrors: []string{ + `annotations.nginx.org/http-redirect-code: Required value`, + }, + msg: "invalid nginx.org/http-redirect-code annotation, empty string", + }, + { + annotations: map[string]string{ + "nginx.org/http-redirect-code": "200", + }, + specServices: map[string]bool{}, + isPlus: false, + appProtectEnabled: false, + appProtectDosEnabled: false, + internalRoutesEnabled: false, + directiveAutoAdjust: false, + expectedErrors: []string{ + `annotations.nginx.org/http-redirect-code: Invalid value: "200": status code out of accepted range. accepted values are '301', '302', '307', '308'`, + }, + msg: "invalid nginx.org/http-redirect-code annotation, invalid code", + }, + { + annotations: map[string]string{ + "nginx.org/http-redirect-code": "invalid", + }, + specServices: map[string]bool{}, + isPlus: false, + appProtectEnabled: false, + appProtectDosEnabled: false, + internalRoutesEnabled: false, + directiveAutoAdjust: false, + expectedErrors: []string{ + `annotations.nginx.org/http-redirect-code: Invalid value: "invalid": invalid redirect code: strconv.Atoi: parsing "invalid": invalid syntax`, + }, + msg: "invalid nginx.org/http-redirect-code annotation, not a number", + }, + { annotations: map[string]string{ "nginx.org/proxy-buffering": "true", @@ -2918,7 +3016,8 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { }, specServices: map[string]bool{ "service-1": true, - }, isPlus: false, + }, + isPlus: false, appProtectEnabled: false, appProtectDosEnabled: false, internalRoutesEnabled: false, @@ -3041,7 +3140,8 @@ func TestValidateNginxIngressAnnotations(t *testing.T) { }, specServices: map[string]bool{ "service-1": true, - }, isPlus: false, + }, + isPlus: false, appProtectEnabled: false, appProtectDosEnabled: false, internalRoutesEnabled: false, diff --git a/internal/telemetry/cluster.go b/internal/telemetry/cluster.go index fe2ee8bb99..b2993281f8 100644 --- a/internal/telemetry/cluster.go +++ b/internal/telemetry/cluster.go @@ -30,6 +30,7 @@ var configMapFilteredKeys = []string{ "http2", "redirect-to-https", "ssl-redirect", + "http-redirect-code", "hsts", "hsts-max-age", "hsts-include-subdomains", diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index 90376fe69b..59a8b92975 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -305,7 +305,7 @@ type Action struct { type ActionRedirect struct { // The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. URL string `json:"url"` - // The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. + // The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. Code int `json:"code"` } From 4a73e2c6d03f60f78928998614f16982c8fe3779 Mon Sep 17 00:00:00 2001 From: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:26:38 +0000 Subject: [PATCH 2/5] Make http-redirect-code configmap not affect VS/VSR CRD defaults --- .../k8s.nginx.org_virtualserverroutes.yaml | 19 +++----- .../bases/k8s.nginx.org_virtualservers.yaml | 19 +++----- deploy/crds.yaml | 38 +++++----------- docs/crd/k8s.nginx.org_virtualserverroutes.md | 10 ++--- docs/crd/k8s.nginx.org_virtualservers.md | 10 ++--- internal/configs/virtualserver.go | 6 +-- internal/configs/virtualserver_test.go | 45 +++++-------------- pkg/apis/configuration/v1/types.go | 2 +- 8 files changed, 49 insertions(+), 100 deletions(-) diff --git a/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml b/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml index 6303fcd55d..f8105b7dcd 100644 --- a/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml +++ b/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml @@ -178,9 +178,7 @@ spec: properties: code: description: 'The status code of a redirect. The allowed - values are: 301, 302, 307 or 308. The default follows - the ConfigMap http-redirect-code setting, which defaults - to 301.' + values are: 301, 302, 307 or 308. The default is 301.' type: integer url: description: 'The URL to redirect the request to. Supported @@ -245,9 +243,8 @@ spec: properties: code: description: 'The status code of a redirect. The allowed - values are: 301, 302, 307 or 308. The default follows - the ConfigMap http-redirect-code setting, which - defaults to 301.' + values are: 301, 302, 307 or 308. The default is + 301.' type: integer url: description: 'The URL to redirect the request to. @@ -403,8 +400,7 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The - default follows the ConfigMap http-redirect-code - setting, which defaults to 301.' + default is 301.' type: integer url: description: 'The URL to redirect the request @@ -589,9 +585,7 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 - or 308. The default follows the ConfigMap - http-redirect-code setting, which defaults - to 301.' + or 308. The default is 301.' type: integer url: description: 'The URL to redirect the request @@ -789,8 +783,7 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The - default follows the ConfigMap http-redirect-code - setting, which defaults to 301.' + default is 301.' type: integer url: description: 'The URL to redirect the request diff --git a/config/crd/bases/k8s.nginx.org_virtualservers.yaml b/config/crd/bases/k8s.nginx.org_virtualservers.yaml index 272fa93e8e..677beb84aa 100644 --- a/config/crd/bases/k8s.nginx.org_virtualservers.yaml +++ b/config/crd/bases/k8s.nginx.org_virtualservers.yaml @@ -265,9 +265,7 @@ spec: properties: code: description: 'The status code of a redirect. The allowed - values are: 301, 302, 307 or 308. The default follows - the ConfigMap http-redirect-code setting, which defaults - to 301.' + values are: 301, 302, 307 or 308. The default is 301.' type: integer url: description: 'The URL to redirect the request to. Supported @@ -332,9 +330,8 @@ spec: properties: code: description: 'The status code of a redirect. The allowed - values are: 301, 302, 307 or 308. The default follows - the ConfigMap http-redirect-code setting, which - defaults to 301.' + values are: 301, 302, 307 or 308. The default is + 301.' type: integer url: description: 'The URL to redirect the request to. @@ -490,8 +487,7 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The - default follows the ConfigMap http-redirect-code - setting, which defaults to 301.' + default is 301.' type: integer url: description: 'The URL to redirect the request @@ -676,9 +672,7 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 - or 308. The default follows the ConfigMap - http-redirect-code setting, which defaults - to 301.' + or 308. The default is 301.' type: integer url: description: 'The URL to redirect the request @@ -876,8 +870,7 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The - default follows the ConfigMap http-redirect-code - setting, which defaults to 301.' + default is 301.' type: integer url: description: 'The URL to redirect the request diff --git a/deploy/crds.yaml b/deploy/crds.yaml index cfc4b92c7f..93fc92efaa 100644 --- a/deploy/crds.yaml +++ b/deploy/crds.yaml @@ -1368,9 +1368,7 @@ spec: properties: code: description: 'The status code of a redirect. The allowed - values are: 301, 302, 307 or 308. The default follows - the ConfigMap http-redirect-code setting, which defaults - to 301.' + values are: 301, 302, 307 or 308. The default is 301.' type: integer url: description: 'The URL to redirect the request to. Supported @@ -1435,9 +1433,8 @@ spec: properties: code: description: 'The status code of a redirect. The allowed - values are: 301, 302, 307 or 308. The default follows - the ConfigMap http-redirect-code setting, which - defaults to 301.' + values are: 301, 302, 307 or 308. The default is + 301.' type: integer url: description: 'The URL to redirect the request to. @@ -1593,8 +1590,7 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The - default follows the ConfigMap http-redirect-code - setting, which defaults to 301.' + default is 301.' type: integer url: description: 'The URL to redirect the request @@ -1779,9 +1775,7 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 - or 308. The default follows the ConfigMap - http-redirect-code setting, which defaults - to 301.' + or 308. The default is 301.' type: integer url: description: 'The URL to redirect the request @@ -1979,8 +1973,7 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The - default follows the ConfigMap http-redirect-code - setting, which defaults to 301.' + default is 301.' type: integer url: description: 'The URL to redirect the request @@ -2699,9 +2692,7 @@ spec: properties: code: description: 'The status code of a redirect. The allowed - values are: 301, 302, 307 or 308. The default follows - the ConfigMap http-redirect-code setting, which defaults - to 301.' + values are: 301, 302, 307 or 308. The default is 301.' type: integer url: description: 'The URL to redirect the request to. Supported @@ -2766,9 +2757,8 @@ spec: properties: code: description: 'The status code of a redirect. The allowed - values are: 301, 302, 307 or 308. The default follows - the ConfigMap http-redirect-code setting, which - defaults to 301.' + values are: 301, 302, 307 or 308. The default is + 301.' type: integer url: description: 'The URL to redirect the request to. @@ -2924,8 +2914,7 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The - default follows the ConfigMap http-redirect-code - setting, which defaults to 301.' + default is 301.' type: integer url: description: 'The URL to redirect the request @@ -3110,9 +3099,7 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 - or 308. The default follows the ConfigMap - http-redirect-code setting, which defaults - to 301.' + or 308. The default is 301.' type: integer url: description: 'The URL to redirect the request @@ -3310,8 +3297,7 @@ spec: code: description: 'The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The - default follows the ConfigMap http-redirect-code - setting, which defaults to 301.' + default is 301.' type: integer url: description: 'The URL to redirect the request diff --git a/docs/crd/k8s.nginx.org_virtualserverroutes.md b/docs/crd/k8s.nginx.org_virtualserverroutes.md index dc39cde5a8..3874555213 100644 --- a/docs/crd/k8s.nginx.org_virtualserverroutes.md +++ b/docs/crd/k8s.nginx.org_virtualserverroutes.md @@ -37,7 +37,7 @@ The `.spec` object supports the following fields: | `subroutes[].action.proxy.rewritePath` | `string` | The rewritten URI. If the route path is a regular expression – starts with ~ – the rewritePath can include capture groups with $1-9. For example $1 for the first group, and so on. For more information, check the rewrite example. | | `subroutes[].action.proxy.upstream` | `string` | The name of the upstream which the requests will be proxied to. The upstream with that name must be defined in the resource. | | `subroutes[].action.redirect` | `object` | Redirects requests to a provided URL. | -| `subroutes[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | +| `subroutes[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | | `subroutes[].action.redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `subroutes[].action.return` | `object` | Returns a preconfigured response. | | `subroutes[].action.return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | @@ -50,7 +50,7 @@ The `.spec` object supports the following fields: | `subroutes[].errorPages` | `array` | The custom responses for error codes. NGINX will use those responses instead of returning the error responses from the upstream servers or the default responses generated by NGINX. A custom response can be a redirect or a canned response. For example, a redirect to another URL if an upstream server responded with a 404 status code. | | `subroutes[].errorPages[].codes` | `array[integer]` | A list of error status codes. | | `subroutes[].errorPages[].redirect` | `object` | The canned response action for the given status codes. | -| `subroutes[].errorPages[].redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | +| `subroutes[].errorPages[].redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | | `subroutes[].errorPages[].redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `subroutes[].errorPages[].return` | `object` | The redirect action for the given status codes. | | `subroutes[].errorPages[].return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | @@ -80,7 +80,7 @@ The `.spec` object supports the following fields: | `subroutes[].matches[].action.proxy.rewritePath` | `string` | The rewritten URI. If the route path is a regular expression – starts with ~ – the rewritePath can include capture groups with $1-9. For example $1 for the first group, and so on. For more information, check the rewrite example. | | `subroutes[].matches[].action.proxy.upstream` | `string` | The name of the upstream which the requests will be proxied to. The upstream with that name must be defined in the resource. | | `subroutes[].matches[].action.redirect` | `object` | Redirects requests to a provided URL. | -| `subroutes[].matches[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | +| `subroutes[].matches[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | | `subroutes[].matches[].action.redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `subroutes[].matches[].action.return` | `object` | Returns a preconfigured response. | | `subroutes[].matches[].action.return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | @@ -115,7 +115,7 @@ The `.spec` object supports the following fields: | `subroutes[].matches[].splits[].action.proxy.rewritePath` | `string` | The rewritten URI. If the route path is a regular expression – starts with ~ – the rewritePath can include capture groups with $1-9. For example $1 for the first group, and so on. For more information, check the rewrite example. | | `subroutes[].matches[].splits[].action.proxy.upstream` | `string` | The name of the upstream which the requests will be proxied to. The upstream with that name must be defined in the resource. | | `subroutes[].matches[].splits[].action.redirect` | `object` | Redirects requests to a provided URL. | -| `subroutes[].matches[].splits[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | +| `subroutes[].matches[].splits[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | | `subroutes[].matches[].splits[].action.redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `subroutes[].matches[].splits[].action.return` | `object` | Returns a preconfigured response. | | `subroutes[].matches[].splits[].action.return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | @@ -150,7 +150,7 @@ The `.spec` object supports the following fields: | `subroutes[].splits[].action.proxy.rewritePath` | `string` | The rewritten URI. If the route path is a regular expression – starts with ~ – the rewritePath can include capture groups with $1-9. For example $1 for the first group, and so on. For more information, check the rewrite example. | | `subroutes[].splits[].action.proxy.upstream` | `string` | The name of the upstream which the requests will be proxied to. The upstream with that name must be defined in the resource. | | `subroutes[].splits[].action.redirect` | `object` | Redirects requests to a provided URL. | -| `subroutes[].splits[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | +| `subroutes[].splits[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | | `subroutes[].splits[].action.redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `subroutes[].splits[].action.return` | `object` | Returns a preconfigured response. | | `subroutes[].splits[].action.return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | diff --git a/docs/crd/k8s.nginx.org_virtualservers.md b/docs/crd/k8s.nginx.org_virtualservers.md index 10a4eadd52..cffd599ffd 100644 --- a/docs/crd/k8s.nginx.org_virtualservers.md +++ b/docs/crd/k8s.nginx.org_virtualservers.md @@ -55,7 +55,7 @@ The `.spec` object supports the following fields: | `routes[].action.proxy.rewritePath` | `string` | The rewritten URI. If the route path is a regular expression – starts with ~ – the rewritePath can include capture groups with $1-9. For example $1 for the first group, and so on. For more information, check the rewrite example. | | `routes[].action.proxy.upstream` | `string` | The name of the upstream which the requests will be proxied to. The upstream with that name must be defined in the resource. | | `routes[].action.redirect` | `object` | Redirects requests to a provided URL. | -| `routes[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | +| `routes[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | | `routes[].action.redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `routes[].action.return` | `object` | Returns a preconfigured response. | | `routes[].action.return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | @@ -68,7 +68,7 @@ The `.spec` object supports the following fields: | `routes[].errorPages` | `array` | The custom responses for error codes. NGINX will use those responses instead of returning the error responses from the upstream servers or the default responses generated by NGINX. A custom response can be a redirect or a canned response. For example, a redirect to another URL if an upstream server responded with a 404 status code. | | `routes[].errorPages[].codes` | `array[integer]` | A list of error status codes. | | `routes[].errorPages[].redirect` | `object` | The canned response action for the given status codes. | -| `routes[].errorPages[].redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | +| `routes[].errorPages[].redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | | `routes[].errorPages[].redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `routes[].errorPages[].return` | `object` | The redirect action for the given status codes. | | `routes[].errorPages[].return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | @@ -98,7 +98,7 @@ The `.spec` object supports the following fields: | `routes[].matches[].action.proxy.rewritePath` | `string` | The rewritten URI. If the route path is a regular expression – starts with ~ – the rewritePath can include capture groups with $1-9. For example $1 for the first group, and so on. For more information, check the rewrite example. | | `routes[].matches[].action.proxy.upstream` | `string` | The name of the upstream which the requests will be proxied to. The upstream with that name must be defined in the resource. | | `routes[].matches[].action.redirect` | `object` | Redirects requests to a provided URL. | -| `routes[].matches[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | +| `routes[].matches[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | | `routes[].matches[].action.redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `routes[].matches[].action.return` | `object` | Returns a preconfigured response. | | `routes[].matches[].action.return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | @@ -133,7 +133,7 @@ The `.spec` object supports the following fields: | `routes[].matches[].splits[].action.proxy.rewritePath` | `string` | The rewritten URI. If the route path is a regular expression – starts with ~ – the rewritePath can include capture groups with $1-9. For example $1 for the first group, and so on. For more information, check the rewrite example. | | `routes[].matches[].splits[].action.proxy.upstream` | `string` | The name of the upstream which the requests will be proxied to. The upstream with that name must be defined in the resource. | | `routes[].matches[].splits[].action.redirect` | `object` | Redirects requests to a provided URL. | -| `routes[].matches[].splits[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | +| `routes[].matches[].splits[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | | `routes[].matches[].splits[].action.redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `routes[].matches[].splits[].action.return` | `object` | Returns a preconfigured response. | | `routes[].matches[].splits[].action.return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | @@ -168,7 +168,7 @@ The `.spec` object supports the following fields: | `routes[].splits[].action.proxy.rewritePath` | `string` | The rewritten URI. If the route path is a regular expression – starts with ~ – the rewritePath can include capture groups with $1-9. For example $1 for the first group, and so on. For more information, check the rewrite example. | | `routes[].splits[].action.proxy.upstream` | `string` | The name of the upstream which the requests will be proxied to. The upstream with that name must be defined in the resource. | | `routes[].splits[].action.redirect` | `object` | Redirects requests to a provided URL. | -| `routes[].splits[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. | +| `routes[].splits[].action.redirect.code` | `integer` | The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. | | `routes[].splits[].action.redirect.url` | `string` | The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. | | `routes[].splits[].action.return` | `object` | Returns a preconfigured response. | | `routes[].splits[].action.return.body` | `string` | The body of the response. Supports NGINX variables*. Variables must be enclosed in curly brackets. For example: Request is ${request_uri}\n. | diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index 502da00ad6..be16b1f3fb 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -425,7 +425,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig( } sslConfig := vsc.generateSSLConfig(vsEx.VirtualServer, vsEx.VirtualServer.Spec.TLS, vsEx.VirtualServer.Namespace, vsEx.SecretRefs, vsc.cfgParams) - tlsRedirectConfig := generateTLSRedirectConfig(vsEx.VirtualServer.Spec.TLS, vsc.cfgParams) + tlsRedirectConfig := generateTLSRedirectConfig(vsEx.VirtualServer.Spec.TLS) policyOpts := policyOptions{ tls: sslConfig != nil, @@ -3346,13 +3346,13 @@ func (vsc *virtualServerConfigurator) generateSSLConfig(owner runtime.Object, tl return &ssl } -func generateTLSRedirectConfig(tls *conf_v1.TLS, cfgParams *ConfigParams) *version2.TLSRedirect { +func generateTLSRedirectConfig(tls *conf_v1.TLS) *version2.TLSRedirect { if tls == nil || tls.Redirect == nil || !tls.Redirect.Enable { return nil } redirect := &version2.TLSRedirect{ - Code: generateIntFromPointer(tls.Redirect.Code, cfgParams.HTTPRedirectCode), + Code: generateIntFromPointer(tls.Redirect.Code, 301), BasedOn: generateTLSRedirectBasedOn(tls.Redirect.BasedOn), } diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index d04891723f..fd08b3dd92 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -16852,34 +16852,30 @@ func TestGenerateSSLConfig(t *testing.T) { func TestGenerateRedirectConfig(t *testing.T) { t.Parallel() tests := []struct { - inputTLS *conf_v1.TLS - defaultRedirectCode int - expected *version2.TLSRedirect - msg string + inputTLS *conf_v1.TLS + expected *version2.TLSRedirect + msg string }{ { - inputTLS: nil, - defaultRedirectCode: 301, - expected: nil, - msg: "no TLS field", + inputTLS: nil, + expected: nil, + msg: "no TLS field", }, { inputTLS: &conf_v1.TLS{ Secret: "secret", Redirect: nil, }, - defaultRedirectCode: 301, - expected: nil, - msg: "no redirect field", + expected: nil, + msg: "no redirect field", }, { inputTLS: &conf_v1.TLS{ Secret: "secret", Redirect: &conf_v1.TLSRedirect{Enable: false}, }, - defaultRedirectCode: 301, - expected: nil, - msg: "redirect disabled", + expected: nil, + msg: "redirect disabled", }, { inputTLS: &conf_v1.TLS{ @@ -16888,7 +16884,6 @@ func TestGenerateRedirectConfig(t *testing.T) { Enable: true, }, }, - defaultRedirectCode: 301, expected: &version2.TLSRedirect{ Code: 301, BasedOn: "$scheme", @@ -16903,34 +16898,16 @@ func TestGenerateRedirectConfig(t *testing.T) { BasedOn: "x-forwarded-proto", }, }, - defaultRedirectCode: 301, expected: &version2.TLSRedirect{ Code: 301, BasedOn: "$http_x_forwarded_proto", }, msg: "normal case with BasedOn set", }, - { - inputTLS: &conf_v1.TLS{ - Secret: "secret", - Redirect: &conf_v1.TLSRedirect{ - Enable: true, - }, - }, - defaultRedirectCode: 308, - expected: &version2.TLSRedirect{ - Code: 308, - BasedOn: "$scheme", - }, - msg: "normal case with non 301 default redirect code", - }, } for _, test := range tests { - cfgParams := &ConfigParams{ - HTTPRedirectCode: test.defaultRedirectCode, - } - result := generateTLSRedirectConfig(test.inputTLS, cfgParams) + result := generateTLSRedirectConfig(test.inputTLS) if !reflect.DeepEqual(result, test.expected) { t.Errorf("generateTLSRedirectConfig() returned %v but expected %v for the case of %s", result, test.expected, test.msg) } diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index 59a8b92975..90376fe69b 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -305,7 +305,7 @@ type Action struct { type ActionRedirect struct { // The URL to redirect the request to. Supported NGINX variables: $scheme, $http_x_forwarded_proto, $request_uri or $host. Variables must be enclosed in curly braces. For example: ${host}${request_uri}. URL string `json:"url"` - // The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default follows the ConfigMap http-redirect-code setting, which defaults to 301. + // The status code of a redirect. The allowed values are: 301, 302, 307 or 308. The default is 301. Code int `json:"code"` } From 788ae638148449d00b81949d95710b4efe25907c Mon Sep 17 00:00:00 2001 From: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:59:23 +0000 Subject: [PATCH 3/5] Skip validation based on the presence of other annotations Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> --- internal/configs/annotations.go | 15 ++++----------- internal/configs/annotations_test.go | 11 +++++++++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/internal/configs/annotations.go b/internal/configs/annotations.go index 00c112d6ef..ba1b043497 100644 --- a/internal/configs/annotations.go +++ b/internal/configs/annotations.go @@ -289,17 +289,10 @@ func parseAnnotations(ingEx *IngressEx, baseCfgParams *ConfigParams, isPlus bool } if httpRedirectCode, exists := ingEx.Ingress.Annotations[HTTPRedirectCodeAnnotation]; exists { - // Check if SSL redirect or redirect-to-https is enabled - sslRedirectEnabled := cfgParams.SSLRedirect - redirectToHTTPSEnabled := cfgParams.RedirectToHTTPS - - // HTTP redirect code annotation only applies when SSL redirect or redirect-to-https is enabled - if sslRedirectEnabled || redirectToHTTPSEnabled { - if code, err := ParseHTTPRedirectCode(httpRedirectCode); err != nil { - nl.Errorf(l, "Ingress %s/%s: Invalid value for nginx.org/http-redirect-code: %q: %v", ingEx.Ingress.GetNamespace(), ingEx.Ingress.GetName(), httpRedirectCode, err) - } else { - cfgParams.HTTPRedirectCode = code - } + if code, err := ParseHTTPRedirectCode(httpRedirectCode); err != nil { + nl.Errorf(l, "Ingress %s/%s: Invalid value for nginx.org/http-redirect-code: %q: %v", ingEx.Ingress.GetNamespace(), ingEx.Ingress.GetName(), httpRedirectCode, err) + } else { + cfgParams.HTTPRedirectCode = code } } diff --git a/internal/configs/annotations_test.go b/internal/configs/annotations_test.go index 4afd0da131..412582ceb1 100644 --- a/internal/configs/annotations_test.go +++ b/internal/configs/annotations_test.go @@ -1029,12 +1029,12 @@ func TestHTTPRedirectCodeAnnotationBehavior(t *testing.T) { expectedCode: 307, }, { - name: "redirect code ignored when SSL redirect disabled", + name: "redirect code applied when SSL redirect disabled", annotations: map[string]string{ "nginx.org/ssl-redirect": "false", "nginx.org/http-redirect-code": "307", }, - expectedCode: 301, // default + expectedCode: 307, }, { name: "redirect code applied with redirect-to-https", @@ -1044,6 +1044,13 @@ func TestHTTPRedirectCodeAnnotationBehavior(t *testing.T) { }, expectedCode: 302, }, + { + name: "redirect code applied without any redirect settings", + annotations: map[string]string{ + "nginx.org/http-redirect-code": "308", + }, + expectedCode: 308, + }, } for _, tt := range tests { From 912f324f1e6fdd67a8b9563cd90c27e098e85f37 Mon Sep 17 00:00:00 2001 From: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:13:02 +0000 Subject: [PATCH 4/5] fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> --- internal/configs/version1/template_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/configs/version1/template_test.go b/internal/configs/version1/template_test.go index 4bd6d2a1a0..9a44397f19 100644 --- a/internal/configs/version1/template_test.go +++ b/internal/configs/version1/template_test.go @@ -2240,7 +2240,7 @@ var ( }, } - // Ingress Config example with ssl-redirect and redirect-to-https enabled with custom http-redirect-ccode + // Ingress Config example with ssl-redirect and redirect-to-https enabled with custom http-redirect-code ingressCfgWithHTTPRedirectCode = IngressNginxConfig{ Servers: []Server{ { From bd157cc3f96c1f1397090be8dde223cf24886b4b Mon Sep 17 00:00:00 2001 From: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:37:22 +0000 Subject: [PATCH 5/5] update readme Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> --- examples/ingress-resources/mergeable-ingress-types/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/ingress-resources/mergeable-ingress-types/README.md b/examples/ingress-resources/mergeable-ingress-types/README.md index 627f0f0df2..ead128f318 100644 --- a/examples/ingress-resources/mergeable-ingress-types/README.md +++ b/examples/ingress-resources/mergeable-ingress-types/README.md @@ -33,8 +33,9 @@ Minions cannot contain the following annotations: - nginx.org/proxy-hide-headers - nginx.org/proxy-pass-headers - nginx.org/redirect-to-https -- ingress.kubernetes.io/ssl-redirect (deprecated, use nginx.org/ssl-redirect instead) - nginx.org/ssl-redirect +- ingress.kubernetes.io/ssl-redirect (deprecated, use nginx.org/ssl-redirect instead) +- nginx.org/http-redirect-code - nginx.org/hsts - nginx.org/hsts-max-age - nginx.org/hsts-include-subdomains