mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 20:46:42 +00:00
feat: upstream enterprise preview --------- Signed-off-by: Oliver Baehler <oliver@sudo-i.net> Co-authored-by: CorentinPtrl <pitrel.corentin@gmail.com>
310 lines
7.3 KiB
Go
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
|
|
}
|