sigs.k8s.io/kueue@v0.6.2/pkg/controller/admissionchecks/multikueue/admissioncheck.go (about)

     1  /*
     2  Copyright 2024 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package multikueue
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  
    24  	"k8s.io/apimachinery/pkg/api/equality"
    25  	apimeta "k8s.io/apimachinery/pkg/api/meta"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/types"
    28  	"k8s.io/client-go/util/workqueue"
    29  	"k8s.io/klog/v2"
    30  	ctrl "sigs.k8s.io/controller-runtime"
    31  	"sigs.k8s.io/controller-runtime/pkg/client"
    32  	"sigs.k8s.io/controller-runtime/pkg/event"
    33  	"sigs.k8s.io/controller-runtime/pkg/handler"
    34  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    35  
    36  	kueuealpha "sigs.k8s.io/kueue/apis/kueue/v1alpha1"
    37  	kueue "sigs.k8s.io/kueue/apis/kueue/v1beta1"
    38  	"sigs.k8s.io/kueue/pkg/util/admissioncheck"
    39  )
    40  
    41  const (
    42  	ControllerName = "kueue.x-k8s.io/multikueue"
    43  )
    44  
    45  type multiKueueStoreHelper = admissioncheck.ConfigHelper[*kueuealpha.MultiKueueConfig, kueuealpha.MultiKueueConfig]
    46  
    47  func newMultiKueueStoreHelper(c client.Client) (*multiKueueStoreHelper, error) {
    48  	return admissioncheck.NewConfigHelper[*kueuealpha.MultiKueueConfig](c)
    49  }
    50  
    51  // ACReconciler implements the reconciler for all the admission checks controlled by multikueue.
    52  // Its main task being to maintain the active state of the admission checks based on the heath
    53  // of its referenced MultiKueueClusters.
    54  type ACReconciler struct {
    55  	client client.Client
    56  	helper *multiKueueStoreHelper
    57  }
    58  
    59  var _ reconcile.Reconciler = (*ACReconciler)(nil)
    60  
    61  func (a *ACReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
    62  	log := ctrl.LoggerFrom(ctx)
    63  	ac := &kueue.AdmissionCheck{}
    64  	if err := a.client.Get(ctx, req.NamespacedName, ac); err != nil || ac.Spec.ControllerName != ControllerName {
    65  		return reconcile.Result{}, client.IgnoreNotFound(err)
    66  	}
    67  
    68  	inactiveReason := ""
    69  
    70  	log.V(2).Info("Reconcile AdmissionCheck")
    71  	if cfg, err := a.helper.ConfigFromRef(ctx, ac.Spec.Parameters); err != nil {
    72  		inactiveReason = fmt.Sprintf("Cannot load the AdmissionChecks parameters: %s", err.Error())
    73  	} else {
    74  		var missingClusters []string
    75  		var inactiveClusters []string
    76  		// check the status of the clusters
    77  		for _, clusterName := range cfg.Spec.Clusters {
    78  			cluster := &kueuealpha.MultiKueueCluster{}
    79  			err := a.client.Get(ctx, types.NamespacedName{Name: clusterName}, cluster)
    80  			if client.IgnoreNotFound(err) != nil {
    81  				log.Error(err, "reading cluster", "multiKueueCluster", clusterName)
    82  				return reconcile.Result{}, err
    83  			}
    84  
    85  			if err != nil {
    86  				missingClusters = append(missingClusters, clusterName)
    87  			} else {
    88  				if !apimeta.IsStatusConditionTrue(cluster.Status.Conditions, kueuealpha.MultiKueueClusterActive) {
    89  					inactiveClusters = append(inactiveClusters, clusterName)
    90  				}
    91  			}
    92  		}
    93  
    94  		var messageParts []string
    95  		if len(missingClusters) > 0 {
    96  			messageParts = []string{fmt.Sprintf("Missing clusters: %v", missingClusters)}
    97  		}
    98  		if len(inactiveClusters) > 0 {
    99  			messageParts = append(messageParts, fmt.Sprintf("Inactive clusters: %v", inactiveClusters))
   100  		}
   101  		inactiveReason = strings.Join(messageParts, ", ")
   102  	}
   103  
   104  	newCondition := metav1.Condition{
   105  		Type: kueue.AdmissionCheckActive,
   106  	}
   107  	if len(inactiveReason) == 0 {
   108  		newCondition.Status = metav1.ConditionTrue
   109  		newCondition.Reason = "Active"
   110  		newCondition.Message = "The admission check is active"
   111  	} else {
   112  		newCondition.Status = metav1.ConditionFalse
   113  		newCondition.Reason = "Inactive"
   114  		newCondition.Message = inactiveReason
   115  	}
   116  
   117  	oldCondition := apimeta.FindStatusCondition(ac.Status.Conditions, kueue.AdmissionCheckActive)
   118  	if !cmpConditionState(oldCondition, &newCondition) {
   119  		apimeta.SetStatusCondition(&ac.Status.Conditions, newCondition)
   120  		err := a.client.Status().Update(ctx, ac)
   121  		if err != nil {
   122  			log.V(2).Error(err, "Updating check condition", "newCondition", newCondition)
   123  		}
   124  		return reconcile.Result{}, err
   125  	}
   126  
   127  	return reconcile.Result{}, nil
   128  }
   129  
   130  // +kubebuilder:rbac:groups="",resources=events,verbs=create;watch;update
   131  // +kubebuilder:rbac:groups=kueue.x-k8s.io,resources=workloads,verbs=get;list;watch;update;patch;delete
   132  // +kubebuilder:rbac:groups=kueue.x-k8s.io,resources=workloads/status,verbs=get;update;patch
   133  // +kubebuilder:rbac:groups=kueue.x-k8s.io,resources=admissionchecks,verbs=get;list;watch
   134  // +kubebuilder:rbac:groups=kueue.x-k8s.io,resources=multikueueconfigs,verbs=get;list;watch
   135  
   136  func newACReconciler(c client.Client, helper *multiKueueStoreHelper) *ACReconciler {
   137  	return &ACReconciler{
   138  		client: c,
   139  		helper: helper,
   140  	}
   141  }
   142  
   143  func (a *ACReconciler) setupWithManager(mgr ctrl.Manager) error {
   144  	return ctrl.NewControllerManagedBy(mgr).
   145  		For(&kueue.AdmissionCheck{}).
   146  		Watches(&kueuealpha.MultiKueueConfig{}, &mkConfigHandler{client: a.client}).
   147  		Watches(&kueuealpha.MultiKueueCluster{}, &mkClusterHandler{client: a.client}).
   148  		Complete(a)
   149  }
   150  
   151  type mkConfigHandler struct {
   152  	client client.Client
   153  }
   154  
   155  var _ handler.EventHandler = (*mkConfigHandler)(nil)
   156  
   157  func (m *mkConfigHandler) Create(ctx context.Context, event event.CreateEvent, q workqueue.RateLimitingInterface) {
   158  	mkc, isMKC := event.Object.(*kueuealpha.MultiKueueConfig)
   159  	if !isMKC {
   160  		return
   161  	}
   162  
   163  	if err := queueReconcileForConfigUsers(ctx, mkc.Name, m.client, q); err != nil {
   164  		ctrl.LoggerFrom(ctx).V(2).Error(err, "Failure on create event", "multiKueueConfig", klog.KObj(mkc))
   165  	}
   166  }
   167  
   168  func (m *mkConfigHandler) Update(ctx context.Context, event event.UpdateEvent, q workqueue.RateLimitingInterface) {
   169  	oldMKC, isOldMKC := event.ObjectOld.(*kueuealpha.MultiKueueConfig)
   170  	newMKC, isNewMKC := event.ObjectNew.(*kueuealpha.MultiKueueConfig)
   171  	if !isOldMKC || !isNewMKC || equality.Semantic.DeepEqual(oldMKC.Spec.Clusters, newMKC.Spec.Clusters) {
   172  		return
   173  	}
   174  
   175  	if err := queueReconcileForConfigUsers(ctx, oldMKC.Name, m.client, q); err != nil {
   176  		ctrl.LoggerFrom(ctx).V(2).Error(err, "Failure on update event", "multiKueueConfig", klog.KObj(oldMKC))
   177  	}
   178  }
   179  
   180  func (m *mkConfigHandler) Delete(ctx context.Context, event event.DeleteEvent, q workqueue.RateLimitingInterface) {
   181  	mkc, isMKC := event.Object.(*kueuealpha.MultiKueueConfig)
   182  	if !isMKC {
   183  		return
   184  	}
   185  
   186  	if err := queueReconcileForConfigUsers(ctx, mkc.Name, m.client, q); err != nil {
   187  		ctrl.LoggerFrom(ctx).V(2).Error(err, "Failure on delete event", "multiKueueConfig", klog.KObj(mkc))
   188  	}
   189  }
   190  
   191  func (m *mkConfigHandler) Generic(ctx context.Context, event event.GenericEvent, q workqueue.RateLimitingInterface) {
   192  	mkc, isMKC := event.Object.(*kueuealpha.MultiKueueConfig)
   193  	if !isMKC {
   194  		return
   195  	}
   196  
   197  	if err := queueReconcileForConfigUsers(ctx, mkc.Name, m.client, q); err != nil {
   198  		ctrl.LoggerFrom(ctx).V(2).Error(err, "Failure on generic event", "multiKueueConfig", klog.KObj(mkc))
   199  	}
   200  }
   201  
   202  func queueReconcileForConfigUsers(ctx context.Context, config string, c client.Client, q workqueue.RateLimitingInterface) error {
   203  	users := &kueue.AdmissionCheckList{}
   204  
   205  	if err := c.List(ctx, users, client.MatchingFields{AdmissionCheckUsingConfigKey: config}); err != nil {
   206  		return err
   207  	}
   208  
   209  	for _, user := range users.Items {
   210  		req := reconcile.Request{
   211  			NamespacedName: types.NamespacedName{
   212  				Name: user.Name,
   213  			},
   214  		}
   215  		q.Add(req)
   216  	}
   217  
   218  	return nil
   219  }
   220  
   221  type mkClusterHandler struct {
   222  	client client.Client
   223  }
   224  
   225  var _ handler.EventHandler = (*mkClusterHandler)(nil)
   226  
   227  func (m *mkClusterHandler) Create(ctx context.Context, event event.CreateEvent, q workqueue.RateLimitingInterface) {
   228  	mkc, isMKC := event.Object.(*kueuealpha.MultiKueueCluster)
   229  	if !isMKC {
   230  		return
   231  	}
   232  
   233  	if err := queueReconcileForConfigUsers(ctx, mkc.Name, m.client, q); err != nil {
   234  		ctrl.LoggerFrom(ctx).V(2).Error(err, "Failure on create event", "multiKueueCluster", klog.KObj(mkc))
   235  	}
   236  }
   237  
   238  func (m *mkClusterHandler) Update(ctx context.Context, event event.UpdateEvent, q workqueue.RateLimitingInterface) {
   239  	oldMKC, isOldMKC := event.ObjectOld.(*kueuealpha.MultiKueueCluster)
   240  	newMKC, isNewMKC := event.ObjectNew.(*kueuealpha.MultiKueueCluster)
   241  	if !isOldMKC || !isNewMKC {
   242  		return
   243  	}
   244  
   245  	oldActive := apimeta.FindStatusCondition(oldMKC.Status.Conditions, kueuealpha.MultiKueueClusterActive)
   246  	newActive := apimeta.FindStatusCondition(newMKC.Status.Conditions, kueuealpha.MultiKueueClusterActive)
   247  	if !cmpConditionState(oldActive, newActive) {
   248  		if err := m.queue(ctx, newMKC, q); err != nil {
   249  			ctrl.LoggerFrom(ctx).V(2).Error(err, "Failure on update event", "multiKueueCluster", klog.KObj(oldMKC))
   250  		}
   251  	}
   252  }
   253  
   254  func (m *mkClusterHandler) Delete(ctx context.Context, event event.DeleteEvent, q workqueue.RateLimitingInterface) {
   255  	mkc, isMKC := event.Object.(*kueuealpha.MultiKueueCluster)
   256  	if !isMKC {
   257  		return
   258  	}
   259  
   260  	if err := m.queue(ctx, mkc, q); err != nil {
   261  		ctrl.LoggerFrom(ctx).V(2).Error(err, "Failure on delete event", "multiKueueCluster", klog.KObj(mkc))
   262  	}
   263  }
   264  
   265  func (m *mkClusterHandler) Generic(ctx context.Context, event event.GenericEvent, q workqueue.RateLimitingInterface) {
   266  	mkc, isMKC := event.Object.(*kueuealpha.MultiKueueCluster)
   267  	if !isMKC {
   268  		return
   269  	}
   270  
   271  	if err := m.queue(ctx, mkc, q); err != nil {
   272  		ctrl.LoggerFrom(ctx).V(2).Error(err, "Failure on generic event", "multiKueueCluster", klog.KObj(mkc))
   273  	}
   274  }
   275  
   276  func (m *mkClusterHandler) queue(ctx context.Context, cluster *kueuealpha.MultiKueueCluster, q workqueue.RateLimitingInterface) error {
   277  	users := &kueuealpha.MultiKueueConfigList{}
   278  	if err := m.client.List(ctx, users, client.MatchingFields{UsingMultiKueueClusters: cluster.Name}); err != nil {
   279  		return err
   280  	}
   281  
   282  	for _, user := range users.Items {
   283  		if err := queueReconcileForConfigUsers(ctx, user.Name, m.client, q); err != nil {
   284  			return err
   285  		}
   286  	}
   287  	return nil
   288  }
   289  
   290  func cmpConditionState(a, b *metav1.Condition) bool {
   291  	if a == b {
   292  		return true
   293  	}
   294  	if a == nil || b == nil {
   295  		return false
   296  	}
   297  	return a.Status == b.Status && a.Reason == b.Reason && a.Message == b.Message
   298  }