mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-04-07 03:07:19 +00:00
287 lines
7.3 KiB
Go
287 lines
7.3 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package resourcepools
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/go-logr/logr"
|
|
gherrors "github.com/pkg/errors"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/client-go/tools/events"
|
|
"k8s.io/client-go/util/retry"
|
|
"sigs.k8s.io/cluster-api/util/patch"
|
|
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/controller"
|
|
"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"
|
|
"github.com/projectcapsule/capsule/pkg/api/meta"
|
|
)
|
|
|
|
type resourceClaimController struct {
|
|
client.Client
|
|
|
|
metrics *metrics.ClaimRecorder
|
|
log logr.Logger
|
|
recorder events.EventRecorder
|
|
}
|
|
|
|
func (r *resourceClaimController) SetupWithManager(mgr ctrl.Manager, cfg utils.ControllerOptions) error {
|
|
return ctrl.NewControllerManagedBy(mgr).
|
|
Named("capsule/resourcepools/claims").
|
|
For(
|
|
&capsulev1beta2.ResourcePoolClaim{},
|
|
builder.WithPredicates(
|
|
predicate.GenerationChangedPredicate{},
|
|
),
|
|
).
|
|
Watches(
|
|
&capsulev1beta2.ResourcePool{},
|
|
handler.EnqueueRequestsFromMapFunc(r.claimsWithoutPoolFromNamespaces),
|
|
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
|
|
).
|
|
WithOptions(controller.Options{MaxConcurrentReconciles: cfg.MaxConcurrentReconciles}).
|
|
Complete(r)
|
|
}
|
|
|
|
func (r resourceClaimController) Reconcile(ctx context.Context, request ctrl.Request) (result ctrl.Result, err error) {
|
|
log := r.log.WithValues("Request.Name", request.Name)
|
|
|
|
instance := &capsulev1beta2.ResourcePoolClaim{}
|
|
if err = r.Get(ctx, request.NamespacedName, instance); err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
log.V(3).Info("Request object not found, could have been deleted after reconcile request")
|
|
|
|
r.metrics.DeleteClaimMetric(request.Name, request.Namespace)
|
|
|
|
return reconcile.Result{}, nil
|
|
}
|
|
|
|
log.Error(err, "Error reading the object")
|
|
|
|
return result, err
|
|
}
|
|
|
|
patchHelper, err := patch.NewHelper(instance, r.Client)
|
|
if err != nil {
|
|
return reconcile.Result{}, gherrors.Wrap(err, "failed to init patch helper")
|
|
}
|
|
|
|
defer func() {
|
|
if uerr := r.updateStatus(ctx, instance, err); uerr != nil {
|
|
err = uerr
|
|
|
|
return
|
|
}
|
|
|
|
r.metrics.RecordClaimCondition(instance)
|
|
|
|
if e := patchHelper.Patch(ctx, instance); e != nil {
|
|
err = e
|
|
|
|
return
|
|
}
|
|
|
|
err = nil
|
|
}()
|
|
|
|
err = r.reconcile(ctx, log, instance)
|
|
|
|
return ctrl.Result{}, err
|
|
}
|
|
|
|
// Trigger claims from a namespace, which are not yet allocated.
|
|
// when a resourcepool updates it's status.
|
|
func (r *resourceClaimController) claimsWithoutPoolFromNamespaces(ctx context.Context, obj client.Object) []reconcile.Request {
|
|
pool, ok := obj.(*capsulev1beta2.ResourcePool)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
var requests []reconcile.Request
|
|
|
|
for _, ns := range pool.Status.Namespaces {
|
|
claimList := &capsulev1beta2.ResourcePoolClaimList{}
|
|
if err := r.List(ctx, claimList, client.InNamespace(ns)); err != nil {
|
|
r.log.Error(err, "Failed to list claims in namespace", "namespace", ns)
|
|
|
|
continue
|
|
}
|
|
|
|
for _, claim := range claimList.Items {
|
|
if claim.Status.Pool.UID == "" {
|
|
requests = append(requests, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{
|
|
Namespace: claim.Namespace,
|
|
Name: claim.Name,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return requests
|
|
}
|
|
|
|
// This Controller is responsible for assigning Claims to ResourcePools.
|
|
// Everything else will be handeled by the ResourcePool Controller.
|
|
func (r resourceClaimController) reconcile(
|
|
ctx context.Context,
|
|
log logr.Logger,
|
|
claim *capsulev1beta2.ResourcePoolClaim,
|
|
) (err error) {
|
|
pool, err := r.evaluateResourcePool(ctx, claim)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return r.allocateResourcePool(ctx, log, claim, pool)
|
|
}
|
|
|
|
// Verify a Pool can be allocated.
|
|
func (r resourceClaimController) evaluateResourcePool(
|
|
ctx context.Context,
|
|
claim *capsulev1beta2.ResourcePoolClaim,
|
|
) (pool *capsulev1beta2.ResourcePool, err error) {
|
|
poolName := claim.Spec.Pool
|
|
|
|
if poolName == "" {
|
|
err = fmt.Errorf("no pool reference was defined")
|
|
|
|
return pool, err
|
|
}
|
|
|
|
pool = &capsulev1beta2.ResourcePool{}
|
|
if err := r.Get(ctx, client.ObjectKey{
|
|
Name: poolName,
|
|
}, pool); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !pool.DeletionTimestamp.IsZero() {
|
|
return nil, fmt.Errorf(
|
|
"resourcepool not available",
|
|
)
|
|
}
|
|
|
|
allowed := false
|
|
|
|
for _, ns := range pool.Status.Namespaces {
|
|
if ns == claim.GetNamespace() {
|
|
allowed = true
|
|
|
|
continue
|
|
}
|
|
}
|
|
|
|
if !allowed {
|
|
return nil, fmt.Errorf(
|
|
"resourcepool not available",
|
|
)
|
|
}
|
|
|
|
// Validates if Resources can be allocated in the first place
|
|
for resourceName := range claim.Spec.ResourceClaims {
|
|
_, exists := pool.Status.Allocation.Hard[resourceName]
|
|
if !exists {
|
|
return nil, fmt.Errorf(
|
|
"resource %s is not available in pool %s",
|
|
resourceName,
|
|
pool.Name,
|
|
)
|
|
}
|
|
}
|
|
|
|
return pool, err
|
|
}
|
|
|
|
func (r resourceClaimController) allocateResourcePool(
|
|
ctx context.Context,
|
|
log logr.Logger,
|
|
cl *capsulev1beta2.ResourcePoolClaim,
|
|
pool *capsulev1beta2.ResourcePool,
|
|
) (err error) {
|
|
target := meta.LocalRFC1123ObjectReferenceWithUID{
|
|
Name: meta.RFC1123Name(pool.GetName()),
|
|
UID: pool.GetUID(),
|
|
}
|
|
|
|
if cl.Status.Pool.Name != "" || cl.Status.Pool.UID != "" {
|
|
if cl.Status.Pool.Name != target.Name ||
|
|
cl.Status.Pool.UID != target.UID {
|
|
if cl.IsBoundInResourcePool() {
|
|
return fmt.Errorf("can not change pool while claim is in use for pool %s", cl.Status.Pool.Name)
|
|
}
|
|
|
|
// Removes old pool reference
|
|
meta.RemoveLooseOwnerReference(cl, metav1.OwnerReference{
|
|
APIVersion: pool.APIVersion,
|
|
Kind: pool.Kind,
|
|
Name: string(cl.Status.Pool.Name),
|
|
UID: cl.Status.Pool.UID,
|
|
})
|
|
|
|
r.metrics.DeletePoolAssociation(cl.GetName(), cl.GetNamespace(), string(cl.Status.Pool.Name))
|
|
}
|
|
}
|
|
|
|
ref := meta.GetLooseOwnerReference(pool)
|
|
|
|
// Sanitize any previous References
|
|
meta.RemoveLooseOwnerReferenceForKindExceptGiven(cl, ref)
|
|
|
|
if !meta.HasLooseOwnerReference(cl, ref) {
|
|
log.V(4).Info("adding ownerreference", "pool", pool.Name)
|
|
|
|
if err := meta.SetLooseOwnerReference(cl, ref); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
cl.Status.Pool = target
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *resourceClaimController) updateStatus(
|
|
ctx context.Context,
|
|
instance *capsulev1beta2.ResourcePoolClaim,
|
|
reconcileError error,
|
|
) error {
|
|
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
|
|
latest := &capsulev1beta2.ResourcePoolClaim{}
|
|
if err = r.Get(ctx, types.NamespacedName{Name: instance.GetName(), Namespace: instance.GetNamespace()}, latest); err != nil {
|
|
return err
|
|
}
|
|
|
|
latest.Status = instance.Status
|
|
|
|
// Set Ready Condition
|
|
readyCondition := meta.NewReadyCondition(instance)
|
|
if reconcileError != nil {
|
|
readyCondition.Message = reconcileError.Error()
|
|
readyCondition.Status = metav1.ConditionFalse
|
|
readyCondition.Reason = meta.FailedReason
|
|
}
|
|
|
|
latest.Status.Conditions.UpdateConditionByType(readyCondition)
|
|
|
|
// Unset legacy Status
|
|
//nolint:staticcheck
|
|
latest.Status.Condition = metav1.Condition{}
|
|
|
|
return r.Client.Status().Update(ctx, latest)
|
|
})
|
|
}
|