mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-20 04:56:44 +00:00
* chore * perfromance improvements Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * perfromance improvements Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(performance): removed duplicate client calls from all admission paths Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(performance): removed duplicate client calls from all admission paths Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(performance): removed duplicate client calls from all admission paths Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(performance): removed duplicate client calls from all admission paths Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(performance): removed duplicate client calls from all admission paths Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(performance): removed duplicate client calls from all admission paths Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(performance): removed duplicate client calls from all admission paths Signed-off-by: Oliver Baehler <oliver@sudo-i.net> --------- Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
322 lines
8.3 KiB
Go
322 lines
8.3 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package validation
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"reflect"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
|
|
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
|
webhookutils "github.com/projectcapsule/capsule/internal/webhook/utils"
|
|
ad "github.com/projectcapsule/capsule/pkg/runtime/admission"
|
|
"github.com/projectcapsule/capsule/pkg/runtime/configuration"
|
|
"github.com/projectcapsule/capsule/pkg/runtime/events"
|
|
"github.com/projectcapsule/capsule/pkg/runtime/handlers"
|
|
"github.com/projectcapsule/capsule/pkg/tenant"
|
|
"github.com/projectcapsule/capsule/pkg/users"
|
|
)
|
|
|
|
func NamespaceHandler(configuration configuration.Configuration, hndlers ...handlers.TypedHandlerWithTenantUser[*corev1.Namespace]) handlers.Handler {
|
|
return &handler{
|
|
cfg: configuration,
|
|
handlers: hndlers,
|
|
}
|
|
}
|
|
|
|
type handler struct {
|
|
cfg configuration.Configuration
|
|
handlers []handlers.TypedHandlerWithTenantUser[*corev1.Namespace]
|
|
}
|
|
|
|
func (h *handler) OnCreate(
|
|
c client.Client,
|
|
reader client.Reader,
|
|
decoder admission.Decoder,
|
|
recorder events.EventRecorder,
|
|
) handlers.Func {
|
|
return func(ctx context.Context, req admission.Request) *admission.Response {
|
|
reader = webhookutils.NewTenantCachingReader(reader)
|
|
|
|
user := handlers.ResolveAdmissionUser(ctx, c, req, h.cfg)
|
|
|
|
ns := &corev1.Namespace{}
|
|
if err := decoder.Decode(req, ns); err != nil {
|
|
return ad.ErroredResponse(err)
|
|
}
|
|
|
|
if !user.IsAdmin() && !user.IsCapsule() && !tenant.HasTenantReference(ns) {
|
|
return nil
|
|
}
|
|
|
|
tnt, err := tenant.ResolveNamespaceTenant(ctx, reader, ns)
|
|
if err != nil {
|
|
return ad.ErroredResponse(err)
|
|
}
|
|
|
|
if !user.IsAdmin() && !user.IsCapsule() && tnt != nil {
|
|
return ad.Deny("only tenant owners can create tenant-owned namespaces")
|
|
}
|
|
|
|
if tnt == nil {
|
|
return nil
|
|
}
|
|
|
|
if terminating := h.rejectOnTermination(ns, tnt); terminating != nil {
|
|
return terminating
|
|
}
|
|
|
|
for _, hndl := range h.handlers {
|
|
if response := hndl.OnCreate(c, reader, user, ns, decoder, recorder, tnt)(ctx, req); response != nil {
|
|
return response
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (h *handler) OnDelete(
|
|
c client.Client,
|
|
reader client.Reader,
|
|
decoder admission.Decoder,
|
|
recorder events.EventRecorder,
|
|
) handlers.Func {
|
|
return func(ctx context.Context, req admission.Request) *admission.Response {
|
|
oldNs := &corev1.Namespace{}
|
|
if err := decoder.DecodeRaw(req.OldObject, oldNs); err != nil {
|
|
return ad.ErroredResponse(err)
|
|
}
|
|
|
|
// A namespace which is already terminating must be allowed to complete
|
|
// deletion even if its Tenant metadata became inconsistent meanwhile.
|
|
// Kubernetes authorization has already guarded the original delete.
|
|
if oldNs.DeletionTimestamp != nil || oldNs.Status.Phase == corev1.NamespaceTerminating {
|
|
return nil
|
|
}
|
|
|
|
reader = webhookutils.NewTenantCachingReader(reader)
|
|
|
|
tnt, err := tenant.ResolveNamespaceTenant(ctx, reader, oldNs)
|
|
if apierrors.IsNotFound(err) {
|
|
// Kubernetes authorization already controls namespace deletion.
|
|
// A stale Tenant reference must not make a namespace undeletable.
|
|
return nil
|
|
}
|
|
|
|
user := handlers.ResolveAdmissionUser(ctx, c, req, h.cfg)
|
|
|
|
if err != nil && !user.IsAdmin() {
|
|
return ad.ErroredResponse(err)
|
|
}
|
|
|
|
if tnt == nil {
|
|
return nil
|
|
}
|
|
|
|
for _, hndl := range h.handlers {
|
|
if response := hndl.OnDelete(c, reader, user, oldNs, decoder, recorder, tnt)(ctx, req); response != nil {
|
|
return response
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (h *handler) OnUpdate(
|
|
c client.Client,
|
|
reader client.Reader,
|
|
decoder admission.Decoder,
|
|
recorder events.EventRecorder,
|
|
) handlers.Func {
|
|
return func(ctx context.Context, req admission.Request) *admission.Response {
|
|
ns := &corev1.Namespace{}
|
|
if err := decoder.Decode(req, ns); err != nil {
|
|
return ad.ErroredResponse(err)
|
|
}
|
|
|
|
oldNs := &corev1.Namespace{}
|
|
if err := decoder.DecodeRaw(req.OldObject, oldNs); err != nil {
|
|
return ad.ErroredResponse(err)
|
|
}
|
|
|
|
if response, stop := validateTerminatingNamespaceUpdate(req, oldNs, ns); stop {
|
|
return response
|
|
}
|
|
|
|
reader = webhookutils.NewTenantCachingReader(reader)
|
|
|
|
user := handlers.ResolveAdmissionUser(ctx, c, req, h.cfg)
|
|
|
|
if response, stop := validateNamespaceTenantReferenceTransition(user, oldNs, ns); stop {
|
|
return response
|
|
}
|
|
|
|
oldTenant, err := tenant.ResolveNamespaceTenant(ctx, reader, oldNs)
|
|
if err != nil && !user.IsAdmin() {
|
|
return ad.ErroredResponse(err)
|
|
}
|
|
|
|
newTenant, err := tenant.ResolveNamespaceTenant(ctx, reader, ns)
|
|
if err != nil {
|
|
return ad.ErroredResponse(err)
|
|
}
|
|
|
|
if !user.IsAdmin() {
|
|
if oldTenant == nil || newTenant == nil {
|
|
return ad.Deny("namespace tenant ownership is incomplete")
|
|
}
|
|
|
|
if oldTenant.GetName() != newTenant.GetName() || oldTenant.GetUID() != newTenant.GetUID() {
|
|
return ad.Deny("namespace can not be migrated between tenants")
|
|
}
|
|
|
|
if user.IsCapsule() && !tenant.NamespaceIsOwned(ctx, c, h.cfg, oldNs, oldTenant, user) {
|
|
recorder.LabeledEvent(
|
|
ns,
|
|
corev1.EventTypeWarning,
|
|
events.ReasonNamespaceHijack,
|
|
events.ActionValidationDenied,
|
|
"namespace can not be patched",
|
|
).
|
|
WithRequestAnnotations(req).
|
|
Emit(ctx)
|
|
|
|
return ad.Deny("denied patch request for this namespace")
|
|
}
|
|
}
|
|
|
|
adminTenantTransition := user.IsAdmin() && namespaceTenantChanged(oldTenant, newTenant)
|
|
|
|
tnt := newTenant
|
|
if !user.IsAdmin() {
|
|
tnt = oldTenant
|
|
}
|
|
|
|
if tnt == nil {
|
|
return nil
|
|
}
|
|
|
|
if terminating := h.rejectOnTermination(ns, newTenant); terminating != nil {
|
|
return terminating
|
|
}
|
|
|
|
if adminTenantTransition {
|
|
return nil
|
|
}
|
|
|
|
for _, hndl := range h.handlers {
|
|
if response := hndl.OnUpdate(c, reader, user, ns, oldNs, decoder, recorder, tnt)(ctx, req); response != nil {
|
|
return response
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func namespaceTenantChanged(oldTenant, newTenant *capsulev1beta2.Tenant) bool {
|
|
if oldTenant == nil || newTenant == nil {
|
|
return oldTenant != newTenant
|
|
}
|
|
|
|
return oldTenant.GetName() != newTenant.GetName() || oldTenant.GetUID() != newTenant.GetUID()
|
|
}
|
|
|
|
func isTerminatingNamespaceUpdate(
|
|
req admission.Request,
|
|
oldNs, newNs *corev1.Namespace,
|
|
) bool {
|
|
return req.SubResource == "finalize" ||
|
|
newNs.DeletionTimestamp != nil ||
|
|
oldNs.DeletionTimestamp != nil ||
|
|
newNs.Status.Phase == corev1.NamespaceTerminating ||
|
|
oldNs.Status.Phase == corev1.NamespaceTerminating
|
|
}
|
|
|
|
func validateTerminatingNamespaceUpdate(
|
|
req admission.Request,
|
|
oldNs, newNs *corev1.Namespace,
|
|
) (*admission.Response, bool) {
|
|
if !isTerminatingNamespaceUpdate(req, oldNs, newNs) {
|
|
return nil, false
|
|
}
|
|
|
|
if namespaceTenantAssignmentChanged(oldNs, newNs) {
|
|
return ad.Deny("namespace tenant ownership can not change during termination"), true
|
|
}
|
|
|
|
return nil, true
|
|
}
|
|
|
|
func validateNamespaceTenantReferenceTransition(
|
|
user users.AdmissionUser,
|
|
oldNs, newNs *corev1.Namespace,
|
|
) (*admission.Response, bool) {
|
|
if user.IsAdmin() {
|
|
if !tenant.HasConsistentTenantReference(newNs) {
|
|
return ad.Deny("tenant label and ownerReference must both be set consistently or both be absent"), true
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
oldHasTenantReference := tenant.HasTenantReference(oldNs)
|
|
newHasTenantReference := tenant.HasTenantReference(newNs)
|
|
|
|
switch {
|
|
case !oldHasTenantReference && newHasTenantReference:
|
|
return ad.Deny("namespace can not be patched into a tenant"), true
|
|
case oldHasTenantReference && !newHasTenantReference:
|
|
return ad.Deny("namespace can not remove tenant ownership"), true
|
|
case !oldHasTenantReference && !newHasTenantReference:
|
|
return nil, true
|
|
default:
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
func namespaceTenantAssignmentChanged(oldNs, newNs *corev1.Namespace) bool {
|
|
if tenant.TenanLabelValue(oldNs) != tenant.TenanLabelValue(newNs) {
|
|
return true
|
|
}
|
|
|
|
return !reflect.DeepEqual(
|
|
tenant.TenantOwnerReferences(oldNs),
|
|
tenant.TenantOwnerReferences(newNs),
|
|
)
|
|
}
|
|
|
|
func (h *handler) rejectOnTermination(
|
|
ns *corev1.Namespace,
|
|
t *capsulev1beta2.Tenant,
|
|
) *admission.Response {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
|
|
if t.DeletionTimestamp == nil {
|
|
return nil
|
|
}
|
|
|
|
instance := t.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{
|
|
Name: ns.GetName(),
|
|
UID: ns.GetUID(),
|
|
})
|
|
|
|
if instance != nil {
|
|
return nil
|
|
}
|
|
|
|
err := fmt.Errorf("tenant is terminating and does not accept new namespaces")
|
|
|
|
return ad.ErroredResponse(err)
|
|
}
|