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 diff --git a/internal/configs/annotations.go b/internal/configs/annotations.go index 8e1f58ef85..ba1b043497 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,14 @@ func parseAnnotations(ingEx *IngressEx, baseCfgParams *ConfigParams, isPlus bool } } + if httpRedirectCode, exists := ingEx.Ingress.Annotations[HTTPRedirectCodeAnnotation]; exists { + 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..412582ceb1 100644 --- a/internal/configs/annotations_test.go +++ b/internal/configs/annotations_test.go @@ -1011,3 +1011,66 @@ 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 applied when SSL redirect disabled", + annotations: map[string]string{ + "nginx.org/ssl-redirect": "false", + "nginx.org/http-redirect-code": "307", + }, + expectedCode: 307, + }, + { + 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, + }, + { + name: "redirect code applied without any redirect settings", + annotations: map[string]string{ + "nginx.org/http-redirect-code": "308", + }, + expectedCode: 308, + }, + } + + 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..9a44397f19 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-code + 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/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",