// Copyright 2020-2026 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 package ingress import ( "context" "fmt" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" networkingv1 "k8s.io/api/networking/v1" networkingv1beta1 "k8s.io/api/networking/v1beta1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/tools/events" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" "github.com/projectcapsule/capsule/internal/webhook/utils" "github.com/projectcapsule/capsule/pkg/api" caperrors "github.com/projectcapsule/capsule/pkg/api/errors" "github.com/projectcapsule/capsule/pkg/runtime/configuration" evt "github.com/projectcapsule/capsule/pkg/runtime/events" "github.com/projectcapsule/capsule/pkg/runtime/handlers" "github.com/projectcapsule/capsule/pkg/runtime/indexers/ingress" ) type collision struct { configuration configuration.Configuration } func Collision(configuration configuration.Configuration) handlers.Handler { return &collision{configuration: configuration} } func (r *collision) OnCreate(client client.Client, decoder admission.Decoder, recorder events.EventRecorder) handlers.Func { return func(ctx context.Context, req admission.Request) *admission.Response { return r.validate(ctx, client, req, decoder, recorder) } } func (r *collision) OnUpdate(client client.Client, decoder admission.Decoder, recorder events.EventRecorder) handlers.Func { return func(ctx context.Context, req admission.Request) *admission.Response { return r.validate(ctx, client, req, decoder, recorder) } } func (r *collision) OnDelete(client.Client, admission.Decoder, events.EventRecorder) handlers.Func { return func(context.Context, admission.Request) *admission.Response { return nil } } func (r *collision) validate(ctx context.Context, client client.Client, req admission.Request, decoder admission.Decoder, recorder events.EventRecorder) *admission.Response { ing, err := FromRequest(req, decoder) if err != nil { return utils.ErroredResponse(err) } var tenant *capsulev1beta2.Tenant tenant, err = TenantFromIngress(ctx, client, ing) if err != nil { return utils.ErroredResponse(err) } if tenant == nil || tenant.Spec.IngressOptions.HostnameCollisionScope == api.HostnameCollisionScopeDisabled { return nil } if err = r.validateCollision(ctx, client, ing, tenant.Spec.IngressOptions.HostnameCollisionScope); err == nil { return nil } var collisionErr *caperrors.IngressHostnameCollisionError if errors.As(err, &collisionErr) { recorder.Eventf(tenant, nil, corev1.EventTypeWarning, evt.ReasonIngressHostnameCollision, evt.ActionValidationDenied, "Ingress %s/%s hostname is colliding", ing.Namespace(), ing.Name()) } response := admission.Denied(err.Error()) return &response } //nolint:gocognit,gocyclo,cyclop func (r *collision) validateCollision(ctx context.Context, clt client.Client, ing Ingress, scope api.HostnameCollisionScope) error { for hostname, paths := range ing.HostnamePathsPairs() { for path := range paths { var ingressObjList client.ObjectList switch ing.(type) { case Extension: ingressObjList = &extensionsv1beta1.IngressList{} case NetworkingV1: ingressObjList = &networkingv1.IngressList{} case NetworkingV1Beta1: ingressObjList = &networkingv1beta1.IngressList{} } namespaces := sets.NewString() //nolint:exhaustive switch scope { case api.HostnameCollisionScopeCluster: tenantList := &capsulev1beta2.TenantList{} if err := clt.List(ctx, tenantList); err != nil { return err } for _, tenant := range tenantList.Items { namespaces.Insert(tenant.Status.Namespaces...) } case api.HostnameCollisionScopeTenant: tenantList := &capsulev1beta2.TenantList{} if err := clt.List(ctx, tenantList, client.MatchingFields{".status.namespaces": ing.Namespace()}); err != nil { return err } for _, tenant := range tenantList.Items { namespaces.Insert(tenant.Status.Namespaces...) } case api.HostnameCollisionScopeNamespace: namespaces.Insert(ing.Namespace()) } if err := clt.List(ctx, ingressObjList, client.MatchingFields{ingress.HostPathPair: fmt.Sprintf("%s;%s", hostname, path)}); err != nil { return err } ingressList := sets.NewInt() switch list := ingressObjList.(type) { case *extensionsv1beta1.IngressList: for index, item := range list.Items { if namespaces.Has(item.GetNamespace()) { ingressList.Insert(index) } } switch len(ingressList) { case 0: break case 1: if index := ingressList.List()[0]; list.Items[index].GetName() == ing.Name() && list.Items[index].GetNamespace() == ing.Namespace() { break } fallthrough default: return caperrors.NewIngressHostnameCollision(hostname) } case *networkingv1.IngressList: for index, item := range list.Items { if namespaces.Has(item.GetNamespace()) { ingressList.Insert(index) } } switch len(ingressList) { case 0: break case 1: if index := ingressList.List()[0]; list.Items[index].GetName() == ing.Name() && list.Items[index].GetNamespace() == ing.Namespace() { break } fallthrough default: return caperrors.NewIngressHostnameCollision(hostname) } case *networkingv1beta1.IngressList: for index, item := range list.Items { if namespaces.Has(item.GetNamespace()) { ingressList.Insert(index) } } switch len(ingressList) { case 0: break case 1: if index := ingressList.List()[0]; list.Items[index].GetName() == ing.Name() && list.Items[index].GetNamespace() == ing.Namespace() { break } fallthrough default: return caperrors.NewIngressHostnameCollision(hostname) } } } } return nil }