Implementing allowed Ingress hostnames (#162)

Co-authored-by: Dario Tranchitella <dario@tranchitella.eu>
This commit is contained in:
Paolo Carta
2021-01-13 22:18:09 +01:00
committed by GitHub
parent a2109b74ef
commit 89c66de7c6
17 changed files with 623 additions and 30 deletions

3
.gitignore vendored
View File

@@ -23,5 +23,6 @@ bin
*.swo
*~
hack/*.kubeconfig
**/*.kubeconfig
.DS_Store

View File

@@ -0,0 +1,42 @@
/*
Copyright 2020 Clastix Labs.
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 v1alpha1
import (
"sort"
)
type IngressHostnamesList []string
func (hostnames IngressHostnamesList) Len() int {
return len(hostnames)
}
func (hostnames IngressHostnamesList) Swap(i, j int) {
hostnames[i], hostnames[j] = hostnames[j], hostnames[i]
}
func (hostnames IngressHostnamesList) Less(i, j int) bool {
return hostnames[i] < hostnames[j]
}
func (hostnames IngressHostnamesList) IsStringInList(value string) (ok bool) {
sort.Sort(hostnames)
i := sort.SearchStrings(hostnames, value)
ok = i < hostnames.Len() && hostnames[i] == value
return
}

View File

@@ -38,6 +38,11 @@ type IngressClassesSpec struct {
AllowedRegex string `json:"allowedRegex,omitempty"`
}
type IngressHostnamesSpec struct {
Allowed IngressHostnamesList `json:"allowed"`
AllowedRegex string `json:"allowedRegex"`
}
type ContainerRegistriesSpec struct {
Allowed RegistryList `json:"allowed,omitempty"`
AllowedRegex string `json:"allowedRegex,omitempty"`
@@ -60,6 +65,7 @@ type TenantSpec struct {
ServicesMetadata AdditionalMetadata `json:"servicesMetadata,omitempty"`
StorageClasses *StorageClassesSpec `json:"storageClasses,omitempty"`
IngressClasses *IngressClassesSpec `json:"ingressClasses,omitempty"`
IngressHostnames *IngressHostnamesSpec `json:"ingressHostnames,omitempty"`
ContainerRegistries *ContainerRegistriesSpec `json:"containerRegistries,omitempty"`
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
NetworkPolicies []networkingv1.NetworkPolicySpec `json:"networkPolicies,omitempty"`

View File

@@ -155,6 +155,45 @@ func (in *IngressClassesSpec) DeepCopy() *IngressClassesSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in IngressHostnamesList) DeepCopyInto(out *IngressHostnamesList) {
{
in := &in
*out = make(IngressHostnamesList, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressHostnamesList.
func (in IngressHostnamesList) DeepCopy() IngressHostnamesList {
if in == nil {
return nil
}
out := new(IngressHostnamesList)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IngressHostnamesSpec) DeepCopyInto(out *IngressHostnamesSpec) {
*out = *in
if in.Allowed != nil {
in, out := &in.Allowed, &out.Allowed
*out = make(IngressHostnamesList, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressHostnamesSpec.
func (in *IngressHostnamesSpec) DeepCopy() *IngressHostnamesSpec {
if in == nil {
return nil
}
out := new(IngressHostnamesSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in NamespaceList) DeepCopyInto(out *NamespaceList) {
{
@@ -327,6 +366,11 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
*out = new(IngressClassesSpec)
(*in).DeepCopyInto(*out)
}
if in.IngressHostnames != nil {
in, out := &in.IngressHostnames, &out.IngressHostnames
*out = new(IngressHostnamesSpec)
(*in).DeepCopyInto(*out)
}
if in.ContainerRegistries != nil {
in, out := &in.ContainerRegistries, &out.ContainerRegistries
*out = new(ContainerRegistriesSpec)

View File

@@ -132,6 +132,18 @@ spec:
allowedRegex:
type: string
type: object
ingressHostnames:
properties:
allowed:
items:
type: string
type: array
allowedRegex:
type: string
required:
- allowed
- allowedRegex
type: object
limitRanges:
items:
description: LimitRangeSpec defines a min/max usage limit for resources

View File

@@ -134,6 +134,18 @@ spec:
allowedRegex:
type: string
type: object
ingressHostnames:
properties:
allowed:
items:
type: string
type: array
allowedRegex:
type: string
required:
- allowed
- allowedRegex
type: object
limitRanges:
items:
description: LimitRangeSpec defines a min/max usage limit for resources

View File

@@ -4,6 +4,11 @@ kind: Tenant
metadata:
name: oil
spec:
ingressHostnames:
allowed:
- my.oil.acmecorp.com
- my.gas.acmecorp.com
allowedRegex: "^.*acmecorp.com$"
ingressClasses:
allowed:
- default

View File

View File

@@ -46,15 +46,19 @@ metadata:
kubernetes.io/ingress.class: oil
spec:
rules:
- host: web.oil.acmecorp.com
http:
paths:
- backend:
serviceName: nginx
servicePort: 80
path: /
- host: web.oil.acmecorp.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80
```
Any tentative of Alice to use a not valid hostname, e.g. `web.gas.acmecorp.org`, will fail.
# Whats next

View File

@@ -0,0 +1,223 @@
//+build e2e
/*
Copyright 2020 Clastix Labs.
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 e2e
import (
"context"
"fmt"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"github.com/clastix/capsule/api/v1alpha1"
)
var _ = Describe("when Tenant handles Ingress hostnames", func() {
tnt := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "ingress-hostnames",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "hostname",
Kind: "User",
},
IngressHostnames: &v1alpha1.IngressHostnamesSpec{
Allowed: []string{"sigs.k8s.io", "operator.sdk", "domain.tld"},
AllowedRegex: `.*\.clastix\.io`,
},
},
}
// scaffold a basic networking.k8s.io Ingress with name and host
networkingIngress := func(name, hostname string) *networkingv1.Ingress {
return &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: networkingv1.IngressSpec{
Rules: []networkingv1.IngressRule{
{
Host: hostname,
IngressRuleValue: networkingv1.IngressRuleValue{
HTTP: &networkingv1.HTTPIngressRuleValue{
Paths: []networkingv1.HTTPIngressPath{
{
Path: "/",
PathType: func(v networkingv1.PathType) *networkingv1.PathType {
return &v
}(networkingv1.PathTypeExact),
Backend: networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "foo",
Port: networkingv1.ServiceBackendPort{Name: "http"},
},
},
},
},
},
},
},
},
},
}
}
// scaffold a basic extensions Ingress with name and host
extensionsIngress := func(name, hostname string) *extensionsv1beta1.Ingress {
return &extensionsv1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: extensionsv1beta1.IngressSpec{
Rules: []extensionsv1beta1.IngressRule{
{
Host: hostname,
IngressRuleValue: extensionsv1beta1.IngressRuleValue{
HTTP: &extensionsv1beta1.HTTPIngressRuleValue{
Paths: []extensionsv1beta1.HTTPIngressPath{
{
Path: "/",
PathType: func(v extensionsv1beta1.PathType) *extensionsv1beta1.PathType {
return &v
}(extensionsv1beta1.PathTypeExact),
Backend: extensionsv1beta1.IngressBackend{
ServiceName: "foo",
ServicePort: intstr.FromInt(8080),
},
},
},
},
},
},
},
},
}
}
JustBeforeEach(func() {
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
})
JustAfterEach(func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
})
It("should block a non allowed Hostname", func() {
maj, min, _ := GetKubernetesSemVer()
ns := NewNamespace("disallowed-hostname")
cs := ownerClient(tnt)
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, podRecreationTimeoutInterval).Should(ContainElement(ns.GetName()))
if maj == 1 && min > 18 {
By("testing networking.k8s.io", func() {
Eventually(func() (err error) {
obj := networkingIngress("denied-networking", "kubernetes.io")
_, err = cs.NetworkingV1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed())
})
}
if maj == 1 && min < 22 {
By("testing extensions", func() {
Eventually(func() (err error) {
obj := extensionsIngress("denied-extensions", "kubernetes.io")
_, err = cs.ExtensionsV1beta1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed())
})
}
})
It("should allow Hostnames in list", func() {
maj, min, _ := GetKubernetesSemVer()
ns := NewNamespace("allowed-hostname-list")
cs := ownerClient(tnt)
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, podRecreationTimeoutInterval).Should(ContainElement(ns.GetName()))
if maj == 1 && min > 18 {
By("testing networking.k8s.io", func() {
for i, h := range tnt.Spec.IngressHostnames.Allowed {
Eventually(func() (err error) {
obj := networkingIngress(fmt.Sprintf("allowed-networking-%d", i), h)
_, err = cs.NetworkingV1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
})
}
if maj == 1 && min < 22 {
By("testing extensions", func() {
for i, h := range tnt.Spec.IngressHostnames.Allowed {
Eventually(func() (err error) {
obj := extensionsIngress(fmt.Sprintf("allowed-extensions-%d", i), h)
_, err = cs.ExtensionsV1beta1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
})
}
})
It("should allow Hostnames in regex", func() {
maj, min, _ := GetKubernetesSemVer()
ns := NewNamespace("allowed-hostname-regex")
cs := ownerClient(tnt)
NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, podRecreationTimeoutInterval).Should(ContainElement(ns.GetName()))
if maj == 1 && min > 18 {
By("testing networking.k8s.io", func() {
for _, h := range []string{"foo", "bar", "bizz"} {
Eventually(func() (err error) {
obj := networkingIngress(fmt.Sprintf("allowed-networking-%s", h), h)
_, err = cs.NetworkingV1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
})
}
if maj == 1 && min < 22 {
By("testing extensions", func() {
for _, h := range []string{"foo", "bar", "bizz"} {
Eventually(func() (err error) {
obj := extensionsIngress(fmt.Sprintf("allowed-extensions-%s", h), h)
_, err = cs.ExtensionsV1beta1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
return
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
})
}
})
})

View File

@@ -0,0 +1,76 @@
//+build e2e
/*
Copyright 2020 Clastix Labs.
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 e2e
import (
"context"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/clastix/capsule/api/v1alpha1"
)
var _ = Describe("when a second Tenant contains an already declared allowed Ingress hostname", func() {
tnt := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "first-ingress-hostnames",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "first-user",
Kind: "User",
},
IngressHostnames: &v1alpha1.IngressHostnamesSpec{
Allowed: []string{"capsule.clastix.io", "docs.capsule.k8s", "42.clatix.io"},
},
},
}
JustBeforeEach(func() {
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
})
JustAfterEach(func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
})
It("should block creation if contains collided Ingress hostnames", func() {
for _, h := range tnt.Spec.IngressHostnames.Allowed {
tnt2 := &v1alpha1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "second-ingress-hostnames",
},
Spec: v1alpha1.TenantSpec{
Owner: v1alpha1.OwnerSpec{
Name: "second-user",
Kind: "User",
},
IngressHostnames: &v1alpha1.IngressHostnamesSpec{
Allowed: []string{h},
},
},
}
Expect(k8sClient.Delete(context.TODO(), tnt2)).ShouldNot(Succeed())
}
})
})

View File

@@ -19,6 +19,7 @@ package indexer
import "github.com/clastix/capsule/pkg/indexer/tenant"
func init() {
AddToIndexerFuncs = append(AddToIndexerFuncs, tenant.IngressHostnames{})
AddToIndexerFuncs = append(AddToIndexerFuncs, tenant.NamespacesReference{})
AddToIndexerFuncs = append(AddToIndexerFuncs, tenant.OwnerReference{})
}

View File

@@ -0,0 +1,44 @@
/*
Copyright 2020 Clastix Labs.
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 tenant
import (
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/clastix/capsule/api/v1alpha1"
)
type IngressHostnames struct {
}
func (IngressHostnames) Object() client.Object {
return &v1alpha1.Tenant{}
}
func (IngressHostnames) Field() string {
return ".spec.ingressHostnames"
}
func (IngressHostnames) Func() client.IndexerFunc {
return func(object client.Object) []string {
tenant := object.(*v1alpha1.Tenant)
if tenant.Spec.IngressHostnames != nil {
return tenant.Spec.IngressHostnames.Allowed.DeepCopy()
}
return nil
}
}

View File

@@ -36,17 +36,23 @@ func NewIngressClassForbidden(className string, spec v1alpha1.IngressClassesSpec
}
func (i ingressClassForbidden) Error() string {
return fmt.Sprintf("Ingress Class %s is forbidden for the current Tenant%s", i.className, appendError(i.spec))
return fmt.Sprintf("Ingress Class %s is forbidden for the current Tenant%s", i.className, appendClassError(i.spec))
}
func appendError(spec v1alpha1.IngressClassesSpec) (append string) {
if len(spec.Allowed) > 0 {
append += fmt.Sprintf(", one of the following (%s)", strings.Join(spec.Allowed, ", "))
}
if len(spec.AllowedRegex) > 0 {
append += fmt.Sprintf(", or matching the regex %s", spec.AllowedRegex)
}
return
type ingressHostnameNotValid struct {
invalidHostnames []string
notMatchingHostnames []string
spec v1alpha1.IngressHostnamesSpec
}
func NewIngressHostnamesNotValid(invalidHostnames []string, notMatchingHostnames []string, spec v1alpha1.IngressHostnamesSpec) error {
return &ingressHostnameNotValid{invalidHostnames: invalidHostnames, notMatchingHostnames: notMatchingHostnames, spec: spec}
}
func (i ingressHostnameNotValid) Error() string {
return fmt.Sprintf("Hostnames %s are not valid for the current Tenant. Hostnames %s not matching for the current Tenant%s",
i.invalidHostnames, i.notMatchingHostnames, appendHostnameError(i.spec))
}
type ingressClassNotValid struct {
@@ -60,5 +66,25 @@ func NewIngressClassNotValid(spec v1alpha1.IngressClassesSpec) error {
}
func (i ingressClassNotValid) Error() string {
return "A valid Ingress Class must be used" + appendError(i.spec)
return "A valid Ingress Class must be used" + appendClassError(i.spec)
}
func appendClassError(spec v1alpha1.IngressClassesSpec) (append string) {
if len(spec.Allowed) > 0 {
append += fmt.Sprintf(", one of the following (%s)", strings.Join(spec.Allowed, ", "))
}
if len(spec.AllowedRegex) > 0 {
append += fmt.Sprintf(", or matching the regex %s", spec.AllowedRegex)
}
return
}
func appendHostnameError(spec v1alpha1.IngressHostnamesSpec) (append string) {
if len(spec.Allowed) > 0 {
append += fmt.Sprintf(", specify one of the following (%s)", strings.Join(spec.Allowed, ", "))
}
if len(spec.AllowedRegex) > 0 {
append += fmt.Sprintf(", or matching the regex %s", spec.AllowedRegex)
}
return
}

View File

@@ -29,6 +29,7 @@ const (
type Ingress interface {
IngressClass() *string
Namespace() string
Hostnames() []string
}
type NetworkingV1 struct {
@@ -36,6 +37,7 @@ type NetworkingV1 struct {
}
func (n NetworkingV1) IngressClass() (res *string) {
res = n.Spec.IngressClassName
if res == nil {
if a := n.GetAnnotations(); a != nil {
@@ -51,11 +53,21 @@ func (n NetworkingV1) Namespace() string {
return n.GetNamespace()
}
func (n NetworkingV1) Hostnames() []string {
rules := n.Spec.Rules
var hostnames []string
for _, el := range rules {
hostnames = append(hostnames, el.Host)
}
return hostnames
}
type NetworkingV1Beta1 struct {
*networkingv1beta.Ingress
}
func (n NetworkingV1Beta1) IngressClass() (res *string) {
res = n.Spec.IngressClassName
if res == nil {
if a := n.GetAnnotations(); a != nil {
@@ -71,6 +83,15 @@ func (n NetworkingV1Beta1) Namespace() string {
return n.GetNamespace()
}
func (n NetworkingV1Beta1) Hostnames() []string {
rules := n.Spec.Rules
var hostnames []string
for _, rule := range rules {
hostnames = append(hostnames, rule.Host)
}
return hostnames
}
type Extension struct {
*extensionsv1beta1.Ingress
}
@@ -90,3 +111,12 @@ func (e Extension) IngressClass() (res *string) {
func (e Extension) Namespace() string {
return e.GetNamespace()
}
func (e Extension) Hostnames() []string {
rules := e.Spec.Rules
var hostnames []string
for _, el := range rules {
hostnames = append(hostnames, el.Host)
}
return hostnames
}

View File

@@ -22,6 +22,7 @@ import (
"net/http"
"regexp"
"github.com/go-logr/logr"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
networkingv1 "k8s.io/api/networking/v1"
networkingv1beta1 "k8s.io/api/networking/v1beta1"
@@ -56,7 +57,9 @@ func (w *webhook) GetPath() string {
return "/validating-ingress"
}
type handler struct{}
type handler struct {
Log logr.Logger
}
func Handler() capsulewebhook.Handler {
return &handler{}
@@ -64,23 +67,23 @@ func Handler() capsulewebhook.Handler {
func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
return func(ctx context.Context, req admission.Request) admission.Response {
i, err := r.ingressFromRequest(req, decoder)
ingress, err := r.ingressFromRequest(req, decoder)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
return r.validateIngress(ctx, client, i)
return r.validateIngress(ctx, client, ingress)
}
}
func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
return func(ctx context.Context, req admission.Request) admission.Response {
i, err := r.ingressFromRequest(req, decoder)
ingress, err := r.ingressFromRequest(req, decoder)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
return r.validateIngress(ctx, client, i)
return r.validateIngress(ctx, client, ingress)
}
}
@@ -98,32 +101,32 @@ func (r *handler) ingressFromRequest(req admission.Request, decoder *admission.D
if err := decoder.Decode(req, n); err != nil {
return nil, err
}
ingress = NetworkingV1{n}
ingress = NetworkingV1{Ingress: n}
break
}
n := &networkingv1beta1.Ingress{}
if err := decoder.Decode(req, n); err != nil {
return nil, err
}
ingress = NetworkingV1Beta1{n}
ingress = NetworkingV1Beta1{Ingress: n}
case "extensions":
e := &extensionsv1beta1.Ingress{}
if err := decoder.Decode(req, e); err != nil {
return nil, err
}
ingress = Extension{e}
ingress = Extension{Ingress: e}
default:
err = fmt.Errorf("cannot recognize type %s", req.Kind.Group)
}
return
}
func (r *handler) validateIngress(ctx context.Context, c client.Client, object Ingress) admission.Response {
func (r *handler) validateIngress(ctx context.Context, c client.Client, ingress Ingress) admission.Response {
var valid, matched bool
tl := &v1alpha1.TenantList{}
if err := c.List(ctx, tl, client.MatchingFieldsSelector{
Selector: fields.OneTermEqualSelector(".status.namespaces", object.Namespace()),
Selector: fields.OneTermEqualSelector(".status.namespaces", ingress.Namespace()),
}); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
@@ -138,7 +141,7 @@ func (r *handler) validateIngress(ctx context.Context, c client.Client, object I
return admission.Allowed("")
}
ingressClass := object.IngressClass()
ingressClass := ingress.IngressClass()
if ingressClass == nil {
return admission.Errored(http.StatusBadRequest, NewIngressClassNotValid(*tnt.Spec.IngressClasses))
}
@@ -155,6 +158,42 @@ func (r *handler) validateIngress(ctx context.Context, c client.Client, object I
return admission.Errored(http.StatusBadRequest, NewIngressClassForbidden(*ingressClass, *tnt.Spec.IngressClasses))
}
if tnt.Spec.IngressHostnames == nil {
return admission.Allowed("")
}
var invalidHostnames []string
hostnames := ingress.Hostnames()
if len(hostnames) > 0 {
for _, currentHostname := range hostnames {
isPresent := tnt.Spec.IngressHostnames.Allowed.IsStringInList(currentHostname)
if !isPresent {
invalidHostnames = append(invalidHostnames, currentHostname)
}
}
if len(invalidHostnames) == 0 {
valid = true
}
}
var notMatchingHostnames []string
allowedRegex := tnt.Spec.IngressHostnames.AllowedRegex
if len(allowedRegex) > 0 {
for _, currentHostname := range hostnames {
matched, _ := regexp.MatchString(tnt.Spec.IngressHostnames.AllowedRegex, currentHostname)
if !matched {
notMatchingHostnames = append(notMatchingHostnames, currentHostname)
}
}
if len(notMatchingHostnames) == 0 {
matched = true
}
}
if !valid && !matched {
return admission.Errored(http.StatusBadRequest, NewIngressHostnamesNotValid(invalidHostnames, notMatchingHostnames, *tnt.Spec.IngressHostnames))
}
return admission.Allowed("")
}

View File

@@ -18,9 +18,11 @@ package tenant
import (
"context"
"fmt"
"net/http"
"regexp"
"k8s.io/apimachinery/pkg/fields"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
@@ -57,7 +59,7 @@ func Handler() capsulewebhook.Handler {
return &handler{}
}
func (h *handler) OnCreate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsulewebhook.Func {
return func(ctx context.Context, req admission.Request) admission.Response {
tnt := &v1alpha1.Tenant{}
if err := decoder.Decode(req, tnt); err != nil {
@@ -90,6 +92,32 @@ func (h *handler) OnCreate(client client.Client, decoder *admission.Decoder) cap
}
}
// Validate ingressHostnames regexp
if tnt.Spec.IngressHostnames != nil && len(tnt.Spec.IngressHostnames.AllowedRegex) > 0 {
if _, err := regexp.Compile(tnt.Spec.IngressHostnames.AllowedRegex); err != nil {
return admission.Denied("Unable to compile ingressHostnames allowedRegex")
}
}
if tnt.Spec.IngressHostnames != nil && len(tnt.Spec.IngressHostnames.Allowed) > 0 {
for _, h := range tnt.Spec.IngressHostnames.Allowed {
tl := &v1alpha1.TenantList{}
err := clt.List(ctx, tl, client.MatchingFieldsSelector{
Selector: fields.OneTermEqualSelector(".spec.ingressHostnames", h),
})
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
if len(tl.Items) > 0 {
return admission.Denied(fmt.Sprintf("The allowed hostname %s is already used by the Tenant %s, cannot proceed", h, tl.Items[0].GetName()))
}
}
if _, err := regexp.Compile(tnt.Spec.IngressHostnames.AllowedRegex); err != nil {
return admission.Denied("Unable to compile ingressHostnames allowedRegex")
}
}
return admission.Allowed("")
}
}