mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-02-14 18:09:58 +00:00
Implementing allowed Ingress hostnames (#162)
Co-authored-by: Dario Tranchitella <dario@tranchitella.eu>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,5 +23,6 @@ bin
|
||||
*.swo
|
||||
*~
|
||||
|
||||
hack/*.kubeconfig
|
||||
**/*.kubeconfig
|
||||
.DS_Store
|
||||
|
||||
|
||||
42
api/v1alpha1/ingress_hostnames_list.go
Normal file
42
api/v1alpha1/ingress_hostnames_list.go
Normal 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
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
0
config/samples/ingress.yaml
Normal file
0
config/samples/ingress.yaml
Normal 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.
|
||||
|
||||
# What’s next
|
||||
|
||||
223
e2e/ingress_hostnames_test.go
Normal file
223
e2e/ingress_hostnames_test.go
Normal 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())
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
76
e2e/tenant_ingress_hostnames_collision_test.go
Normal file
76
e2e/tenant_ingress_hostnames_collision_test.go
Normal 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())
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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{})
|
||||
}
|
||||
|
||||
44
pkg/indexer/tenant/hostnames.go
Normal file
44
pkg/indexer/tenant/hostnames.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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("")
|
||||
|
||||
}
|
||||
|
||||
@@ -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("")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user