pkg/controller/elasticquota.go (252 lines of code) (raw):
/*
Copyright 2021 The Kubernetes 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 controller
import (
"context"
"fmt"
"time"
v1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
apierrs "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
quota "k8s.io/apiserver/pkg/quota/v1"
utilfeature "k8s.io/apiserver/pkg/util/feature"
coreinformer "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
corelister "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
kubefeatures "k8s.io/kubernetes/pkg/features"
"sigs.k8s.io/scheduler-plugins/pkg/util"
schedv1alpha1 "sigs.k8s.io/scheduler-plugins/pkg/apis/scheduling/v1alpha1"
schedclientset "sigs.k8s.io/scheduler-plugins/pkg/generated/clientset/versioned"
schedinformer "sigs.k8s.io/scheduler-plugins/pkg/generated/informers/externalversions/scheduling/v1alpha1"
schedlister "sigs.k8s.io/scheduler-plugins/pkg/generated/listers/scheduling/v1alpha1"
)
// ElasticQuotaController is a controller that process elastic quota using provided Handler interface
type ElasticQuotaController struct {
// schedClient is a clientSet for SchedulingV1alpha1 API group
schedClient schedclientset.Interface
eqLister schedlister.ElasticQuotaLister
// podLister is lister for pod event and uses to compute namespaced resource used
podLister corelister.PodLister
eqListerSynced cache.InformerSynced
podListerSynced cache.InformerSynced
eqQueue workqueue.RateLimitingInterface
recorder record.EventRecorder
}
// NewElasticQuotaController returns a new *ElasticQuotaController
func NewElasticQuotaController(
kubeClient kubernetes.Interface,
eqInformer schedinformer.ElasticQuotaInformer,
podInformer coreinformer.PodInformer,
schedClient schedclientset.Interface,
newOpt ...func(ctrl *ElasticQuotaController),
) *ElasticQuotaController {
eventBroadcaster := record.NewBroadcaster()
eventBroadcaster.StartRecordingToSink(&corev1.EventSinkImpl{Interface: kubeClient.CoreV1().Events(v1.NamespaceAll)})
recorder := eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "ElasticQuotaController"})
// set up elastic quota ctrl
ctrl := &ElasticQuotaController{
schedClient: schedClient,
eqLister: eqInformer.Lister(),
podLister: podInformer.Lister(),
eqListerSynced: eqInformer.Informer().HasSynced,
podListerSynced: podInformer.Informer().HasSynced,
eqQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "ElasticQuota"),
recorder: recorder,
}
for _, f := range newOpt {
f(ctrl)
}
klog.V(5).Info("Setting up elastic quota event handlers")
eqInformer.Informer().AddEventHandler(
cache.ResourceEventHandlerFuncs{
AddFunc: ctrl.eqAdded,
UpdateFunc: ctrl.eqUpdated,
DeleteFunc: ctrl.eqDeleted,
},
)
klog.V(5).Info("Setting up pod event handlers")
podInformer.Informer().AddEventHandler(
cache.ResourceEventHandlerFuncs{
AddFunc: ctrl.podAdded,
UpdateFunc: ctrl.podUpdated,
DeleteFunc: ctrl.podDeleted,
},
)
return ctrl
}
func (ctrl *ElasticQuotaController) Run(workers int, stopCh <-chan struct{}) {
defer runtime.HandleCrash()
defer ctrl.eqQueue.ShutDown()
// Start the informer factories to begin populating the informer caches
klog.Info("Starting Elastic Quota control loop")
// Wait for the caches to be synced before starting workers
klog.Info("Waiting for informer caches to sync")
if ok := cache.WaitForCacheSync(stopCh, ctrl.eqListerSynced, ctrl.podListerSynced); !ok {
klog.Fatalf("Cannot sync caches")
}
klog.Info("Elastic Quota sync finished")
klog.V(5).Infof("Starting %d workers to process elastic quota", workers)
// Launch workers to process elastic quota resources
for i := 0; i < workers; i++ {
go wait.Until(ctrl.worker, time.Second, stopCh)
}
<-stopCh
klog.V(2).Info("Shutting down elastic quota workers")
}
// WithFakeRecorder will set a fake recorder.It is usually used for unit testing
func WithFakeRecorder(bufferSize int) func(ctrl *ElasticQuotaController) {
return func(ctrl *ElasticQuotaController) {
ctrl.recorder = record.NewFakeRecorder(bufferSize)
}
}
func (ctrl *ElasticQuotaController) worker() {
for ctrl.processNextWorkItem() {
}
}
// sync deals with one key off the queue. It returns false when it's time to quit.
func (ctrl *ElasticQuotaController) processNextWorkItem() bool {
keyObj, quit := ctrl.eqQueue.Get()
if quit {
return false
}
defer ctrl.eqQueue.Done(keyObj)
key, ok := keyObj.(string)
if !ok {
ctrl.eqQueue.Forget(keyObj)
runtime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", keyObj))
return true
}
if err := ctrl.syncHandler(key); err != nil {
runtime.HandleError(err)
klog.Errorf("error syncing elastic quota %q: %s", key, err.Error())
return true
}
ctrl.eqQueue.Forget(keyObj)
klog.V(5).Infof("Successfully synced %s", key)
return true
}
// syncHandler syncs elastic quota and convert status.used
func (ctrl *ElasticQuotaController) syncHandler(key string) error {
// Convert the namespace/name string into a distinct namespace and name
namespace, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
runtime.HandleError(fmt.Errorf("invalid resource key: %s", key))
return nil
}
// Get the elastic quota resource with this namespace/name
eq, err := ctrl.eqLister.ElasticQuotas(namespace).Get(name)
if apierrs.IsNotFound(err) {
klog.V(5).Infof("Elastic quota %q has been deleted ", key)
return nil
}
if err != nil {
klog.V(3).Infof("Unable to retrieve elastic quota %q from store: %v", key, err)
return err
}
klog.V(5).Infof("Try to process elastic quota: %q", key)
used, err := ctrl.computeElasticQuotaUsed(namespace, eq)
if err != nil {
return err
}
// create a usage object that is based on the elastic quota version that will handle updates
// by default, we set used to the current status
newEQ := eq.DeepCopy()
newEQ.Status.Used = used
// Ignore this loop if the usage value has not changed
if apiequality.Semantic.DeepEqual(newEQ.Status, eq.Status) {
return nil
}
patch, err := util.CreateMergePatch(eq, newEQ)
if err != nil {
return err
}
if _, err = ctrl.schedClient.SchedulingV1alpha1().ElasticQuotas(namespace).
Patch(context.TODO(), eq.Name, types.MergePatchType,
patch, metav1.PatchOptions{}); err != nil {
return err
}
ctrl.recorder.Event(eq, v1.EventTypeNormal, "Synced", fmt.Sprintf("Elastic Quota %s synced successfully", key))
return nil
}
func (ctrl *ElasticQuotaController) computeElasticQuotaUsed(namespace string, eq *schedv1alpha1.ElasticQuota) (v1.ResourceList, error) {
used := newZeroUsed(eq)
pods, err := ctrl.podLister.Pods(namespace).List(labels.NewSelector())
if err != nil {
return nil, err
}
for _, p := range pods {
if p.Status.Phase == v1.PodRunning {
used = quota.Add(used, computePodResourceRequest(p))
}
}
return used, nil
}
// eqAdded reacts to a ElasticQuota creation
func (ctrl *ElasticQuotaController) eqAdded(obj interface{}) {
key, err := cache.MetaNamespaceKeyFunc(obj)
if err != nil {
runtime.HandleError(err)
return
}
klog.V(5).Infof("Enqueue new elastic quota key %q", key)
ctrl.eqQueue.AddRateLimited(key)
}
// eqUpdated reacts to a ElasticQuota update
func (ctrl *ElasticQuotaController) eqUpdated(oldObj, newObj interface{}) {
ctrl.eqAdded(newObj)
}
// eqDeleted reacts to a ElasticQuota deletion
func (ctrl *ElasticQuotaController) eqDeleted(obj interface{}) {
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
if err != nil {
runtime.HandleError(err)
return
}
klog.V(5).Infof("Enqueue deleted elastic quota key %q", key)
ctrl.eqQueue.AddRateLimited(key)
}
// podAdded reacts to a pod creation
func (ctrl *ElasticQuotaController) podAdded(obj interface{}) {
pod := obj.(*v1.Pod)
list, err := ctrl.eqLister.ElasticQuotas(pod.Namespace).List(labels.Everything())
if err != nil {
runtime.HandleError(err)
return
}
if len(list) == 0 {
return
}
// todo(yuchen-sun) When elastic quota supports multiple instances in a namespace, modify this
ctrl.eqAdded(list[0])
}
// podUpdated reacts to a pod update
func (ctrl *ElasticQuotaController) podUpdated(oldObj, newObj interface{}) {
oldPod := oldObj.(*v1.Pod)
newPod := newObj.(*v1.Pod)
if newPod.ResourceVersion == oldPod.ResourceVersion {
return
}
ctrl.podAdded(newObj)
}
// podDeleted reacts to a pod delete
func (ctrl *ElasticQuotaController) podDeleted(obj interface{}) {
_, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
if err != nil {
runtime.HandleError(err)
return
}
ctrl.podAdded(obj)
}
// computePodResourceRequest returns a v1.ResourceList that covers the largest
// width in each resource dimension. Because init-containers run sequentially, we collect
// the max in each dimension iteratively. In contrast, we sum the resource vectors for
// regular containers since they run simultaneously.
//
// If Pod Overhead is specified and the feature gate is set, the resources defined for Overhead
// are added to the calculated Resource request sum
//
// Example:
//
// Pod:
// InitContainers
// IC1:
// CPU: 2
// Memory: 1G
// IC2:
// CPU: 2
// Memory: 3G
// Containers
// C1:
// CPU: 2
// Memory: 1G
// C2:
// CPU: 1
// Memory: 1G
//
// Result: CPU: 3, Memory: 3G
func computePodResourceRequest(pod *v1.Pod) v1.ResourceList {
result := v1.ResourceList{}
for _, container := range pod.Spec.Containers {
result = quota.Add(result, container.Resources.Requests)
}
initRes := v1.ResourceList{}
// take max_resource for init_containers
for _, container := range pod.Spec.InitContainers {
initRes = quota.Max(initRes, container.Resources.Requests)
}
// If Overhead is being utilized, add to the total requests for the pod
if pod.Spec.Overhead != nil && utilfeature.DefaultFeatureGate.Enabled(kubefeatures.PodOverhead) {
quota.Add(result, pod.Spec.Overhead)
}
// take max_resource for init_containers and containers
return quota.Max(result, initRes)
}
// newZeroUsed will return the zero value of the union of min and max
func newZeroUsed(eq *schedv1alpha1.ElasticQuota) v1.ResourceList {
minResources := quota.ResourceNames(eq.Spec.Min)
maxResources := quota.ResourceNames(eq.Spec.Max)
res := v1.ResourceList{}
for _, v := range minResources {
res[v] = *resource.NewQuantity(0, resource.DecimalSI)
}
for _, v := range maxResources {
res[v] = *resource.NewQuantity(0, resource.DecimalSI)
}
return res
}