mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 12:36:39 +00:00
feat: Ingress hostnames collision check
Disabled by default to avoid breaking changes for upcoming release, although minor will be enabled by default. Using the new `--allow-ingress-hostname-collision` flag Capsule can ignore the Ingress hostnames collision allowing the Cluster Administrator to put in place a non-opinionated hostnames allocation.
This commit is contained in:
@@ -7,6 +7,7 @@ require (
|
||||
github.com/hashicorp/go-multierror v1.1.0
|
||||
github.com/onsi/ginkgo v1.14.1
|
||||
github.com/onsi/gomega v1.10.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/stretchr/testify v1.5.1
|
||||
go.uber.org/zap v1.15.0
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
|
||||
|
||||
@@ -80,6 +80,7 @@ func main() {
|
||||
var capsuleGroup string
|
||||
var protectedNamespaceRegexpString string
|
||||
var protectedNamespaceRegexp *regexp.Regexp
|
||||
var allowIngressHostnamesCollision bool
|
||||
var namespace string
|
||||
|
||||
flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
|
||||
@@ -92,6 +93,7 @@ func main() {
|
||||
"during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash. "+
|
||||
"This is useful to avoid Namespace name collision in a public CaaS environment.")
|
||||
flag.StringVar(&protectedNamespaceRegexpString, "protected-namespace-regex", "", "Disallow creation of namespaces, whose name matches this regexp")
|
||||
flag.BoolVar(&allowIngressHostnamesCollision, "allow-ingress-hostname-collision", true, "Allow the Ingress hostname collision at Ingress resource level across all the Tenants.")
|
||||
|
||||
opts := zap.Options{
|
||||
EncoderConfigOptions: append([]zap.EncoderConfigOption{}, func(config *zapcore.EncoderConfig) {
|
||||
@@ -157,7 +159,7 @@ func main() {
|
||||
// webhooks: the order matters, don't change it and just append
|
||||
wl := append(
|
||||
make([]webhook.Webhook, 0),
|
||||
ingress.Webhook(ingress.Handler()),
|
||||
ingress.Webhook(ingress.Handler(allowIngressHostnamesCollision)),
|
||||
pvc.Webhook(pvc.Handler()),
|
||||
registry.Webhook(registry.Handler()),
|
||||
services.Webhook(services.Handler()),
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
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 indexer
|
||||
|
||||
import (
|
||||
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
networkingv1beta1 "k8s.io/api/networking/v1beta1"
|
||||
|
||||
"github.com/clastix/capsule/pkg/indexer/ingress"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddToIndexerFuncs = append(AddToIndexerFuncs, ingress.Hostname{Obj: &extensionsv1beta1.Ingress{}})
|
||||
AddToIndexerFuncs = append(AddToIndexerFuncs, ingress.Hostname{Obj: &networkingv1.Ingress{}})
|
||||
AddToIndexerFuncs = append(AddToIndexerFuncs, ingress.Hostname{Obj: &networkingv1beta1.Ingress{}})
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
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 ingress
|
||||
|
||||
import (
|
||||
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
networkingv1beta1 "k8s.io/api/networking/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type Hostname struct {
|
||||
Obj metav1.Object
|
||||
}
|
||||
|
||||
func (h Hostname) Object() client.Object {
|
||||
return h.Obj.(client.Object)
|
||||
}
|
||||
|
||||
func (h Hostname) Field() string {
|
||||
return ".spec.rules[*].host"
|
||||
}
|
||||
|
||||
func (h Hostname) Func() client.IndexerFunc {
|
||||
return func(object client.Object) (hostnames []string) {
|
||||
switch h.Obj.(type) {
|
||||
case *networkingv1.Ingress:
|
||||
ing := object.(*networkingv1.Ingress)
|
||||
for _, r := range ing.Spec.Rules {
|
||||
hostnames = append(hostnames, r.Host)
|
||||
}
|
||||
return
|
||||
case *networkingv1beta1.Ingress:
|
||||
ing := object.(*networkingv1beta1.Ingress)
|
||||
for _, r := range ing.Spec.Rules {
|
||||
hostnames = append(hostnames, r.Host)
|
||||
}
|
||||
return
|
||||
case *extensionsv1beta1.Ingress:
|
||||
ing := object.(*extensionsv1beta1.Ingress)
|
||||
for _, r := range ing.Spec.Rules {
|
||||
hostnames = append(hostnames, r.Host)
|
||||
}
|
||||
return
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,18 @@ type ingressHostnameNotValid struct {
|
||||
spec v1alpha1.AllowedListSpec
|
||||
}
|
||||
|
||||
type ingressHostnameCollision struct {
|
||||
hostname string
|
||||
}
|
||||
|
||||
func (i ingressHostnameCollision) Error() string {
|
||||
return fmt.Sprintf("hostname %s is already used across the cluster: please, reach out to the system administrators", i.hostname)
|
||||
}
|
||||
|
||||
func NewIngressHostnameCollision(hostname string) error {
|
||||
return ingressHostnameCollision{hostname: hostname}
|
||||
}
|
||||
|
||||
func NewIngressHostnamesNotValid(invalidHostnames []string, notMatchingHostnames []string, spec v1alpha1.AllowedListSpec) error {
|
||||
return &ingressHostnameNotValid{invalidHostnames: invalidHostnames, notMatchingHostnames: notMatchingHostnames, spec: spec}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ const (
|
||||
type Ingress interface {
|
||||
IngressClass() *string
|
||||
Namespace() string
|
||||
Name() string
|
||||
Hostnames() []string
|
||||
}
|
||||
|
||||
@@ -36,6 +37,10 @@ type NetworkingV1 struct {
|
||||
*networkingv1.Ingress
|
||||
}
|
||||
|
||||
func (n NetworkingV1) Name() string {
|
||||
return n.GetName()
|
||||
}
|
||||
|
||||
func (n NetworkingV1) IngressClass() (res *string) {
|
||||
res = n.Spec.IngressClassName
|
||||
if res == nil {
|
||||
@@ -65,6 +70,10 @@ type NetworkingV1Beta1 struct {
|
||||
*networkingv1beta.Ingress
|
||||
}
|
||||
|
||||
func (n NetworkingV1Beta1) Name() string {
|
||||
return n.GetName()
|
||||
}
|
||||
|
||||
func (n NetworkingV1Beta1) IngressClass() (res *string) {
|
||||
res = n.Spec.IngressClassName
|
||||
if res == nil {
|
||||
@@ -94,6 +103,10 @@ type Extension struct {
|
||||
*extensionsv1beta1.Ingress
|
||||
}
|
||||
|
||||
func (e Extension) Name() string {
|
||||
return e.GetName()
|
||||
}
|
||||
|
||||
func (e Extension) IngressClass() (res *string) {
|
||||
res = e.Spec.IngressClassName
|
||||
if res == nil {
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/pkg/errors"
|
||||
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
networkingv1beta1 "k8s.io/api/networking/v1beta1"
|
||||
@@ -58,11 +58,11 @@ func (w *webhook) GetPath() string {
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
Log logr.Logger
|
||||
allowHostnamesCollision bool
|
||||
}
|
||||
|
||||
func Handler() capsulewebhook.Handler {
|
||||
return &handler{}
|
||||
func Handler(allowIngressHostnamesCollision bool) capsulewebhook.Handler {
|
||||
return &handler{allowHostnamesCollision: allowIngressHostnamesCollision}
|
||||
}
|
||||
|
||||
func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
|
||||
@@ -206,5 +206,74 @@ func (r *handler) validateIngress(ctx context.Context, c client.Client, ingress
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
if err := r.validateCollision(ctx, c, ingress); err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
return admission.Allowed("")
|
||||
}
|
||||
|
||||
func (r *handler) validateCollision(ctx context.Context, clt client.Client, ingress Ingress) error {
|
||||
if r.allowHostnamesCollision {
|
||||
return nil
|
||||
}
|
||||
for _, hostname := range ingress.Hostnames() {
|
||||
collisionErr := NewIngressHostnameCollision(hostname)
|
||||
var err error
|
||||
// Listing for networking.k8s.io/v1 Ingress resources
|
||||
nl := &networkingv1.IngressList{}
|
||||
err = clt.List(ctx, nl, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".spec.rules[*].host", hostname),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot list *networkingv1.IngressList by MatchingFieldsSelector")
|
||||
}
|
||||
switch len(nl.Items) {
|
||||
case 0:
|
||||
continue
|
||||
case 1:
|
||||
if nl.Items[0].GetName() != ingress.Name() {
|
||||
return collisionErr
|
||||
}
|
||||
default:
|
||||
return collisionErr
|
||||
}
|
||||
// Listing for networking.k8s.io/v1beta1 Ingress resources
|
||||
nlb := &networkingv1beta1.IngressList{}
|
||||
err = clt.List(ctx, nlb, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".spec.rules[*].host", hostname),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot list *networkingv1beta1.IngressList by MatchingFieldsSelector")
|
||||
}
|
||||
switch len(nlb.Items) {
|
||||
case 0:
|
||||
continue
|
||||
case 1:
|
||||
if nlb.Items[0].GetName() != ingress.Name() {
|
||||
return collisionErr
|
||||
}
|
||||
default:
|
||||
return collisionErr
|
||||
}
|
||||
// Listing for extensions.k8s.io Ingress resources
|
||||
el := &extensionsv1beta1.IngressList{}
|
||||
err = clt.List(ctx, nl, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".spec.rules[*].host", hostname),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch len(el.Items) {
|
||||
case 0:
|
||||
continue
|
||||
case 1:
|
||||
if el.Items[0].GetName() != ingress.Name() {
|
||||
return collisionErr
|
||||
}
|
||||
default:
|
||||
return collisionErr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user