Skip to content

Commit 7151e14

Browse files
authored
feat(annotations): introduce enable-custom-http-errors annotation
Signed-off-by: GitHub <noreply@github.com>
1 parent 973c1c9 commit 7151e14

File tree

5 files changed

+140
-31
lines changed

5 files changed

+140
-31
lines changed

docs/user-guide/nginx-configuration/annotations.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ You can add these Kubernetes annotations to specific Ingress objects to customiz
4949
|[nginx.ingress.kubernetes.io/canary-weight-total](#canary)|number|
5050
|[nginx.ingress.kubernetes.io/client-body-buffer-size](#client-body-buffer-size)|string|
5151
|[nginx.ingress.kubernetes.io/configuration-snippet](#configuration-snippet)|string|
52+
|[nginx.ingress.kubernetes.io/enable-custom-http-errors](#custom-http-errors)|"true" or "false"|
5253
|[nginx.ingress.kubernetes.io/custom-http-errors](#custom-http-errors)|[]int|
5354
|[nginx.ingress.kubernetes.io/custom-headers](#custom-headers)|string|
5455
|[nginx.ingress.kubernetes.io/default-backend](#default-backend)|string|
@@ -332,6 +333,10 @@ Like the [`custom-http-errors`](./configmap.md#custom-http-errors) value in the
332333
Different ingresses can specify different sets of error codes. Even if multiple ingress objects share the same hostname, this annotation can be used to intercept different error codes for each ingress (for example, different error codes to be intercepted for different paths on the same hostname, if each path is on a different ingress).
333334
If `custom-http-errors` is also specified globally, the error values specified in this annotation will override the global value for the given ingress' hostname and path.
334335

336+
If you want to disable this behavior for a specific ingress, you can use the annotation `nginx.ingress.kubernetes.io/enable-custom-http-errors: "false"`.
337+
`nginx.ingress.kubernetes.io/enable-custom-http-errors`:
338+
indicates if the custom http errors feature should be enabled or not to this Ingress rule. The default value is `"true"`.
339+
335340
Example usage:
336341
```
337342
nginx.ingress.kubernetes.io/custom-http-errors: "404,415"

internal/ingress/annotations/annotations_test.go

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,22 @@ import (
2929
)
3030

3131
var (
32-
annotationPassthrough = parser.GetAnnotationWithPrefix("ssl-passthrough")
33-
annotationAffinityType = parser.GetAnnotationWithPrefix("affinity")
34-
annotationAffinityMode = parser.GetAnnotationWithPrefix("affinity-mode")
35-
annotationAffinityCanaryBehavior = parser.GetAnnotationWithPrefix("affinity-canary-behavior")
36-
annotationCorsEnabled = parser.GetAnnotationWithPrefix("enable-cors")
37-
annotationCorsAllowMethods = parser.GetAnnotationWithPrefix("cors-allow-methods")
38-
annotationCorsAllowHeaders = parser.GetAnnotationWithPrefix("cors-allow-headers")
39-
annotationCorsExposeHeaders = parser.GetAnnotationWithPrefix("cors-expose-headers")
40-
annotationCorsAllowCredentials = parser.GetAnnotationWithPrefix("cors-allow-credentials")
41-
defaultCorsMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS"
42-
defaultCorsHeaders = "DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"
43-
annotationAffinityCookieName = parser.GetAnnotationWithPrefix("session-cookie-name")
44-
annotationUpstreamHashBy = parser.GetAnnotationWithPrefix("upstream-hash-by")
45-
annotationCustomHTTPErrors = parser.GetAnnotationWithPrefix("custom-http-errors")
46-
annotationCustomHeaders = parser.GetAnnotationWithPrefix("custom-headers")
32+
annotationPassthrough = parser.GetAnnotationWithPrefix("ssl-passthrough")
33+
annotationAffinityType = parser.GetAnnotationWithPrefix("affinity")
34+
annotationAffinityMode = parser.GetAnnotationWithPrefix("affinity-mode")
35+
annotationAffinityCanaryBehavior = parser.GetAnnotationWithPrefix("affinity-canary-behavior")
36+
annotationCorsEnabled = parser.GetAnnotationWithPrefix("enable-cors")
37+
annotationCorsAllowMethods = parser.GetAnnotationWithPrefix("cors-allow-methods")
38+
annotationCorsAllowHeaders = parser.GetAnnotationWithPrefix("cors-allow-headers")
39+
annotationCorsExposeHeaders = parser.GetAnnotationWithPrefix("cors-expose-headers")
40+
annotationCorsAllowCredentials = parser.GetAnnotationWithPrefix("cors-allow-credentials")
41+
defaultCorsMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS"
42+
defaultCorsHeaders = "DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"
43+
annotationAffinityCookieName = parser.GetAnnotationWithPrefix("session-cookie-name")
44+
annotationUpstreamHashBy = parser.GetAnnotationWithPrefix("upstream-hash-by")
45+
annotationCustomHTTPErrorsEnabled = parser.GetAnnotationWithPrefix("enable-custom-http-errors")
46+
annotationCustomHTTPErrors = parser.GetAnnotationWithPrefix("custom-http-errors")
47+
annotationCustomHeaders = parser.GetAnnotationWithPrefix("custom-headers")
4748
)
4849

4950
type mockCfg struct {
@@ -298,6 +299,14 @@ func TestCustomHTTPErrors(t *testing.T) {
298299
{map[string]string{annotationCustomHTTPErrors: "404"}, []int{404}},
299300
{map[string]string{annotationCustomHTTPErrors: ""}, []int{}},
300301
{map[string]string{annotationCustomHTTPErrors + "_no": "404"}, []int{}},
302+
{map[string]string{annotationCustomHTTPErrorsEnabled: "true", annotationCustomHTTPErrors: "404,415"}, []int{404, 415}},
303+
{map[string]string{annotationCustomHTTPErrorsEnabled: "true", annotationCustomHTTPErrors: "404"}, []int{404}},
304+
{map[string]string{annotationCustomHTTPErrorsEnabled: "true", annotationCustomHTTPErrors: ""}, []int{}},
305+
{map[string]string{annotationCustomHTTPErrorsEnabled: "true", annotationCustomHTTPErrors + "_no": "404"}, []int{}},
306+
{map[string]string{annotationCustomHTTPErrorsEnabled: "false", annotationCustomHTTPErrors: "404,415"}, []int{}},
307+
{map[string]string{annotationCustomHTTPErrorsEnabled: "false", annotationCustomHTTPErrors: "404"}, []int{}},
308+
{map[string]string{annotationCustomHTTPErrorsEnabled: "false", annotationCustomHTTPErrors: ""}, []int{}},
309+
{map[string]string{annotationCustomHTTPErrorsEnabled: "false", annotationCustomHTTPErrors + "_no": "404"}, []int{}},
301310
{map[string]string{}, []int{}},
302311
{nil, []int{}},
303312
}

internal/ingress/annotations/customhttperrors/main.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@ import (
2222
"strings"
2323

2424
networking "k8s.io/api/networking/v1"
25+
"k8s.io/klog/v2"
2526

2627
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
28+
"k8s.io/ingress-nginx/internal/ingress/errors"
2729
"k8s.io/ingress-nginx/internal/ingress/resolver"
2830
)
2931

3032
const (
31-
customHTTPErrorsAnnotation = "custom-http-errors"
33+
customHTTPErrorsAnnotation = "custom-http-errors"
34+
customHTTPErrorsEnabledAnnotation = "enable-custom-http-errors"
3235
)
3336

3437
// We accept anything between 400 and 599, on a comma separated.
@@ -37,13 +40,20 @@ var arrayOfHTTPErrors = regexp.MustCompile(`^(?:[4,5]\d{2},?)*$`)
3740
var customHTTPErrorsAnnotations = parser.Annotation{
3841
Group: "backend",
3942
Annotations: parser.AnnotationFields{
43+
customHTTPErrorsEnabledAnnotation: {
44+
Validator: parser.ValidateBool,
45+
Scope: parser.AnnotationScopeLocation,
46+
Risk: parser.AnnotationRiskLow,
47+
Documentation: `Indicates if the custom http errors feature should be enabled or not for this Ingress rule. The default value is "true".`,
48+
},
49+
4050
customHTTPErrorsAnnotation: {
4151
Validator: parser.ValidateRegex(arrayOfHTTPErrors, true),
4252
Scope: parser.AnnotationScopeLocation,
4353
Risk: parser.AnnotationRiskLow,
4454
Documentation: `If a default backend annotation is specified on the ingress, the errors code specified on this annotation
4555
will be routed to that annotation's default backend service. Otherwise they will be routed to the global default backend.
46-
A comma-separated list of error codes is accepted (anything between 400 and 599, like 403, 503)`,
56+
A comma-separated list of error codes is accepted (anything between 400 and 599, like 403, 503).`,
4757
},
4858
},
4959
}
@@ -64,6 +74,18 @@ func NewParser(r resolver.Resolver) parser.IngressAnnotation {
6474
// Parse parses the annotations contained in the ingress to use
6575
// custom http errors
6676
func (e customhttperrors) Parse(ing *networking.Ingress) (interface{}, error) {
77+
enabled, err := parser.GetBoolAnnotation(customHTTPErrorsEnabledAnnotation, ing, e.annotationConfig.Annotations)
78+
if err != nil {
79+
if errors.IsValidationError(err) {
80+
klog.Warningf("%s is invalid, defaulting to 'true'", customHTTPErrorsEnabledAnnotation)
81+
}
82+
enabled = true
83+
}
84+
85+
if !enabled {
86+
return []int{}, nil
87+
}
88+
6789
c, err := parser.GetStringAnnotation(customHTTPErrorsAnnotation, ing, e.annotationConfig.Annotations)
6890
if err != nil {
6991
return nil, err

internal/ingress/annotations/customhttperrors/main_test.go

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,31 +47,55 @@ func buildIngress() *networking.Ingress {
4747
}
4848
}
4949

50-
func TestParseInvalidAnnotations(t *testing.T) {
50+
func TestParseAnnotations(t *testing.T) {
5151
ing := buildIngress()
52-
53-
_, err := NewParser(&resolver.Mock{}).Parse(ing)
54-
if err == nil {
55-
t.Errorf("expected error parsing ingress with custom-http-errors")
52+
data := map[string]string{}
53+
data[parser.GetAnnotationWithPrefix("custom-http-errors")] = "400,404,500,502"
54+
ing.SetAnnotations(data)
55+
i, err := NewParser(&resolver.Mock{}).Parse(ing)
56+
if err != nil {
57+
t.Errorf("unexpected error parsing ingress with custom-http-errors")
58+
}
59+
val, ok := i.([]int)
60+
if !ok {
61+
t.Errorf("expected a []int type")
62+
}
63+
expected := []int{400, 404, 500, 502}
64+
sort.Ints(val)
65+
if !reflect.DeepEqual(expected, val) {
66+
t.Errorf("expected %v but got %v", expected, val)
5667
}
68+
}
69+
70+
func TestParseEnabledAnnotations(t *testing.T) {
71+
ing := buildIngress()
5772

5873
data := map[string]string{}
59-
data[parser.GetAnnotationWithPrefix("custom-http-errors")] = "400,404,abc,502"
74+
data[parser.GetAnnotationWithPrefix("enable-custom-http-errors")] = "true"
75+
data[parser.GetAnnotationWithPrefix("custom-http-errors")] = "400,404,500,502"
6076
ing.SetAnnotations(data)
6177

6278
i, err := NewParser(&resolver.Mock{}).Parse(ing)
63-
if err == nil {
64-
t.Errorf("expected error parsing ingress with custom-http-errors")
79+
if err != nil {
80+
t.Errorf("unexpected error parsing ingress with custom-http-errors")
81+
}
82+
val, ok := i.([]int)
83+
if !ok {
84+
t.Errorf("expected a []int type")
6585
}
66-
if i != nil {
67-
t.Errorf("expected %v but got %v", nil, i)
86+
87+
expected := []int{}
88+
89+
if !reflect.DeepEqual(expected, val) {
90+
t.Errorf("expected %v but got %v", expected, val)
6891
}
6992
}
7093

71-
func TestParseAnnotations(t *testing.T) {
94+
func TestParseDisabledAnnotations(t *testing.T) {
7295
ing := buildIngress()
7396

7497
data := map[string]string{}
98+
data[parser.GetAnnotationWithPrefix("enable-custom-http-errors")] = "false"
7599
data[parser.GetAnnotationWithPrefix("custom-http-errors")] = "400,404,500,502"
76100
ing.SetAnnotations(data)
77101

@@ -84,8 +108,31 @@ func TestParseAnnotations(t *testing.T) {
84108
t.Errorf("expected a []int type")
85109
}
86110

87-
expected := []int{400, 404, 500, 502}
88-
sort.Ints(val)
111+
expected := []int{}
112+
113+
if !reflect.DeepEqual(expected, val) {
114+
t.Errorf("expected %v but got %v", expected, val)
115+
}
116+
}
117+
118+
func TestParseInvalidAnnotations(t *testing.T) {
119+
ing := buildIngress()
120+
121+
data := map[string]string{}
122+
data[parser.GetAnnotationWithPrefix("enable-custom-http-errors")] = "fakebool"
123+
data[parser.GetAnnotationWithPrefix("custom-http-errors")] = "400,404,fakeint,502"
124+
ing.SetAnnotations(data)
125+
126+
i, err := NewParser(&resolver.Mock{}).Parse(ing)
127+
if err != nil {
128+
t.Errorf("unexpected error parsing ingress with custom-http-errors")
129+
}
130+
val, ok := i.([]int)
131+
if !ok {
132+
t.Errorf("expected a []int type")
133+
}
134+
135+
expected := []int{}
89136

90137
if !reflect.DeepEqual(expected, val) {
91138
t.Errorf("expected %v but got %v", expected, val)

test/e2e/annotations/customhttperrors.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ var _ = framework.DescribeAnnotation("custom-http-errors", func() {
3838
f.NewEchoDeployment()
3939
})
4040

41-
ginkgo.It("configures Nginx correctly", func() {
41+
ginkgo.It("configures Nginx correctly when enabled", func() {
4242
host := "customerrors.foo.com"
4343

4444
errorCodes := []string{"404", "500"}
@@ -119,4 +119,30 @@ var _ = framework.DescribeAnnotation("custom-http-errors", func() {
119119
assert.Contains(ginkgo.GinkgoT(), serverConfig, errorBlockName(fmt.Sprintf("custom-default-backend-%s-%s", f.Namespace, customDefaultBackend), "503"))
120120
assert.Contains(ginkgo.GinkgoT(), serverConfig, fmt.Sprintf("error_page %s = %s", "503", errorBlockName(fmt.Sprintf("custom-default-backend-%s-%s", f.Namespace, customDefaultBackend), "503")))
121121
})
122+
123+
ginkgo.It("configures Nginx correctly when disabled", func() {
124+
host := "customerrors.foo.com"
125+
126+
annotations := map[string]string{
127+
"nginx.ingress.kubernetes.io/enable-custom-http-errors": "false",
128+
"nginx.ingress.kubernetes.io/custom-http-errors": "404, 503",
129+
}
130+
131+
ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations)
132+
f.EnsureIngress(ing)
133+
134+
var serverConfig string
135+
f.WaitForNginxServer(host, func(sc string) bool {
136+
serverConfig = sc
137+
return strings.Contains(serverConfig, fmt.Sprintf("server_name %s", host))
138+
})
139+
140+
f.UpdateNginxConfigMapData("custom-http-errors", "404, 503")
141+
142+
ginkgo.By("not turning on proxy_intercept_errors directive")
143+
assert.NotContains(ginkgo.GinkgoT(), serverConfig, "proxy_intercept_errors on;")
144+
145+
ginkgo.By("not creating error locations")
146+
assert.NotContains(ginkgo.GinkgoT(), serverConfig, fmt.Sprintf("location %s", errorBlockName("upstream-default-backend", "off")))
147+
})
122148
})

0 commit comments

Comments
 (0)