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/events" "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(ctx, 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 } if err := c.collectQuotaStatus(quota); err != nil { klog.Errorf("Failed to collect status from works to federatedResourceQuota(%s), error: %v", req.NamespacedName.String(), err) c.EventRecorder.Eventf(quota, corev1.EventTypeWarning, events.EventReasonCollectFederatedResourceQuotaStatusFailed, err.Error()) return controllerruntime.Result{Requeue: true}, err } c.EventRecorder.Eventf(quota, corev1.EventTypeNormal, events.EventReasonCollectFederatedResourceQuotaStatusSucceed, "Collect status of FederatedResourceQuota(%s) succeed.", req.NamespacedName.String()) 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 }