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

240 lines
8.3 KiB
Go

/*
Copyright 2022 The Karmada Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
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"
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 and metrics.
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{}, err
}
if !quota.DeletionTimestamp.IsZero() {
return controllerruntime.Result{}, nil
}
if err := c.collectQuotaStatus(ctx, 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{}, 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(_ context.Context, 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(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(event.DeleteEvent) bool {
return false
},
GenericFunc: func(event.GenericEvent) bool {
return false
},
})
return controllerruntime.NewControllerManagedBy(mgr).
Named(StatusControllerName).
For(&policyv1alpha1.FederatedResourceQuota{}).
Watches(&workv1alpha1.Work{}, handler.EnqueueRequestsFromMapFunc(fn), workPredicate).
Complete(c)
}
func (c *StatusController) collectQuotaStatus(ctx context.Context, quota *policyv1alpha1.FederatedResourceQuota) error {
workList, err := helper.GetWorksByLabelsSet(ctx, 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 {
_, err = helper.UpdateStatus(ctx, c.Client, quota, func() error {
quota.Status = *quotaStatus
return nil
})
return err
})
}
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
}