Files
capsule/pkg/tenant/namespaces.go
T
cc4fb45d70 feat: upstream enterprise preview (#1841)
feat: upstream enterprise preview

---------

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
Co-authored-by: CorentinPtrl <pitrel.corentin@gmail.com>
2026-05-28 00:58:58 +02:00

310 lines
7.3 KiB
Go

// 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
}