mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-03-06 03:30:53 +00:00
183 lines
3.9 KiB
Go
183 lines
3.9 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package resourcepools
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/client-go/tools/events"
|
|
"k8s.io/client-go/util/retry"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
|
"github.com/projectcapsule/capsule/pkg/api/meta"
|
|
evt "github.com/projectcapsule/capsule/pkg/runtime/events"
|
|
)
|
|
|
|
// Update the Status of a claim and emit an event if Status changed.
|
|
func updateStatusAndEmitEvent(
|
|
ctx context.Context,
|
|
c client.Client,
|
|
recorder events.EventRecorder,
|
|
claim *capsulev1beta2.ResourcePoolClaim,
|
|
condition meta.Condition,
|
|
) (err error) {
|
|
updated := claim.Status.Conditions.UpdateConditionByTypeWithStatus(condition)
|
|
|
|
if !updated {
|
|
return nil
|
|
}
|
|
|
|
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
|
current := &capsulev1beta2.ResourcePoolClaim{}
|
|
if err := c.Get(ctx, client.ObjectKeyFromObject(claim), current); err != nil {
|
|
return fmt.Errorf("failed to refetch instance before update: %w", err)
|
|
}
|
|
|
|
current.Status.Conditions = claim.Status.Conditions
|
|
|
|
return c.Status().Update(ctx, current)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
eventType := corev1.EventTypeNormal
|
|
if condition.Status == metav1.ConditionFalse {
|
|
eventType = corev1.EventTypeWarning
|
|
}
|
|
|
|
recorder.Eventf(
|
|
claim,
|
|
claim,
|
|
eventType,
|
|
condition.Reason,
|
|
evt.ActionReconciled,
|
|
condition.Message,
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
func filterResourceListByKeys(in corev1.ResourceList, keys corev1.ResourceList) corev1.ResourceList {
|
|
out := corev1.ResourceList{}
|
|
|
|
for k := range keys {
|
|
if v, ok := in[k]; ok {
|
|
out[k] = v
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func selectClaimsCoveringUsageGreedy(
|
|
used corev1.ResourceList,
|
|
claims []capsulev1beta2.ResourcePoolClaim,
|
|
) map[string]struct{} {
|
|
selected := map[string]struct{}{}
|
|
|
|
if resourceListAllZero(used) || len(claims) == 0 {
|
|
return selected
|
|
}
|
|
|
|
// Stable deterministic order for ties (creation ts, name, namespace)
|
|
sort.Slice(claims, func(i, j int) bool {
|
|
a, b := claims[i], claims[j]
|
|
if !a.CreationTimestamp.Equal(&b.CreationTimestamp) {
|
|
return a.CreationTimestamp.Before(&b.CreationTimestamp)
|
|
}
|
|
|
|
if a.Name != b.Name {
|
|
return a.Name < b.Name
|
|
}
|
|
|
|
return a.Namespace < b.Namespace
|
|
})
|
|
|
|
remaining := used.DeepCopy()
|
|
// Greedy: repeatedly pick best claim against remaining.
|
|
for !resourceListAllZero(remaining) {
|
|
bestIdx := -1
|
|
bestScore := float64(0)
|
|
|
|
for i := range claims {
|
|
uid := string(claims[i].UID)
|
|
if _, ok := selected[uid]; ok {
|
|
continue
|
|
}
|
|
|
|
score := claimCoverageScore(remaining, claims[i].Spec.ResourceClaims)
|
|
if score > bestScore {
|
|
bestScore = score
|
|
bestIdx = i
|
|
}
|
|
}
|
|
|
|
if bestIdx == -1 || bestScore == 0 {
|
|
// Can't cover remaining with available claims (or used contains resources not claimed).
|
|
break
|
|
}
|
|
|
|
chosen := claims[bestIdx]
|
|
selected[string(chosen.UID)] = struct{}{}
|
|
|
|
// remaining -= chosen.request (clamped at zero)
|
|
for rName, req := range chosen.Spec.ResourceClaims {
|
|
if rem, ok := remaining[rName]; ok {
|
|
rem.Sub(req)
|
|
|
|
if rem.Sign() < 0 {
|
|
rem = rem.DeepCopy()
|
|
rem.Set(0)
|
|
}
|
|
|
|
remaining[rName] = rem
|
|
}
|
|
}
|
|
}
|
|
|
|
return selected
|
|
}
|
|
|
|
func claimCoverageScore(remaining corev1.ResourceList, reqs corev1.ResourceList) float64 {
|
|
var score float64
|
|
|
|
for rName, rem := range remaining {
|
|
if rem.IsZero() {
|
|
continue
|
|
}
|
|
|
|
req, ok := reqs[rName]
|
|
if !ok || req.IsZero() {
|
|
continue
|
|
}
|
|
|
|
// covered = min(rem, req)
|
|
covered := req.DeepCopy()
|
|
if covered.Cmp(rem) > 0 {
|
|
covered = rem.DeepCopy()
|
|
}
|
|
|
|
// Approx score: float is OK as heuristic
|
|
score += covered.AsApproximateFloat64()
|
|
}
|
|
|
|
return score
|
|
}
|
|
|
|
func resourceListAllZero(rl corev1.ResourceList) bool {
|
|
for _, q := range rl {
|
|
if !q.IsZero() && q.Sign() > 0 {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|