Add support for custom response headers annotation

What:
Add annotation `nginx.ingress.kubernetes.io/custom-response-headers` to allow users to provide custom response headers to the response for the specific ingress. It uses `more_set_headers` directive to add headers to the response. This will provide more flexibility to users to specify custom response headers than via config map approach.

Why:
There's no way except the vulnerable server config snippet annotation to
supply the custom response headers. Hence, we need a way to specify
custom response headers at ingress level.

Signed-off-by: Satyam Zode <satyamz@stellar.org>
This commit is contained in:
Satyam Zode 2022-07-14 17:34:42 +05:30
parent c9ca168364
commit 6034c82946
No known key found for this signature in database
GPG key ID: E29C62B5F5F64A11
12 changed files with 540 additions and 81 deletions

View file

@ -50,6 +50,7 @@ You can add these Kubernetes annotations to specific Ingress objects to customiz
|[nginx.ingress.kubernetes.io/client-body-buffer-size](#client-body-buffer-size)|string|
|[nginx.ingress.kubernetes.io/configuration-snippet](#configuration-snippet)|string|
|[nginx.ingress.kubernetes.io/custom-http-errors](#custom-http-errors)|[]int|
|[nginx.ingress.kubernetes.io/custom-response-headers](#custom-response-headers)|string|
|[nginx.ingress.kubernetes.io/default-backend](#default-backend)|string|
|[nginx.ingress.kubernetes.io/enable-cors](#enable-cors)|"true" or "false"|
|[nginx.ingress.kubernetes.io/cors-allow-origin](#enable-cors)|string|
@ -329,6 +330,15 @@ Example usage:
nginx.ingress.kubernetes.io/custom-http-errors: "404,415"
```
### Custom Response Headers
This annotation is of the form `nginx.ingress.kubernetes.io/custom-response-headers: "<header>"` to specify a custom response header. To specify multiple headers, you can use newline to seperate multiple response headers:
```yaml
nginx.ingress.kubernetes.io/custom-response-headers: |
Content-Type: application/json
Cache-Control: no-cache
```
This annotation uses `more_set_headers` nginx directive.
### Default Backend
This annotation is of the form `nginx.ingress.kubernetes.io/default-backend: <svc name>` to specify a custom default backend. This `<svc name>` is a reference to a service inside of the same namespace in which you are applying this annotation. This annotation overrides the global default backend. In case the service has [multiple ports](https://kubernetes.io/docs/concepts/services-networking/service/#multi-port-services), the first one is the one which will receive the backend traffic.

View file

@ -19,6 +19,7 @@ package annotations
import (
"github.com/imdario/mergo"
"k8s.io/ingress-nginx/internal/ingress/annotations/canary"
"k8s.io/ingress-nginx/internal/ingress/annotations/customresponseheaders"
"k8s.io/ingress-nginx/internal/ingress/annotations/modsecurity"
"k8s.io/ingress-nginx/internal/ingress/annotations/opentelemetry"
"k8s.io/ingress-nginx/internal/ingress/annotations/proxyssl"
@ -75,47 +76,48 @@ const DeniedKeyName = "Denied"
// Ingress defines the valid annotations present in one NGINX Ingress rule
type Ingress struct {
metav1.ObjectMeta
BackendProtocol string
Aliases []string
BasicDigestAuth auth.Config
Canary canary.Config
CertificateAuth authtls.Config
ClientBodyBufferSize string
ConfigurationSnippet string
Connection connection.Config
CorsConfig cors.Config
CustomHTTPErrors []int
DefaultBackend *apiv1.Service
FastCGI fastcgi.Config
Denied *string
ExternalAuth authreq.Config
EnableGlobalAuth bool
HTTP2PushPreload bool
Opentracing opentracing.Config
Opentelemetry opentelemetry.Config
Proxy proxy.Config
ProxySSL proxyssl.Config
RateLimit ratelimit.Config
GlobalRateLimit globalratelimit.Config
Redirect redirect.Config
Rewrite rewrite.Config
Satisfy string
ServerSnippet string
ServiceUpstream bool
SessionAffinity sessionaffinity.Config
SSLPassthrough bool
UsePortInRedirects bool
UpstreamHashBy upstreamhashby.Config
LoadBalancing string
UpstreamVhost string
Denylist ipdenylist.SourceRange
XForwardedPrefix string
SSLCipher sslcipher.Config
Logs log.Config
ModSecurity modsecurity.Config
Mirror mirror.Config
StreamSnippet string
Allowlist ipallowlist.SourceRange
BackendProtocol string
Aliases []string
BasicDigestAuth auth.Config
Canary canary.Config
CertificateAuth authtls.Config
ClientBodyBufferSize string
ConfigurationSnippet string
Connection connection.Config
CorsConfig cors.Config
CustomHTTPErrors []int
CustomResponseHeaders customresponseheaders.Config
DefaultBackend *apiv1.Service
FastCGI fastcgi.Config
Denied *string
ExternalAuth authreq.Config
EnableGlobalAuth bool
HTTP2PushPreload bool
Opentracing opentracing.Config
Opentelemetry opentelemetry.Config
Proxy proxy.Config
ProxySSL proxyssl.Config
RateLimit ratelimit.Config
GlobalRateLimit globalratelimit.Config
Redirect redirect.Config
Rewrite rewrite.Config
Satisfy string
ServerSnippet string
ServiceUpstream bool
SessionAffinity sessionaffinity.Config
SSLPassthrough bool
UsePortInRedirects bool
UpstreamHashBy upstreamhashby.Config
LoadBalancing string
UpstreamVhost string
Denylist ipdenylist.SourceRange
XForwardedPrefix string
SSLCipher sslcipher.Config
Logs log.Config
ModSecurity modsecurity.Config
Mirror mirror.Config
StreamSnippet string
Allowlist ipallowlist.SourceRange
}
// Extractor defines the annotation parsers to be used in the extraction of annotations
@ -127,46 +129,47 @@ type Extractor struct {
func NewAnnotationExtractor(cfg resolver.Resolver) Extractor {
return Extractor{
map[string]parser.IngressAnnotation{
"Aliases": alias.NewParser(cfg),
"BasicDigestAuth": auth.NewParser(auth.AuthDirectory, cfg),
"Canary": canary.NewParser(cfg),
"CertificateAuth": authtls.NewParser(cfg),
"ClientBodyBufferSize": clientbodybuffersize.NewParser(cfg),
"ConfigurationSnippet": snippet.NewParser(cfg),
"Connection": connection.NewParser(cfg),
"CorsConfig": cors.NewParser(cfg),
"CustomHTTPErrors": customhttperrors.NewParser(cfg),
"DefaultBackend": defaultbackend.NewParser(cfg),
"FastCGI": fastcgi.NewParser(cfg),
"ExternalAuth": authreq.NewParser(cfg),
"EnableGlobalAuth": authreqglobal.NewParser(cfg),
"HTTP2PushPreload": http2pushpreload.NewParser(cfg),
"Opentracing": opentracing.NewParser(cfg),
"Opentelemetry": opentelemetry.NewParser(cfg),
"Proxy": proxy.NewParser(cfg),
"ProxySSL": proxyssl.NewParser(cfg),
"RateLimit": ratelimit.NewParser(cfg),
"GlobalRateLimit": globalratelimit.NewParser(cfg),
"Redirect": redirect.NewParser(cfg),
"Rewrite": rewrite.NewParser(cfg),
"Satisfy": satisfy.NewParser(cfg),
"ServerSnippet": serversnippet.NewParser(cfg),
"ServiceUpstream": serviceupstream.NewParser(cfg),
"SessionAffinity": sessionaffinity.NewParser(cfg),
"SSLPassthrough": sslpassthrough.NewParser(cfg),
"UsePortInRedirects": portinredirect.NewParser(cfg),
"UpstreamHashBy": upstreamhashby.NewParser(cfg),
"LoadBalancing": loadbalancing.NewParser(cfg),
"UpstreamVhost": upstreamvhost.NewParser(cfg),
"Allowlist": ipallowlist.NewParser(cfg),
"Denylist": ipdenylist.NewParser(cfg),
"XForwardedPrefix": xforwardedprefix.NewParser(cfg),
"SSLCipher": sslcipher.NewParser(cfg),
"Logs": log.NewParser(cfg),
"BackendProtocol": backendprotocol.NewParser(cfg),
"ModSecurity": modsecurity.NewParser(cfg),
"Mirror": mirror.NewParser(cfg),
"StreamSnippet": streamsnippet.NewParser(cfg),
"Aliases": alias.NewParser(cfg),
"BasicDigestAuth": auth.NewParser(auth.AuthDirectory, cfg),
"Canary": canary.NewParser(cfg),
"CertificateAuth": authtls.NewParser(cfg),
"ClientBodyBufferSize": clientbodybuffersize.NewParser(cfg),
"ConfigurationSnippet": snippet.NewParser(cfg),
"Connection": connection.NewParser(cfg),
"CorsConfig": cors.NewParser(cfg),
"CustomHTTPErrors": customhttperrors.NewParser(cfg),
"CustomResponseHeaders": customresponseheaders.NewParser(cfg),
"DefaultBackend": defaultbackend.NewParser(cfg),
"FastCGI": fastcgi.NewParser(cfg),
"ExternalAuth": authreq.NewParser(cfg),
"EnableGlobalAuth": authreqglobal.NewParser(cfg),
"HTTP2PushPreload": http2pushpreload.NewParser(cfg),
"Opentracing": opentracing.NewParser(cfg),
"Opentelemetry": opentelemetry.NewParser(cfg),
"Proxy": proxy.NewParser(cfg),
"ProxySSL": proxyssl.NewParser(cfg),
"RateLimit": ratelimit.NewParser(cfg),
"GlobalRateLimit": globalratelimit.NewParser(cfg),
"Redirect": redirect.NewParser(cfg),
"Rewrite": rewrite.NewParser(cfg),
"Satisfy": satisfy.NewParser(cfg),
"ServerSnippet": serversnippet.NewParser(cfg),
"ServiceUpstream": serviceupstream.NewParser(cfg),
"SessionAffinity": sessionaffinity.NewParser(cfg),
"SSLPassthrough": sslpassthrough.NewParser(cfg),
"UsePortInRedirects": portinredirect.NewParser(cfg),
"UpstreamHashBy": upstreamhashby.NewParser(cfg),
"LoadBalancing": loadbalancing.NewParser(cfg),
"UpstreamVhost": upstreamvhost.NewParser(cfg),
"Allowlist": ipallowlist.NewParser(cfg),
"Denylist": ipdenylist.NewParser(cfg),
"XForwardedPrefix": xforwardedprefix.NewParser(cfg),
"SSLCipher": sslcipher.NewParser(cfg),
"Logs": log.NewParser(cfg),
"BackendProtocol": backendprotocol.NewParser(cfg),
"ModSecurity": modsecurity.NewParser(cfg),
"Mirror": mirror.NewParser(cfg),
"StreamSnippet": streamsnippet.NewParser(cfg),
},
}
}

View file

@ -43,6 +43,7 @@ var (
annotationAffinityCookieName = parser.GetAnnotationWithPrefix("session-cookie-name")
annotationUpstreamHashBy = parser.GetAnnotationWithPrefix("upstream-hash-by")
annotationCustomHTTPErrors = parser.GetAnnotationWithPrefix("custom-http-errors")
annotationCustomResponseHeaders = parser.GetAnnotationWithPrefix("custom-response-headers")
)
type mockCfg struct {
@ -316,3 +317,44 @@ func TestCustomHTTPErrors(t *testing.T) {
}
}
}
func TestCustomResponseHeaders(t *testing.T) {
ec := NewAnnotationExtractor(mockCfg{})
ing := buildIngress()
fooAnns := []struct {
annotations map[string]string
headers map[string]string
}{
{map[string]string{annotationCustomResponseHeaders: "Content-Type: application/json"}, map[string]string{"Content-Type": "application/json"}},
{map[string]string{annotationCustomResponseHeaders: `Content-Type: application/json
Accept: application/json
`}, map[string]string{"Content-Type": "application/json", "Accept": "application/json"}},
{map[string]string{annotationCustomResponseHeaders: `Content-Type application/json
Accept: application/json
`}, map[string]string{}},
{nil, map[string]string{}},
}
for _, foo := range fooAnns {
ing.SetAnnotations(foo.annotations)
rann, err := ec.Extract(ing)
if err != nil {
t.Errorf("error should be null: %v", err)
}
r := rann.CustomResponseHeaders.ResponseHeaders
// Check that expected headers were created
for i := range foo.headers {
if r[i] != foo.headers[i] {
t.Errorf("Returned %v but expected %v", r, foo.headers)
}
}
// Check that no unexpected headers were created
for i := range r {
if r[i] != foo.headers[i] {
t.Errorf("Returned %v but expected %v", r, foo.headers)
}
}
}
}

View file

@ -0,0 +1,143 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package customresponseheaders
import (
"reflect"
"regexp"
"strings"
networking "k8s.io/api/networking/v1"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
var (
headerRegexp = regexp.MustCompile(`^[a-zA-Z\d\-_]+$`)
//Regex below requires review. Following regex has been picked up from PR: https://github.com/kubernetes/ingress-nginx/pull/9742
headerValueRegexp = regexp.MustCompile(`^[a-zA-Z\d_ :;.,\\/"'?!(){}\[\]@<>=\-+*#$&\x60|~^%]+$`)
completeHeaderRegexp = regexp.MustCompile(`^[a-zA-Z\d_ :;.,\\/\011\012"'?!(){}\[\]@<>=\-+*#$&\x60|~^%]+$`)
)
const (
customResponseHeadersAnnotation = "custom-response-headers"
)
var customResponseHeadersAnnotations = parser.Annotation{
Group: "backend",
Annotations: parser.AnnotationFields{
customResponseHeadersAnnotation: {
Validator: parser.ValidateRegex(completeHeaderRegexp, true),
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation will allows setting the custom response headers for the given ingress`,
},
},
}
// Config returns the custom response headers for an Ingress rule
type Config struct {
ResponseHeaders map[string]string `json:"custom-response-headers,omitempty"`
}
type customresponseheaders struct {
r resolver.Resolver
annotationConfig parser.Annotation
}
// NewParser creates a new custom response headers annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return customresponseheaders{
r: r,
annotationConfig: customResponseHeadersAnnotations,
}
}
// Equal tests for equality between two Configuration types
func (l1 *Config) Equal(l2 *Config) bool {
if l1 == l2 {
return true
}
if l1 == nil || l2 == nil {
return false
}
return reflect.DeepEqual(l1.ResponseHeaders, l2.ResponseHeaders)
}
// Parse parses the annotations contained in the ingress to use
// custom response headers
func (e customresponseheaders) Parse(ing *networking.Ingress) (interface{}, error) {
headersMap := map[string]string{}
responseHeader, err := parser.GetStringAnnotation(customResponseHeadersAnnotation, ing, e.annotationConfig.Annotations)
if err != nil {
return nil, err
}
headers := strings.Split(responseHeader, "\n")
for i := 0; i < len(headers); i++ {
if len(headers[i]) == 0 {
continue
}
if !strings.Contains(headers[i], ":") {
return nil, ing_errors.NewLocationDenied("Invalid header format")
}
headerSplit := strings.SplitN(headers[i], ":", 2)
for j := range headerSplit {
headerSplit[j] = strings.TrimSpace(headerSplit[j])
}
if len(headerSplit) < 2 {
return nil, ing_errors.NewLocationDenied("Invalid header size")
}
if !ValidHeader(headerSplit[0]) {
return nil, ing_errors.NewLocationDenied("Invalid header name")
}
if !ValidValue(headerSplit[1]) {
return nil, ing_errors.NewLocationDenied("Invalid header value")
}
headersMap[strings.TrimSpace(headerSplit[0])] = strings.TrimSpace(headerSplit[1])
}
return &Config{headersMap}, nil
}
// ValidHeader checks is the provided string satisfies the header's name regex
func ValidHeader(header string) bool {
return headerRegexp.Match([]byte(header))
}
// ValidValue checks if the provided string satisfies the header value regex
func ValidValue(header string) bool {
return headerValueRegexp.MatchString(header)
}
func (e customresponseheaders) GetDocumentation() parser.AnnotationFields {
return e.annotationConfig.Annotations
}
func (a customresponseheaders) Validate(anns map[string]string) error {
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
return parser.CheckAnnotationRisk(anns, maxrisk, customResponseHeadersAnnotations.Annotations)
}

View file

@ -0,0 +1,136 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package customresponseheaders
import (
"reflect"
"testing"
api "k8s.io/api/core/v1"
networking "k8s.io/api/networking/v1"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
func buildIngress() *networking.Ingress {
return &networking.Ingress{
ObjectMeta: meta_v1.ObjectMeta{
Name: "foo",
Namespace: api.NamespaceDefault,
},
Spec: networking.IngressSpec{
DefaultBackend: &networking.IngressBackend{
Service: &networking.IngressServiceBackend{
Name: "default-backend",
Port: networking.ServiceBackendPort{
Number: 80,
},
},
},
},
}
}
func TestParseInvalidAnnotations(t *testing.T) {
ing := buildIngress()
_, err := NewParser(&resolver.Mock{}).Parse(ing)
if err == nil {
t.Errorf("expected error parsing ingress with custom-response-headers")
}
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("custom-response-headers")] = `
Content-Type application/json
Access-Control-Max-Age: 600
Nameok k8s.io/ingress-nginx/internal/ingress/annotations/customresponseheaders 0.003s coverage: 88.5% of statements
`
ing.SetAnnotations(data)
i, err := NewParser(&resolver.Mock{}).Parse(ing)
if err == nil {
t.Errorf("expected error parsing ingress with custom-response-headers")
}
if i != nil {
t.Errorf("expected %v but got %v", nil, i)
}
}
func TestParseAnnotations(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("custom-response-headers")] = `
Content-Type: application/json
Access-Control-Max-Age: 600
`
ing.SetAnnotations(data)
i, err := NewParser(&resolver.Mock{}).Parse(ing)
if err != nil {
t.Errorf("unexpected error parsing ingress with custom-response-headers")
}
val, ok := i.(*Config)
if !ok {
t.Errorf("expected a *Config type")
}
expected_response_headers := map[string]string{}
expected_response_headers["Content-Type"] = "application/json"
expected_response_headers["Access-Control-Max-Age"] = "600"
c := &Config{expected_response_headers}
if !reflect.DeepEqual(c, val) {
t.Errorf("expected %v but got %v", c, val)
}
}
func TestConfig_Equal(t *testing.T) {
var nilConfig *Config
config := &Config{
ResponseHeaders: map[string]string{
"nginx.ingress.kubernetes.io/custom-response-headers": "Cache-Control: no-cache",
},
}
config2 := &Config{
ResponseHeaders: map[string]string{
"nginx.ingress.kubernetes.io/custom-response-headers": "Cache-Control: cache",
},
}
configCopy := &Config{
ResponseHeaders: map[string]string{
"nginx.ingress.kubernetes.io/custom-response-headers": "Cache-Control: no-cache",
},
}
if config.Equal(config2) {
t.Errorf("config2 should not be equal to config")
}
if !config.Equal(configCopy) {
t.Errorf("config should not be equal to configCopy")
}
if config.Equal(nilConfig) {
t.Errorf("config should not be equal to nilConfig")
}
}

View file

@ -207,6 +207,9 @@ type Configuration struct {
// DisableIpv6 disable listening on ipv6 address
DisableIpv6 bool `json:"disable-ipv6,omitempty"`
// DisableCustomResponseHeaders disable custom response headers annotation
DisableCustomResponseHeaders bool `json:"disable-custom-response-headers,omitempty"`
// EnableUnderscoresInHeaders enables underscores in header names
// http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers
// By default this is disabled
@ -969,6 +972,7 @@ func NewDefault() Configuration {
SSLRedirect: true,
CustomHTTPErrors: []int{},
DenylistSourceRange: []string{},
CustomResponseHeaders: map[string]string{},
WhitelistSourceRange: []string{},
SkipAccessLogURLs: []string{},
LimitRate: 0,

View file

@ -1527,6 +1527,7 @@ func locationApplyAnnotations(loc *ingress.Location, anns *annotations.Ingress)
loc.BackendProtocol = anns.BackendProtocol
loc.FastCGI = anns.FastCGI
loc.CustomHTTPErrors = anns.CustomHTTPErrors
loc.CustomResponseHeaders = anns.CustomResponseHeaders
loc.ModSecurity = anns.ModSecurity
loc.Satisfy = anns.Satisfy
loc.Mirror = anns.Mirror

View file

@ -31,6 +31,11 @@ type Backend struct {
// By default this is disabled
CustomHTTPErrors []int `json:"custom-http-errors"`
// Defines custom response HTTP headers which should be passed to more_set_headers directive
// https://github.com/openresty/headers-more-nginx-module#more_set_headers
// By default this is empty
CustomResponseHeaders map[string]string `json:"custom-response-headers"`
// toggles whether or not to remove trailing slashes during TLS redirects
PreserveTrailingSlash bool `json:"preserve-trailing-slash"`

View file

@ -27,6 +27,7 @@ import (
"k8s.io/ingress-nginx/internal/ingress/annotations/authtls"
"k8s.io/ingress-nginx/internal/ingress/annotations/connection"
"k8s.io/ingress-nginx/internal/ingress/annotations/cors"
"k8s.io/ingress-nginx/internal/ingress/annotations/customresponseheaders"
"k8s.io/ingress-nginx/internal/ingress/annotations/fastcgi"
"k8s.io/ingress-nginx/internal/ingress/annotations/globalratelimit"
"k8s.io/ingress-nginx/internal/ingress/annotations/ipallowlist"
@ -360,6 +361,9 @@ type Location struct {
// Opentelemetry allows the global opentelemetry setting to be overridden for a location
// +optional
Opentelemetry opentelemetry.Config `json:"opentelemetry"`
// CustomResponseHeaders allows to pass custom response headers.
// +optional
CustomResponseHeaders customresponseheaders.Config `json:"custom-response-headers,omitempty"`
}
// SSLPassthroughBackend describes a SSL upstream server configured

View file

@ -470,6 +470,10 @@ func (l1 *Location) Equal(l2 *Location) bool {
return false
}
if !l1.CustomResponseHeaders.Equal(&l2.CustomResponseHeaders) {
return false
}
return true
}

View file

@ -1513,6 +1513,15 @@ stream {
fastcgi_param {{ $k }} {{ $v | quote }};
{{ end }}
{{ if $location.CustomResponseHeaders }}
# Custom Response Headers
{{ if not $all.Cfg.DisableCustomResponseHeaders }}
{{ range $k, $v := $location.CustomResponseHeaders.ResponseHeaders }}
more_set_headers {{ printf "%s: %s" $k $v | escapeLiteralDollar | quote }};
{{ end }}
{{ end }}
{{ end }}
{{ if not (empty $location.Redirect.URL) }}
return {{ $location.Redirect.Code }} {{ $location.Redirect.URL }};
{{ end }}

View file

@ -0,0 +1,98 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package annotations
import (
"net/http"
"strings"
"github.com/onsi/ginkgo/v2"
"k8s.io/ingress-nginx/test/e2e/framework"
)
var _ = framework.DescribeAnnotation("custom-response-headers", func() {
f := framework.NewDefaultFramework("custom-response-headers")
ginkgo.BeforeEach(func() {
f.NewEchoDeployment()
})
ginkgo.It("should add custom response header via the custom-response-headers annotation", func() {
host := "customresponseheaders.foo.com"
annotations := map[string]string{
"nginx.ingress.kubernetes.io/custom-response-headers": `
Content-Type: application/json
`,
}
ing := framework.NewSingleIngress(host, "/foo", host, f.Namespace, framework.EchoService, 80, annotations)
f.EnsureIngress(ing)
f.WaitForNginxServer(host,
func(server string) bool {
return strings.Contains(server, `more_set_headers "Content-Type: application/json";`)
})
f.HTTPTestClient().
GET("/foo").
WithHeader("Host", host).
Expect().
Status(http.StatusOK).Headers().ContainsKey("Content-Type")
})
ginkgo.It("should work without custom-response-headers annotation present on the ingress", func() {
host := "customresponseheaders.foo.com"
ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, nil)
f.EnsureIngress(ing)
f.WaitForNginxServer(host,
func(server string) bool {
return strings.Contains(server, "server_name customresponseheaders.foo.com")
})
f.HTTPTestClient().
GET("/").
WithHeader("Host", host).
Expect().
Status(http.StatusOK).Headers().NotContainsKey("application/json")
})
ginkgo.It("should disable the custom-response-headers annotation using global flag", func() {
host := "customresponseheaders.foo.com"
annotations := map[string]string{
"nginx.ingress.kubernetes.io/custom-response-headers": `
Cache-Control: no-cache
`,
}
f.UpdateNginxConfigMapData("disable-custom-response-headers", "true")
ing := framework.NewSingleIngress(host, "/bar", host, f.Namespace, framework.EchoService, 80, annotations)
f.EnsureIngress(ing)
f.WaitForNginxServer(host,
func(server string) bool {
return !strings.Contains(server, `more_set_headers "Cache-Control: no-cache";`)
})
f.HTTPTestClient().
GET("/bar").
WithHeader("Host", host).
Expect().
Status(http.StatusOK).Headers().NotContainsKey("Cache-Control")
})
})