From 9e20453abec1be35f110c50b779ce0f992b5f10a Mon Sep 17 00:00:00 2001 From: Ricardo Pchevuzinske Katz Date: Fri, 17 Sep 2021 19:22:46 -0300 Subject: [PATCH] Add option to force enabling snippet directives Signed-off-by: Ricardo Pchevuzinske Katz --- .../ci/daemonset-customconfig-values.yaml | 1 + .../ci/deployment-customconfig-values.yaml | 1 + .../templates/controller-configmap.yaml | 1 + charts/ingress-nginx/values.yaml | 6 + .../nginx-configuration/configmap.md | 7 + internal/ingress/controller/config/config.go | 5 + internal/ingress/controller/controller.go | 65 ++++++-- .../ingress/controller/controller_test.go | 109 ++++++++++++- .../annotations/modsecurity/modsecurity.go | 42 ++++- test/e2e/annotations/serversnippet.go | 51 +++++- test/e2e/annotations/snippet.go | 44 +++++- test/e2e/settings/server_snippet.go | 149 ++++++++++++++++++ 12 files changed, 458 insertions(+), 23 deletions(-) create mode 100644 test/e2e/settings/server_snippet.go diff --git a/charts/ingress-nginx/ci/daemonset-customconfig-values.yaml b/charts/ingress-nginx/ci/daemonset-customconfig-values.yaml index 43dd2b2ac9..fdaf6e5c33 100644 --- a/charts/ingress-nginx/ci/daemonset-customconfig-values.yaml +++ b/charts/ingress-nginx/ci/daemonset-customconfig-values.yaml @@ -4,6 +4,7 @@ controller: tag: 1.0.0-dev digest: null kind: DaemonSet + enableSnippetDirectives: false admissionWebhooks: enabled: false service: diff --git a/charts/ingress-nginx/ci/deployment-customconfig-values.yaml b/charts/ingress-nginx/ci/deployment-customconfig-values.yaml index 85715ddb76..f0d827dbca 100644 --- a/charts/ingress-nginx/ci/deployment-customconfig-values.yaml +++ b/charts/ingress-nginx/ci/deployment-customconfig-values.yaml @@ -5,6 +5,7 @@ controller: digest: null config: use-proxy-protocol: "true" + enableSnippetDirectives: false admissionWebhooks: enabled: false service: diff --git a/charts/ingress-nginx/templates/controller-configmap.yaml b/charts/ingress-nginx/templates/controller-configmap.yaml index 630545140e..0099bd0a23 100644 --- a/charts/ingress-nginx/templates/controller-configmap.yaml +++ b/charts/ingress-nginx/templates/controller-configmap.yaml @@ -10,6 +10,7 @@ metadata: name: {{ include "ingress-nginx.controller.fullname" . }} namespace: {{ .Release.Namespace }} data: + enable-snippet-directives: "{{ .Values.controller.enableSnippetDirectives }}" {{- if .Values.controller.addHeaders }} add-headers: {{ .Release.Namespace }}/{{ include "ingress-nginx.fullname" . }}-custom-add-headers {{- end }} diff --git a/charts/ingress-nginx/values.yaml b/charts/ingress-nginx/values.yaml index dff4403138..a41d30c04a 100644 --- a/charts/ingress-nginx/values.yaml +++ b/charts/ingress-nginx/values.yaml @@ -69,6 +69,12 @@ controller: # Process IngressClass per name (additionally as per spec.controller) ingressClassByName: false + # This configuration defines if Ingress Controller should allow users to set + # their own *-snippet directives/annotations, otherwise this is forbidden / dropped + # when users add those annotations. + # Global snippets in ConfigMap are still respected + enableSnippetDirectives: true + # Required for use with CNI based kubernetes installations (such as ones set up by kubeadm), # since CNI and hostport don't mix yet. Can be deprecated once https://github.com/kubernetes/kubernetes/issues/23920 # is merged diff --git a/docs/user-guide/nginx-configuration/configmap.md b/docs/user-guide/nginx-configuration/configmap.md index c8aa27373f..dd1828ed89 100755 --- a/docs/user-guide/nginx-configuration/configmap.md +++ b/docs/user-guide/nginx-configuration/configmap.md @@ -46,6 +46,7 @@ The following table shows a configuration option's name, type, and the default v |[disable-access-log](#disable-access-log)|bool|false| |[disable-ipv6](#disable-ipv6)|bool|false| |[disable-ipv6-dns](#disable-ipv6-dns)|bool|false| +|[enable-snippet-directives](#enable-snippet-directives)|bool|true| |[enable-underscores-in-headers](#enable-underscores-in-headers)|bool|false| |[enable-ocsp](#enable-ocsp)|bool|false| |[ignore-invalid-headers](#ignore-invalid-headers)|bool|true| @@ -316,6 +317,12 @@ Disable listening on IPV6. _**default:**_ `false`; IPv6 listening is enabled Disable IPV6 for nginx DNS resolver. _**default:**_ `false`; IPv6 resolving enabled. +## enable-snippet-directives + +Enables Ingress to parse and add *-snippet annotations/directives created by the user. _**default:**_ `true`; +Obs.: We recommend enabling this option only if you TRUST users with permission to create Ingress objects, as this +may allow a user to add restricted configurations to the final nginx.conf file + ## enable-underscores-in-headers Enables underscores in header names. _**default:**_ is disabled diff --git a/internal/ingress/controller/config/config.go b/internal/ingress/controller/config/config.go index 0546e96180..89bbb33cf4 100644 --- a/internal/ingress/controller/config/config.go +++ b/internal/ingress/controller/config/config.go @@ -93,6 +93,10 @@ const ( type Configuration struct { defaults.Backend `json:",squash"` + // EnableSnippetDirectives enable users to add their own snippets via ingress annotation. + // If disabled, only snippets added via ConfigMap are added to ingress. + EnableSnippetDirectives bool `json:"enable-snippet-directives"` + // Sets the name of the configmap that contains the headers to pass to the client AddHeaders string `json:"add-headers,omitempty"` @@ -757,6 +761,7 @@ func NewDefault() Configuration { defGlobalExternalAuth := GlobalExternalAuth{"", "", "", "", "", append(defResponseHeaders, ""), "", "", "", []string{}, map[string]string{}} cfg := Configuration{ + EnableSnippetDirectives: true, AllowBackendServerHeader: false, AccessLogPath: "/var/log/nginx/access.log", AccessLogParams: "", diff --git a/internal/ingress/controller/controller.go b/internal/ingress/controller/controller.go index 38357d4916..2c42041a5d 100644 --- a/internal/ingress/controller/controller.go +++ b/internal/ingress/controller/controller.go @@ -234,27 +234,28 @@ func (n *NGINXController) CheckIngress(ing *networking.Ingress) error { return fmt.Errorf("This deployment is trying to create a catch-all ingress while DisableCatchAll flag is set to true. Remove '.spec.backend' or set DisableCatchAll flag to false.") } - if parser.AnnotationsPrefix != parser.DefaultAnnotationsPrefix { - for key := range ing.ObjectMeta.GetAnnotations() { + cfg := n.store.GetBackendConfiguration() + cfg.Resolver = n.resolver + + for key := range ing.ObjectMeta.GetAnnotations() { + if parser.AnnotationsPrefix != parser.DefaultAnnotationsPrefix { if strings.HasPrefix(key, fmt.Sprintf("%s/", parser.DefaultAnnotationsPrefix)) { return fmt.Errorf("This deployment has a custom annotation prefix defined. Use '%s' instead of '%s'", parser.AnnotationsPrefix, parser.DefaultAnnotationsPrefix) } } - } - k8s.SetDefaultNGINXPathType(ing) - - cfg := n.store.GetBackendConfiguration() - cfg.Resolver = n.resolver + if !cfg.EnableSnippetDirectives && strings.HasSuffix(key, "-snippet") { + return fmt.Errorf("%s annotation cannot be used. Snippet directives are disabled by the Ingress administrator", key) + } - if len(cfg.GlobalRateLimitMemcachedHost) == 0 { - for key := range ing.ObjectMeta.GetAnnotations() { - if strings.HasPrefix(key, fmt.Sprintf("%s/%s", parser.AnnotationsPrefix, "global-rate-limit")) { - return fmt.Errorf("'global-rate-limit*' annotations require 'global-rate-limit-memcached-host' settings configured in the global configmap") - } + if len(cfg.GlobalRateLimitMemcachedHost) == 0 && strings.HasPrefix(key, fmt.Sprintf("%s/%s", parser.AnnotationsPrefix, "global-rate-limit")) { + return fmt.Errorf("'global-rate-limit*' annotations require 'global-rate-limit-memcached-host' settings configured in the global configmap") } + } + k8s.SetDefaultNGINXPathType(ing) + allIngresses := n.store.ListIngresses() filter := func(toCheck *ingress.Ingress) bool { @@ -511,6 +512,30 @@ func (n *NGINXController) getConfiguration(ingresses []*ingress.Ingress) (sets.S } } +func dropSnippetDirectives(anns *annotations.Ingress, ingKey string) { + if anns != nil { + if anns.ConfigurationSnippet != "" { + klog.V(3).Infof("Ingress %q tried to use configuration-snippet and the annotation is disabled by the admin. Removing the annotation", ingKey) + anns.ConfigurationSnippet = "" + } + if anns.ServerSnippet != "" { + klog.V(3).Infof("Ingress %q tried to use server-snippet and the annotation is disabled by the admin. Removing the annotation", ingKey) + anns.ServerSnippet = "" + } + + if anns.ModSecurity.Snippet != "" { + klog.V(3).Infof("Ingress %q tried to use modsecurity-snippet and the annotation is disabled by the admin. Removing the annotation", ingKey) + anns.ModSecurity.Snippet = "" + } + + if anns.ExternalAuth.AuthSnippet != "" { + klog.V(3).Infof("Ingress %q tried to use auth-snippet and the annotation is disabled by the admin. Removing the annotation", ingKey) + anns.ExternalAuth.AuthSnippet = "" + } + + } +} + // getBackendServers returns a list of Upstream and Server to be used by the // backend. An upstream can be used in multiple servers if the namespace, // service name and port are the same. @@ -525,6 +550,10 @@ func (n *NGINXController) getBackendServers(ingresses []*ingress.Ingress) ([]*in ingKey := k8s.MetaNamespaceKey(ing) anns := ing.ParsedAnnotations + if !n.store.GetBackendConfiguration().EnableSnippetDirectives { + dropSnippetDirectives(anns, ingKey) + } + for _, rule := range ing.Spec.Rules { host := rule.Host if host == "" { @@ -801,6 +830,10 @@ func (n *NGINXController) createUpstreams(data []*ingress.Ingress, du *ingress.B ingKey := k8s.MetaNamespaceKey(ing) anns := ing.ParsedAnnotations + if !n.store.GetBackendConfiguration().EnableSnippetDirectives { + dropSnippetDirectives(anns, ingKey) + } + var defBackend string if ing.Spec.DefaultBackend != nil && ing.Spec.DefaultBackend.Service != nil { defBackend = upstreamName(ing.Namespace, ing.Spec.DefaultBackend.Service) @@ -1091,6 +1124,10 @@ func (n *NGINXController) createServers(data []*ingress.Ingress, ingKey := k8s.MetaNamespaceKey(ing) anns := ing.ParsedAnnotations + if !n.store.GetBackendConfiguration().EnableSnippetDirectives { + dropSnippetDirectives(anns, ingKey) + } + // default upstream name un := du.Name @@ -1167,6 +1204,10 @@ func (n *NGINXController) createServers(data []*ingress.Ingress, ingKey := k8s.MetaNamespaceKey(ing) anns := ing.ParsedAnnotations + if !n.store.GetBackendConfiguration().EnableSnippetDirectives { + dropSnippetDirectives(anns, ingKey) + } + if anns.Canary.Enabled { klog.V(2).Infof("Ingress %v is marked as Canary, ignoring", ingKey) continue diff --git a/internal/ingress/controller/controller_test.go b/internal/ingress/controller/controller_test.go index 139e7b2545..1ed8766005 100644 --- a/internal/ingress/controller/controller_test.go +++ b/internal/ingress/controller/controller_test.go @@ -42,6 +42,7 @@ import ( "k8s.io/ingress-nginx/internal/ingress" "k8s.io/ingress-nginx/internal/ingress/annotations" "k8s.io/ingress-nginx/internal/ingress/annotations/canary" + "k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" "k8s.io/ingress-nginx/internal/ingress/annotations/proxyssl" "k8s.io/ingress-nginx/internal/ingress/annotations/sessionaffinity" @@ -57,11 +58,12 @@ import ( ) type fakeIngressStore struct { - ingresses []*ingress.Ingress + ingresses []*ingress.Ingress + configuration ngx_config.Configuration } -func (fakeIngressStore) GetBackendConfiguration() ngx_config.Configuration { - return ngx_config.Configuration{} +func (fis fakeIngressStore) GetBackendConfiguration() ngx_config.Configuration { + return fis.configuration } func (fakeIngressStore) GetConfigMap(key string) (*corev1.ConfigMap, error) { @@ -235,6 +237,9 @@ func TestCheckIngress(t *testing.T) { }) t.Run("When the default annotation prefix is used despite an override", func(t *testing.T) { + defer func() { + parser.AnnotationsPrefix = "nginx.ingress.kubernetes.io" + }() parser.AnnotationsPrefix = "ingress.kubernetes.io" ing.ObjectMeta.Annotations["nginx.ingress.kubernetes.io/backend-protocol"] = "GRPC" nginx.command = testNginxTestCommand{ @@ -246,6 +251,23 @@ func TestCheckIngress(t *testing.T) { } }) + t.Run("When snippets are disabled and user tries to use snippet annotation", func(t *testing.T) { + nginx.store = fakeIngressStore{ + ingresses: []*ingress.Ingress{}, + configuration: ngx_config.Configuration{ + EnableSnippetDirectives: false, + }, + } + nginx.command = testNginxTestCommand{ + t: t, + err: nil, + } + ing.ObjectMeta.Annotations["nginx.ingress.kubernetes.io/server-snippet"] = "bla" + if err := nginx.CheckIngress(ing); err == nil { + t.Errorf("with a snippet annotation, ingresses using the default should be rejected") + } + }) + t.Run("When a new catch-all ingress is being created despite catch-alls being disabled ", func(t *testing.T) { backendBefore := ing.Spec.DefaultBackend disableCatchAllBefore := nginx.cfg.DisableCatchAll @@ -275,6 +297,9 @@ func TestCheckIngress(t *testing.T) { }) t.Run("When the ingress is in a different namespace than the watched one", func(t *testing.T) { + defer func() { + nginx.cfg.Namespace = "test-namespace" + }() nginx.command = testNginxTestCommand{ t: t, err: fmt.Errorf("test error"), @@ -2211,6 +2236,84 @@ func TestGetBackendServers(t *testing.T) { } }, }, + { + Ingresses: []*ingress.Ingress{ + { + Ingress: networking.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "not-allowed-snippet", + Namespace: "default", + Annotations: map[string]string{ + "nginx.ingress.kubernetes.io/server-snippet": "bla", + "nginx.ingress.kubernetes.io/configuration-snippet": "blo", + "nginx.ingress.kubernetes.io/whitelist-source-range": "10.0.0.0/24", + }, + }, + Spec: networking.IngressSpec{ + Rules: []networking.IngressRule{ + { + Host: "example.com", + IngressRuleValue: networking.IngressRuleValue{ + HTTP: &networking.HTTPIngressRuleValue{ + Paths: []networking.HTTPIngressPath{ + { + Path: "/path1", + PathType: &pathTypePrefix, + Backend: networking.IngressBackend{ + Service: &networking.IngressServiceBackend{ + Name: "path1-svc", + Port: networking.ServiceBackendPort{ + Number: 80, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + ParsedAnnotations: &annotations.Ingress{ + Whitelist: ipwhitelist.SourceRange{CIDR: []string{"10.0.0.0/24"}}, + ServerSnippet: "bla", + ConfigurationSnippet: "blo", + }, + }, + }, + Validate: func(ingresses []*ingress.Ingress, upstreams []*ingress.Backend, servers []*ingress.Server) { + if len(servers) != 2 { + t.Errorf("servers count should be 2, got %d", len(servers)) + return + } + s := servers[1] + + if s.ServerSnippet != "" { + t.Errorf("server snippet should be empty, got '%s'", s.ServerSnippet) + } + + if s.Locations[0].ConfigurationSnippet != "" { + t.Errorf("config snippet should be empty, got '%s'", s.Locations[0].ConfigurationSnippet) + } + + if len(s.Locations[0].Whitelist.CIDR) != 1 || s.Locations[0].Whitelist.CIDR[0] != "10.0.0.0/24" { + t.Errorf("allow list was incorrectly dropped, len should be 1 and contain 10.0.0.0/24") + } + + }, + SetConfigMap: func(ns string) *v1.ConfigMap { + return &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + SelfLink: fmt.Sprintf("/api/v1/namespaces/%s/configmaps/config", ns), + }, + Data: map[string]string{ + "enable-snippet-directives": "false", + }, + } + }, + }, } for _, testCase := range testCases { diff --git a/test/e2e/annotations/modsecurity/modsecurity.go b/test/e2e/annotations/modsecurity/modsecurity.go index fad3a2dee5..c4babf21a5 100644 --- a/test/e2e/annotations/modsecurity/modsecurity.go +++ b/test/e2e/annotations/modsecurity/modsecurity.go @@ -282,7 +282,7 @@ var _ = framework.DescribeAnnotation("modsecurity owasp", func() { f.WaitForNginxServer(host, func(server string) bool { - return true + return strings.Contains(server, "SecRequestBodyAccess On") }) f.HTTPTestClient(). @@ -292,4 +292,44 @@ var _ = framework.DescribeAnnotation("modsecurity owasp", func() { Expect(). Status(http.StatusForbidden) }) + + ginkgo.It("should enable modsecurity through the config map but ignore snippet as disabled by admin", func() { + host := "modsecurity.foo.com" + nameSpace := f.Namespace + + snippet := `SecRequestBodyAccess On + SecAuditEngine RelevantOnly + SecAuditLogParts ABIJDEFHZ + SecAuditLog /dev/stdout + SecAuditLogType Serial + SecRule REQUEST_HEADERS:User-Agent \"block-ua\" \"log,deny,id:107,status:403,msg:\'UA blocked\'\"` + + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/modsecurity-snippet": snippet, + } + + ing := framework.NewSingleIngress(host, "/", host, nameSpace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + expectedComment := "SecRuleEngine On" + + f.SetNginxConfigMapData(map[string]string{ + "enable-modsecurity": "true", + "enable-owasp-modsecurity-crs": "true", + "enable-snippet-directives": "false", + "modsecurity-snippet": expectedComment, + }) + + f.WaitForNginxServer(host, + func(server string) bool { + return !strings.Contains(server, "block-ua") + }) + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("User-Agent", "block-ua"). + Expect(). + Status(http.StatusOK) + }) }) diff --git a/test/e2e/annotations/serversnippet.go b/test/e2e/annotations/serversnippet.go index 8a4f25ea43..ccd235e13c 100644 --- a/test/e2e/annotations/serversnippet.go +++ b/test/e2e/annotations/serversnippet.go @@ -17,6 +17,7 @@ limitations under the License. package annotations import ( + "net/http" "strings" "github.com/onsi/ginkgo" @@ -35,8 +36,8 @@ var _ = framework.DescribeAnnotation("server-snippet", func() { host := "serversnippet.foo.com" annotations := map[string]string{ "nginx.ingress.kubernetes.io/server-snippet": ` - more_set_headers "Content-Length: $content_length"; - more_set_headers "Content-Type: $content_type";`, + more_set_headers "Foo: Bar"; + more_set_headers "Xpto: Lalala";`, } ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) @@ -44,8 +45,50 @@ var _ = framework.DescribeAnnotation("server-snippet", func() { f.WaitForNginxServer(host, func(server string) bool { - return strings.Contains(server, `more_set_headers "Content-Length: $content_length`) && - strings.Contains(server, `more_set_headers "Content-Type: $content_type";`) + return strings.Contains(server, `more_set_headers "Foo: Bar`) && + strings.Contains(server, `more_set_headers "Xpto: Lalala";`) }) + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Foo", []string{"Bar"}). + ValueEqual("Xpto", []string{"Lalala"}) + }) + + ginkgo.It(`drops server snippet if disabled by the administrator`, func() { + host := "noserversnippet.foo.com" + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/server-snippet": ` + more_set_headers "Foo: Bar"; + more_set_headers "Xpto: Lalala";`, + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.UpdateNginxConfigMapData("enable-snippet-directives", "false") + defer func() { + // Return to the original value + f.UpdateNginxConfigMapData("enable-snippet-directives", "true") + }() + // Sleep a while just to guarantee that the configmap is applied + framework.Sleep() + f.EnsureIngress(ing) + + f.WaitForNginxServer(host, + func(server string) bool { + return !strings.Contains(server, `more_set_headers "Foo: Bar`) && + !strings.Contains(server, `more_set_headers "Xpto: Lalala";`) + }) + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + Expect(). + Status(http.StatusOK).Headers(). + NotContainsKey("Foo"). + NotContainsKey("Xpto") + }) }) diff --git a/test/e2e/annotations/snippet.go b/test/e2e/annotations/snippet.go index 61c39fa652..82f0622207 100644 --- a/test/e2e/annotations/snippet.go +++ b/test/e2e/annotations/snippet.go @@ -17,6 +17,7 @@ limitations under the License. package annotations import ( + "net/http" "strings" "github.com/onsi/ginkgo" @@ -31,11 +32,11 @@ var _ = framework.DescribeAnnotation("configuration-snippet", func() { f.NewEchoDeployment() }) - ginkgo.It(`set snippet "more_set_headers "Request-Id: $req_id";" in all locations"`, func() { + ginkgo.It(`set snippet "more_set_headers "Foo1: Bar1";" in all locations"`, func() { host := "configurationsnippet.foo.com" annotations := map[string]string{ "nginx.ingress.kubernetes.io/configuration-snippet": ` - more_set_headers "Request-Id: $req_id";`, + more_set_headers "Foo1: Bar1";`, } ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) @@ -43,7 +44,44 @@ var _ = framework.DescribeAnnotation("configuration-snippet", func() { f.WaitForNginxServer(host, func(server string) bool { - return strings.Contains(server, `more_set_headers "Request-Id: $req_id";`) + return strings.Contains(server, `more_set_headers "Foo1: Bar1";`) }) + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Foo1", []string{"Bar1"}) + }) + + ginkgo.It(`drops snippet "more_set_headers "Foo1: Bar1";" in all locations if disabled by admin"`, func() { + host := "noconfigurationsnippet.foo.com" + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/configuration-snippet": ` + more_set_headers "Foo1: Bar1";`, + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.UpdateNginxConfigMapData("enable-snippet-directives", "false") + defer func() { + // Return to the original value + f.UpdateNginxConfigMapData("enable-snippet-directives", "true") + }() + // Sleep a while just to guarantee that the configmap is applied + framework.Sleep() + f.EnsureIngress(ing) + + f.WaitForNginxServer(host, + func(server string) bool { + return !strings.Contains(server, `more_set_headers "Foo1: Bar1";`) + }) + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + Expect(). + Status(http.StatusOK).Headers(). + NotContainsKey("Foo1") }) }) diff --git a/test/e2e/settings/server_snippet.go b/test/e2e/settings/server_snippet.go new file mode 100644 index 0000000000..c3ff5aa468 --- /dev/null +++ b/test/e2e/settings/server_snippet.go @@ -0,0 +1,149 @@ +/* +Copyright 2021 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 settings + +import ( + "net/http" + "strings" + + "github.com/onsi/ginkgo" + + "k8s.io/ingress-nginx/test/e2e/framework" +) + +var _ = framework.DescribeSetting("configmap server-snippet", func() { + f := framework.NewDefaultFramework("cm-server-snippet") + + ginkgo.BeforeEach(func() { + f.NewEchoDeployment() + }) + + ginkgo.It("should add value of server-snippet setting to all ingress config", func() { + host := "serverglobalsnippet1.foo.com" + hostAnnots := "serverannotssnippet1.foo.com" + + f.SetNginxConfigMapData(map[string]string{ + "server-snippet": ` + more_set_headers "Globalfoo: Foooo";`, + }) + + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/server-snippet": ` + more_set_headers "Foo: Bar"; + more_set_headers "Xpto: Lalala";`, + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, nil) + f.EnsureIngress(ing) + + ing1 := framework.NewSingleIngress(hostAnnots, "/", hostAnnots, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing1) + + // Sleep a while just to guarantee that the configmap is applied + framework.Sleep() + + f.WaitForNginxServer(host, + func(server string) bool { + return strings.Contains(server, `more_set_headers "Globalfoo: Foooo`) && + !strings.Contains(server, `more_set_headers "Foo: Bar";`) && + !strings.Contains(server, `more_set_headers "Xpto: Lalala";`) + }) + + f.WaitForNginxServer(hostAnnots, + func(server string) bool { + return strings.Contains(server, `more_set_headers "Globalfoo: Foooo`) && + strings.Contains(server, `more_set_headers "Foo: Bar";`) && + strings.Contains(server, `more_set_headers "Xpto: Lalala";`) + }) + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Globalfoo", []string{"Foooo"}). + NotContainsKey("Foo"). + NotContainsKey("Xpto") + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", hostAnnots). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Foo", []string{"Bar"}). + ValueEqual("Xpto", []string{"Lalala"}). + ValueEqual("Globalfoo", []string{"Foooo"}) + }) + + ginkgo.It("should add global server-snippet and drop annotations per admin config", func() { + host := "serverglobalsnippet2.foo.com" + hostAnnots := "serverannotssnippet2.foo.com" + + f.SetNginxConfigMapData(map[string]string{ + "enable-snippet-directives": "false", + "server-snippet": ` + more_set_headers "Globalfoo: Foooo";`, + }) + + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/server-snippet": ` + more_set_headers "Foo: Bar"; + more_set_headers "Xpto: Lalala";`, + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, nil) + f.EnsureIngress(ing) + + ing1 := framework.NewSingleIngress(hostAnnots, "/", hostAnnots, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing1) + + // Sleep a while just to guarantee that the configmap is applied + framework.Sleep() + + f.WaitForNginxServer(host, + func(server string) bool { + return strings.Contains(server, `more_set_headers "Globalfoo: Foooo`) && + !strings.Contains(server, `more_set_headers "Foo: Bar";`) && + !strings.Contains(server, `more_set_headers "Xpto: Lalala";`) + }) + + f.WaitForNginxServer(hostAnnots, + func(server string) bool { + return strings.Contains(server, `more_set_headers "Globalfoo: Foooo`) && + !strings.Contains(server, `more_set_headers "Foo: Bar";`) && + !strings.Contains(server, `more_set_headers "Xpto: Lalala";`) + }) + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Globalfoo", []string{"Foooo"}). + NotContainsKey("Foo"). + NotContainsKey("Xpto") + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", hostAnnots). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Globalfoo", []string{"Foooo"}). + NotContainsKey("Foo"). + NotContainsKey("Xpto") + }) +})