sigs.k8s.io/kueue@v0.6.2/pkg/podset/podset.go (about)

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6      http://www.apache.org/licenses/LICENSE-2.0
     7  Unless required by applicable law or agreed to in writing, software
     8  distributed under the License is distributed on an "AS IS" BASIS,
     9  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    10  See the License for the specific language governing permissions and
    11  limitations under the License.
    12  */
    13  
    14  package podset
    15  
    16  import (
    17  	"context"
    18  	"errors"
    19  	"fmt"
    20  	"maps"
    21  	"slices"
    22  
    23  	corev1 "k8s.io/api/core/v1"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	"k8s.io/apimachinery/pkg/types"
    26  	"k8s.io/apimachinery/pkg/util/sets"
    27  	"k8s.io/utils/ptr"
    28  	"sigs.k8s.io/controller-runtime/pkg/client"
    29  
    30  	kueue "sigs.k8s.io/kueue/apis/kueue/v1beta1"
    31  	utilmaps "sigs.k8s.io/kueue/pkg/util/maps"
    32  )
    33  
    34  var (
    35  	ErrInvalidPodsetInfo   = errors.New("invalid podset infos")
    36  	ErrInvalidPodSetUpdate = errors.New("invalid admission check PodSetUpdate")
    37  )
    38  
    39  type PodSetInfo struct {
    40  	Name         string
    41  	Count        int32
    42  	Annotations  map[string]string
    43  	Labels       map[string]string
    44  	NodeSelector map[string]string
    45  	Tolerations  []corev1.Toleration
    46  }
    47  
    48  // FromAssignment returns a PodSetInfo based on the provided assignment and an error if unable
    49  // to get any of the referenced flavors.
    50  func FromAssignment(ctx context.Context, client client.Client, assignment *kueue.PodSetAssignment, defaultCount int32) (PodSetInfo, error) {
    51  	processedFlvs := sets.New[kueue.ResourceFlavorReference]()
    52  	info := PodSetInfo{
    53  		Name:         assignment.Name,
    54  		NodeSelector: make(map[string]string),
    55  		Count:        ptr.Deref(assignment.Count, defaultCount),
    56  		Labels:       make(map[string]string),
    57  		Annotations:  make(map[string]string),
    58  	}
    59  	for _, flvRef := range assignment.Flavors {
    60  		if processedFlvs.Has(flvRef) {
    61  			continue
    62  		}
    63  		// Lookup the ResourceFlavors to fetch the node affinity labels and toleration to apply on the job.
    64  		flv := kueue.ResourceFlavor{}
    65  		if err := client.Get(ctx, types.NamespacedName{Name: string(flvRef)}, &flv); err != nil {
    66  			return info, err
    67  		}
    68  		info.NodeSelector = utilmaps.MergeKeepFirst(info.NodeSelector, flv.Spec.NodeLabels)
    69  		info.Tolerations = append(info.Tolerations, flv.Spec.Tolerations...)
    70  
    71  		processedFlvs.Insert(flvRef)
    72  	}
    73  	return info, nil
    74  }
    75  
    76  // FromUpdate returns a PodSetInfo based on the provided PodSetUpdate
    77  func FromUpdate(update *kueue.PodSetUpdate) PodSetInfo {
    78  	return PodSetInfo{
    79  		Annotations:  update.Annotations,
    80  		Labels:       update.Labels,
    81  		NodeSelector: update.NodeSelector,
    82  		Tolerations:  update.Tolerations,
    83  	}
    84  }
    85  
    86  // FromPodSet returns a PodSeeInfo based on the provided PodSet
    87  func FromPodSet(ps *kueue.PodSet) PodSetInfo {
    88  	return PodSetInfo{
    89  		Name:         ps.Name,
    90  		Count:        ps.Count,
    91  		Annotations:  maps.Clone(ps.Template.Annotations),
    92  		Labels:       maps.Clone(ps.Template.Labels),
    93  		NodeSelector: maps.Clone(ps.Template.Spec.NodeSelector),
    94  		Tolerations:  slices.Clone(ps.Template.Spec.Tolerations),
    95  	}
    96  }
    97  
    98  func (podSetInfo *PodSetInfo) Merge(o PodSetInfo) error {
    99  	if err := utilmaps.HaveConflict(podSetInfo.Annotations, o.Annotations); err != nil {
   100  		return BadPodSetsUpdateError("annotations", err)
   101  	}
   102  	if err := utilmaps.HaveConflict(podSetInfo.Labels, o.Labels); err != nil {
   103  		return BadPodSetsUpdateError("labels", err)
   104  	}
   105  	if err := utilmaps.HaveConflict(podSetInfo.NodeSelector, o.NodeSelector); err != nil {
   106  		return BadPodSetsUpdateError("nodeSelector", err)
   107  	}
   108  	podSetInfo.Annotations = utilmaps.MergeKeepFirst(podSetInfo.Annotations, o.Annotations)
   109  	podSetInfo.Labels = utilmaps.MergeKeepFirst(podSetInfo.Labels, o.Labels)
   110  	podSetInfo.NodeSelector = utilmaps.MergeKeepFirst(podSetInfo.NodeSelector, o.NodeSelector)
   111  	podSetInfo.Tolerations = append(podSetInfo.Tolerations, o.Tolerations...)
   112  	return nil
   113  }
   114  
   115  // AddOrUpdateLabel adds or updates the label identified by k with value v
   116  // allocating a new Labels nap if nil
   117  func (podSetInfo *PodSetInfo) AddOrUpdateLabel(k, v string) {
   118  	if podSetInfo.Labels == nil {
   119  		podSetInfo.Labels = map[string]string{k: v}
   120  	} else {
   121  		podSetInfo.Labels[k] = v
   122  	}
   123  }
   124  
   125  // Merge updates or appends the replica metadata & spec fields based on PodSetInfo.
   126  // It returns error if there is a conflict.
   127  func Merge(meta *metav1.ObjectMeta, spec *corev1.PodSpec, info PodSetInfo) error {
   128  	tmp := PodSetInfo{
   129  		Annotations:  meta.Annotations,
   130  		Labels:       meta.Labels,
   131  		NodeSelector: spec.NodeSelector,
   132  		Tolerations:  spec.Tolerations,
   133  	}
   134  	if err := tmp.Merge(info); err != nil {
   135  		return err
   136  	}
   137  	meta.Annotations = tmp.Annotations
   138  	meta.Labels = tmp.Labels
   139  	spec.NodeSelector = tmp.NodeSelector
   140  	spec.Tolerations = tmp.Tolerations
   141  	return nil
   142  }
   143  
   144  // RestorePodSpec sets replica metadata and spec fields based on PodSetInfo.
   145  // It returns true if there is any change.
   146  func RestorePodSpec(meta *metav1.ObjectMeta, spec *corev1.PodSpec, info PodSetInfo) bool {
   147  	changed := false
   148  	if !maps.Equal(meta.Annotations, info.Annotations) {
   149  		meta.Annotations = maps.Clone(info.Annotations)
   150  		changed = true
   151  	}
   152  	if !maps.Equal(meta.Labels, info.Labels) {
   153  		meta.Labels = maps.Clone(info.Labels)
   154  		changed = true
   155  	}
   156  	if !maps.Equal(spec.NodeSelector, info.NodeSelector) {
   157  		spec.NodeSelector = maps.Clone(info.NodeSelector)
   158  		changed = true
   159  	}
   160  	if !slices.Equal(spec.Tolerations, info.Tolerations) {
   161  		spec.Tolerations = slices.Clone(info.Tolerations)
   162  		changed = true
   163  	}
   164  	return changed
   165  }
   166  
   167  func BadPodSetsInfoLenError(want, got int) error {
   168  	return fmt.Errorf("%w: expecting %d podset, got %d", ErrInvalidPodsetInfo, got, want)
   169  }
   170  
   171  func BadPodSetsUpdateError(update string, err error) error {
   172  	return fmt.Errorf("%w: conflict for %v: %v", ErrInvalidPodSetUpdate, update, err)
   173  }
   174  
   175  func IsPermanent(e error) bool {
   176  	return errors.Is(e, ErrInvalidPodsetInfo) || errors.Is(e, ErrInvalidPodSetUpdate)
   177  }