Files
Oliver BählerandGitHub 99e05220ab feat: implement performance optimizations (#2045)
* feat: implement performance optimizations

---------

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
2026-07-21 16:27:29 +02:00

251 lines
6.8 KiB
Go

// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package tenantowners
import (
"context"
"fmt"
"reflect"
"sort"
"github.com/go-logr/logr"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
"k8s.io/client-go/util/workqueue"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/internal/controllers/utils"
"github.com/projectcapsule/capsule/internal/metrics"
capmeta "github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/rbac"
indexer "github.com/projectcapsule/capsule/pkg/runtime/indexers/tenant"
"github.com/projectcapsule/capsule/pkg/tenant"
)
// TenantOwnerManager reconciles TenantOwner objects and keeps
// status.tenants / status.matchedTenants / status.conditions in sync.
//
// It watches both TenantOwner objects (for spec changes) and Tenant objects
// (for selector changes), re-enqueuing every TenantOwner whenever any Tenant
// changes, because a single Tenant change can affect any subset of TenantOwners.
type TenantOwnerManager struct {
client.Client
reader client.Reader
metrics *metrics.TenantOwnerRecorder
Log logr.Logger
}
func (r *TenantOwnerManager) SetupWithManager(
mgr ctrl.Manager,
ctrlConfig utils.ControllerOptions,
metrics *metrics.TenantOwnerRecorder,
) error {
r.metrics = metrics
r.reader = mgr.GetAPIReader()
return ctrl.NewControllerManagedBy(mgr).
Named("capsule/tenantowner").
For(
&capsulev1beta2.TenantOwner{},
builder.WithPredicates(
predicate.Or(
predicate.GenerationChangedPredicate{},
predicate.LabelChangedPredicate{},
),
),
).
Watches(
&capsulev1beta2.Tenant{},
handler.TypedFuncs[client.Object, ctrl.Request]{
CreateFunc: func(
ctx context.Context,
e event.TypedCreateEvent[client.Object],
q workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
tnt, ok := e.Object.(*capsulev1beta2.Tenant)
if !ok {
return
}
r.enqueueTenantOwnerRequests(ctx, q, tnt.Status.Owners)
},
UpdateFunc: func(
ctx context.Context,
e event.TypedUpdateEvent[client.Object],
q workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
oldTnt, ok1 := e.ObjectOld.(*capsulev1beta2.Tenant)
newTnt, ok2 := e.ObjectNew.(*capsulev1beta2.Tenant)
if !ok1 || !ok2 {
return
}
owners := make(
rbac.OwnerStatusListSpec,
0,
len(oldTnt.Status.Owners)+len(newTnt.Status.Owners),
)
owners = append(owners, oldTnt.Status.Owners...)
owners = append(owners, newTnt.Status.Owners...)
r.enqueueTenantOwnerRequests(ctx, q, owners)
},
DeleteFunc: func(
ctx context.Context,
e event.TypedDeleteEvent[client.Object],
q workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
tnt, ok := e.Object.(*capsulev1beta2.Tenant)
if !ok {
return
}
r.enqueueTenantOwnerRequests(ctx, q, tnt.Status.Owners)
},
},
).
WithOptions(ctrlConfig.Runtime.ToControllerOptions()).
Complete(r)
}
func (r *TenantOwnerManager) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) {
log := r.Log.WithValues("tenantowner", req.Name)
instance := &capsulev1beta2.TenantOwner{}
if err = r.Get(ctx, req.NamespacedName, instance); err != nil {
if apierrors.IsNotFound(err) {
r.metrics.DeleteMetrics(req.Name)
return reconcile.Result{}, nil
}
return reconcile.Result{}, err
}
matchedTenants, reconcileErr := r.reconcileMatchedTenants(ctx, log, instance)
log.V(5).Info("matched tenants", "count", len(matchedTenants))
if statusErr := r.updateTenantOwnerStatus(ctx, instance, matchedTenants, reconcileErr); statusErr != nil {
if apierrors.IsNotFound(statusErr) {
return reconcile.Result{}, nil
}
return reconcile.Result{}, fmt.Errorf("cannot update TenantOwner status: %w", statusErr)
}
r.metrics.RecordConditions(instance)
return reconcile.Result{}, reconcileErr
}
// reconcileMatchedTenants lists all Tenants and returns the sorted names of
// those whose spec.permissions.matchOwners selectors select this TenantOwner.
// All work is done against the in-memory cache — no direct API server calls.
func (r *TenantOwnerManager) reconcileMatchedTenants(ctx context.Context, log logr.Logger, to *capsulev1beta2.TenantOwner) ([]string, error) {
if to.Spec.Name == "" || to.Spec.Kind.String() == "" {
return nil, fmt.Errorf(
"TenantOwner %s has incomplete owner reference: kind=%q name=%q",
to.Name,
to.Spec.Kind,
to.Spec.Name,
)
}
ownerKey := tenant.OwnerKindIndexKey(to.Spec.Kind.String(), to.Spec.Name)
tnts := &capsulev1beta2.TenantList{}
if err := r.List(
ctx,
tnts,
client.MatchingFields{
indexer.OwnerKindIndexerFieldName: ownerKey,
},
); err != nil {
return nil, fmt.Errorf(
"listing Tenants by owner %s/%s: %w",
to.Spec.Kind,
to.Spec.Name,
err,
)
}
log.V(4).Info("found tenant references", "tenants", len(tnts.Items))
matched := make([]string, 0, len(tnts.Items))
for i := range tnts.Items {
matched = append(matched, tnts.Items[i].Name)
}
sort.Strings(matched)
return matched, nil
}
// updateTenantOwnerStatus writes the reconciled status back to the API server
// using a RetryOnConflict re-GET loop to avoid clobbering concurrent updates.
func (r *TenantOwnerManager) updateTenantOwnerStatus(
ctx context.Context,
instance *capsulev1beta2.TenantOwner,
matchedTenants []string,
reconcileError error,
) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
latest := &capsulev1beta2.TenantOwner{}
if err := r.reader.Get(ctx, types.NamespacedName{Name: instance.Name}, latest); err != nil {
if apierrors.IsNotFound(err) {
return nil
}
return err
}
originalStatus := latest.Status.DeepCopy()
latest.Status.ObservedGeneration = latest.GetGeneration()
latest.Status.Tenants = matchedTenants
readyCondition := capmeta.NewReadyCondition(latest)
if reconcileError != nil {
readyCondition.Status = metav1.ConditionFalse
readyCondition.Reason = capmeta.FailedReason
readyCondition.Message = reconcileError.Error()
}
latest.Status.Conditions.UpdateConditionByType(readyCondition)
if reflect.DeepEqual(*originalStatus, latest.Status) {
return nil
}
if err := r.Client.Status().Update(ctx, latest); err != nil {
if apierrors.IsNotFound(err) {
return nil
}
return err
}
instance.Status = latest.Status
return nil
})
}