mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 04:26:45 +00:00
* feat: implement performance optimizations Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add administrator capabilities Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add administrator capabilities Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * chore * feat: add administrator capabilities Signed-off-by: Oliver Baehler <oliver@sudo-i.net> --------- Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
473 lines
12 KiB
Go
473 lines
12 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package tenant
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"time"
|
|
|
|
"github.com/go-logr/logr"
|
|
"golang.org/x/sync/errgroup"
|
|
corev1 "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/client-go/util/retry"
|
|
"k8s.io/utils/ptr"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
|
|
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
|
"github.com/projectcapsule/capsule/pkg/api/meta"
|
|
"github.com/projectcapsule/capsule/pkg/tenant"
|
|
)
|
|
|
|
const namespaceCascadingCleanupGracePeriod = 10 * time.Second
|
|
|
|
// Ensuring all annotations are applied to each Namespace handled by the Tenant.
|
|
func (r *Manager) reconcileNamespaces(
|
|
ctx context.Context,
|
|
log logr.Logger,
|
|
tnt *capsulev1beta2.Tenant,
|
|
) error {
|
|
if tnt.DeletionTimestamp != nil {
|
|
return r.reconcileDeletingTenantNamespaces(ctx, log, tnt)
|
|
}
|
|
|
|
reconcileErr := r.reconcileActiveTenantNamespaces(ctx, log, tnt)
|
|
garbageCollectionErr := r.runGarbageCollection(ctx, tnt, &capsulev1beta2.RuleStatus{})
|
|
|
|
return errors.Join(reconcileErr, garbageCollectionErr)
|
|
}
|
|
|
|
func (r *Manager) reconcileDeletingTenantNamespaces(
|
|
ctx context.Context,
|
|
log logr.Logger,
|
|
tnt *capsulev1beta2.Tenant,
|
|
) error {
|
|
templateTenant := tnt.DeepCopy()
|
|
|
|
group, ctx := errgroup.WithContext(ctx)
|
|
group.SetLimit(8)
|
|
|
|
results := make(chan *capsulev1beta2.TenantStatusNamespaceItem, len(tnt.Status.Spaces))
|
|
removed := make(chan string, len(tnt.Status.Spaces))
|
|
errs := make(chan error, len(tnt.Status.Spaces))
|
|
|
|
for i := range tnt.Status.Spaces {
|
|
statusNamespace := tnt.Status.Spaces[i]
|
|
|
|
group.Go(func() error {
|
|
namespace := &corev1.Namespace{}
|
|
|
|
// Deletion decisions must not depend on informer-cache freshness. A
|
|
// direct read observes NotFound and deletionTimestamp promptly, which
|
|
// lets the Tenant finalizer advance without waiting for cache delivery.
|
|
err := r.reader.Get(ctx, client.ObjectKey{Name: statusNamespace.Name}, namespace)
|
|
if apierrors.IsNotFound(err) {
|
|
removed <- statusNamespace.Name
|
|
|
|
return nil
|
|
}
|
|
|
|
if err != nil {
|
|
errs <- fmt.Errorf("get namespace %q: %w", statusNamespace.Name, err)
|
|
|
|
return nil
|
|
}
|
|
|
|
if namespace.DeletionTimestamp == nil {
|
|
if err := r.Delete(ctx, namespace, &client.DeleteOptions{
|
|
PropagationPolicy: ptr.To(metav1.DeletePropagationBackground),
|
|
}); err != nil && !apierrors.IsNotFound(err) {
|
|
log.Error(err, "unable to delete tenant namespace",
|
|
"tenant", tnt.GetName(),
|
|
"namespace", namespace.GetName(),
|
|
)
|
|
|
|
errs <- fmt.Errorf("delete namespace %q: %w", namespace.Name, err)
|
|
|
|
return nil
|
|
}
|
|
|
|
latest := &corev1.Namespace{}
|
|
|
|
err := r.reader.Get(ctx, client.ObjectKey{Name: namespace.Name}, latest)
|
|
if apierrors.IsNotFound(err) {
|
|
removed <- statusNamespace.Name
|
|
|
|
return nil
|
|
}
|
|
|
|
if err != nil {
|
|
errs <- fmt.Errorf("get namespace %q after delete: %w", namespace.Name, err)
|
|
|
|
return nil
|
|
}
|
|
|
|
namespace = latest
|
|
}
|
|
|
|
stat, err := r.reconcileNamespace(ctx, log, namespace, tnt, templateTenant)
|
|
if stat != nil {
|
|
results <- stat
|
|
}
|
|
|
|
if err != nil {
|
|
log.Error(err, "failed to reconcile deleting namespace",
|
|
"tenant", tnt.GetName(),
|
|
"namespace", namespace.GetName(),
|
|
)
|
|
|
|
errs <- fmt.Errorf("namespace %q: %w", namespace.Name, err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
_ = group.Wait()
|
|
|
|
close(results)
|
|
close(removed)
|
|
close(errs)
|
|
|
|
for name := range removed {
|
|
r.Metrics.DeleteAllMetricsForNamespace(name)
|
|
|
|
tnt.Status.RemoveInstance(&capsulev1beta2.TenantStatusNamespaceItem{
|
|
Name: name,
|
|
})
|
|
}
|
|
|
|
for stat := range results {
|
|
if stat == nil {
|
|
continue
|
|
}
|
|
|
|
tnt.Status.UpdateInstance(stat)
|
|
}
|
|
|
|
var joined []error
|
|
for itemErr := range errs {
|
|
joined = append(joined, itemErr)
|
|
}
|
|
|
|
tnt.Status.Size = uint(len(tnt.Status.Spaces))
|
|
|
|
return errors.Join(joined...)
|
|
}
|
|
|
|
func (r *Manager) reconcileActiveTenantNamespaces(
|
|
ctx context.Context,
|
|
log logr.Logger,
|
|
tnt *capsulev1beta2.Tenant,
|
|
) error {
|
|
list := &corev1.NamespaceList{}
|
|
if err := r.List(ctx, list, client.MatchingFields{".metadata.ownerReferences[*].capsule": tnt.GetName()}); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Rule rendering reads the complete Tenant, including status. Keep one
|
|
// immutable snapshot for all namespace workers so their status updates do
|
|
// not race with template conversion.
|
|
templateTenant := tnt.DeepCopy()
|
|
|
|
oldStatus := make(map[string]struct{}, len(tnt.Status.Spaces))
|
|
for i := range tnt.Status.Spaces {
|
|
oldStatus[tnt.Status.Spaces[i].Name] = struct{}{}
|
|
}
|
|
|
|
group, ctx := errgroup.WithContext(ctx)
|
|
group.SetLimit(8)
|
|
|
|
results := make(chan *capsulev1beta2.TenantStatusNamespaceItem, len(list.Items))
|
|
errs := make(chan error, len(list.Items))
|
|
|
|
for i := range list.Items {
|
|
ns := list.Items[i].DeepCopy()
|
|
|
|
group.Go(func() error {
|
|
stat, err := r.reconcileNamespace(ctx, log, ns, tnt, templateTenant)
|
|
if stat != nil {
|
|
results <- stat
|
|
}
|
|
|
|
if err != nil {
|
|
log.Error(err, "failed to reconcile namespace",
|
|
"tenant", tnt.GetName(),
|
|
"namespace", ns.GetName(),
|
|
)
|
|
|
|
errs <- fmt.Errorf("namespace %q: %w", ns.Name, err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
_ = group.Wait()
|
|
|
|
close(results)
|
|
close(errs)
|
|
|
|
var joined []error
|
|
for itemErr := range errs {
|
|
joined = append(joined, itemErr)
|
|
}
|
|
|
|
err := errors.Join(joined...)
|
|
|
|
desiredStatus := make(map[string]struct{}, len(list.Items))
|
|
|
|
for stat := range results {
|
|
if stat == nil {
|
|
continue
|
|
}
|
|
|
|
tnt.Status.UpdateInstance(stat)
|
|
|
|
desiredStatus[stat.Name] = struct{}{}
|
|
}
|
|
|
|
for name := range oldStatus {
|
|
if _, keep := desiredStatus[name]; keep {
|
|
continue
|
|
}
|
|
|
|
r.Metrics.DeleteAllMetricsForNamespace(name)
|
|
|
|
tnt.Status.RemoveInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: name})
|
|
}
|
|
|
|
tnt.Status.Size = uint(len(tnt.Status.Spaces))
|
|
|
|
tnt.AssignNamespaces(list.Items)
|
|
|
|
return err
|
|
}
|
|
|
|
func (r *Manager) reconcileNamespace(
|
|
ctx context.Context,
|
|
log logr.Logger,
|
|
namespace *corev1.Namespace,
|
|
tnt *capsulev1beta2.Tenant,
|
|
templateTenant *capsulev1beta2.Tenant,
|
|
) (
|
|
stat *capsulev1beta2.TenantStatusNamespaceItem,
|
|
err error,
|
|
) {
|
|
terminating := false
|
|
|
|
stat = &capsulev1beta2.TenantStatusNamespaceItem{
|
|
Name: namespace.GetName(),
|
|
UID: namespace.GetUID(),
|
|
}
|
|
|
|
metaStatus := &capsulev1beta2.TenantStatusNamespaceMetadata{}
|
|
|
|
instance := tnt.Status.GetInstance(stat)
|
|
if instance != nil {
|
|
stat = instance
|
|
}
|
|
|
|
// Always update tenant status condition after reconciliation.
|
|
defer func() {
|
|
readCondition := meta.NewReadyCondition(namespace)
|
|
|
|
switch {
|
|
case terminating:
|
|
readCondition.Status = metav1.ConditionFalse
|
|
readCondition.Reason = meta.TerminatingReason
|
|
readCondition.Message = "Namespace is terminating"
|
|
case err != nil:
|
|
readCondition.Status = metav1.ConditionFalse
|
|
readCondition.Reason = meta.FailedReason
|
|
readCondition.Message = fmt.Sprintf("Failed to reconcile: %v", err)
|
|
|
|
if instance != nil && instance.Metadata != nil {
|
|
stat.Metadata = instance.Metadata
|
|
}
|
|
default:
|
|
if metaStatus != nil {
|
|
stat.Metadata = metaStatus
|
|
}
|
|
}
|
|
|
|
stat.Conditions.UpdateConditionByType(readCondition)
|
|
|
|
cordonedCondition := meta.NewCordonedCondition(namespace)
|
|
|
|
if namespace.Labels[meta.CordonedLabel] == meta.ValueTrue {
|
|
cordonedCondition.Reason = meta.CordonedReason
|
|
cordonedCondition.Message = "namespace is cordoned"
|
|
cordonedCondition.Status = metav1.ConditionTrue
|
|
}
|
|
|
|
stat.Conditions.UpdateConditionByType(cordonedCondition)
|
|
}()
|
|
|
|
// Verify if namespace is still active or terminating.
|
|
//nolint:nestif
|
|
if namespace.DeletionTimestamp != nil {
|
|
terminating = true
|
|
|
|
terminatingState := meta.NewTerminatingConditionReason(namespace)
|
|
|
|
pending, err := tenant.NamespaceIsPendingPodTerminating(ctx, r.Client, namespace)
|
|
if err != nil {
|
|
terminatingState.Reason = meta.FailedReason
|
|
terminatingState.Status = metav1.ConditionFalse
|
|
terminatingState.Message = err.Error()
|
|
stat.Conditions.UpdateConditionByType(terminatingState)
|
|
|
|
return stat, err
|
|
}
|
|
|
|
if pending {
|
|
terminatingState.Reason = meta.PendingUnmanagedContentReason
|
|
terminatingState.Status = metav1.ConditionFalse
|
|
terminatingState.Message = "waiting for pods to finalize"
|
|
stat.Conditions.UpdateConditionByType(terminatingState)
|
|
|
|
return stat, nil
|
|
}
|
|
|
|
// Give Kubernetes' namespace controller time to perform its normal
|
|
// deletion before issuing a discovery-wide forced cleanup. Most namespaces
|
|
// disappear during this window, avoiding hundreds of list requests and
|
|
// substantial API pressure during Tenant teardown. Stuck namespaces still
|
|
// receive the existing finalizer cleanup after the grace period.
|
|
if time.Since(namespace.DeletionTimestamp.Time) < namespaceCascadingCleanupGracePeriod {
|
|
terminatingState.Reason = meta.TerminatingReason
|
|
terminatingState.Status = metav1.ConditionFalse
|
|
terminatingState.Message = "waiting for namespace finalization"
|
|
stat.Conditions.UpdateConditionByType(terminatingState)
|
|
|
|
return stat, nil
|
|
}
|
|
|
|
cleaned, err := tenant.NamespacedCascadingCleanup(ctx, r.Client, r.DiscoveryClient, &r.discoveryCache, r.DynamicClient, namespace)
|
|
if err != nil {
|
|
terminatingState.Reason = meta.FailedReason
|
|
terminatingState.Status = metav1.ConditionFalse
|
|
terminatingState.Message = err.Error()
|
|
stat.Conditions.UpdateConditionByType(terminatingState)
|
|
|
|
return stat, err
|
|
}
|
|
|
|
if cleaned {
|
|
terminatingState.Reason = meta.PendingUnmanagedContentReason
|
|
terminatingState.Status = metav1.ConditionFalse
|
|
terminatingState.Message = "performing cascading deletion"
|
|
stat.Conditions.UpdateConditionByType(terminatingState)
|
|
|
|
return stat, nil
|
|
}
|
|
|
|
terminatingState.Reason = meta.TerminatingReason
|
|
terminatingState.Status = metav1.ConditionFalse
|
|
terminatingState.Message = "waiting for namespace finalization"
|
|
stat.Conditions.UpdateConditionByType(terminatingState)
|
|
|
|
return stat, nil
|
|
}
|
|
|
|
// Collect Rules for namespace.
|
|
err = r.reconcileRuleStatus(ctx, log, tnt, templateTenant, namespace)
|
|
if err != nil {
|
|
return stat, err
|
|
}
|
|
|
|
err = retry.RetryOnConflict(retry.DefaultBackoff, func() (conflictErr error) {
|
|
_, conflictErr = controllerutil.CreateOrUpdate(ctx, r.Client, namespace, func() error {
|
|
metaStatus, err = r.reconcileNamespaceMetadata(ctx, namespace, tnt, stat)
|
|
|
|
return err
|
|
})
|
|
|
|
return conflictErr
|
|
})
|
|
|
|
return stat, err
|
|
}
|
|
|
|
//nolint:nestif
|
|
func (r *Manager) reconcileNamespaceMetadata(
|
|
ctx context.Context,
|
|
ns *corev1.Namespace,
|
|
tnt *capsulev1beta2.Tenant,
|
|
stat *capsulev1beta2.TenantStatusNamespaceItem,
|
|
) (
|
|
managed *capsulev1beta2.TenantStatusNamespaceMetadata,
|
|
err error,
|
|
) {
|
|
originLabels := ns.GetLabels()
|
|
if originLabels == nil {
|
|
originLabels = make(map[string]string)
|
|
}
|
|
|
|
originAnnotations := ns.GetAnnotations()
|
|
if originAnnotations == nil {
|
|
originAnnotations = make(map[string]string)
|
|
}
|
|
|
|
managedLabels, managedAnnotations, err := tenant.BuildNamespaceMetadataForTenant(ns, tnt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
managedMetadataOnly := tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.ManagedMetadataOnly
|
|
|
|
// Handle User-Defined Metadata, if allowed.
|
|
if !managedMetadataOnly {
|
|
if originLabels != nil {
|
|
maps.Copy(originLabels, managedLabels)
|
|
}
|
|
|
|
if originAnnotations != nil {
|
|
maps.Copy(originAnnotations, managedAnnotations)
|
|
}
|
|
|
|
// Cleanup old Metadata.
|
|
instance := tnt.Status.GetInstance(stat)
|
|
if instance != nil && instance.Metadata != nil {
|
|
for label := range instance.Metadata.Labels {
|
|
if _, ok := managedLabels[label]; ok {
|
|
continue
|
|
}
|
|
|
|
delete(originLabels, label)
|
|
}
|
|
|
|
for annotation := range instance.Metadata.Annotations {
|
|
if _, ok := managedAnnotations[annotation]; ok {
|
|
continue
|
|
}
|
|
|
|
delete(originAnnotations, annotation)
|
|
}
|
|
}
|
|
|
|
managed = &capsulev1beta2.TenantStatusNamespaceMetadata{
|
|
Labels: managedLabels,
|
|
Annotations: managedAnnotations,
|
|
}
|
|
} else {
|
|
originLabels = managedLabels
|
|
originAnnotations = managedAnnotations
|
|
}
|
|
|
|
tenant.AddNamespaceNameLabels(originLabels, ns)
|
|
tenant.AddTenantNameLabel(originLabels, tnt)
|
|
|
|
ns.SetLabels(originLabels)
|
|
ns.SetAnnotations(originAnnotations)
|
|
|
|
return managed, err
|
|
}
|