Skip to content

Commit 58bdab7

Browse files
authored
feat(annotations): introduce enable-custom-http-errors annotation
Signed-off-by: GitHub <noreply@github.com>
1 parent 0a927b6 commit 58bdab7

File tree

5 files changed

+160
-32
lines changed

5 files changed

+160
-32
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|
@@ -329,6 +330,10 @@ Like the [`custom-http-errors`](./configmap.md#custom-http-errors) value in the
329330
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).
330331
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.
331332

333+
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"`.
334+
`nginx.ingress.kubernetes.io/enable-custom-http-errors`:
335+
indicates if the custom http errors feature should be enabled or not to this Ingress rule. The default value is `"true"`.
336+
332337
Example usage:
333338
```
334339
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: 80 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package customhttperrors
1818

1919
import (
2020
"reflect"
21-
"sort"
2221
"testing"
2322

2423
api "k8s.io/api/core/v1"
@@ -28,6 +27,10 @@ import (
2827
"k8s.io/ingress-nginx/internal/ingress/resolver"
2928
)
3029

30+
const DefaultHTTPErrorsString = "400,404,500,502"
31+
32+
var DefaultHTTPErrorsSlice = []int{400, 404, 500, 502}
33+
3134
func buildIngress() *networking.Ingress {
3235
return &networking.Ingress{
3336
ObjectMeta: meta_v1.ObjectMeta{
@@ -47,32 +50,57 @@ func buildIngress() *networking.Ingress {
4750
}
4851
}
4952

50-
func TestParseInvalidAnnotations(t *testing.T) {
53+
func TestParseCodes(t *testing.T) {
5154
ing := buildIngress()
5255

53-
_, err := NewParser(&resolver.Mock{}).Parse(ing)
54-
if err == nil {
55-
t.Errorf("expected error parsing ingress with custom-http-errors")
56+
data := map[string]string{}
57+
data[parser.GetAnnotationWithPrefix("custom-http-errors")] = DefaultHTTPErrorsString
58+
ing.SetAnnotations(data)
59+
60+
i, err := NewParser(&resolver.Mock{}).Parse(ing)
61+
if err != nil {
62+
t.Errorf("unexpected error parsing ingress with custom-http-errors")
63+
}
64+
val, ok := i.([]int)
65+
if !ok {
66+
t.Errorf("expected a []int type")
5667
}
5768

69+
expected := DefaultHTTPErrorsSlice
70+
if !reflect.DeepEqual(expected, val) {
71+
t.Errorf("expected %v but got %v", expected, val)
72+
}
73+
}
74+
75+
func TestEnabledSwitch(t *testing.T) {
76+
ing := buildIngress()
77+
5878
data := map[string]string{}
59-
data[parser.GetAnnotationWithPrefix("custom-http-errors")] = "400,404,abc,502"
79+
data[parser.GetAnnotationWithPrefix("enable-custom-http-errors")] = "true"
80+
data[parser.GetAnnotationWithPrefix("custom-http-errors")] = DefaultHTTPErrorsString
6081
ing.SetAnnotations(data)
6182

6283
i, err := NewParser(&resolver.Mock{}).Parse(ing)
63-
if err == nil {
64-
t.Errorf("expected error parsing ingress with custom-http-errors")
84+
if err != nil {
85+
t.Errorf("unexpected error parsing ingress with custom-http-errors")
6586
}
66-
if i != nil {
67-
t.Errorf("expected %v but got %v", nil, i)
87+
val, ok := i.([]int)
88+
if !ok {
89+
t.Errorf("expected a []int type")
90+
}
91+
92+
expected := DefaultHTTPErrorsSlice
93+
if !reflect.DeepEqual(expected, val) {
94+
t.Errorf("expected %v but got %v", expected, val)
6895
}
6996
}
7097

71-
func TestParseAnnotations(t *testing.T) {
98+
func TestDisabledSwitch(t *testing.T) {
7299
ing := buildIngress()
73100

74101
data := map[string]string{}
75-
data[parser.GetAnnotationWithPrefix("custom-http-errors")] = "400,404,500,502"
102+
data[parser.GetAnnotationWithPrefix("enable-custom-http-errors")] = "false"
103+
data[parser.GetAnnotationWithPrefix("custom-http-errors")] = DefaultHTTPErrorsString
76104
ing.SetAnnotations(data)
77105

78106
i, err := NewParser(&resolver.Mock{}).Parse(ing)
@@ -84,10 +112,48 @@ func TestParseAnnotations(t *testing.T) {
84112
t.Errorf("expected a []int type")
85113
}
86114

87-
expected := []int{400, 404, 500, 502}
88-
sort.Ints(val)
115+
expected := []int{}
116+
if !reflect.DeepEqual(expected, val) {
117+
t.Errorf("expected %v but got %v", expected, val)
118+
}
119+
}
89120

121+
func TestEnabledByDefault(t *testing.T) {
122+
ing := buildIngress()
123+
124+
data := map[string]string{}
125+
data[parser.GetAnnotationWithPrefix("enable-custom-http-errors")] = "fakebool"
126+
data[parser.GetAnnotationWithPrefix("custom-http-errors")] = DefaultHTTPErrorsString
127+
ing.SetAnnotations(data)
128+
129+
i, err := NewParser(&resolver.Mock{}).Parse(ing)
130+
if err != nil {
131+
t.Errorf("unexpected error parsing ingress with custom-http-errors")
132+
}
133+
val, ok := i.([]int)
134+
if !ok {
135+
t.Errorf("expected a []int type")
136+
}
137+
138+
expected := DefaultHTTPErrorsSlice
90139
if !reflect.DeepEqual(expected, val) {
91140
t.Errorf("expected %v but got %v", expected, val)
92141
}
93142
}
143+
144+
func TestErrorOnInvalidCode(t *testing.T) {
145+
ing := buildIngress()
146+
147+
data := map[string]string{}
148+
data[parser.GetAnnotationWithPrefix("enable-custom-http-errors")] = "true"
149+
data[parser.GetAnnotationWithPrefix("custom-http-errors")] = "400,404,fakeint,502"
150+
ing.SetAnnotations(data)
151+
152+
i, err := NewParser(&resolver.Mock{}).Parse(ing)
153+
if err == nil {
154+
t.Errorf("expected error parsing ingress with custom-http-errors")
155+
}
156+
if i != nil {
157+
t.Errorf("expected %v but got %v", nil, i)
158+
}
159+
}

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)