karmada/pkg/controllers/federatedresourcequota/federated_resource_quota_st...

241 lines
8.2 KiB
Go

package federatedresourcequota
import (
"context"
"encoding/json"
"reflect"
"sort"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/retry"
"k8s.io/klog/v2"
controllerruntime "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"
"sigs.k8s.io/controller-runtime/pkg/source"
policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1"
"github.com/karmada-io/karmada/pkg/util"
"github.com/karmada-io/karmada/pkg/util/helper"
"github.com/karmada-io/karmada/pkg/util/names"
)
const (
// StatusControllerName is the controller name that will be used when reporting events.
StatusControllerName = "federated-resource-quota-status-controller"
)
// StatusController is to collect status from work to FederatedResourceQuota.
type StatusController struct {
client.Client // used to operate Work resources.
EventRecorder record.EventRecorder
}
// Reconcile performs a full reconciliation for the object referred to by the Request.
// The SyncController will requeue the Request to be processed again if an error is non-nil or
// Result.Requeue is true, otherwise upon completion it will remove the work from the queue.
func (c *StatusController) Reconcile(ctx context.Context, req controllerruntime.Request) (controllerruntime.Result, error) {
klog.V(4).Infof("FederatedResourceQuota status controller reconciling %s", req.NamespacedName.String())
quota := &policyv1alpha1.FederatedResourceQuota{}
if err := c.Get(context.TODO(), req.NamespacedName, quota); err != nil {
// The resource may no longer exist, in which case we stop processing.
if apierrors.IsNotFound(err) {
return controllerruntime.Result{}, nil
}
return controllerruntime.Result{Requeue: true}, err
}
if !quota.DeletionTimestamp.IsZero() {
return controllerruntime.Result{}, nil
}
workList := &workv1alpha1.WorkList{}
if err := c.List(context.TODO(), workList, &client.ListOptions{
LabelSelector: labels.SelectorFromSet(labels.Set{
util.FederatedResourceQuotaNamespaceLabel: quota.Namespace,
util.FederatedResourceQuotaNameLabel: quota.Name,
}),
}); err != nil {
klog.Errorf("Failed to list workList created by federatedResourceQuota(%s), error: %v", req.NamespacedName.String(), err)
}
if err := c.collectQuotaStatus(quota); err != nil {
klog.Errorf("Failed to collect status from works to federatedResourceQuota(%s), error: %v", req.NamespacedName.String(), err)
return controllerruntime.Result{Requeue: true}, err
}
return controllerruntime.Result{}, nil
}
// SetupWithManager creates a controller and register to controller manager.
func (c *StatusController) SetupWithManager(mgr controllerruntime.Manager) error {
fn := handler.MapFunc(
func(obj client.Object) []reconcile.Request {
var requests []reconcile.Request
quotaNamespace, namespaceExist := obj.GetLabels()[util.FederatedResourceQuotaNamespaceLabel]
quotaName, nameExist := obj.GetLabels()[util.FederatedResourceQuotaNameLabel]
if namespaceExist && nameExist {
requests = append(requests, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: quotaNamespace,
Name: quotaName,
},
})
}
return requests
},
)
workPredicate := builder.WithPredicates(predicate.Funcs{
CreateFunc: func(e event.CreateEvent) bool {
return false
},
UpdateFunc: func(e event.UpdateEvent) bool {
objOld := e.ObjectOld.(*workv1alpha1.Work)
objNew := e.ObjectNew.(*workv1alpha1.Work)
_, namespaceExist := objNew.GetLabels()[util.FederatedResourceQuotaNamespaceLabel]
_, nameExist := objNew.GetLabels()[util.FederatedResourceQuotaNameLabel]
if !namespaceExist || !nameExist {
return false
}
return !reflect.DeepEqual(objOld.Status, objNew.Status)
},
DeleteFunc: func(e event.DeleteEvent) bool {
return false
},
GenericFunc: func(e event.GenericEvent) bool {
return false
},
})
return controllerruntime.NewControllerManagedBy(mgr).
For(&policyv1alpha1.FederatedResourceQuota{}).
Watches(&source.Kind{Type: &workv1alpha1.Work{}}, handler.EnqueueRequestsFromMapFunc(fn), workPredicate).
Complete(c)
}
func (c *StatusController) collectQuotaStatus(quota *policyv1alpha1.FederatedResourceQuota) error {
workList, err := helper.GetWorksByLabelsSet(c.Client, labels.Set{
util.FederatedResourceQuotaNamespaceLabel: quota.Namespace,
util.FederatedResourceQuotaNameLabel: quota.Name,
})
if err != nil {
klog.Errorf("Failed to list workList created by federatedResourceQuota(%s), error: %v", klog.KObj(quota).String(), err)
return err
}
aggregatedStatuses, err := aggregatedStatusFormWorks(workList.Items)
if err != nil {
return err
}
quotaStatus := quota.Status.DeepCopy()
quotaStatus.Overall = quota.Spec.Overall
quotaStatus.AggregatedStatus = aggregatedStatuses
quotaStatus.OverallUsed = calculateUsed(aggregatedStatuses)
if reflect.DeepEqual(quota.Status, quotaStatus) {
klog.V(4).Infof("New quotaStatus are equal with old federatedResourceQuota(%s) status, no update required.", klog.KObj(quota).String())
return nil
}
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
quota.Status = *quotaStatus
updateErr := c.Status().Update(context.TODO(), quota)
if updateErr == nil {
return nil
}
updated := &policyv1alpha1.FederatedResourceQuota{}
if err = c.Get(context.TODO(), client.ObjectKey{Namespace: quota.Namespace, Name: quota.Name}, updated); err == nil {
// make a copy, so we don't mutate the shared cache
quota = updated.DeepCopy()
} else {
klog.Errorf("failed to get updated federatedResourceQuota(%s): %v", klog.KObj(quota).String(), err)
}
return updateErr
})
}
func aggregatedStatusFormWorks(works []workv1alpha1.Work) ([]policyv1alpha1.ClusterQuotaStatus, error) {
var aggregatedStatuses []policyv1alpha1.ClusterQuotaStatus
for index := range works {
work := works[index]
var applied bool
if cond := meta.FindStatusCondition(work.Status.Conditions, workv1alpha1.WorkApplied); cond != nil {
switch cond.Status {
case metav1.ConditionTrue:
applied = true
case metav1.ConditionUnknown:
fallthrough
case metav1.ConditionFalse:
applied = false
default: // should not happen unless the condition api changed.
panic("unexpected status")
}
}
if !applied {
klog.Warningf("Work(%s) applied failed, skip aggregated status", klog.KObj(&work).String())
continue
}
if len(work.Status.ManifestStatuses) == 0 {
klog.Warningf("The ManifestStatuses length of work(%s) is zero", klog.KObj(&work).String())
continue
}
clusterName, err := names.GetClusterName(work.Namespace)
if err != nil {
klog.Errorf("Failed to get clusterName from work namespace %s. Error: %v.", work.Namespace, err)
return nil, err
}
status := &corev1.ResourceQuotaStatus{}
if err := json.Unmarshal(work.Status.ManifestStatuses[0].Status.Raw, status); err != nil {
klog.Errorf("Failed to unmarshal work(%s) status to ResourceQuotaStatus", klog.KObj(&work).String())
return nil, err
}
aggregatedStatus := policyv1alpha1.ClusterQuotaStatus{
ClusterName: clusterName,
ResourceQuotaStatus: *status,
}
aggregatedStatuses = append(aggregatedStatuses, aggregatedStatus)
}
sort.Slice(aggregatedStatuses, func(i, j int) bool {
return aggregatedStatuses[i].ClusterName < aggregatedStatuses[j].ClusterName
})
return aggregatedStatuses, nil
}
func calculateUsed(aggregatedStatuses []policyv1alpha1.ClusterQuotaStatus) corev1.ResourceList {
overallUsed := corev1.ResourceList{}
for index := range aggregatedStatuses {
used := aggregatedStatuses[index].Used
for resourceName, quantity := range used {
r, exist := overallUsed[resourceName]
if !exist {
overallUsed[resourceName] = quantity
} else {
r.Add(quantity)
overallUsed[resourceName] = r
}
}
}
return overallUsed
}