// Copyright 2020-2026 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 package tenant import ( "context" "errors" "fmt" "sync" "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/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/selection" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "sigs.k8s.io/controller-runtime/pkg/client" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" "github.com/projectcapsule/capsule/pkg/api/meta" ) type NamespacedResourceCache interface { Get(disco discovery.DiscoveryInterface) ([]schema.GroupVersionResource, error) } func NamespaceIsPendingPodTerminating( ctx context.Context, c client.Reader, ns *corev1.Namespace, ) (pending bool, err error) { // Pods behave differently, we manually check if they are still present as they are the largest attack vector var podList corev1.PodList if err := c.List(ctx, &podList, client.InNamespace(ns.Name)); err != nil { return false, fmt.Errorf("list pods in namespace %q: %w", ns.Name, err) } if len(podList.Items) > 0 { return true, nil } return false, nil } func NamespaceIsPendingUnmanagedTerminationByStatus(ctx context.Context, c client.Reader, ns *corev1.Namespace) (bool, error) { tnt, err := GetTenantByNamespace(ctx, c, ns.GetName()) if err != nil { return false, err } if tnt == nil { return false, nil } instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{ Name: ns.GetName(), }) if instance == nil { return false, nil } cond := instance.Conditions.GetConditionByType(meta.TerminatingCondition) if cond == nil || cond.Status != metav1.ConditionTrue { return false, nil } return true, nil } // First deletes (remove finalizers and deleted) all resources which are not managed by capsule // Once fulfilled, remove all managed resources. func NamespacedCascadingCleanup( ctx context.Context, c client.Reader, disco discovery.DiscoveryInterface, resourceCache NamespacedResourceCache, dyn dynamic.Interface, ns *corev1.Namespace, ) (cleaned bool, err error) { _, err = removeFinalizersFromRemainingNamespacedResources(ctx, disco, dyn, resourceCache, ns.Name, []string{}) if err != nil { return true, err } return false, nil } func removeFinalizersFromRemainingNamespacedResources( ctx context.Context, disco discovery.DiscoveryInterface, dyn dynamic.Interface, resourceCache NamespacedResourceCache, namespace string, ignoredFinalizers []string, ) (bool, error) { var errs []error gvrs, err := resourceCache.Get(disco) if err != nil { return false, err } ignored := make(map[string]struct{}, len(ignoredFinalizers)) for _, f := range ignoredFinalizers { ignored[f] = struct{}{} } var ( mu sync.Mutex cleanedAny bool ) g, ctx := errgroup.WithContext(ctx) g.SetLimit(4) for _, gvr := range gvrs { g.Go(func() error { cleaned, err := processResourceType(ctx, dyn, gvr, namespace, ignored) mu.Lock() defer mu.Unlock() if cleaned { cleanedAny = true } if err != nil { errs = append(errs, fmt.Errorf("process %s in namespace %q: %w", gvr.String(), namespace, err)) } return nil }) } _ = g.Wait() return cleanedAny, errors.Join(errs...) } func processResourceType( ctx context.Context, dyn dynamic.Interface, gvr schema.GroupVersionResource, namespace string, ignoredFinalizers map[string]struct{}, ) (bool, error) { list, err := dyn.Resource(gvr).Namespace(namespace).List(ctx, metav1.ListOptions{}) if err != nil { if apierrors.IsNotFound(err) || apierrors.IsMethodNotSupported(err) { return false, nil } return false, fmt.Errorf("list %s in namespace %q: %w", gvr.String(), namespace, err) } var errs []error cleanedAny := false for i := range list.Items { obj := &list.Items[i] if gvr.Group == "" && gvr.Resource == "pods" { continue } // If the object is not yet deleting, issue a delete first. if obj.GetDeletionTimestamp() == nil { if err := dyn.Resource(gvr).Namespace(namespace).Delete(ctx, obj.GetName(), metav1.DeleteOptions{}); err != nil { if !apierrors.IsNotFound(err) { errs = append(errs, fmt.Errorf( "delete %s %q/%q: %w", gvr.String(), namespace, obj.GetName(), err, )) continue } } else { cleanedAny = true } } finalizers := obj.GetFinalizers() if len(finalizers) == 0 { continue } remainingFinalizers, removed := meta.FilterFinalizers(finalizers, ignoredFinalizers) if !removed { continue } patch := meta.BuildFinalizersMergePatch(remainingFinalizers) _, err := dyn.Resource(gvr).Namespace(namespace).Patch( ctx, obj.GetName(), types.MergePatchType, patch, metav1.PatchOptions{}, ) if err != nil { if !apierrors.IsNotFound(err) { errs = append(errs, fmt.Errorf( "patch finalizers on %s %q/%q: %w", gvr.String(), namespace, obj.GetName(), err, )) continue } } else { cleanedAny = true } } return cleanedAny, errors.Join(errs...) } func ResolveNamespaceTenant( ctx context.Context, reader client.Reader, ns *corev1.Namespace, ) (*capsulev1beta2.Tenant, error) { if ns == nil { return nil, nil } label := TenanLabelValue(ns) refs := TenantOwnerReferences(ns) switch { case label == "" && len(refs) == 0: return nil, nil case len(refs) > 1: return nil, fmt.Errorf("namespace can not have multiple Tenant ownerReferences") case label == "" && len(refs) == 1: return nil, fmt.Errorf("namespace has Tenant ownerReference %q but no tenant label", refs[0].Name) case label != "" && len(refs) == 0: return nil, fmt.Errorf("namespace has tenant label %q but no Tenant ownerReference", label) } tnt, err := GetTenantByOwnerreferences(ctx, reader, refs) if err != nil { return nil, err } if tnt == nil { return nil, fmt.Errorf("namespace references unknown Tenant %q", refs[0].Name) } if tnt.GetName() != label { return nil, fmt.Errorf("namespace label %q does not match owner reference %q", label, tnt.GetName()) } return tnt, nil } func CollectTenantNamespaceByLabel( ctx context.Context, c client.Client, tnt capsulev1beta2.Tenant, additionalSelector *metav1.LabelSelector, ) (namespaces []corev1.Namespace, err error) { // Creating Namespace selector var selector labels.Selector if additionalSelector != nil { selector, err = metav1.LabelSelectorAsSelector(additionalSelector) if err != nil { return nil, err } } else { selector = labels.NewSelector() } // Resources can be replicated only on Namespaces belonging to the same Global: // preventing a boundary cross by enforcing the selection. tntRequirement, err := labels.NewRequirement(meta.TenantLabel, selection.Equals, []string{tnt.GetName()}) if err != nil { err = fmt.Errorf("unable to create requirement for Namespace filtering and resource replication: %w", err) return nil, err } selector = selector.Add(*tntRequirement) // Selecting the targeted Namespace according to the TenantResource specification. ns := corev1.NamespaceList{} if err = c.List(ctx, &ns, client.MatchingLabelsSelector{Selector: selector}); err != nil { err = fmt.Errorf("cannot retrieve Namespaces for resource: %w", err) return nil, err } return ns.Items, nil }