ingress-nginx-helm/test/e2e/admission/admission.go
2024-08-25 21:03:29 +01:00

344 lines
12 KiB
Go

/*
Copyright 2020 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 admission
import (
"bytes"
"context"
"fmt"
"net/http"
"os/exec"
"strings"
"github.com/onsi/ginkgo/v2"
"github.com/stretchr/testify/assert"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/ingress-nginx/test/e2e/framework"
networking "k8s.io/api/networking/v1"
)
const admissionTestHost = "admission-test"
var _ = framework.IngressNginxDescribeSerial("[Admission] admission controller", func() {
f := framework.NewDefaultFramework("admission")
ginkgo.BeforeEach(func() {
f.NewEchoDeployment()
f.NewSlowEchoDeployment()
})
ginkgo.It("should not allow overlaps of host and paths without canary annotations", func() {
host := admissionTestHost
firstIngress := framework.NewSingleIngress("first-ingress", "/", host, f.Namespace, framework.EchoService, 80, nil)
_, err := f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), firstIngress, metav1.CreateOptions{})
assert.Nil(ginkgo.GinkgoT(), err, "creating ingress")
f.WaitForNginxServer(host,
func(server string) bool {
return strings.Contains(server, fmt.Sprintf("server_name %v", host))
})
secondIngress := framework.NewSingleIngress("second-ingress", "/", host, f.Namespace, framework.EchoService, 80, nil)
_, err = f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), secondIngress, metav1.CreateOptions{})
assert.NotNil(ginkgo.GinkgoT(), err, "creating an ingress with the same host and path should return an error")
})
ginkgo.It("should allow overlaps of host and paths with canary annotation", func() {
host := admissionTestHost
firstIngress := framework.NewSingleIngress("first-ingress", "/", host, f.Namespace, framework.EchoService, 80, nil)
_, err := f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), firstIngress, metav1.CreateOptions{})
assert.Nil(ginkgo.GinkgoT(), err, "creating ingress")
f.WaitForNginxServer(host,
func(server string) bool {
return strings.Contains(server, fmt.Sprintf("server_name %v", host))
})
canaryAnnotations := map[string]string{
"nginx.ingress.kubernetes.io/canary": "true",
"nginx.ingress.kubernetes.io/canary-by-header": "CanaryByHeader",
}
secondIngress := framework.NewSingleIngress("second-ingress", "/", host, f.Namespace, framework.SlowEchoService, 80, canaryAnnotations)
_, err = f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), secondIngress, metav1.CreateOptions{})
assert.Nil(ginkgo.GinkgoT(), err, "creating an ingress with the same host and path should not return an error using a canary annotation")
})
ginkgo.It("should block ingress with invalid path", func() {
host := "invalid-path"
firstIngress := framework.NewSingleIngress("valid-path", "/mypage", host, f.Namespace, framework.EchoService, 80, nil)
_, err := f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), firstIngress, metav1.CreateOptions{})
assert.Nil(ginkgo.GinkgoT(), err, "creating ingress")
f.WaitForNginxServer(host,
func(server string) bool {
return strings.Contains(server, fmt.Sprintf("server_name %v", host))
})
secondIngress := framework.NewSingleIngress("second-ingress", "/etc/nginx", host, f.Namespace, framework.EchoService, 80, nil)
_, err = f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), secondIngress, metav1.CreateOptions{})
assert.NotNil(ginkgo.GinkgoT(), err, "creating an ingress with invalid path should return an error")
})
ginkgo.It("should return an error if there is an error validating the ingress definition", func() {
disableSnippet := f.AllowSnippetConfiguration()
defer disableSnippet()
host := admissionTestHost
annotations := map[string]string{
"nginx.ingress.kubernetes.io/configuration-snippet": "something invalid",
}
firstIngress := framework.NewSingleIngress("first-ingress", "/", host, f.Namespace, framework.EchoService, 80, annotations)
_, err := f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), firstIngress, metav1.CreateOptions{})
assert.NotNil(ginkgo.GinkgoT(), err, "creating an ingress with invalid configuration should return an error")
})
ginkgo.It("should return an error if there is an invalid value in some annotation", func() {
host := admissionTestHost
annotations := map[string]string{
"nginx.ingress.kubernetes.io/connection-proxy-header": "a;}",
}
f.UpdateNginxConfigMapData("annotation-value-word-blocklist", "}")
firstIngress := framework.NewSingleIngress("first-ingress", "/", host, f.Namespace, framework.EchoService, 80, annotations)
_, err := f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), firstIngress, metav1.CreateOptions{})
assert.NotNil(ginkgo.GinkgoT(), err, "creating an ingress with invalid annotation value should return an error")
})
ginkgo.It("should return an error if there is a forbidden value in some annotation", func() {
host := admissionTestHost
annotations := map[string]string{
"nginx.ingress.kubernetes.io/connection-proxy-header": "set_by_lua",
}
f.UpdateNginxConfigMapData("annotation-value-word-blocklist", "set_by_lua")
firstIngress := framework.NewSingleIngress("first-ingress", "/", host, f.Namespace, framework.EchoService, 80, annotations)
_, err := f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), firstIngress, metav1.CreateOptions{})
assert.NotNil(ginkgo.GinkgoT(), err, "creating an ingress with invalid annotation value should return an error")
})
ginkgo.It("should return an error if there is an invalid path and wrong pathType is set", func() {
host := "path-validation"
var (
exactPathType = networking.PathTypeExact
prefixPathType = networking.PathTypePrefix
implSpecific = networking.PathTypeImplementationSpecific
)
f.UpdateNginxConfigMapData("strict-validate-path-type", "true")
invalidPath := framework.NewSingleIngress("first-ingress", "/foo/bar/[a-z]{3}", host, f.Namespace, framework.EchoService, 80, nil)
invalidPath.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].PathType = &exactPathType
_, err := f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), invalidPath, metav1.CreateOptions{})
assert.NotNil(ginkgo.GinkgoT(), err, "creating an ingress with invalid path value should return an error")
invalidPath.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].PathType = &prefixPathType
_, err = f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), invalidPath, metav1.CreateOptions{})
assert.NotNil(ginkgo.GinkgoT(), err, "creating an ingress with invalid path value should return an error")
annotations := map[string]string{
"nginx.ingress.kubernetes.io/use-regex": "true",
"nginx.ingress.kubernetes.io/rewrite-target": "/new/backend",
}
pathSpecific := framework.NewSingleIngress("pathspec-ingress", "/foo/bar/[a-z]{3}", host, f.Namespace, framework.EchoService, 80, annotations)
pathSpecific.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].PathType = &implSpecific
_, err = f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), pathSpecific, metav1.CreateOptions{})
assert.Nil(ginkgo.GinkgoT(), err, "creating an ingress with arbitrary path and implSpecific value should not return an error")
validPath := framework.NewSingleIngress("second-ingress", "/bloblo", host, f.Namespace, framework.EchoService, 80, nil)
_, err = f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Create(context.TODO(), validPath, metav1.CreateOptions{})
assert.Nil(ginkgo.GinkgoT(), err, "creating an ingress with valid path should not return an error")
})
ginkgo.It("should not return an error if the Ingress V1 definition is valid with Ingress Class", func() {
out, err := createIngress(f.Namespace, validV1Ingress)
assert.Equal(ginkgo.GinkgoT(), "ingress.networking.k8s.io/extensions created\n", out)
assert.Nil(ginkgo.GinkgoT(), err, "creating an ingress using kubectl")
f.WaitForNginxConfiguration(func(cfg string) bool {
return strings.Contains(cfg, "extensions")
})
f.HTTPTestClient().
GET("/").
WithHeader("Host", "extensions").
Expect().
Status(http.StatusOK)
})
ginkgo.It("should not return an error if the Ingress V1 definition is valid with IngressClass annotation", func() {
out, err := createIngress(f.Namespace, validV1IngressAnnotation)
assert.Equal(ginkgo.GinkgoT(), "ingress.networking.k8s.io/extensions-class created\n", out)
assert.Nil(ginkgo.GinkgoT(), err, "creating an ingress using kubectl")
f.WaitForNginxConfiguration(func(cfg string) bool {
return strings.Contains(cfg, "extensions-class")
})
f.HTTPTestClient().
GET("/").
WithHeader("Host", "extensions-class").
Expect().
Status(http.StatusOK)
})
ginkgo.It("should return an error if the Ingress V1 definition contains invalid annotations", func() {
disableSnippet := f.AllowSnippetConfiguration()
defer disableSnippet()
out, err := createIngress(f.Namespace, invalidV1Ingress)
assert.Empty(ginkgo.GinkgoT(), out)
assert.NotNil(ginkgo.GinkgoT(), err, "creating an ingress using kubectl")
_, err = f.KubeClientSet.NetworkingV1().Ingresses(f.Namespace).Get(context.TODO(), "extensions-invalid", metav1.GetOptions{})
if !apierrors.IsNotFound(err) {
assert.NotNil(ginkgo.GinkgoT(), err, "creating an ingress with invalid configuration should return an error")
}
})
ginkgo.It("should not return an error for an invalid Ingress when it has unknown class", func() {
disableSnippet := f.AllowSnippetConfiguration()
defer disableSnippet()
out, err := createIngress(f.Namespace, invalidV1IngressWithOtherClass)
assert.Equal(ginkgo.GinkgoT(), "ingress.networking.k8s.io/extensions-invalid-other created\n", out)
assert.Nil(ginkgo.GinkgoT(), err, "creating an invalid ingress with unknown class using kubectl")
})
})
const (
validV1Ingress = `
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: extensions
spec:
ingressClassName: nginx
rules:
- host: extensions
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: echo
port:
number: 80
---
`
validV1IngressAnnotation = `
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: extensions-class
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: extensions-class
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: echo
port:
number: 80
---
`
invalidV1Ingress = `
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: extensions-invalid
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
invalid directive
spec:
ingressClassName: nginx
rules:
- host: extensions-invalid
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: echo
port:
number: 80
---
`
invalidV1IngressWithOtherClass = `
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: extensions-invalid-other
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
invalid directive
spec:
ingressClassName: nginx-other
rules:
- host: extensions-invalid
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: echo
port:
number: 80
---
`
)
func createIngress(namespace, ingressDefinition string) (string, error) {
var (
execOut bytes.Buffer
execErr bytes.Buffer
)
//nolint:gosec // Ignore G204 error
cmd := exec.Command("/bin/bash", "-c", fmt.Sprintf("%v --warnings-as-errors=false apply --namespace %s -f -", framework.KubectlPath, namespace))
cmd.Stdin = strings.NewReader(ingressDefinition)
cmd.Stdout = &execOut
cmd.Stderr = &execErr
err := cmd.Run()
if err != nil {
stderr := strings.TrimSpace(execErr.String())
return "", fmt.Errorf("kubectl error: %v\n%v", err, stderr)
}
return execOut.String(), nil
}