diff --git a/go.mod b/go.mod index 33d1a0fc..f68b6eeb 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/main.go b/main.go index 4043b3bd..2edd631c 100644 --- a/main.go +++ b/main.go @@ -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()), diff --git a/pkg/indexer/add_ingress.go b/pkg/indexer/add_ingress.go new file mode 100644 index 00000000..c18c3f81 --- /dev/null +++ b/pkg/indexer/add_ingress.go @@ -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{}}) +} diff --git a/pkg/indexer/ingress/hostname.go b/pkg/indexer/ingress/hostname.go new file mode 100644 index 00000000..db6e9db5 --- /dev/null +++ b/pkg/indexer/ingress/hostname.go @@ -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 + } + } +} diff --git a/pkg/webhook/ingress/errors.go b/pkg/webhook/ingress/errors.go index dab88d28..1bbd0ef7 100644 --- a/pkg/webhook/ingress/errors.go +++ b/pkg/webhook/ingress/errors.go @@ -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} } diff --git a/pkg/webhook/ingress/types.go b/pkg/webhook/ingress/types.go index 2fde5811..14fafa43 100644 --- a/pkg/webhook/ingress/types.go +++ b/pkg/webhook/ingress/types.go @@ -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 { diff --git a/pkg/webhook/ingress/validating.go b/pkg/webhook/ingress/validating.go index c70ec99d..41b2cc1d 100644 --- a/pkg/webhook/ingress/validating.go +++ b/pkg/webhook/ingress/validating.go @@ -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 +}