DRA scheduler: adapt to v1alpha3 API

The structured parameter allocation logic was written from scratch in
staging/src/k8s.io/dynamic-resource-allocation/structured where it might be
useful for out-of-tree components.

Besides the new features (amount, admin access) and API it now supports
backtracking when the initial device selection doesn't lead to a complete
allocation of all claims.

Co-authored-by: Ed Bartosh <eduard.bartosh@intel.com>
Co-authored-by: John Belamaric <jbelamaric@google.com>
This commit is contained in:
Patrick Ohly
2024-07-11 16:42:51 +02:00
parent 0fc78b9bcc
commit 599fe605f9
31 changed files with 2472 additions and 3115 deletions

View File

@@ -532,28 +532,10 @@ func addAllEventHandlers(
)
handlers = append(handlers, handlerRegistration)
}
case framework.ResourceClass:
case framework.DeviceClass:
if utilfeature.DefaultFeatureGate.Enabled(features.DynamicResourceAllocation) {
if handlerRegistration, err = informerFactory.Resource().V1alpha3().ResourceClasses().Informer().AddEventHandler(
buildEvtResHandler(at, framework.ResourceClass, "ResourceClass"),
); err != nil {
return err
}
handlers = append(handlers, handlerRegistration)
}
case framework.ResourceClaimParameters:
if utilfeature.DefaultFeatureGate.Enabled(features.DynamicResourceAllocation) {
if handlerRegistration, err = informerFactory.Resource().V1alpha3().ResourceClaimParameters().Informer().AddEventHandler(
buildEvtResHandler(at, framework.ResourceClaimParameters, "ResourceClaimParameters"),
); err != nil {
return err
}
handlers = append(handlers, handlerRegistration)
}
case framework.ResourceClassParameters:
if utilfeature.DefaultFeatureGate.Enabled(features.DynamicResourceAllocation) {
if handlerRegistration, err = informerFactory.Resource().V1alpha3().ResourceClassParameters().Informer().AddEventHandler(
buildEvtResHandler(at, framework.ResourceClassParameters, "ResourceClassParameters"),
if handlerRegistration, err = informerFactory.Resource().V1alpha3().DeviceClasses().Informer().AddEventHandler(
buildEvtResHandler(at, framework.DeviceClass, "DeviceClass"),
); err != nil {
return err
}

View File

@@ -232,11 +232,9 @@ func TestAddAllEventHandlers(t *testing.T) {
{
name: "DRA events disabled",
gvkMap: map[framework.GVK]framework.ActionType{
framework.PodSchedulingContext: framework.Add,
framework.ResourceClaim: framework.Add,
framework.ResourceClass: framework.Add,
framework.ResourceClaimParameters: framework.Add,
framework.ResourceClassParameters: framework.Add,
framework.PodSchedulingContext: framework.Add,
framework.ResourceClaim: framework.Add,
framework.DeviceClass: framework.Add,
},
expectStaticInformers: map[reflect.Type]bool{
reflect.TypeOf(&v1.Pod{}): true,
@@ -248,22 +246,18 @@ func TestAddAllEventHandlers(t *testing.T) {
{
name: "DRA events enabled",
gvkMap: map[framework.GVK]framework.ActionType{
framework.PodSchedulingContext: framework.Add,
framework.ResourceClaim: framework.Add,
framework.ResourceClass: framework.Add,
framework.ResourceClaimParameters: framework.Add,
framework.ResourceClassParameters: framework.Add,
framework.PodSchedulingContext: framework.Add,
framework.ResourceClaim: framework.Add,
framework.DeviceClass: framework.Add,
},
enableDRA: true,
expectStaticInformers: map[reflect.Type]bool{
reflect.TypeOf(&v1.Pod{}): true,
reflect.TypeOf(&v1.Node{}): true,
reflect.TypeOf(&v1.Namespace{}): true,
reflect.TypeOf(&resourceapi.PodSchedulingContext{}): true,
reflect.TypeOf(&resourceapi.ResourceClaim{}): true,
reflect.TypeOf(&resourceapi.ResourceClaimParameters{}): true,
reflect.TypeOf(&resourceapi.ResourceClass{}): true,
reflect.TypeOf(&resourceapi.ResourceClassParameters{}): true,
reflect.TypeOf(&v1.Pod{}): true,
reflect.TypeOf(&v1.Node{}): true,
reflect.TypeOf(&v1.Namespace{}): true,
reflect.TypeOf(&resourceapi.PodSchedulingContext{}): true,
reflect.TypeOf(&resourceapi.ResourceClaim{}): true,
reflect.TypeOf(&resourceapi.DeviceClass{}): true,
},
expectDynamicInformers: map[schema.GroupVersionResource]bool{},
},

View File

@@ -38,10 +38,10 @@ import (
resourceapiapply "k8s.io/client-go/applyconfigurations/resource/v1alpha3"
"k8s.io/client-go/kubernetes"
resourcelisters "k8s.io/client-go/listers/resource/v1alpha3"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/retry"
"k8s.io/component-helpers/scheduling/corev1/nodeaffinity"
"k8s.io/dynamic-resource-allocation/resourceclaim"
"k8s.io/dynamic-resource-allocation/structured"
"k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/scheduler/framework"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/feature"
@@ -56,10 +56,6 @@ const (
Name = names.DynamicResources
stateKey framework.StateKey = Name
// generatedFromIndex is the lookup name for the index function
// which indexes by other resource which generated the parameters object.
generatedFromIndex = "generated-from-index"
)
// The state is initialized in PreFilter phase. Because we save the pointer in
@@ -82,9 +78,8 @@ type stateData struct {
// (if one exists) and the changes made to it.
podSchedulingState podSchedulingState
// resourceModel contains the information about available and allocated resources when using
// structured parameters and the pod needs this information.
resources resources
// Allocator handles claims with structured parameters.
allocator *structured.Allocator
// mutex must be locked while accessing any of the fields below.
mutex sync.Mutex
@@ -99,6 +94,9 @@ type stateData struct {
unavailableClaims sets.Set[int]
informationsForClaim []informationForClaim
// nodeAllocations caches the result of Filter for the nodes.
nodeAllocations map[string][]*resourceapi.AllocationResult
}
func (d *stateData) Clone() framework.StateData {
@@ -106,24 +104,20 @@ func (d *stateData) Clone() framework.StateData {
}
type informationForClaim struct {
// The availableOnNode node filter of the claim converted from the
// v1 API to nodeaffinity.NodeSelector by PreFilter for repeated
// evaluation in Filter. Nil for claim which don't have it.
availableOnNode *nodeaffinity.NodeSelector
// Node selectors based on the claim status (single entry, key is empty) if allocated,
// otherwise the device class AvailableOnNodes selectors (potentially multiple entries,
// key is the device class name).
availableOnNodes map[string]*nodeaffinity.NodeSelector
// The status of the claim got from the
// schedulingCtx by PreFilter for repeated
// evaluation in Filter. Nil for claim which don't have it.
status *resourceapi.ResourceClaimSchedulingStatus
// structuredParameters is true if the claim is handled via the builtin
// controller.
structuredParameters bool
controller *claimController
// Set by Reserved, published by PreBind.
allocation *resourceapi.AllocationResult
allocationDriverName string
allocation *resourceapi.AllocationResult
}
type podSchedulingState struct {
@@ -276,19 +270,9 @@ type dynamicResources struct {
enabled bool
fh framework.Handle
clientset kubernetes.Interface
classLister resourcelisters.ResourceClassLister
classLister resourcelisters.DeviceClassLister
podSchedulingContextLister resourcelisters.PodSchedulingContextLister
claimParametersLister resourcelisters.ResourceClaimParametersLister
classParametersLister resourcelisters.ResourceClassParametersLister
resourceSliceLister resourcelisters.ResourceSliceLister
claimNameLookup *resourceclaim.Lookup
// claimParametersIndexer has the common claimParametersGeneratedFrom indexer installed to
// limit iteration over claimParameters to those of interest.
claimParametersIndexer cache.Indexer
// classParametersIndexer has the common classParametersGeneratedFrom indexer installed to
// limit iteration over classParameters to those of interest.
classParametersIndexer cache.Indexer
sliceLister resourcelisters.ResourceSliceLister
// claimAssumeCache enables temporarily storing a newer claim object
// while the scheduler has allocated it and the corresponding object
@@ -357,61 +341,15 @@ func New(ctx context.Context, plArgs runtime.Object, fh framework.Handle, fts fe
enabled: true,
fh: fh,
clientset: fh.ClientSet(),
classLister: fh.SharedInformerFactory().Resource().V1alpha3().ResourceClasses().Lister(),
classLister: fh.SharedInformerFactory().Resource().V1alpha3().DeviceClasses().Lister(),
podSchedulingContextLister: fh.SharedInformerFactory().Resource().V1alpha3().PodSchedulingContexts().Lister(),
claimParametersLister: fh.SharedInformerFactory().Resource().V1alpha3().ResourceClaimParameters().Lister(),
claimParametersIndexer: fh.SharedInformerFactory().Resource().V1alpha3().ResourceClaimParameters().Informer().GetIndexer(),
classParametersLister: fh.SharedInformerFactory().Resource().V1alpha3().ResourceClassParameters().Lister(),
classParametersIndexer: fh.SharedInformerFactory().Resource().V1alpha3().ResourceClassParameters().Informer().GetIndexer(),
resourceSliceLister: fh.SharedInformerFactory().Resource().V1alpha3().ResourceSlices().Lister(),
claimNameLookup: resourceclaim.NewNameLookup(fh.ClientSet()),
sliceLister: fh.SharedInformerFactory().Resource().V1alpha3().ResourceSlices().Lister(),
claimAssumeCache: fh.ResourceClaimCache(),
}
if err := pl.claimParametersIndexer.AddIndexers(cache.Indexers{generatedFromIndex: claimParametersGeneratedFromIndexFunc}); err != nil {
return nil, fmt.Errorf("add claim parameters cache indexer: %w", err)
}
if err := pl.classParametersIndexer.AddIndexers(cache.Indexers{generatedFromIndex: classParametersGeneratedFromIndexFunc}); err != nil {
return nil, fmt.Errorf("add class parameters cache indexer: %w", err)
}
return pl, nil
}
func claimParametersReferenceKeyFunc(namespace string, ref *resourceapi.ResourceClaimParametersReference) string {
return ref.APIGroup + "/" + ref.Kind + "/" + namespace + "/" + ref.Name
}
// claimParametersGeneratedFromIndexFunc is an index function that returns other resource keys
// (= apiGroup/kind/namespace/name) for ResourceClaimParametersReference in a given claim parameters.
func claimParametersGeneratedFromIndexFunc(obj interface{}) ([]string, error) {
parameters, ok := obj.(*resourceapi.ResourceClaimParameters)
if !ok {
return nil, nil
}
if parameters.GeneratedFrom == nil {
return nil, nil
}
return []string{claimParametersReferenceKeyFunc(parameters.Namespace, parameters.GeneratedFrom)}, nil
}
func classParametersReferenceKeyFunc(ref *resourceapi.ResourceClassParametersReference) string {
return ref.APIGroup + "/" + ref.Kind + "/" + ref.Namespace + "/" + ref.Name
}
// classParametersGeneratedFromIndexFunc is an index function that returns other resource keys
// (= apiGroup/kind/namespace/name) for ResourceClassParametersReference in a given class parameters.
func classParametersGeneratedFromIndexFunc(obj interface{}) ([]string, error) {
parameters, ok := obj.(*resourceapi.ResourceClassParameters)
if !ok {
return nil, nil
}
if parameters.GeneratedFrom == nil {
return nil, nil
}
return []string{classParametersReferenceKeyFunc(parameters.GeneratedFrom)}, nil
}
var _ framework.PreEnqueuePlugin = &dynamicResources{}
var _ framework.PreFilterPlugin = &dynamicResources{}
var _ framework.FilterPlugin = &dynamicResources{}
@@ -435,11 +373,6 @@ func (pl *dynamicResources) EventsToRegister(_ context.Context) ([]framework.Clu
}
events := []framework.ClusterEventWithHint{
// Changes for claim or class parameters creation may make pods
// schedulable which depend on claims using those parameters.
{Event: framework.ClusterEvent{Resource: framework.ResourceClaimParameters, ActionType: framework.Add | framework.Update}, QueueingHintFn: pl.isSchedulableAfterClaimParametersChange},
{Event: framework.ClusterEvent{Resource: framework.ResourceClassParameters, ActionType: framework.Add | framework.Update}, QueueingHintFn: pl.isSchedulableAfterClassParametersChange},
// Allocation is tracked in ResourceClaims, so any changes may make the pods schedulable.
{Event: framework.ClusterEvent{Resource: framework.ResourceClaim, ActionType: framework.Add | framework.Update}, QueueingHintFn: pl.isSchedulableAfterClaimChange},
// When a driver has provided additional information, a pod waiting for that information
@@ -458,7 +391,7 @@ func (pl *dynamicResources) EventsToRegister(_ context.Context) ([]framework.Clu
// See: https://github.com/kubernetes/kubernetes/issues/110175
{Event: framework.ClusterEvent{Resource: framework.Node, ActionType: framework.Add | framework.UpdateNodeLabel | framework.UpdateNodeTaint}},
// A pod might be waiting for a class to get created or modified.
{Event: framework.ClusterEvent{Resource: framework.ResourceClass, ActionType: framework.Add | framework.Update}},
{Event: framework.ClusterEvent{Resource: framework.DeviceClass, ActionType: framework.Add | framework.Update}},
}
return events, nil
}
@@ -473,149 +406,6 @@ func (pl *dynamicResources) PreEnqueue(ctx context.Context, pod *v1.Pod) (status
return nil
}
// isSchedulableAfterClaimParametersChange is invoked for add and update claim parameters events reported by
// an informer. It checks whether that change made a previously unschedulable
// pod schedulable. It errs on the side of letting a pod scheduling attempt
// happen. The delete claim event will not invoke it, so newObj will never be nil.
func (pl *dynamicResources) isSchedulableAfterClaimParametersChange(logger klog.Logger, pod *v1.Pod, oldObj, newObj interface{}) (framework.QueueingHint, error) {
originalParameters, modifiedParameters, err := schedutil.As[*resourceapi.ResourceClaimParameters](oldObj, newObj)
if err != nil {
// Shouldn't happen.
return framework.Queue, fmt.Errorf("unexpected object in isSchedulableAfterClaimParametersChange: %w", err)
}
usesParameters := false
if err := pl.foreachPodResourceClaim(pod, func(_ string, claim *resourceapi.ResourceClaim) {
ref := claim.Spec.ParametersRef
if ref == nil {
return
}
// Using in-tree parameters directly?
if ref.APIGroup == resourceapi.SchemeGroupVersion.Group &&
ref.Kind == "ResourceClaimParameters" {
if modifiedParameters.Name == ref.Name {
usesParameters = true
}
return
}
// Need to look for translated parameters.
generatedFrom := modifiedParameters.GeneratedFrom
if generatedFrom == nil {
return
}
if generatedFrom.APIGroup == ref.APIGroup &&
generatedFrom.Kind == ref.Kind &&
generatedFrom.Name == ref.Name {
usesParameters = true
}
}); err != nil {
// This is not an unexpected error: we know that
// foreachPodResourceClaim only returns errors for "not
// schedulable".
logger.V(4).Info("pod is not schedulable", "pod", klog.KObj(pod), "claim", klog.KObj(modifiedParameters), "reason", err.Error())
return framework.QueueSkip, nil
}
if !usesParameters {
// This were not the parameters the pod was waiting for.
logger.V(6).Info("unrelated claim parameters got modified", "pod", klog.KObj(pod), "claimParameters", klog.KObj(modifiedParameters))
return framework.QueueSkip, nil
}
if originalParameters == nil {
logger.V(4).Info("claim parameters for pod got created", "pod", klog.KObj(pod), "claimParameters", klog.KObj(modifiedParameters))
return framework.Queue, nil
}
// Modifications may or may not be relevant. If the entire
// requests are as before, then something else must have changed
// and we don't care.
if apiequality.Semantic.DeepEqual(&originalParameters.DriverRequests, &modifiedParameters.DriverRequests) {
logger.V(6).Info("claim parameters for pod got modified where the pod doesn't care", "pod", klog.KObj(pod), "claimParameters", klog.KObj(modifiedParameters))
return framework.QueueSkip, nil
}
logger.V(4).Info("requests in claim parameters for pod got updated", "pod", klog.KObj(pod), "claimParameters", klog.KObj(modifiedParameters))
return framework.Queue, nil
}
// isSchedulableAfterClassParametersChange is invoked for add and update class parameters events reported by
// an informer. It checks whether that change made a previously unschedulable
// pod schedulable. It errs on the side of letting a pod scheduling attempt
// happen. The delete class event will not invoke it, so newObj will never be nil.
func (pl *dynamicResources) isSchedulableAfterClassParametersChange(logger klog.Logger, pod *v1.Pod, oldObj, newObj interface{}) (framework.QueueingHint, error) {
originalParameters, modifiedParameters, err := schedutil.As[*resourceapi.ResourceClassParameters](oldObj, newObj)
if err != nil {
// Shouldn't happen.
return framework.Queue, fmt.Errorf("unexpected object in isSchedulableAfterClassParametersChange: %w", err)
}
usesParameters := false
if err := pl.foreachPodResourceClaim(pod, func(_ string, claim *resourceapi.ResourceClaim) {
class, err := pl.classLister.Get(claim.Spec.ResourceClassName)
if err != nil {
if !apierrors.IsNotFound(err) {
logger.Error(err, "look up resource class")
}
return
}
ref := class.ParametersRef
if ref == nil {
return
}
// Using in-tree parameters directly?
if ref.APIGroup == resourceapi.SchemeGroupVersion.Group &&
ref.Kind == "ResourceClassParameters" {
if modifiedParameters.Name == ref.Name {
usesParameters = true
}
return
}
// Need to look for translated parameters.
generatedFrom := modifiedParameters.GeneratedFrom
if generatedFrom == nil {
return
}
if generatedFrom.APIGroup == ref.APIGroup &&
generatedFrom.Kind == ref.Kind &&
generatedFrom.Name == ref.Name {
usesParameters = true
}
}); err != nil {
// This is not an unexpected error: we know that
// foreachPodResourceClaim only returns errors for "not
// schedulable".
logger.V(4).Info("pod is not schedulable", "pod", klog.KObj(pod), "classParameters", klog.KObj(modifiedParameters), "reason", err.Error())
return framework.QueueSkip, nil
}
if !usesParameters {
// This were not the parameters the pod was waiting for.
logger.V(6).Info("unrelated class parameters got modified", "pod", klog.KObj(pod), "classParameters", klog.KObj(modifiedParameters))
return framework.QueueSkip, nil
}
if originalParameters == nil {
logger.V(4).Info("class parameters for pod got created", "pod", klog.KObj(pod), "class", klog.KObj(modifiedParameters))
return framework.Queue, nil
}
// Modifications may or may not be relevant. If the entire
// requests are as before, then something else must have changed
// and we don't care.
if apiequality.Semantic.DeepEqual(&originalParameters.Filters, &modifiedParameters.Filters) {
logger.V(6).Info("class parameters for pod got modified where the pod doesn't care", "pod", klog.KObj(pod), "classParameters", klog.KObj(modifiedParameters))
return framework.QueueSkip, nil
}
logger.V(4).Info("filters in class parameters for pod got updated", "pod", klog.KObj(pod), "classParameters", klog.KObj(modifiedParameters))
return framework.Queue, nil
}
// isSchedulableAfterClaimChange is invoked for add and update claim events reported by
// an informer. It checks whether that change made a previously unschedulable
// pod schedulable. It errs on the side of letting a pod scheduling attempt
@@ -641,7 +431,8 @@ func (pl *dynamicResources) isSchedulableAfterClaimChange(logger klog.Logger, po
}
if originalClaim != nil &&
resourceclaim.IsAllocatedWithStructuredParameters(originalClaim) &&
originalClaim.Status.Allocation != nil &&
originalClaim.Status.Allocation.Controller == "" &&
modifiedClaim.Status.Allocation == nil {
// A claim with structured parameters was deallocated. This might have made
// resources available for other pods.
@@ -823,7 +614,7 @@ func (pl *dynamicResources) podResourceClaims(pod *v1.Pod) ([]*resourceapi.Resou
// It calls an optional handler for those claims that it finds.
func (pl *dynamicResources) foreachPodResourceClaim(pod *v1.Pod, cb func(podResourceName string, claim *resourceapi.ResourceClaim)) error {
for _, resource := range pod.Spec.ResourceClaims {
claimName, mustCheckOwner, err := pl.claimNameLookup.Name(pod, &resource)
claimName, mustCheckOwner, err := resourceclaim.Name(pod, &resource)
if err != nil {
return err
}
@@ -892,8 +683,10 @@ func (pl *dynamicResources) PreFilter(ctx context.Context, state *framework.Cycl
return nil, statusError(logger, err)
}
// All claims which the scheduler needs to allocate itself.
allocateClaims := make([]*resourceapi.ResourceClaim, 0, len(claims))
s.informationsForClaim = make([]informationForClaim, len(claims))
needResourceInformation := false
for index, claim := range claims {
if claim.Status.DeallocationRequested {
// This will get resolved by the resource driver.
@@ -907,44 +700,19 @@ func (pl *dynamicResources) PreFilter(ctx context.Context, state *framework.Cycl
}
if claim.Status.Allocation != nil {
if claim.Status.Allocation.AvailableOnNodes != nil {
nodeSelector, err := nodeaffinity.NewNodeSelector(claim.Status.Allocation.AvailableOnNodes)
s.informationsForClaim[index].structuredParameters = claim.Status.Allocation.Controller == ""
if claim.Status.Allocation.NodeSelector != nil {
nodeSelector, err := nodeaffinity.NewNodeSelector(claim.Status.Allocation.NodeSelector)
if err != nil {
return nil, statusError(logger, err)
}
s.informationsForClaim[index].availableOnNode = nodeSelector
s.informationsForClaim[index].availableOnNodes = map[string]*nodeaffinity.NodeSelector{"": nodeSelector}
}
// The claim was allocated by the scheduler if it has the finalizer that is
// reserved for Kubernetes.
s.informationsForClaim[index].structuredParameters = slices.Contains(claim.Finalizers, resourceapi.Finalizer)
} else {
// The ResourceClass might have a node filter. This is
// useful for trimming the initial set of potential
// nodes before we ask the driver(s) for information
// about the specific pod.
class, err := pl.classLister.Get(claim.Spec.ResourceClassName)
if err != nil {
// If the class cannot be retrieved, allocation cannot proceed.
if apierrors.IsNotFound(err) {
// Here we mark the pod as "unschedulable", so it'll sleep in
// the unscheduleable queue until a ResourceClass event occurs.
return nil, statusUnschedulable(logger, fmt.Sprintf("resource class %s does not exist", claim.Spec.ResourceClassName))
}
// Other error, retry with backoff.
return nil, statusError(logger, fmt.Errorf("look up resource class: %v", err))
}
if class.SuitableNodes != nil {
selector, err := nodeaffinity.NewNodeSelector(class.SuitableNodes)
if err != nil {
return nil, statusError(logger, err)
}
s.informationsForClaim[index].availableOnNode = selector
}
s.informationsForClaim[index].status = statusForClaim(s.podSchedulingState.schedulingCtx, pod.Spec.ResourceClaims[index].Name)
if class.StructuredParameters != nil && *class.StructuredParameters {
s.informationsForClaim[index].structuredParameters = true
structuredParameters := claim.Spec.Controller == ""
s.informationsForClaim[index].structuredParameters = structuredParameters
if structuredParameters {
allocateClaims = append(allocateClaims, claim)
// Allocation in flight? Better wait for that
// to finish, see inFlightAllocations
@@ -952,164 +720,93 @@ func (pl *dynamicResources) PreFilter(ctx context.Context, state *framework.Cycl
if _, found := pl.inFlightAllocations.Load(claim.UID); found {
return nil, statusUnschedulable(logger, fmt.Sprintf("resource claim %s is in the process of being allocated", klog.KObj(claim)))
}
} else {
s.informationsForClaim[index].status = statusForClaim(s.podSchedulingState.schedulingCtx, pod.Spec.ResourceClaims[index].Name)
}
// We need the claim and class parameters. If
// they don't exist yet, the pod has to wait.
//
// TODO (https://github.com/kubernetes/kubernetes/issues/123697):
// check this already in foreachPodResourceClaim, together with setting up informationsForClaim.
// Then PreEnqueue will also check for existence of parameters.
classParameters, claimParameters, status := pl.lookupParameters(logger, class, claim)
if status != nil {
return nil, status
// Check all requests and device classes. If a class
// does not exist, scheduling cannot proceed, no matter
// how the claim is being allocated.
//
// When using a control plane controller, a class might
// have a node filter. This is useful for trimming the
// initial set of potential nodes before we ask the
// driver(s) for information about the specific pod.
for _, request := range claim.Spec.Devices.Requests {
if request.DeviceClassName == "" {
return nil, statusError(logger, fmt.Errorf("request %s: unsupported request type", request.Name))
}
controller, err := newClaimController(logger, class, classParameters, claimParameters)
class, err := pl.classLister.Get(request.DeviceClassName)
if err != nil {
return nil, statusError(logger, err)
// If the class cannot be retrieved, allocation cannot proceed.
if apierrors.IsNotFound(err) {
// Here we mark the pod as "unschedulable", so it'll sleep in
// the unscheduleable queue until a DeviceClass event occurs.
return nil, statusUnschedulable(logger, fmt.Sprintf("request %s: device class %s does not exist", request.Name, request.DeviceClassName))
}
// Other error, retry with backoff.
return nil, statusError(logger, fmt.Errorf("request %s: look up device class: %w", request.Name, err))
}
if class.Spec.SuitableNodes != nil && !structuredParameters {
selector, err := nodeaffinity.NewNodeSelector(class.Spec.SuitableNodes)
if err != nil {
return nil, statusError(logger, err)
}
if s.informationsForClaim[index].availableOnNodes == nil {
s.informationsForClaim[index].availableOnNodes = make(map[string]*nodeaffinity.NodeSelector)
}
s.informationsForClaim[index].availableOnNodes[class.Name] = selector
}
s.informationsForClaim[index].controller = controller
needResourceInformation = true
}
}
}
if needResourceInformation {
if len(allocateClaims) > 0 {
logger.V(5).Info("Preparing allocation with structured parameters", "pod", klog.KObj(pod), "resourceclaims", klog.KObjSlice(allocateClaims))
// Doing this over and over again for each pod could be avoided
// by parsing once when creating the plugin and then updating
// that state in informer callbacks. But that would cause
// problems for using the plugin in the Cluster Autoscaler. If
// this step here turns out to be expensive, we may have to
// maintain and update state more persistently.
// by setting the allocator up once and then keeping it up-to-date
// as changes are observed.
//
// But that would cause problems for using the plugin in the
// Cluster Autoscaler. If this step here turns out to be
// expensive, we may have to maintain and update state more
// persistently.
//
// Claims are treated as "allocated" if they are in the assume cache
// or currently their allocation is in-flight.
resources, err := newResourceModel(logger, pl.resourceSliceLister, pl.claimAssumeCache, &pl.inFlightAllocations)
logger.V(5).Info("Resource usage", "resources", klog.Format(resources))
allocator, err := structured.NewAllocator(ctx, allocateClaims, &claimListerForAssumeCache{assumeCache: pl.claimAssumeCache, inFlightAllocations: &pl.inFlightAllocations}, pl.classLister, pl.sliceLister)
if err != nil {
return nil, statusError(logger, err)
}
s.resources = resources
s.allocator = allocator
s.nodeAllocations = make(map[string][]*resourceapi.AllocationResult)
}
s.claims = claims
return nil, nil
}
func (pl *dynamicResources) lookupParameters(logger klog.Logger, class *resourceapi.ResourceClass, claim *resourceapi.ResourceClaim) (classParameters *resourceapi.ResourceClassParameters, claimParameters *resourceapi.ResourceClaimParameters, status *framework.Status) {
classParameters, status = pl.lookupClassParameters(logger, class)
if status != nil {
return
}
claimParameters, status = pl.lookupClaimParameters(logger, class, claim)
return
type claimListerForAssumeCache struct {
assumeCache *assumecache.AssumeCache
inFlightAllocations *sync.Map
}
func (pl *dynamicResources) lookupClassParameters(logger klog.Logger, class *resourceapi.ResourceClass) (*resourceapi.ResourceClassParameters, *framework.Status) {
defaultClassParameters := resourceapi.ResourceClassParameters{}
if class.ParametersRef == nil {
return &defaultClassParameters, nil
}
if class.ParametersRef.APIGroup == resourceapi.SchemeGroupVersion.Group &&
class.ParametersRef.Kind == "ResourceClassParameters" {
// Use the parameters which were referenced directly.
parameters, err := pl.classParametersLister.ResourceClassParameters(class.ParametersRef.Namespace).Get(class.ParametersRef.Name)
if err != nil {
if apierrors.IsNotFound(err) {
return nil, statusUnschedulable(logger, fmt.Sprintf("class parameters %s not found", klog.KRef(class.ParametersRef.Namespace, class.ParametersRef.Name)))
}
return nil, statusError(logger, fmt.Errorf("get class parameters %s: %v", klog.KRef(class.Namespace, class.ParametersRef.Name), err))
func (cl *claimListerForAssumeCache) ListAllAllocated() ([]*resourceapi.ResourceClaim, error) {
// Probably not worth adding an index for?
objs := cl.assumeCache.List(nil)
allocated := make([]*resourceapi.ResourceClaim, 0, len(objs))
for _, obj := range objs {
claim := obj.(*resourceapi.ResourceClaim)
if obj, ok := cl.inFlightAllocations.Load(claim.UID); ok {
claim = obj.(*resourceapi.ResourceClaim)
}
return parameters, nil
}
objs, err := pl.classParametersIndexer.ByIndex(generatedFromIndex, classParametersReferenceKeyFunc(class.ParametersRef))
if err != nil {
return nil, statusError(logger, fmt.Errorf("listing class parameters failed: %v", err))
}
switch len(objs) {
case 0:
return nil, statusUnschedulable(logger, fmt.Sprintf("generated class parameters for %s.%s %s not found", class.ParametersRef.Kind, class.ParametersRef.APIGroup, klog.KRef(class.ParametersRef.Namespace, class.ParametersRef.Name)))
case 1:
parameters, ok := objs[0].(*resourceapi.ResourceClassParameters)
if !ok {
return nil, statusError(logger, fmt.Errorf("unexpected object in class parameters index: %T", objs[0]))
if claim.Status.Allocation != nil {
allocated = append(allocated, claim)
}
return parameters, nil
default:
sort.Slice(objs, func(i, j int) bool {
obj1, obj2 := objs[i].(*resourceapi.ResourceClassParameters), objs[j].(*resourceapi.ResourceClassParameters)
if obj1 == nil || obj2 == nil {
return false
}
return obj1.Name < obj2.Name
})
return nil, statusError(logger, fmt.Errorf("multiple generated class parameters for %s.%s %s found: %s", class.ParametersRef.Kind, class.ParametersRef.APIGroup, klog.KRef(class.Namespace, class.ParametersRef.Name), klog.KObjSlice(objs)))
}
}
func (pl *dynamicResources) lookupClaimParameters(logger klog.Logger, class *resourceapi.ResourceClass, claim *resourceapi.ResourceClaim) (*resourceapi.ResourceClaimParameters, *framework.Status) {
defaultClaimParameters := resourceapi.ResourceClaimParameters{
DriverRequests: []resourceapi.DriverRequests{
{
DriverName: class.DriverName,
Requests: []resourceapi.ResourceRequest{
{
ResourceRequestModel: resourceapi.ResourceRequestModel{
// TODO: This only works because NamedResources is
// the only model currently implemented. We need to
// match the default to how the resources of this
// class are being advertized in a ResourceSlice.
NamedResources: &resourceapi.NamedResourcesRequest{
Selector: "true",
},
},
},
},
},
},
}
if claim.Spec.ParametersRef == nil {
return &defaultClaimParameters, nil
}
if claim.Spec.ParametersRef.APIGroup == resourceapi.SchemeGroupVersion.Group &&
claim.Spec.ParametersRef.Kind == "ResourceClaimParameters" {
// Use the parameters which were referenced directly.
parameters, err := pl.claimParametersLister.ResourceClaimParameters(claim.Namespace).Get(claim.Spec.ParametersRef.Name)
if err != nil {
if apierrors.IsNotFound(err) {
return nil, statusUnschedulable(logger, fmt.Sprintf("claim parameters %s not found", klog.KRef(claim.Namespace, claim.Spec.ParametersRef.Name)))
}
return nil, statusError(logger, fmt.Errorf("get claim parameters %s: %v", klog.KRef(claim.Namespace, claim.Spec.ParametersRef.Name), err))
}
return parameters, nil
}
objs, err := pl.claimParametersIndexer.ByIndex(generatedFromIndex, claimParametersReferenceKeyFunc(claim.Namespace, claim.Spec.ParametersRef))
if err != nil {
return nil, statusError(logger, fmt.Errorf("listing claim parameters failed: %v", err))
}
switch len(objs) {
case 0:
return nil, statusUnschedulable(logger, fmt.Sprintf("generated claim parameters for %s.%s %s not found", claim.Spec.ParametersRef.Kind, claim.Spec.ParametersRef.APIGroup, klog.KRef(claim.Namespace, claim.Spec.ParametersRef.Name)))
case 1:
parameters, ok := objs[0].(*resourceapi.ResourceClaimParameters)
if !ok {
return nil, statusError(logger, fmt.Errorf("unexpected object in claim parameters index: %T", objs[0]))
}
return parameters, nil
default:
sort.Slice(objs, func(i, j int) bool {
obj1, obj2 := objs[i].(*resourceapi.ResourceClaimParameters), objs[j].(*resourceapi.ResourceClaimParameters)
if obj1 == nil || obj2 == nil {
return false
}
return obj1.Name < obj2.Name
})
return nil, statusError(logger, fmt.Errorf("multiple generated claim parameters for %s.%s %s found: %s", claim.Spec.ParametersRef.Kind, claim.Spec.ParametersRef.APIGroup, klog.KRef(claim.Namespace, claim.Spec.ParametersRef.Name), klog.KObjSlice(objs)))
}
return allocated, nil
}
// PreFilterExtensions returns prefilter extensions, pod add and remove.
@@ -1158,10 +855,11 @@ func (pl *dynamicResources) Filter(ctx context.Context, cs *framework.CycleState
logger.V(10).Info("filtering based on resource claims of the pod", "pod", klog.KObj(pod), "node", klog.KObj(node), "resourceclaim", klog.KObj(claim))
if claim.Status.Allocation != nil {
if nodeSelector := state.informationsForClaim[index].availableOnNode; nodeSelector != nil {
for _, nodeSelector := range state.informationsForClaim[index].availableOnNodes {
if !nodeSelector.Match(node) {
logger.V(5).Info("AvailableOnNodes does not match", "pod", klog.KObj(pod), "node", klog.KObj(node), "resourceclaim", klog.KObj(claim))
unavailableClaims = append(unavailableClaims, index)
break
}
}
continue
@@ -1172,40 +870,61 @@ func (pl *dynamicResources) Filter(ctx context.Context, cs *framework.CycleState
return statusUnschedulable(logger, "resourceclaim must be reallocated", "pod", klog.KObj(pod), "node", klog.KObj(node), "resourceclaim", klog.KObj(claim))
}
if selector := state.informationsForClaim[index].availableOnNode; selector != nil {
if matches := selector.Match(node); !matches {
return statusUnschedulable(logger, "excluded by resource class node filter", "pod", klog.KObj(pod), "node", klog.KObj(node), "resourceclassName", claim.Spec.ResourceClassName)
for className, nodeSelector := range state.informationsForClaim[index].availableOnNodes {
if !nodeSelector.Match(node) {
return statusUnschedulable(logger, "excluded by device class node filter", "pod", klog.KObj(pod), "node", klog.KObj(node), "deviceclass", klog.KRef("", className))
}
}
// Can the builtin controller tell us whether the node is suitable?
if state.informationsForClaim[index].structuredParameters {
suitable, err := state.informationsForClaim[index].controller.nodeIsSuitable(ctx, node.Name, state.resources)
if err != nil {
// An error indicates that something wasn't configured correctly, for example
// writing a CEL expression which doesn't handle a map lookup error. Normally
// this should never fail. We could return an error here, but then the pod
// would get retried. Instead we ignore the node.
return statusUnschedulable(logger, fmt.Sprintf("checking structured parameters failed: %v", err), "pod", klog.KObj(pod), "node", klog.KObj(node), "resourceclaim", klog.KObj(claim))
}
if !suitable {
return statusUnschedulable(logger, "resourceclaim cannot be allocated for the node (unsuitable)", "pod", klog.KObj(pod), "node", klog.KObj(node), "resourceclaim", klog.KObj(claim))
}
} else {
if status := state.informationsForClaim[index].status; status != nil {
for _, unsuitableNode := range status.UnsuitableNodes {
if node.Name == unsuitableNode {
return statusUnschedulable(logger, "resourceclaim cannot be allocated for the node (unsuitable)", "pod", klog.KObj(pod), "node", klog.KObj(node), "resourceclaim", klog.KObj(claim), "unsuitablenodes", status.UnsuitableNodes)
}
// Use information from control plane controller?
if status := state.informationsForClaim[index].status; status != nil {
for _, unsuitableNode := range status.UnsuitableNodes {
if node.Name == unsuitableNode {
return statusUnschedulable(logger, "resourceclaim cannot be allocated for the node (unsuitable)", "pod", klog.KObj(pod), "node", klog.KObj(node), "resourceclaim", klog.KObj(claim), "unsuitablenodes", status.UnsuitableNodes)
}
}
}
}
// Use allocator to check the node and cache the result in case that the node is picked.
var allocations []*resourceapi.AllocationResult
if state.allocator != nil {
allocCtx := ctx
if loggerV := logger.V(5); loggerV.Enabled() {
allocCtx = klog.NewContext(allocCtx, klog.LoggerWithValues(logger, "node", klog.KObj(node)))
}
a, err := state.allocator.Allocate(allocCtx, node)
if err != nil {
// This should only fail if there is something wrong with the claim or class.
// Return an error to abort scheduling of it.
//
// This will cause retries. It would be slightly nicer to mark it as unschedulable
// *and* abort scheduling. Then only cluster event for updating the claim or class
// with the broken CEL expression would trigger rescheduling.
//
// But we cannot do both. As this shouldn't occur often, aborting like this is
// better than the more complicated alternative (return Unschedulable here, remember
// the error, then later raise it again later if needed).
return statusError(logger, err, "pod", klog.KObj(pod), "node", klog.KObj(node), "resourceclaims", klog.KObjSlice(state.allocator.ClaimsToAllocate()))
}
// Check for exact length just to be sure. In practice this is all-or-nothing.
if len(a) != len(state.allocator.ClaimsToAllocate()) {
return statusUnschedulable(logger, "cannot allocate all claims", "pod", klog.KObj(pod), "node", klog.KObj(node), "resourceclaims", klog.KObjSlice(state.allocator.ClaimsToAllocate()))
}
// Reserve uses this information.
allocations = a
}
// Store information in state while holding the mutex.
if state.allocator != nil || len(unavailableClaims) > 0 {
state.mutex.Lock()
defer state.mutex.Unlock()
}
if len(unavailableClaims) > 0 {
// Remember all unavailable claims. This might be observed
// concurrently, so we have to lock the state before writing.
state.mutex.Lock()
defer state.mutex.Unlock()
if state.unavailableClaims == nil {
state.unavailableClaims = sets.New[int]()
@@ -1217,6 +936,10 @@ func (pl *dynamicResources) Filter(ctx context.Context, cs *framework.CycleState
return statusUnschedulable(logger, "resourceclaim not available on the node", "pod", klog.KObj(pod))
}
if state.allocator != nil {
state.nodeAllocations[node.Name] = allocations
}
return nil
}
@@ -1266,7 +989,6 @@ func (pl *dynamicResources) PostFilter(ctx context.Context, cs *framework.CycleS
claim := claim.DeepCopy()
claim.Status.ReservedFor = nil
if clearAllocation {
claim.Status.DriverName = ""
claim.Status.Allocation = nil
} else {
claim.Status.DeallocationRequested = true
@@ -1303,7 +1025,7 @@ func (pl *dynamicResources) PreScore(ctx context.Context, cs *framework.CycleSta
pending := false
for index, claim := range state.claims {
if claim.Status.Allocation == nil &&
state.informationsForClaim[index].controller == nil {
!state.informationsForClaim[index].structuredParameters {
pending = true
break
}
@@ -1379,10 +1101,11 @@ func (pl *dynamicResources) Reserve(ctx context.Context, cs *framework.CycleStat
return nil
}
logger := klog.FromContext(ctx)
numDelayedAllocationPending := 0
numClaimsWithStatusInfo := 0
claimsWithBuiltinController := make([]int, 0, len(state.claims))
logger := klog.FromContext(ctx)
numClaimsWithAllocator := 0
for index, claim := range state.claims {
if claim.Status.Allocation != nil {
// Allocated, but perhaps not reserved yet. We checked in PreFilter that
@@ -1393,9 +1116,9 @@ func (pl *dynamicResources) Reserve(ctx context.Context, cs *framework.CycleStat
continue
}
// Do we have the builtin controller?
if state.informationsForClaim[index].controller != nil {
claimsWithBuiltinController = append(claimsWithBuiltinController, index)
// Do we use the allocator for it?
if state.informationsForClaim[index].structuredParameters {
numClaimsWithAllocator++
continue
}
@@ -1409,7 +1132,7 @@ func (pl *dynamicResources) Reserve(ctx context.Context, cs *framework.CycleStat
}
}
if numDelayedAllocationPending == 0 && len(claimsWithBuiltinController) == 0 {
if numDelayedAllocationPending == 0 && numClaimsWithAllocator == 0 {
// Nothing left to do.
return nil
}
@@ -1430,27 +1153,41 @@ func (pl *dynamicResources) Reserve(ctx context.Context, cs *framework.CycleStat
}
// Prepare allocation of claims handled by the schedulder.
for _, index := range claimsWithBuiltinController {
claim := state.claims[index]
driverName, allocation, err := state.informationsForClaim[index].controller.allocate(ctx, nodeName, state.resources)
if err != nil {
if state.allocator != nil {
// Entries in these two slices match each other.
claimsToAllocate := state.allocator.ClaimsToAllocate()
allocations, ok := state.nodeAllocations[nodeName]
if !ok {
// We checked before that the node is suitable. This shouldn't have failed,
// so treat this as an error.
return statusError(logger, fmt.Errorf("claim allocation failed unexpectedly: %v", err))
return statusError(logger, errors.New("claim allocation not found for node"))
}
state.informationsForClaim[index].allocation = allocation
state.informationsForClaim[index].allocationDriverName = driverName
// Strictly speaking, we don't need to store the full modified object.
// The allocation would be enough. The full object is useful for
// debugging and testing, so let's make it realistic.
claim = claim.DeepCopy()
if !slices.Contains(claim.Finalizers, resourceapi.Finalizer) {
claim.Finalizers = append(claim.Finalizers, resourceapi.Finalizer)
// Sanity check: do we have results for all pending claims?
if len(allocations) != len(claimsToAllocate) ||
len(allocations) != numClaimsWithAllocator {
return statusError(logger, fmt.Errorf("internal error, have %d allocations, %d claims to allocate, want %d claims", len(allocations), len(claimsToAllocate), numClaimsWithAllocator))
}
for i, claim := range claimsToAllocate {
index := slices.Index(state.claims, claim)
if index < 0 {
return statusError(logger, fmt.Errorf("internal error, claim %s with allocation not found", claim.Name))
}
allocation := allocations[i]
state.informationsForClaim[index].allocation = allocation
// Strictly speaking, we don't need to store the full modified object.
// The allocation would be enough. The full object is useful for
// debugging, testing and the allocator, so let's make it realistic.
claim = claim.DeepCopy()
if !slices.Contains(claim.Finalizers, resourceapi.Finalizer) {
claim.Finalizers = append(claim.Finalizers, resourceapi.Finalizer)
}
claim.Status.Allocation = allocation
pl.inFlightAllocations.Store(claim.UID, claim)
logger.V(5).Info("Reserved resource in allocation result", "claim", klog.KObj(claim), "allocation", klog.Format(allocation))
}
claim.Status.DriverName = driverName
claim.Status.Allocation = allocation
pl.inFlightAllocations.Store(claim.UID, claim)
logger.V(5).Info("Reserved resource in allocation result", "claim", klog.KObj(claim), "driver", driverName, "allocation", klog.Format(allocation))
}
// When there is only one pending resource, we can go ahead with
@@ -1460,8 +1197,8 @@ func (pl *dynamicResources) Reserve(ctx context.Context, cs *framework.CycleStat
//
// If all pending claims are handled with the builtin controller,
// there is no need for a PodSchedulingContext change.
if numDelayedAllocationPending == 1 && len(claimsWithBuiltinController) == 0 ||
numClaimsWithStatusInfo+len(claimsWithBuiltinController) == numDelayedAllocationPending && len(claimsWithBuiltinController) < numDelayedAllocationPending {
if numDelayedAllocationPending == 1 && numClaimsWithAllocator == 0 ||
numClaimsWithStatusInfo+numClaimsWithAllocator == numDelayedAllocationPending && numClaimsWithAllocator < numDelayedAllocationPending {
// TODO: can we increase the chance that the scheduler picks
// the same node as before when allocation is on-going,
// assuming that that node still fits the pod? Picking a
@@ -1530,7 +1267,7 @@ func (pl *dynamicResources) Unreserve(ctx context.Context, cs *framework.CycleSt
for index, claim := range state.claims {
// If allocation was in-flight, then it's not anymore and we need to revert the
// claim object in the assume cache to what it was before.
if state.informationsForClaim[index].controller != nil {
if state.informationsForClaim[index].structuredParameters {
if _, found := pl.inFlightAllocations.LoadAndDelete(state.claims[index].UID); found {
pl.claimAssumeCache.Restore(claim.Namespace + "/" + claim.Name)
}
@@ -1661,8 +1398,6 @@ func (pl *dynamicResources) bindClaim(ctx context.Context, state *stateData, ind
}
claim = updatedClaim
}
claim.Status.DriverName = state.informationsForClaim[index].allocationDriverName
claim.Status.Allocation = allocation
}

View File

@@ -51,6 +51,11 @@ import (
var (
podKind = v1.SchemeGroupVersion.WithKind("Pod")
nodeName = "worker"
node2Name = "worker-2"
node3Name = "worker-3"
controller = "some-driver"
driver = controller
podName = "my-pod"
podUID = "1234"
resourceName = "my-resource"
@@ -59,45 +64,12 @@ var (
claimName2 = podName + "-" + resourceName + "-2"
className = "my-resource-class"
namespace = "default"
attrName = resourceapi.QualifiedName("healthy") // device attribute only available on non-default node
resourceClass = &resourceapi.ResourceClass{
deviceClass = &resourceapi.DeviceClass{
ObjectMeta: metav1.ObjectMeta{
Name: className,
},
DriverName: "some-driver",
}
structuredResourceClass = &resourceapi.ResourceClass{
ObjectMeta: metav1.ObjectMeta{
Name: className,
},
DriverName: "some-driver",
StructuredParameters: ptr.To(true),
}
structuredResourceClassWithParams = &resourceapi.ResourceClass{
ObjectMeta: metav1.ObjectMeta{
Name: className,
},
DriverName: "some-driver",
StructuredParameters: ptr.To(true),
ParametersRef: &resourceapi.ResourceClassParametersReference{
Name: className,
Namespace: namespace,
Kind: "ResourceClassParameters",
APIGroup: "resource.k8s.io",
},
}
structuredResourceClassWithCRD = &resourceapi.ResourceClass{
ObjectMeta: metav1.ObjectMeta{
Name: className,
},
DriverName: "some-driver",
StructuredParameters: ptr.To(true),
ParametersRef: &resourceapi.ResourceClassParametersReference{
Name: className,
Namespace: namespace,
Kind: "ResourceClassParameters",
APIGroup: "example.com",
},
}
podWithClaimName = st.MakePod().Name(podName).Namespace(namespace).
@@ -124,39 +96,29 @@ var (
PodResourceClaims(v1.PodResourceClaim{Name: resourceName2, ResourceClaimName: &claimName2}).
Obj()
workerNode = &st.MakeNode().Name("worker").Label("kubernetes.io/hostname", "worker").Node
workerNodeSlice = st.MakeResourceSlice("worker", "some-driver").NamedResourcesInstances("instance-1").Obj()
// Node with "instance-1" device and no device attributes.
workerNode = &st.MakeNode().Name(nodeName).Label("kubernetes.io/hostname", nodeName).Node
workerNodeSlice = st.MakeResourceSlice(nodeName, driver).Device("instance-1", nil).Obj()
claimParameters = st.MakeClaimParameters().Name(claimName).Namespace(namespace).
NamedResourcesRequests("some-driver", "true").
GeneratedFrom(&resourceapi.ResourceClaimParametersReference{
Name: claimName,
Kind: "ResourceClaimParameters",
APIGroup: "example.com",
}).
Obj()
claimParametersOtherNamespace = st.MakeClaimParameters().Name(claimName).Namespace(namespace+"-2").
NamedResourcesRequests("some-driver", "true").
GeneratedFrom(&resourceapi.ResourceClaimParametersReference{
Name: claimName,
Kind: "ResourceClaimParameters",
APIGroup: "example.com",
}).
Obj()
classParameters = st.MakeClassParameters().Name(className).Namespace(namespace).
NamedResourcesFilters("some-driver", "true").
GeneratedFrom(&resourceapi.ResourceClassParametersReference{
Name: className,
Namespace: namespace,
Kind: "ResourceClassParameters",
APIGroup: "example.com",
}).
Obj()
// Node with same device, but now with a "healthy" boolean attribute.
workerNode2 = &st.MakeNode().Name(node2Name).Label("kubernetes.io/hostname", node2Name).Node
workerNode2Slice = st.MakeResourceSlice(node2Name, driver).Device("instance-1", map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{attrName: {BoolValue: ptr.To(true)}}).Obj()
claim = st.MakeResourceClaim().
// Yet another node, same as the second one.
workerNode3 = &st.MakeNode().Name(node3Name).Label("kubernetes.io/hostname", node3Name).Node
workerNode3Slice = st.MakeResourceSlice(node3Name, driver).Device("instance-1", map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{attrName: {BoolValue: ptr.To(true)}}).Obj()
brokenSelector = resourceapi.DeviceSelector{
CEL: &resourceapi.CELDeviceSelector{
// Not set for workerNode.
Expression: fmt.Sprintf(`device.attributes["%s"].%s`, driver, attrName),
},
}
claim = st.MakeResourceClaim(controller).
Name(claimName).
Namespace(namespace).
ResourceClassName(className).
Request(className).
Obj()
pendingClaim = st.FromResourceClaim(claim).
OwnerReference(podName, podUID, podKind).
@@ -164,44 +126,53 @@ var (
pendingClaim2 = st.FromResourceClaim(pendingClaim).
Name(claimName2).
Obj()
allocationResult = &resourceapi.AllocationResult{
Controller: controller,
Devices: resourceapi.DeviceAllocationResult{
Results: []resourceapi.DeviceRequestAllocationResult{{
Driver: driver,
Pool: nodeName,
Device: "instance-1",
Request: "req-1",
}},
},
NodeSelector: func() *v1.NodeSelector {
// Label selector...
nodeSelector := st.MakeNodeSelector().In("metadata.name", []string{nodeName}).Obj()
// ... but we need a field selector, so let's swap.
nodeSelector.NodeSelectorTerms[0].MatchExpressions, nodeSelector.NodeSelectorTerms[0].MatchFields = nodeSelector.NodeSelectorTerms[0].MatchFields, nodeSelector.NodeSelectorTerms[0].MatchExpressions
return nodeSelector
}(),
}
deallocatingClaim = st.FromResourceClaim(pendingClaim).
Allocation("some-driver", &resourceapi.AllocationResult{}).
Allocation(allocationResult).
DeallocationRequested(true).
Obj()
inUseClaim = st.FromResourceClaim(pendingClaim).
Allocation("some-driver", &resourceapi.AllocationResult{}).
Allocation(allocationResult).
ReservedForPod(podName, types.UID(podUID)).
Obj()
structuredInUseClaim = st.FromResourceClaim(inUseClaim).
Structured("worker", "instance-1").
Structured().
Obj()
allocatedClaim = st.FromResourceClaim(pendingClaim).
Allocation("some-driver", &resourceapi.AllocationResult{}).
Allocation(allocationResult).
Obj()
pendingClaimWithParams = st.FromResourceClaim(pendingClaim).ParametersRef(claimName).Obj()
structuredAllocatedClaim = st.FromResourceClaim(allocatedClaim).Structured("worker", "instance-1").Obj()
structuredAllocatedClaimWithParams = st.FromResourceClaim(structuredAllocatedClaim).ParametersRef(claimName).Obj()
otherStructuredAllocatedClaim = st.FromResourceClaim(structuredAllocatedClaim).Name(structuredAllocatedClaim.Name + "-other").Obj()
allocatedClaimWithWrongTopology = st.FromResourceClaim(allocatedClaim).
Allocation("some-driver", &resourceapi.AllocationResult{AvailableOnNodes: st.MakeNodeSelector().In("no-such-label", []string{"no-such-value"}).Obj()}).
Allocation(&resourceapi.AllocationResult{Controller: controller, NodeSelector: st.MakeNodeSelector().In("no-such-label", []string{"no-such-value"}).Obj()}).
Obj()
structuredAllocatedClaimWithWrongTopology = st.FromResourceClaim(allocatedClaimWithWrongTopology).
Structured("worker-2", "instance-1").
Obj()
allocatedClaimWithGoodTopology = st.FromResourceClaim(allocatedClaim).
Allocation("some-driver", &resourceapi.AllocationResult{AvailableOnNodes: st.MakeNodeSelector().In("kubernetes.io/hostname", []string{"worker"}).Obj()}).
Allocation(&resourceapi.AllocationResult{Controller: controller, NodeSelector: st.MakeNodeSelector().In("kubernetes.io/hostname", []string{nodeName}).Obj()}).
Obj()
structuredAllocatedClaimWithGoodTopology = st.FromResourceClaim(allocatedClaimWithGoodTopology).
Structured("worker", "instance-1").
Obj()
otherClaim = st.MakeResourceClaim().
otherClaim = st.MakeResourceClaim(controller).
Name("not-my-claim").
Namespace(namespace).
ResourceClassName(className).
Request(className).
Obj()
otherAllocatedClaim = st.FromResourceClaim(otherClaim).
Allocation(allocationResult).
Obj()
scheduling = st.MakePodSchedulingContexts().Name(podName).Namespace(namespace).
OwnerReference(podName, podUID, podKind).
@@ -224,38 +195,37 @@ func reserve(claim *resourceapi.ResourceClaim, pod *v1.Pod) *resourceapi.Resourc
Obj()
}
// claimWithCRD replaces the in-tree group with "example.com".
func claimWithCRD(claim *resourceapi.ResourceClaim) *resourceapi.ResourceClaim {
func structuredClaim(claim *resourceapi.ResourceClaim) *resourceapi.ResourceClaim {
return st.FromResourceClaim(claim).
Structured().
Obj()
}
func breakCELInClaim(claim *resourceapi.ResourceClaim) *resourceapi.ResourceClaim {
claim = claim.DeepCopy()
claim.Spec.ParametersRef.APIGroup = "example.com"
for i := range claim.Spec.Devices.Requests {
for e := range claim.Spec.Devices.Requests[i].Selectors {
claim.Spec.Devices.Requests[i].Selectors[e] = brokenSelector
}
if len(claim.Spec.Devices.Requests[i].Selectors) == 0 {
claim.Spec.Devices.Requests[i].Selectors = []resourceapi.DeviceSelector{brokenSelector}
}
}
return claim
}
// classWithCRD replaces the in-tree group with "example.com".
func classWithCRD(class *resourceapi.ResourceClass) *resourceapi.ResourceClass {
func breakCELInClass(class *resourceapi.DeviceClass) *resourceapi.DeviceClass {
class = class.DeepCopy()
class.ParametersRef.APIGroup = "example.com"
for i := range class.Spec.Selectors {
class.Spec.Selectors[i] = brokenSelector
}
if len(class.Spec.Selectors) == 0 {
class.Spec.Selectors = []resourceapi.DeviceSelector{brokenSelector}
}
return class
}
func breakCELInClaimParameters(parameters *resourceapi.ResourceClaimParameters) *resourceapi.ResourceClaimParameters {
parameters = parameters.DeepCopy()
for i := range parameters.DriverRequests {
for e := range parameters.DriverRequests[i].Requests {
parameters.DriverRequests[i].Requests[e].NamedResources.Selector = `attributes.bool["no-such-attribute"]`
}
}
return parameters
}
func breakCELInClassParameters(parameters *resourceapi.ResourceClassParameters) *resourceapi.ResourceClassParameters {
parameters = parameters.DeepCopy()
for i := range parameters.Filters {
parameters.Filters[i].NamedResources.Selector = `attributes.bool["no-such-attribute"]`
}
return parameters
}
// result defines the expected outcome of some operation. It covers
// operation's status and the state of the world (= objects).
type result struct {
@@ -337,7 +307,7 @@ func TestPlugin(t *testing.T) {
nodes []*v1.Node // default if unset is workerNode
pod *v1.Pod
claims []*resourceapi.ResourceClaim
classes []*resourceapi.ResourceClass
classes []*resourceapi.DeviceClass
schedulings []*resourceapi.PodSchedulingContext
// objs get stored directly in the fake client, without passing
@@ -378,7 +348,7 @@ func TestPlugin(t *testing.T) {
},
"claim-reference-structured": {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{structuredAllocatedClaim, otherClaim},
claims: []*resourceapi.ResourceClaim{structuredClaim(allocatedClaim), otherClaim},
want: want{
prebind: result{
changes: change{
@@ -412,7 +382,7 @@ func TestPlugin(t *testing.T) {
},
"claim-template-structured": {
pod: podWithClaimTemplateInStatus,
claims: []*resourceapi.ResourceClaim{structuredAllocatedClaim, otherClaim},
claims: []*resourceapi.ResourceClaim{structuredClaim(allocatedClaim), otherClaim},
want: want{
prebind: result{
changes: change{
@@ -464,12 +434,12 @@ func TestPlugin(t *testing.T) {
},
"structured-no-resources": {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{pendingClaim},
classes: []*resourceapi.ResourceClass{structuredResourceClass},
claims: []*resourceapi.ResourceClaim{structuredClaim(pendingClaim)},
classes: []*resourceapi.DeviceClass{deviceClass},
want: want{
filter: perNodeResult{
workerNode.Name: {
status: framework.NewStatus(framework.UnschedulableAndUnresolvable, `resourceclaim cannot be allocated for the node (unsuitable)`),
status: framework.NewStatus(framework.UnschedulableAndUnresolvable, `cannot allocate all claims`),
},
},
postfilter: result{
@@ -479,28 +449,28 @@ func TestPlugin(t *testing.T) {
},
"structured-with-resources": {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{pendingClaim},
classes: []*resourceapi.ResourceClass{structuredResourceClass},
claims: []*resourceapi.ResourceClaim{structuredClaim(pendingClaim)},
classes: []*resourceapi.DeviceClass{deviceClass},
objs: []apiruntime.Object{workerNodeSlice},
want: want{
reserve: result{
inFlightClaim: structuredAllocatedClaim,
inFlightClaim: structuredClaim(allocatedClaim),
},
prebind: result{
assumedClaim: reserve(structuredAllocatedClaim, podWithClaimName),
assumedClaim: reserve(structuredClaim(allocatedClaim), podWithClaimName),
changes: change{
claim: func(claim *resourceapi.ResourceClaim) *resourceapi.ResourceClaim {
if claim.Name == claimName {
claim = claim.DeepCopy()
claim.Finalizers = structuredAllocatedClaim.Finalizers
claim.Status = structuredInUseClaim.Status
claim.Finalizers = structuredClaim(allocatedClaim).Finalizers
claim.Status = structuredClaim(inUseClaim).Status
}
return claim
},
},
},
postbind: result{
assumedClaim: reserve(structuredAllocatedClaim, podWithClaimName),
assumedClaim: reserve(structuredClaim(allocatedClaim), podWithClaimName),
},
},
},
@@ -509,18 +479,18 @@ func TestPlugin(t *testing.T) {
// the scheduler got interrupted.
pod: podWithClaimName,
claims: func() []*resourceapi.ResourceClaim {
claim := pendingClaim.DeepCopy()
claim.Finalizers = structuredAllocatedClaim.Finalizers
claim := structuredClaim(pendingClaim)
claim.Finalizers = structuredClaim(allocatedClaim).Finalizers
return []*resourceapi.ResourceClaim{claim}
}(),
classes: []*resourceapi.ResourceClass{structuredResourceClass},
classes: []*resourceapi.DeviceClass{deviceClass},
objs: []apiruntime.Object{workerNodeSlice},
want: want{
reserve: result{
inFlightClaim: structuredAllocatedClaim,
inFlightClaim: structuredClaim(allocatedClaim),
},
prebind: result{
assumedClaim: reserve(structuredAllocatedClaim, podWithClaimName),
assumedClaim: reserve(structuredClaim(allocatedClaim), podWithClaimName),
changes: change{
claim: func(claim *resourceapi.ResourceClaim) *resourceapi.ResourceClaim {
if claim.Name == claimName {
@@ -532,7 +502,7 @@ func TestPlugin(t *testing.T) {
},
},
postbind: result{
assumedClaim: reserve(structuredAllocatedClaim, podWithClaimName),
assumedClaim: reserve(structuredClaim(allocatedClaim), podWithClaimName),
},
},
},
@@ -541,11 +511,11 @@ func TestPlugin(t *testing.T) {
// removed before the scheduler reaches PreBind.
pod: podWithClaimName,
claims: func() []*resourceapi.ResourceClaim {
claim := pendingClaim.DeepCopy()
claim.Finalizers = structuredAllocatedClaim.Finalizers
claim := structuredClaim(pendingClaim)
claim.Finalizers = structuredClaim(allocatedClaim).Finalizers
return []*resourceapi.ResourceClaim{claim}
}(),
classes: []*resourceapi.ResourceClass{structuredResourceClass},
classes: []*resourceapi.DeviceClass{deviceClass},
objs: []apiruntime.Object{workerNodeSlice},
prepare: prepare{
prebind: change{
@@ -557,15 +527,15 @@ func TestPlugin(t *testing.T) {
},
want: want{
reserve: result{
inFlightClaim: structuredAllocatedClaim,
inFlightClaim: structuredClaim(allocatedClaim),
},
prebind: result{
assumedClaim: reserve(structuredAllocatedClaim, podWithClaimName),
assumedClaim: reserve(structuredClaim(allocatedClaim), podWithClaimName),
changes: change{
claim: func(claim *resourceapi.ResourceClaim) *resourceapi.ResourceClaim {
if claim.Name == claimName {
claim = claim.DeepCopy()
claim.Finalizers = structuredAllocatedClaim.Finalizers
claim.Finalizers = structuredClaim(allocatedClaim).Finalizers
claim.Status = structuredInUseClaim.Status
}
return claim
@@ -573,7 +543,7 @@ func TestPlugin(t *testing.T) {
},
},
postbind: result{
assumedClaim: reserve(structuredAllocatedClaim, podWithClaimName),
assumedClaim: reserve(structuredClaim(allocatedClaim), podWithClaimName),
},
},
},
@@ -581,23 +551,23 @@ func TestPlugin(t *testing.T) {
// No finalizer initially, then it gets added before
// the scheduler reaches PreBind. Shouldn't happen?
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{pendingClaim},
classes: []*resourceapi.ResourceClass{structuredResourceClass},
claims: []*resourceapi.ResourceClaim{structuredClaim(pendingClaim)},
classes: []*resourceapi.DeviceClass{deviceClass},
objs: []apiruntime.Object{workerNodeSlice},
prepare: prepare{
prebind: change{
claim: func(claim *resourceapi.ResourceClaim) *resourceapi.ResourceClaim {
claim.Finalizers = structuredAllocatedClaim.Finalizers
claim.Finalizers = structuredClaim(allocatedClaim).Finalizers
return claim
},
},
},
want: want{
reserve: result{
inFlightClaim: structuredAllocatedClaim,
inFlightClaim: structuredClaim(allocatedClaim),
},
prebind: result{
assumedClaim: reserve(structuredAllocatedClaim, podWithClaimName),
assumedClaim: reserve(structuredClaim(allocatedClaim), podWithClaimName),
changes: change{
claim: func(claim *resourceapi.ResourceClaim) *resourceapi.ResourceClaim {
if claim.Name == claimName {
@@ -609,31 +579,31 @@ func TestPlugin(t *testing.T) {
},
},
postbind: result{
assumedClaim: reserve(structuredAllocatedClaim, podWithClaimName),
assumedClaim: reserve(structuredClaim(allocatedClaim), podWithClaimName),
},
},
},
"structured-skip-bind": {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{pendingClaim},
classes: []*resourceapi.ResourceClass{structuredResourceClass},
claims: []*resourceapi.ResourceClaim{structuredClaim(pendingClaim)},
classes: []*resourceapi.DeviceClass{deviceClass},
objs: []apiruntime.Object{workerNodeSlice},
want: want{
reserve: result{
inFlightClaim: structuredAllocatedClaim,
inFlightClaim: structuredClaim(allocatedClaim),
},
unreserveBeforePreBind: &result{},
},
},
"structured-exhausted-resources": {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{pendingClaim, otherStructuredAllocatedClaim},
classes: []*resourceapi.ResourceClass{structuredResourceClass},
claims: []*resourceapi.ResourceClaim{structuredClaim(pendingClaim), structuredClaim(otherAllocatedClaim)},
classes: []*resourceapi.DeviceClass{deviceClass},
objs: []apiruntime.Object{workerNodeSlice},
want: want{
filter: perNodeResult{
workerNode.Name: {
status: framework.NewStatus(framework.UnschedulableAndUnresolvable, `resourceclaim cannot be allocated for the node (unsuitable)`),
status: framework.NewStatus(framework.UnschedulableAndUnresolvable, `cannot allocate all claims`),
},
},
postfilter: result{
@@ -642,182 +612,70 @@ func TestPlugin(t *testing.T) {
},
},
"with-parameters": {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{pendingClaimWithParams},
classes: []*resourceapi.ResourceClass{structuredResourceClassWithParams},
objs: []apiruntime.Object{claimParameters, classParameters, workerNodeSlice},
want: want{
reserve: result{
inFlightClaim: structuredAllocatedClaimWithParams,
},
prebind: result{
assumedClaim: reserve(structuredAllocatedClaimWithParams, podWithClaimName),
changes: change{
claim: func(claim *resourceapi.ResourceClaim) *resourceapi.ResourceClaim {
if claim.Name == claimName {
claim = claim.DeepCopy()
claim.Finalizers = structuredAllocatedClaim.Finalizers
claim.Status = structuredInUseClaim.Status
}
return claim
},
},
},
postbind: result{
assumedClaim: reserve(structuredAllocatedClaimWithParams, podWithClaimName),
},
},
},
"with-translated-parameters": {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{claimWithCRD(pendingClaimWithParams)},
classes: []*resourceapi.ResourceClass{classWithCRD(structuredResourceClassWithCRD)},
objs: []apiruntime.Object{claimParameters, claimParametersOtherNamespace /* must be ignored */, classParameters, workerNodeSlice},
want: want{
reserve: result{
inFlightClaim: claimWithCRD(structuredAllocatedClaimWithParams),
},
prebind: result{
assumedClaim: reserve(claimWithCRD(structuredAllocatedClaimWithParams), podWithClaimName),
changes: change{
claim: func(claim *resourceapi.ResourceClaim) *resourceapi.ResourceClaim {
if claim.Name == claimName {
claim = claim.DeepCopy()
claim.Finalizers = structuredAllocatedClaim.Finalizers
claim.Status = structuredInUseClaim.Status
}
return claim
},
},
},
postbind: result{
assumedClaim: reserve(claimWithCRD(structuredAllocatedClaimWithParams), podWithClaimName),
},
},
},
"missing-class-parameters": {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{pendingClaimWithParams},
classes: []*resourceapi.ResourceClass{structuredResourceClassWithParams},
objs: []apiruntime.Object{claimParameters, workerNodeSlice},
want: want{
prefilter: result{
status: framework.NewStatus(framework.UnschedulableAndUnresolvable, `class parameters default/my-resource-class not found`),
},
postfilter: result{
status: framework.NewStatus(framework.Unschedulable, `no new claims to deallocate`),
},
},
},
"missing-claim-parameters": {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{pendingClaimWithParams},
classes: []*resourceapi.ResourceClass{structuredResourceClassWithParams},
objs: []apiruntime.Object{classParameters, workerNodeSlice},
want: want{
prefilter: result{
status: framework.NewStatus(framework.UnschedulableAndUnresolvable, `claim parameters default/my-pod-my-resource not found`),
},
postfilter: result{
status: framework.NewStatus(framework.Unschedulable, `no new claims to deallocate`),
},
},
},
"missing-translated-class-parameters": {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{claimWithCRD(pendingClaimWithParams)},
classes: []*resourceapi.ResourceClass{classWithCRD(structuredResourceClassWithCRD)},
objs: []apiruntime.Object{claimParameters, workerNodeSlice},
want: want{
prefilter: result{
status: framework.NewStatus(framework.UnschedulableAndUnresolvable, `generated class parameters for ResourceClassParameters.example.com default/my-resource-class not found`),
},
postfilter: result{
status: framework.NewStatus(framework.Unschedulable, `no new claims to deallocate`),
},
},
},
"missing-translated-claim-parameters": {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{claimWithCRD(pendingClaimWithParams)},
classes: []*resourceapi.ResourceClass{classWithCRD(structuredResourceClassWithCRD)},
objs: []apiruntime.Object{classParameters, workerNodeSlice},
want: want{
prefilter: result{
status: framework.NewStatus(framework.UnschedulableAndUnresolvable, `generated claim parameters for ResourceClaimParameters.example.com default/my-pod-my-resource not found`),
},
postfilter: result{
status: framework.NewStatus(framework.Unschedulable, `no new claims to deallocate`),
},
},
},
"too-many-translated-class-parameters": {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{claimWithCRD(pendingClaimWithParams)},
classes: []*resourceapi.ResourceClass{classWithCRD(structuredResourceClassWithCRD)},
objs: []apiruntime.Object{claimParameters, classParameters, st.FromClassParameters(classParameters).Name("other").Obj() /* too many */, workerNodeSlice},
want: want{
prefilter: result{
status: framework.AsStatus(errors.New(`multiple generated class parameters for ResourceClassParameters.example.com my-resource-class found: [default/my-resource-class default/other]`)),
},
postfilter: result{
status: framework.NewStatus(framework.Unschedulable, `no new claims to deallocate`),
},
},
},
"too-many-translated-claim-parameters": {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{claimWithCRD(pendingClaimWithParams)},
classes: []*resourceapi.ResourceClass{classWithCRD(structuredResourceClassWithCRD)},
objs: []apiruntime.Object{claimParameters, st.FromClaimParameters(claimParameters).Name("other").Obj() /* too many */, classParameters, workerNodeSlice},
want: want{
prefilter: result{
status: framework.AsStatus(errors.New(`multiple generated claim parameters for ResourceClaimParameters.example.com default/my-pod-my-resource found: [default/my-pod-my-resource default/other]`)),
},
postfilter: result{
status: framework.NewStatus(framework.Unschedulable, `no new claims to deallocate`),
},
},
},
"claim-parameters-CEL-runtime-error": {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{pendingClaimWithParams},
classes: []*resourceapi.ResourceClass{structuredResourceClassWithParams},
objs: []apiruntime.Object{breakCELInClaimParameters(claimParameters), classParameters, workerNodeSlice},
claims: []*resourceapi.ResourceClaim{breakCELInClaim(structuredClaim(pendingClaim))},
classes: []*resourceapi.DeviceClass{deviceClass},
objs: []apiruntime.Object{workerNodeSlice},
want: want{
filter: perNodeResult{
workerNode.Name: {
status: framework.NewStatus(framework.UnschedulableAndUnresolvable, `checking structured parameters failed: checking node "worker" and resources of driver "some-driver": evaluate request CEL expression: no such key: no-such-attribute`),
status: framework.AsStatus(errors.New(`claim default/my-pod-my-resource: selector #0: CEL runtime error: no such key: ` + string(attrName))),
},
},
postfilter: result{
status: framework.NewStatus(framework.Unschedulable, `still not schedulable`),
},
},
},
"class-parameters-CEL-runtime-error": {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{pendingClaimWithParams},
classes: []*resourceapi.ResourceClass{structuredResourceClassWithParams},
objs: []apiruntime.Object{claimParameters, breakCELInClassParameters(classParameters), workerNodeSlice},
claims: []*resourceapi.ResourceClaim{structuredClaim(pendingClaim)},
classes: []*resourceapi.DeviceClass{breakCELInClass(deviceClass)},
objs: []apiruntime.Object{workerNodeSlice},
want: want{
filter: perNodeResult{
workerNode.Name: {
status: framework.NewStatus(framework.UnschedulableAndUnresolvable, `checking structured parameters failed: checking node "worker" and resources of driver "some-driver": evaluate filter CEL expression: no such key: no-such-attribute`),
status: framework.AsStatus(errors.New(`class my-resource-class: selector #0: CEL runtime error: no such key: ` + string(attrName))),
},
},
postfilter: result{
status: framework.NewStatus(framework.Unschedulable, `still not schedulable`),
},
},
// When pod scheduling encounters CEL runtime errors for some nodes, but not all,
// it should still not schedule the pod because there is something wrong with it.
// Scheduling it would make it harder to detect that there is a problem.
//
// This matches the "keeps pod pending because of CEL runtime errors" E2E test.
"CEL-runtime-error-for-one-of-two-nodes": {
nodes: []*v1.Node{workerNode, workerNode2},
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{breakCELInClaim(structuredClaim(pendingClaim))},
classes: []*resourceapi.DeviceClass{deviceClass},
objs: []apiruntime.Object{workerNodeSlice, workerNode2Slice},
want: want{
filter: perNodeResult{
workerNode.Name: {
status: framework.AsStatus(errors.New(`claim default/my-pod-my-resource: selector #0: CEL runtime error: no such key: ` + string(attrName))),
},
},
},
},
// When two nodes where found, PreScore gets called.
"CEL-runtime-error-for-one-of-three-nodes": {
nodes: []*v1.Node{workerNode, workerNode2, workerNode3},
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{breakCELInClaim(structuredClaim(pendingClaim))},
classes: []*resourceapi.DeviceClass{deviceClass},
objs: []apiruntime.Object{workerNodeSlice, workerNode2Slice, workerNode3Slice},
want: want{
filter: perNodeResult{
workerNode.Name: {
status: framework.NewStatus(framework.UnschedulableAndUnresolvable, `claim default/my-pod-my-resource: selector #0: CEL runtime error: no such key: `+string(attrName)),
},
},
prescore: result{
// This is the error found during Filter.
status: framework.NewStatus(framework.UnschedulableAndUnresolvable, `filter node worker: claim default/my-pod-my-resource: selector #0: CEL runtime error: no such key: healthy`),
},
},
},
@@ -839,7 +697,7 @@ func TestPlugin(t *testing.T) {
claims: []*resourceapi.ResourceClaim{pendingClaim},
want: want{
prefilter: result{
status: framework.NewStatus(framework.UnschedulableAndUnresolvable, fmt.Sprintf("resource class %s does not exist", className)),
status: framework.NewStatus(framework.UnschedulableAndUnresolvable, fmt.Sprintf("request req-1: device class %s does not exist", className)),
},
postfilter: result{
status: framework.NewStatus(framework.Unschedulable, `no new claims to deallocate`),
@@ -851,7 +709,7 @@ func TestPlugin(t *testing.T) {
// and select a node.
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{pendingClaim},
classes: []*resourceapi.ResourceClass{resourceClass},
classes: []*resourceapi.DeviceClass{deviceClass},
want: want{
prebind: result{
status: framework.NewStatus(framework.Pending, `waiting for resource driver`),
@@ -865,7 +723,7 @@ func TestPlugin(t *testing.T) {
// there are multiple claims.
pod: podWithTwoClaimNames,
claims: []*resourceapi.ResourceClaim{pendingClaim, pendingClaim2},
classes: []*resourceapi.ResourceClass{resourceClass},
classes: []*resourceapi.DeviceClass{deviceClass},
want: want{
prebind: result{
status: framework.NewStatus(framework.Pending, `waiting for resource driver`),
@@ -879,7 +737,7 @@ func TestPlugin(t *testing.T) {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{pendingClaim},
schedulings: []*resourceapi.PodSchedulingContext{schedulingInfo},
classes: []*resourceapi.ResourceClass{resourceClass},
classes: []*resourceapi.DeviceClass{deviceClass},
want: want{
prebind: result{
status: framework.NewStatus(framework.Pending, `waiting for resource driver`),
@@ -899,7 +757,7 @@ func TestPlugin(t *testing.T) {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{pendingClaim},
schedulings: []*resourceapi.PodSchedulingContext{schedulingInfo},
classes: []*resourceapi.ResourceClass{resourceClass},
classes: []*resourceapi.DeviceClass{deviceClass},
prepare: prepare{
prebind: change{
scheduling: func(in *resourceapi.PodSchedulingContext) *resourceapi.PodSchedulingContext {
@@ -923,7 +781,7 @@ func TestPlugin(t *testing.T) {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{allocatedClaim},
schedulings: []*resourceapi.PodSchedulingContext{schedulingInfo},
classes: []*resourceapi.ResourceClass{resourceClass},
classes: []*resourceapi.DeviceClass{deviceClass},
want: want{
prebind: result{
changes: change{
@@ -967,7 +825,7 @@ func TestPlugin(t *testing.T) {
// PostFilter tries to get the pod scheduleable by
// deallocating the claim.
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{structuredAllocatedClaimWithWrongTopology},
claims: []*resourceapi.ResourceClaim{structuredClaim(allocatedClaimWithWrongTopology)},
want: want{
filter: perNodeResult{
workerNode.Name: {
@@ -979,7 +837,7 @@ func TestPlugin(t *testing.T) {
changes: change{
claim: func(in *resourceapi.ResourceClaim) *resourceapi.ResourceClaim {
return st.FromResourceClaim(in).
Allocation("", nil).
Allocation(nil).
Obj()
},
},
@@ -1028,7 +886,7 @@ func TestPlugin(t *testing.T) {
},
"bind-failure-structured": {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{structuredAllocatedClaimWithGoodTopology},
claims: []*resourceapi.ResourceClaim{structuredClaim(allocatedClaimWithGoodTopology)},
want: want{
prebind: result{
changes: change{
@@ -1109,15 +967,20 @@ func TestPlugin(t *testing.T) {
t.Run(fmt.Sprintf("filter/%s", nodeInfo.Node().Name), func(t *testing.T) {
testCtx.verify(t, tc.want.filter.forNode(nodeName), initialObjects, nil, status)
})
if status.Code() != framework.Success {
unschedulable = true
} else {
if status.Code() == framework.Success {
potentialNodes = append(potentialNodes, nodeInfo)
}
if status.Code() == framework.Error {
// An error aborts scheduling.
return
}
}
if len(potentialNodes) == 0 {
unschedulable = true
}
}
if !unschedulable && len(potentialNodes) > 0 {
if !unschedulable && len(potentialNodes) > 1 {
initialObjects = testCtx.listAll(t)
initialObjects = testCtx.updateAPIServer(t, initialObjects, tc.prepare.prescore)
status := testCtx.p.PreScore(testCtx.ctx, testCtx.state, tc.pod, potentialNodes)
@@ -1184,7 +1047,7 @@ func TestPlugin(t *testing.T) {
})
}
}
} else {
} else if len(potentialNodes) == 0 {
initialObjects = testCtx.listAll(t)
initialObjects = testCtx.updateAPIServer(t, initialObjects, tc.prepare.postfilter)
result, status := testCtx.p.PostFilter(testCtx.ctx, testCtx.state, tc.pod, nil /* filteredNodeStatusMap not used by plugin */)
@@ -1209,7 +1072,12 @@ type testContext struct {
func (tc *testContext) verify(t *testing.T, expected result, initialObjects []metav1.Object, result interface{}, status *framework.Status) {
t.Helper()
assert.Equal(t, expected.status, status)
if expectedErr := status.AsError(); expectedErr != nil {
// Compare only the error strings.
assert.ErrorContains(t, status.AsError(), expectedErr.Error())
} else {
assert.Equal(t, expected.status, status)
}
objects := tc.listAll(t)
wantObjects := update(t, initialObjects, expected.changes)
wantObjects = append(wantObjects, expected.added...)
@@ -1351,7 +1219,7 @@ func update(t *testing.T, objects []metav1.Object, updates change) []metav1.Obje
return updated
}
func setup(t *testing.T, nodes []*v1.Node, claims []*resourceapi.ResourceClaim, classes []*resourceapi.ResourceClass, schedulings []*resourceapi.PodSchedulingContext, objs []apiruntime.Object) (result *testContext) {
func setup(t *testing.T, nodes []*v1.Node, claims []*resourceapi.ResourceClaim, classes []*resourceapi.DeviceClass, schedulings []*resourceapi.PodSchedulingContext, objs []apiruntime.Object) (result *testContext) {
t.Helper()
tc := &testContext{}
@@ -1387,7 +1255,7 @@ func setup(t *testing.T, nodes []*v1.Node, claims []*resourceapi.ResourceClaim,
require.NoError(t, err, "create resource claim")
}
for _, class := range classes {
_, err := tc.client.ResourceV1alpha3().ResourceClasses().Create(tc.ctx, class, metav1.CreateOptions{})
_, err := tc.client.ResourceV1alpha3().DeviceClasses().Create(tc.ctx, class, metav1.CreateOptions{})
require.NoError(t, err, "create resource class")
}
for _, scheduling := range schedulings {
@@ -1552,10 +1420,10 @@ func Test_isSchedulableAfterClaimChange(t *testing.T) {
},
"structured-claim-deallocate": {
pod: podWithClaimName,
claims: []*resourceapi.ResourceClaim{pendingClaim, otherStructuredAllocatedClaim},
oldObj: otherStructuredAllocatedClaim,
claims: []*resourceapi.ResourceClaim{pendingClaim, structuredClaim(otherAllocatedClaim)},
oldObj: structuredClaim(otherAllocatedClaim),
newObj: func() *resourceapi.ResourceClaim {
claim := otherStructuredAllocatedClaim.DeepCopy()
claim := structuredClaim(otherAllocatedClaim).DeepCopy()
claim.Status.Allocation = nil
return claim
}(),

View File

@@ -1,153 +0,0 @@
/*
Copyright 2024 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 namedresources
import (
"context"
"errors"
"fmt"
"slices"
resourceapi "k8s.io/api/resource/v1alpha3"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/dynamic-resource-allocation/structured/namedresources/cel"
)
// These types and fields are all exported to allow logging them with
// pretty-printed JSON.
type Model struct {
Instances []InstanceAllocation
}
type InstanceAllocation struct {
Allocated bool
Instance *resourceapi.NamedResourcesInstance
}
// AddResources must be called first to create entries for all existing
// resource instances. The resources parameter may be nil.
func AddResources(m *Model, resources *resourceapi.NamedResourcesResources) {
if resources == nil {
return
}
for i := range resources.Instances {
m.Instances = append(m.Instances, InstanceAllocation{Instance: &resources.Instances[i]})
}
}
// AddAllocation may get called after AddResources to mark some resource
// instances as allocated. The result parameter may be nil.
func AddAllocation(m *Model, result *resourceapi.NamedResourcesAllocationResult) {
if result == nil {
return
}
for i := range m.Instances {
if m.Instances[i].Instance.Name == result.Name {
m.Instances[i].Allocated = true
break
}
}
}
func NewClaimController(filter *resourceapi.NamedResourcesFilter, requests []*resourceapi.NamedResourcesRequest) (*Controller, error) {
c := &Controller{}
if filter != nil {
compilation := cel.GetCompiler().CompileCELExpression(filter.Selector, environment.StoredExpressions)
if compilation.Error != nil {
// Shouldn't happen because of validation.
return nil, fmt.Errorf("compile class filter CEL expression: %w", compilation.Error)
}
c.filter = &compilation
}
for _, request := range requests {
compilation := cel.GetCompiler().CompileCELExpression(request.Selector, environment.StoredExpressions)
if compilation.Error != nil {
// Shouldn't happen because of validation.
return nil, fmt.Errorf("compile request CEL expression: %w", compilation.Error)
}
c.requests = append(c.requests, compilation)
}
return c, nil
}
type Controller struct {
filter *cel.CompilationResult
requests []cel.CompilationResult
}
func (c *Controller) NodeIsSuitable(ctx context.Context, model Model) (bool, error) {
indices, err := c.allocate(ctx, model)
return len(indices) == len(c.requests), err
}
func (c *Controller) Allocate(ctx context.Context, model Model) ([]*resourceapi.NamedResourcesAllocationResult, error) {
indices, err := c.allocate(ctx, model)
if err != nil {
return nil, err
}
if len(indices) != len(c.requests) {
return nil, errors.New("insufficient resources")
}
results := make([]*resourceapi.NamedResourcesAllocationResult, len(c.requests))
for i := range c.requests {
results[i] = &resourceapi.NamedResourcesAllocationResult{Name: model.Instances[indices[i]].Instance.Name}
}
return results, nil
}
func (c *Controller) allocate(ctx context.Context, model Model) ([]int, error) {
// Shallow copy, we need to modify the allocated boolean.
instances := slices.Clone(model.Instances)
indices := make([]int, 0, len(c.requests))
for _, request := range c.requests {
for i, instance := range instances {
if instance.Allocated {
continue
}
if c.filter != nil {
okay, err := c.filter.Evaluate(ctx, instance.Instance.Attributes)
if err != nil {
return nil, fmt.Errorf("evaluate filter CEL expression: %w", err)
}
if !okay {
continue
}
}
okay, err := request.Evaluate(ctx, instance.Instance.Attributes)
if err != nil {
return nil, fmt.Errorf("evaluate request CEL expression: %w", err)
}
if !okay {
continue
}
// Found a matching, unallocated instance. Let's use it.
//
// A more thorough search would include backtracking because
// allocating one "large" instances for a "small" request may
// make a following "large" request impossible to satisfy when
// only "small" instances are left.
instances[i].Allocated = true
indices = append(indices, i)
break
}
}
return indices, nil
}

View File

@@ -1,327 +0,0 @@
/*
Copyright 2024 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 namedresources
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
resourceapi "k8s.io/api/resource/v1alpha3"
"k8s.io/kubernetes/test/utils/ktesting"
"k8s.io/utils/ptr"
)
func instance(allocated bool, name string, attributes ...resourceapi.NamedResourcesAttribute) InstanceAllocation {
return InstanceAllocation{
Allocated: allocated,
Instance: &resourceapi.NamedResourcesInstance{
Name: name,
Attributes: attributes,
},
}
}
func TestModel(t *testing.T) {
testcases := map[string]struct {
resources []*resourceapi.NamedResourcesResources
allocations []*resourceapi.NamedResourcesAllocationResult
expectModel Model
}{
"empty": {},
"nil": {
resources: []*resourceapi.NamedResourcesResources{nil},
allocations: []*resourceapi.NamedResourcesAllocationResult{nil},
},
"available": {
resources: []*resourceapi.NamedResourcesResources{
{
Instances: []resourceapi.NamedResourcesInstance{
{Name: "a"},
{Name: "b"},
},
},
{
Instances: []resourceapi.NamedResourcesInstance{
{Name: "x"},
{Name: "y"},
},
},
},
expectModel: Model{Instances: []InstanceAllocation{instance(false, "a"), instance(false, "b"), instance(false, "x"), instance(false, "y")}},
},
"allocated": {
resources: []*resourceapi.NamedResourcesResources{
{
Instances: []resourceapi.NamedResourcesInstance{
{Name: "a"},
{Name: "b"},
},
},
{
Instances: []resourceapi.NamedResourcesInstance{
{Name: "x"},
{Name: "y"},
},
},
},
allocations: []*resourceapi.NamedResourcesAllocationResult{
{
Name: "something-else",
},
{
Name: "a",
},
},
expectModel: Model{Instances: []InstanceAllocation{instance(true, "a"), instance(false, "b"), instance(false, "x"), instance(false, "y")}},
},
}
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
var actualModel Model
for _, resources := range tc.resources {
AddResources(&actualModel, resources)
}
for _, allocation := range tc.allocations {
AddAllocation(&actualModel, allocation)
}
require.Equal(t, tc.expectModel, actualModel)
})
}
}
func TestController(t *testing.T) {
filterAny := &resourceapi.NamedResourcesFilter{
Selector: "true",
}
filterNone := &resourceapi.NamedResourcesFilter{
Selector: "false",
}
filterBrokenType := &resourceapi.NamedResourcesFilter{
Selector: "1",
}
filterBrokenEvaluation := &resourceapi.NamedResourcesFilter{
Selector: `attributes.bool["no-such-attribute"]`,
}
filterAttribute := &resourceapi.NamedResourcesFilter{
Selector: `attributes.bool["usable"]`,
}
requestAny := &resourceapi.NamedResourcesRequest{
Selector: "true",
}
requestNone := &resourceapi.NamedResourcesRequest{
Selector: "false",
}
requestBrokenType := &resourceapi.NamedResourcesRequest{
Selector: "1",
}
requestBrokenEvaluation := &resourceapi.NamedResourcesRequest{
Selector: `attributes.bool["no-such-attribute"]`,
}
requestAttribute := &resourceapi.NamedResourcesRequest{
Selector: `attributes.bool["usable"]`,
}
instance1 := "instance-1"
oneInstance := Model{
Instances: []InstanceAllocation{{
Instance: &resourceapi.NamedResourcesInstance{
Name: instance1,
},
}},
}
instance2 := "instance-2"
twoInstances := Model{
Instances: []InstanceAllocation{
{
Instance: &resourceapi.NamedResourcesInstance{
Name: instance1,
Attributes: []resourceapi.NamedResourcesAttribute{{
Name: "usable",
NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{
BoolValue: ptr.To(false),
},
}},
},
},
{
Instance: &resourceapi.NamedResourcesInstance{
Name: instance2,
Attributes: []resourceapi.NamedResourcesAttribute{{
Name: "usable",
NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{
BoolValue: ptr.To(true),
},
}},
},
},
},
}
testcases := map[string]struct {
model Model
filter *resourceapi.NamedResourcesFilter
requests []*resourceapi.NamedResourcesRequest
expectCreateErr bool
expectAllocation []string
expectAllocateErr bool
}{
"empty": {},
"broken-filter": {
filter: filterBrokenType,
expectCreateErr: true,
},
"broken-request": {
requests: []*resourceapi.NamedResourcesRequest{requestBrokenType},
expectCreateErr: true,
},
"no-resources": {
filter: filterAny,
requests: []*resourceapi.NamedResourcesRequest{requestAny},
expectAllocateErr: true,
},
"okay": {
model: oneInstance,
filter: filterAny,
requests: []*resourceapi.NamedResourcesRequest{requestAny},
expectAllocation: []string{instance1},
},
"filter-mismatch": {
model: oneInstance,
filter: filterNone,
requests: []*resourceapi.NamedResourcesRequest{requestAny},
expectAllocateErr: true,
},
"request-mismatch": {
model: oneInstance,
filter: filterAny,
requests: []*resourceapi.NamedResourcesRequest{requestNone},
expectAllocateErr: true,
},
"many": {
model: twoInstances,
filter: filterAny,
requests: []*resourceapi.NamedResourcesRequest{requestAny, requestAny},
expectAllocation: []string{instance1, instance2},
},
"too-many": {
model: oneInstance,
filter: filterAny,
requests: []*resourceapi.NamedResourcesRequest{requestAny, requestAny},
expectAllocateErr: true,
},
"filter-evaluation-error": {
model: oneInstance,
filter: filterBrokenEvaluation,
requests: []*resourceapi.NamedResourcesRequest{requestAny},
expectAllocateErr: true,
},
"request-evaluation-error": {
model: oneInstance,
filter: filterAny,
requests: []*resourceapi.NamedResourcesRequest{requestBrokenEvaluation},
expectAllocateErr: true,
},
"filter-attribute": {
model: twoInstances,
filter: filterAttribute,
requests: []*resourceapi.NamedResourcesRequest{requestAny},
expectAllocation: []string{instance2},
},
"request-attribute": {
model: twoInstances,
filter: filterAny,
requests: []*resourceapi.NamedResourcesRequest{requestAttribute},
expectAllocation: []string{instance2},
},
}
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
tCtx := ktesting.Init(t)
controller, createErr := NewClaimController(tc.filter, tc.requests)
if createErr != nil {
if !tc.expectCreateErr {
tCtx.Fatalf("unexpected create error: %v", createErr)
}
return
}
if tc.expectCreateErr {
tCtx.Fatalf("did not get expected create error")
}
allocation, createErr := controller.Allocate(tCtx, tc.model)
if createErr != nil {
if !tc.expectAllocateErr {
tCtx.Fatalf("unexpected allocate error: %v", createErr)
}
return
}
if tc.expectAllocateErr {
tCtx.Fatalf("did not get expected allocate error")
}
expectAllocation := []*resourceapi.NamedResourcesAllocationResult{}
for _, name := range tc.expectAllocation {
expectAllocation = append(expectAllocation, &resourceapi.NamedResourcesAllocationResult{Name: name})
}
require.Equal(tCtx, expectAllocation, allocation)
isSuitable, isSuitableErr := controller.NodeIsSuitable(tCtx, tc.model)
assert.Equal(tCtx, len(expectAllocation) == len(tc.requests), isSuitable, "is suitable")
assert.Equal(tCtx, createErr, isSuitableErr)
})
}
}

View File

@@ -1,274 +0,0 @@
/*
Copyright 2024 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 dynamicresources
import (
"context"
"fmt"
"sync"
v1 "k8s.io/api/core/v1"
resourceapi "k8s.io/api/resource/v1alpha3"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/klog/v2"
namedresourcesmodel "k8s.io/kubernetes/pkg/scheduler/framework/plugins/dynamicresources/structured/namedresources"
)
// resources is a map "node name" -> "driver name" -> available and
// allocated resources per structured parameter model.
type resources map[string]map[string]ResourceModels
// ResourceModels may have more than one entry because it is valid for a driver to
// use more than one structured parameter model.
type ResourceModels struct {
NamedResources namedresourcesmodel.Model
}
// resourceSliceLister is the subset of resourcelisters.ResourceSliceLister needed by
// newResourceModel.
type resourceSliceLister interface {
List(selector labels.Selector) (ret []*resourceapi.ResourceSlice, err error)
}
// assumeCacheLister is the subset of volumebinding.AssumeCache needed by newResourceModel.
type assumeCacheLister interface {
List(indexObj interface{}) []interface{}
}
// newResourceModel parses the available information about resources. Objects
// with an unknown structured parameter model silently ignored. An error gets
// logged later when parameters required for a pod depend on such an unknown
// model.
func newResourceModel(logger klog.Logger, resourceSliceLister resourceSliceLister, claimAssumeCache assumeCacheLister, inFlightAllocations *sync.Map) (resourceMap, error) {
model := make(resourceMap)
slices, err := resourceSliceLister.List(labels.Everything())
if err != nil {
return nil, fmt.Errorf("list node resource slices: %w", err)
}
for _, slice := range slices {
if slice.NamedResources == nil {
// Ignore unknown resource. We don't know what it is,
// so we cannot allocated anything depending on
// it. This is only an error if we actually see a claim
// which needs this unknown model.
continue
}
instances := slice.NamedResources.Instances
if model[slice.NodeName] == nil {
model[slice.NodeName] = make(map[string]Resources)
}
resources := model[slice.NodeName][slice.DriverName]
resources.Instances = make([]Instance, 0, len(instances))
for i := range instances {
instance := Instance{
NodeName: slice.NodeName,
DriverName: slice.DriverName,
NamedResourcesInstance: &instances[i],
}
resources.Instances = append(resources.Instances, instance)
}
model[slice.NodeName][slice.DriverName] = resources
}
objs := claimAssumeCache.List(nil)
for _, obj := range objs {
claim, ok := obj.(*resourceapi.ResourceClaim)
if !ok {
return nil, fmt.Errorf("got unexpected object of type %T from claim assume cache", obj)
}
if obj, ok := inFlightAllocations.Load(claim.UID); ok {
// If the allocation is in-flight, then we have to use the allocation
// from that claim.
claim = obj.(*resourceapi.ResourceClaim)
}
if claim.Status.Allocation == nil {
continue
}
for _, handle := range claim.Status.Allocation.ResourceHandles {
structured := handle.StructuredData
if structured == nil {
continue
}
if model[structured.NodeName] == nil {
model[structured.NodeName] = make(map[string]Resources)
}
resources := model[structured.NodeName][handle.DriverName]
for _, result := range structured.Results {
// Same as above: if we don't know the allocation result model, ignore it.
if result.NamedResources == nil {
continue
}
instanceName := result.NamedResources.Name
for i := range resources.Instances {
if resources.Instances[i].NamedResourcesInstance.Name == instanceName {
resources.Instances[i].Allocated = true
break
}
}
// It could be that we don't know the instance. That's okay,
// we simply ignore the allocation result.
}
}
}
return model, nil
}
func newClaimController(logger klog.Logger, class *resourceapi.ResourceClass, classParameters *resourceapi.ResourceClassParameters, claimParameters *resourceapi.ResourceClaimParameters) (*claimController, error) {
// Each node driver is separate from the others. Each driver may have
// multiple requests which need to be allocated together, so here
// we have to collect them per model.
type perDriverRequests struct {
parameters []runtime.RawExtension
requests []*resourceapi.NamedResourcesRequest
}
namedresourcesRequests := make(map[string]perDriverRequests)
for i, request := range claimParameters.DriverRequests {
driverName := request.DriverName
p := namedresourcesRequests[driverName]
for e, request := range request.Requests {
switch {
case request.ResourceRequestModel.NamedResources != nil:
p.parameters = append(p.parameters, request.VendorParameters)
p.requests = append(p.requests, request.ResourceRequestModel.NamedResources)
default:
return nil, fmt.Errorf("claim parameters %s: driverRequests[%d].requests[%d]: no supported structured parameters found", klog.KObj(claimParameters), i, e)
}
}
if len(p.requests) > 0 {
namedresourcesRequests[driverName] = p
}
}
c := &claimController{
class: class,
classParameters: classParameters,
claimParameters: claimParameters,
namedresources: make(map[string]perDriverController, len(namedresourcesRequests)),
}
for driverName, perDriver := range namedresourcesRequests {
var filter *resourceapi.NamedResourcesFilter
for _, f := range classParameters.Filters {
if f.DriverName == driverName && f.ResourceFilterModel.NamedResources != nil {
filter = f.ResourceFilterModel.NamedResources
break
}
}
controller, err := namedresourcesmodel.NewClaimController(filter, perDriver.requests)
if err != nil {
return nil, fmt.Errorf("creating claim controller for named resources structured model: %w", err)
}
c.namedresources[driverName] = perDriverController{
parameters: perDriver.parameters,
controller: controller,
}
}
return c, nil
}
// claimController currently wraps exactly one structured parameter model.
type claimController struct {
class *resourceapi.ResourceClass
classParameters *resourceapi.ResourceClassParameters
claimParameters *resourceapi.ResourceClaimParameters
namedresources map[string]perDriverController
}
type perDriverController struct {
parameters []runtime.RawExtension
controller *namedresourcesmodel.Controller
}
func (c claimController) nodeIsSuitable(ctx context.Context, nodeName string, resources resources) (bool, error) {
nodeResources := resources[nodeName]
for driverName, perDriver := range c.namedresources {
okay, err := perDriver.controller.NodeIsSuitable(ctx, nodeResources[driverName].NamedResources)
if err != nil {
// This is an error in the CEL expression which needs
// to be fixed. Better fail very visibly instead of
// ignoring the node.
return false, fmt.Errorf("checking node %q and resources of driver %q: %w", nodeName, driverName, err)
}
if !okay {
return false, nil
}
}
return true, nil
}
func (c claimController) allocate(ctx context.Context, nodeName string, resources resources) (string, *resourceapi.AllocationResult, error) {
allocation := &resourceapi.AllocationResult{
AvailableOnNodes: &v1.NodeSelector{
NodeSelectorTerms: []v1.NodeSelectorTerm{
{
MatchExpressions: []v1.NodeSelectorRequirement{
{Key: "kubernetes.io/hostname", Operator: v1.NodeSelectorOpIn, Values: []string{nodeName}},
},
},
},
},
}
nodeResources := resources[nodeName]
for driverName, perDriver := range c.namedresources {
// Must return one entry for each request. The entry may be nil. This way,
// the result can be correlated with the per-request parameters.
results, err := perDriver.controller.Allocate(ctx, nodeResources[driverName].NamedResources)
if err != nil {
return "", nil, fmt.Errorf("allocating via named resources structured model: %w", err)
}
handle := resourceapi.ResourceHandle{
DriverName: driverName,
StructuredData: &resourceapi.StructuredResourceHandle{
NodeName: nodeName,
},
}
for i, result := range results {
if result == nil {
continue
}
handle.StructuredData.Results = append(handle.StructuredData.Results,
resourceapi.DriverAllocationResult{
VendorRequestParameters: perDriver.parameters[i],
AllocationResultModel: resourceapi.AllocationResultModel{
NamedResources: result,
},
},
)
}
if c.classParameters != nil {
for _, p := range c.classParameters.VendorParameters {
if p.DriverName == driverName {
handle.StructuredData.VendorClassParameters = p.Parameters
break
}
}
}
for _, request := range c.claimParameters.DriverRequests {
if request.DriverName == driverName {
handle.StructuredData.VendorClaimParameters = request.VendorParameters
break
}
}
allocation.ResourceHandles = append(allocation.ResourceHandles, handle)
}
return c.class.DriverName, allocation, nil
}

View File

@@ -93,18 +93,16 @@ const (
// unschedulable pod pool.
// This behavior will be removed when we remove the preCheck feature.
// See: https://github.com/kubernetes/kubernetes/issues/110175
Node GVK = "Node"
PersistentVolume GVK = "PersistentVolume"
PersistentVolumeClaim GVK = "PersistentVolumeClaim"
CSINode GVK = "storage.k8s.io/CSINode"
CSIDriver GVK = "storage.k8s.io/CSIDriver"
CSIStorageCapacity GVK = "storage.k8s.io/CSIStorageCapacity"
StorageClass GVK = "storage.k8s.io/StorageClass"
PodSchedulingContext GVK = "PodSchedulingContext"
ResourceClaim GVK = "ResourceClaim"
ResourceClass GVK = "ResourceClass"
ResourceClaimParameters GVK = "ResourceClaimParameters"
ResourceClassParameters GVK = "ResourceClassParameters"
Node GVK = "Node"
PersistentVolume GVK = "PersistentVolume"
PersistentVolumeClaim GVK = "PersistentVolumeClaim"
CSINode GVK = "storage.k8s.io/CSINode"
CSIDriver GVK = "storage.k8s.io/CSIDriver"
CSIStorageCapacity GVK = "storage.k8s.io/CSIStorageCapacity"
StorageClass GVK = "storage.k8s.io/StorageClass"
PodSchedulingContext GVK = "PodSchedulingContext"
ResourceClaim GVK = "ResourceClaim"
DeviceClass GVK = "DeviceClass"
// WildCard is a special GVK to match all resources.
// e.g., If you register `{Resource: "*", ActionType: All}` in EventsToRegister,
@@ -197,9 +195,7 @@ func UnrollWildCardResource() []ClusterEventWithHint {
{Event: ClusterEvent{Resource: StorageClass, ActionType: All}},
{Event: ClusterEvent{Resource: PodSchedulingContext, ActionType: All}},
{Event: ClusterEvent{Resource: ResourceClaim, ActionType: All}},
{Event: ClusterEvent{Resource: ResourceClass, ActionType: All}},
{Event: ClusterEvent{Resource: ResourceClaimParameters, ActionType: All}},
{Event: ClusterEvent{Resource: ResourceClassParameters, ActionType: All}},
{Event: ClusterEvent{Resource: DeviceClass, ActionType: All}},
}
}

View File

@@ -646,13 +646,7 @@ func Test_buildQueueingHintMap(t *testing.T) {
{Resource: framework.ResourceClaim, ActionType: framework.All}: {
{PluginName: filterWithoutEnqueueExtensions, QueueingHintFn: defaultQueueingHintFn},
},
{Resource: framework.ResourceClass, ActionType: framework.All}: {
{PluginName: filterWithoutEnqueueExtensions, QueueingHintFn: defaultQueueingHintFn},
},
{Resource: framework.ResourceClaimParameters, ActionType: framework.All}: {
{PluginName: filterWithoutEnqueueExtensions, QueueingHintFn: defaultQueueingHintFn},
},
{Resource: framework.ResourceClassParameters, ActionType: framework.All}: {
{Resource: framework.DeviceClass, ActionType: framework.All}: {
{PluginName: filterWithoutEnqueueExtensions, QueueingHintFn: defaultQueueingHintFn},
},
},
@@ -803,19 +797,17 @@ func Test_UnionedGVKs(t *testing.T) {
Disabled: []schedulerapi.Plugin{{Name: "*"}}, // disable default plugins
},
want: map[framework.GVK]framework.ActionType{
framework.Pod: framework.All,
framework.Node: framework.All,
framework.CSINode: framework.All,
framework.CSIDriver: framework.All,
framework.CSIStorageCapacity: framework.All,
framework.PersistentVolume: framework.All,
framework.PersistentVolumeClaim: framework.All,
framework.StorageClass: framework.All,
framework.PodSchedulingContext: framework.All,
framework.ResourceClaim: framework.All,
framework.ResourceClass: framework.All,
framework.ResourceClaimParameters: framework.All,
framework.ResourceClassParameters: framework.All,
framework.Pod: framework.All,
framework.Node: framework.All,
framework.CSINode: framework.All,
framework.CSIDriver: framework.All,
framework.CSIStorageCapacity: framework.All,
framework.PersistentVolume: framework.All,
framework.PersistentVolumeClaim: framework.All,
framework.StorageClass: framework.All,
framework.PodSchedulingContext: framework.All,
framework.ResourceClaim: framework.All,
framework.DeviceClass: framework.All,
},
},
{

View File

@@ -900,8 +900,8 @@ func (p *PersistentVolumeWrapper) NodeAffinityIn(key string, vals []string) *Per
type ResourceClaimWrapper struct{ resourceapi.ResourceClaim }
// MakeResourceClaim creates a ResourceClaim wrapper.
func MakeResourceClaim() *ResourceClaimWrapper {
return &ResourceClaimWrapper{resourceapi.ResourceClaim{}}
func MakeResourceClaim(controller string) *ResourceClaimWrapper {
return &ResourceClaimWrapper{resourceapi.ResourceClaim{Spec: resourceapi.ResourceClaimSpec{Controller: controller}}}
}
// FromResourceClaim creates a ResourceClaim wrapper from some existing object.
@@ -946,72 +946,34 @@ func (wrapper *ResourceClaimWrapper) OwnerReference(name, uid string, gvk schema
return wrapper
}
// ParametersRef sets a reference to a ResourceClaimParameters.resource.k8s.io.
func (wrapper *ResourceClaimWrapper) ParametersRef(name string) *ResourceClaimWrapper {
wrapper.ResourceClaim.Spec.ParametersRef = &resourceapi.ResourceClaimParametersReference{
Name: name,
Kind: "ResourceClaimParameters",
APIGroup: "resource.k8s.io",
}
return wrapper
}
// ResourceClassName sets the resource class name of the inner object.
func (wrapper *ResourceClaimWrapper) ResourceClassName(name string) *ResourceClaimWrapper {
wrapper.ResourceClaim.Spec.ResourceClassName = name
// Request adds one device request for the given device class.
func (wrapper *ResourceClaimWrapper) Request(deviceClassName string) *ResourceClaimWrapper {
wrapper.Spec.Devices.Requests = append(wrapper.Spec.Devices.Requests,
resourceapi.DeviceRequest{
Name: fmt.Sprintf("req-%d", len(wrapper.Spec.Devices.Requests)+1),
// Cannot rely on defaulting here, this is used in unit tests.
AllocationMode: resourceapi.DeviceAllocationModeExactCount,
Count: 1,
DeviceClassName: deviceClassName,
},
)
return wrapper
}
// Allocation sets the allocation of the inner object.
func (wrapper *ResourceClaimWrapper) Allocation(driverName string, allocation *resourceapi.AllocationResult) *ResourceClaimWrapper {
wrapper.ResourceClaim.Status.DriverName = driverName
func (wrapper *ResourceClaimWrapper) Allocation(allocation *resourceapi.AllocationResult) *ResourceClaimWrapper {
wrapper.ResourceClaim.Status.Allocation = allocation
return wrapper
}
// Structured turns a "normal" claim into one which was allocated via structured parameters.
// This modifies the allocation result and adds the reserved finalizer if the claim
// is allocated. The claim has to become local to a node. The assumption is that
// "named resources" are used.
func (wrapper *ResourceClaimWrapper) Structured(nodeName string, namedResourcesInstances ...string) *ResourceClaimWrapper {
// The only difference is that there is no controller name and the special finalizer
// gets added.
func (wrapper *ResourceClaimWrapper) Structured() *ResourceClaimWrapper {
wrapper.Spec.Controller = ""
if wrapper.ResourceClaim.Status.Allocation != nil {
wrapper.ResourceClaim.Finalizers = append(wrapper.ResourceClaim.Finalizers, resourceapi.Finalizer)
for i, resourceHandle := range wrapper.ResourceClaim.Status.Allocation.ResourceHandles {
resourceHandle.Data = ""
resourceHandle.StructuredData = &resourceapi.StructuredResourceHandle{
NodeName: nodeName,
}
wrapper.ResourceClaim.Status.Allocation.ResourceHandles[i] = resourceHandle
}
if len(wrapper.ResourceClaim.Status.Allocation.ResourceHandles) == 0 {
wrapper.ResourceClaim.Status.Allocation.ResourceHandles = []resourceapi.ResourceHandle{{
DriverName: wrapper.ResourceClaim.Status.DriverName,
StructuredData: &resourceapi.StructuredResourceHandle{
NodeName: nodeName,
},
}}
}
for _, resourceHandle := range wrapper.ResourceClaim.Status.Allocation.ResourceHandles {
for _, name := range namedResourcesInstances {
result := resourceapi.DriverAllocationResult{
AllocationResultModel: resourceapi.AllocationResultModel{
NamedResources: &resourceapi.NamedResourcesAllocationResult{
Name: name,
},
},
}
resourceHandle.StructuredData.Results = append(resourceHandle.StructuredData.Results, result)
}
}
wrapper.ResourceClaim.Status.Allocation.AvailableOnNodes = &v1.NodeSelector{
NodeSelectorTerms: []v1.NodeSelectorTerm{{
MatchExpressions: []v1.NodeSelectorRequirement{{
Key: "kubernetes.io/hostname",
Operator: v1.NodeSelectorOpIn,
Values: []string{nodeName},
}},
}},
}
wrapper.ResourceClaim.Status.Allocation.Controller = ""
}
return wrapper
}
@@ -1120,8 +1082,9 @@ type ResourceSliceWrapper struct {
func MakeResourceSlice(nodeName, driverName string) *ResourceSliceWrapper {
wrapper := new(ResourceSliceWrapper)
wrapper.Name = nodeName + "-" + driverName
wrapper.NodeName = nodeName
wrapper.DriverName = driverName
wrapper.Spec.NodeName = nodeName
wrapper.Spec.Pool.Name = nodeName
wrapper.Spec.Driver = driverName
return wrapper
}
@@ -1129,119 +1092,14 @@ func (wrapper *ResourceSliceWrapper) Obj() *resourceapi.ResourceSlice {
return &wrapper.ResourceSlice
}
func (wrapper *ResourceSliceWrapper) NamedResourcesInstances(names ...string) *ResourceSliceWrapper {
wrapper.ResourceModel = resourceapi.ResourceModel{NamedResources: &resourceapi.NamedResourcesResources{}}
func (wrapper *ResourceSliceWrapper) Devices(names ...string) *ResourceSliceWrapper {
for _, name := range names {
wrapper.ResourceModel.NamedResources.Instances = append(wrapper.ResourceModel.NamedResources.Instances,
resourceapi.NamedResourcesInstance{Name: name},
)
wrapper.Spec.Devices = append(wrapper.Spec.Devices, resourceapi.Device{Name: name})
}
return wrapper
}
type ClaimParametersWrapper struct {
resourceapi.ResourceClaimParameters
}
func MakeClaimParameters() *ClaimParametersWrapper {
return &ClaimParametersWrapper{}
}
// FromClaimParameters creates a ResourceClaimParameters wrapper from an existing object.
func FromClaimParameters(other *resourceapi.ResourceClaimParameters) *ClaimParametersWrapper {
return &ClaimParametersWrapper{*other.DeepCopy()}
}
func (wrapper *ClaimParametersWrapper) Obj() *resourceapi.ResourceClaimParameters {
return &wrapper.ResourceClaimParameters
}
func (wrapper *ClaimParametersWrapper) Name(s string) *ClaimParametersWrapper {
wrapper.SetName(s)
return wrapper
}
func (wrapper *ClaimParametersWrapper) UID(s string) *ClaimParametersWrapper {
wrapper.SetUID(types.UID(s))
return wrapper
}
func (wrapper *ClaimParametersWrapper) Namespace(s string) *ClaimParametersWrapper {
wrapper.SetNamespace(s)
return wrapper
}
func (wrapper *ClaimParametersWrapper) GeneratedFrom(value *resourceapi.ResourceClaimParametersReference) *ClaimParametersWrapper {
wrapper.ResourceClaimParameters.GeneratedFrom = value
return wrapper
}
func (wrapper *ClaimParametersWrapper) NamedResourcesRequests(driverName string, selectors ...string) *ClaimParametersWrapper {
requests := resourceapi.DriverRequests{
DriverName: driverName,
}
for _, selector := range selectors {
request := resourceapi.ResourceRequest{
ResourceRequestModel: resourceapi.ResourceRequestModel{
NamedResources: &resourceapi.NamedResourcesRequest{
Selector: selector,
},
},
}
requests.Requests = append(requests.Requests, request)
}
wrapper.DriverRequests = append(wrapper.DriverRequests, requests)
return wrapper
}
type ClassParametersWrapper struct {
resourceapi.ResourceClassParameters
}
func MakeClassParameters() *ClassParametersWrapper {
return &ClassParametersWrapper{}
}
// FromClassParameters creates a ResourceClassParameters wrapper from an existing object.
func FromClassParameters(other *resourceapi.ResourceClassParameters) *ClassParametersWrapper {
return &ClassParametersWrapper{*other.DeepCopy()}
}
func (wrapper *ClassParametersWrapper) Obj() *resourceapi.ResourceClassParameters {
return &wrapper.ResourceClassParameters
}
func (wrapper *ClassParametersWrapper) Name(s string) *ClassParametersWrapper {
wrapper.SetName(s)
return wrapper
}
func (wrapper *ClassParametersWrapper) UID(s string) *ClassParametersWrapper {
wrapper.SetUID(types.UID(s))
return wrapper
}
func (wrapper *ClassParametersWrapper) Namespace(s string) *ClassParametersWrapper {
wrapper.SetNamespace(s)
return wrapper
}
func (wrapper *ClassParametersWrapper) GeneratedFrom(value *resourceapi.ResourceClassParametersReference) *ClassParametersWrapper {
wrapper.ResourceClassParameters.GeneratedFrom = value
return wrapper
}
func (wrapper *ClassParametersWrapper) NamedResourcesFilters(driverName string, selectors ...string) *ClassParametersWrapper {
for _, selector := range selectors {
filter := resourceapi.ResourceFilter{
DriverName: driverName,
ResourceFilterModel: resourceapi.ResourceFilterModel{
NamedResources: &resourceapi.NamedResourcesFilter{
Selector: selector,
},
},
}
wrapper.Filters = append(wrapper.Filters, filter)
}
func (wrapper *ResourceSliceWrapper) Device(name string, attrs map[resourceapi.QualifiedName]resourceapi.DeviceAttribute) *ResourceSliceWrapper {
wrapper.Spec.Devices = append(wrapper.Spec.Devices, resourceapi.Device{Name: name, Basic: &resourceapi.BasicDevice{Attributes: attrs}})
return wrapper
}

View File

@@ -256,6 +256,7 @@
- k8s.io/apiserver/pkg/cel
- k8s.io/apiserver/pkg/cel/environment
- k8s.io/client-go
- k8s.io/component-helpers/scheduling/corev1/nodeaffinity
- k8s.io/dynamic-resource-allocation
- k8s.io/klog
- k8s.io/kubelet

View File

@@ -0,0 +1,844 @@
/*
Copyright 2024 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 structured
import (
"context"
"errors"
"fmt"
"math"
"strings"
v1 "k8s.io/api/core/v1"
resourceapi "k8s.io/api/resource/v1alpha3"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/cel/environment"
resourcelisters "k8s.io/client-go/listers/resource/v1alpha3"
"k8s.io/dynamic-resource-allocation/cel"
"k8s.io/klog/v2"
)
// ClaimLister returns a subset of the claims that a
// resourcelisters.ResourceClaimLister would return.
type ClaimLister interface {
// ListAllAllocated returns only claims which are allocated.
ListAllAllocated() ([]*resourceapi.ResourceClaim, error)
}
// Allocator calculates how to allocate a set of unallocated claims which use
// structured parameters.
//
// It needs as input the node where the allocated claims are meant to be
// available and the current state of the cluster (claims, classes, resource
// slices).
type Allocator struct {
claimsToAllocate []*resourceapi.ResourceClaim
claimLister ClaimLister
classLister resourcelisters.DeviceClassLister
sliceLister resourcelisters.ResourceSliceLister
}
// NewAllocator returns an allocator for a certain set of claims or an error if
// some problem was detected which makes it impossible to allocate claims.
func NewAllocator(ctx context.Context,
claimsToAllocate []*resourceapi.ResourceClaim,
claimLister ClaimLister,
classLister resourcelisters.DeviceClassLister,
sliceLister resourcelisters.ResourceSliceLister,
) (*Allocator, error) {
return &Allocator{
claimsToAllocate: claimsToAllocate,
claimLister: claimLister,
classLister: classLister,
sliceLister: sliceLister,
}, nil
}
// ClaimsToAllocate returns the claims that the allocated was created for.
func (a *Allocator) ClaimsToAllocate() []*resourceapi.ResourceClaim {
return a.claimsToAllocate
}
// Allocate calculates the allocation(s) for one particular node.
//
// It returns an error only if some fatal problem occurred. These are errors
// caused by invalid input data, like for example errors in CEL selectors, so a
// scheduler should abort and report that problem instead of trying to find
// other nodes where the error doesn't occur.
//
// In the future, special errors will be defined which enable the caller to
// identify which object (like claim or class) caused the problem. This will
// enable reporting the problem as event for those objects.
//
// If the claims cannot be allocated, it returns nil. This includes the
// situation where the resource slices are incomplete at the moment.
//
// If the claims can be allocated, then it prepares one allocation result for
// each unallocated claim. It is the responsibility of the caller to persist
// those allocations, if desired.
//
// Allocate is thread-safe. If the caller wants to get the node name included
// in log output, it can use contextual logging and add the node as an
// additional value. A name can also be useful because log messages do not
// have a common prefix. V(5) is used for one-time log entries, V(6) for important
// progress reports, and V(7) for detailed debug output.
func (a *Allocator) Allocate(ctx context.Context, node *v1.Node) (finalResult []*resourceapi.AllocationResult, finalErr error) {
alloc := &allocator{
Allocator: a,
ctx: ctx, // all methods share the same a and thus ctx
logger: klog.FromContext(ctx),
deviceMatchesRequest: make(map[matchKey]bool),
constraints: make([][]constraint, len(a.claimsToAllocate)),
requestData: make(map[requestIndices]requestData),
allocated: make(map[DeviceID]bool),
result: make([]*resourceapi.AllocationResult, len(a.claimsToAllocate)),
}
alloc.logger.V(5).Info("Starting allocation", "numClaims", len(alloc.claimsToAllocate))
defer alloc.logger.V(5).Info("Done with allocation", "success", len(finalResult) == len(alloc.claimsToAllocate), "err", finalErr)
// First determine all eligible pools.
pools, err := GatherPools(ctx, alloc.sliceLister, node)
if err != nil {
return nil, fmt.Errorf("gather pool information: %w", err)
}
alloc.pools = pools
if loggerV := alloc.logger.V(7); loggerV.Enabled() {
loggerV.Info("Gathered pool information", "numPools", len(pools), "pools", pools)
} else {
alloc.logger.V(5).Info("Gathered pool information", "numPools", len(pools))
}
// We allocate one claim after the other and for each claim, all of
// its requests. For each individual device we pick one possible
// candidate after the other, checking constraints as we go.
// Each chosen candidate is marked as "in use" and the process
// continues, recursively. This way, all requests get matched against
// all candidates in all possible orders.
//
// The first full solution is chosen.
//
// In other words, this is an exhaustive search. This is okay because
// it aborts early. Once scoring gets added, more intelligence may be
// needed to avoid trying "equivalent" solutions (two identical
// requests, two identical devices, two solutions that are the same in
// practice).
// This is where we sanity check that we can actually handle the claims
// and their requests. For each claim we determine how many devices
// need to be allocated. If not all can be stored in the result, the
// claim cannot be allocated.
for claimIndex, claim := range alloc.claimsToAllocate {
numDevices := 0
// If we have any any request that wants "all" devices, we need to
// figure out how much "all" is. If some pool is incomplete, we stop
// here because allocation cannot succeed. Once we do scoring, we should
// stop in all cases, not just when "all" devices are needed, because
// pulling from an incomplete might not pick the best solution and it's
// better to wait. This does not matter yet as long the incomplete pool
// has some matching device.
for requestIndex := range claim.Spec.Devices.Requests {
request := &claim.Spec.Devices.Requests[requestIndex]
for i, selector := range request.Selectors {
if selector.CEL == nil {
// Unknown future selector type!
return nil, fmt.Errorf("claim %s, request %s, selector #%d: CEL expression empty (unsupported selector type?)", klog.KObj(claim), request.Name, i)
}
}
// Should be set. If it isn't, something changed and we should refuse to proceed.
if request.DeviceClassName == "" {
return nil, fmt.Errorf("claim %s, request %s: missing device class name (unsupported request type?)", klog.KObj(claim), request.Name)
}
class, err := alloc.classLister.Get(request.DeviceClassName)
if err != nil {
return nil, fmt.Errorf("claim %s, request %s: could not retrieve device class %s: %w", klog.KObj(claim), request.Name, request.DeviceClassName, err)
}
requestData := requestData{
class: class,
}
switch request.AllocationMode {
case resourceapi.DeviceAllocationModeExactCount:
numDevices := request.Count
if numDevices > math.MaxInt {
// Allowed by API validation, but doesn't make sense.
return nil, fmt.Errorf("claim %s, request %s: exact count %d is too large", klog.KObj(claim), request.Name, numDevices)
}
requestData.numDevices = int(numDevices)
case resourceapi.DeviceAllocationModeAll:
requestData.allDevices = make([]deviceWithID, 0, resourceapi.AllocationResultsMaxSize)
for _, pool := range pools {
if pool.IsIncomplete {
return nil, fmt.Errorf("claim %s, request %s: asks for all devices, but resource pool %s is currently being updated", klog.KObj(claim), request.Name, pool.PoolID)
}
for _, slice := range pool.Slices {
for deviceIndex := range slice.Spec.Devices {
selectable, err := alloc.isSelectable(requestIndices{claimIndex: claimIndex, requestIndex: requestIndex}, slice, deviceIndex)
if err != nil {
return nil, err
}
if selectable {
requestData.allDevices = append(requestData.allDevices, deviceWithID{device: slice.Spec.Devices[deviceIndex].Basic, DeviceID: DeviceID{Driver: slice.Spec.Driver, Pool: slice.Spec.Pool.Name, Device: slice.Spec.Devices[deviceIndex].Name}})
}
}
}
}
requestData.numDevices = len(requestData.allDevices)
alloc.logger.V(6).Info("Request for 'all' devices", "claim", klog.KObj(claim), "request", request.Name, "numDevicesPerRequest", requestData.numDevices)
default:
return nil, fmt.Errorf("claim %s, request %s: unsupported count mode %s", klog.KObj(claim), request.Name, request.AllocationMode)
}
alloc.requestData[requestIndices{claimIndex: claimIndex, requestIndex: requestIndex}] = requestData
numDevices += requestData.numDevices
}
alloc.logger.Info("Checked claim", "claim", klog.KObj(claim), "numDevices", numDevices)
// Check that we don't end up with too many results.
if numDevices > resourceapi.AllocationResultsMaxSize {
return nil, fmt.Errorf("claim %s: number of requested devices %d exceeds the claim limit of %d", klog.KObj(claim), numDevices, resourceapi.AllocationResultsMaxSize)
}
// If we don't, then we can pre-allocate the result slices for
// appending the actual results later.
alloc.result[claimIndex] = &resourceapi.AllocationResult{
Devices: resourceapi.DeviceAllocationResult{
Results: make([]resourceapi.DeviceRequestAllocationResult, 0, numDevices),
},
}
// Constraints are assumed to be monotonic: once a constraint returns
// false, adding more devices will not cause it to return true. This
// allows the search to stop early once a constraint returns false.
var constraints = make([]constraint, len(claim.Spec.Devices.Constraints))
for i, constraint := range claim.Spec.Devices.Constraints {
switch {
case constraint.MatchAttribute != nil:
logger := alloc.logger
if loggerV := alloc.logger.V(6); loggerV.Enabled() {
logger = klog.LoggerWithName(logger, "matchAttributeConstraint")
logger = klog.LoggerWithValues(logger, "matchAttribute", *constraint.MatchAttribute)
}
m := &matchAttributeConstraint{
logger: logger,
requestNames: sets.New(constraint.Requests...),
attributeName: *constraint.MatchAttribute,
}
constraints[i] = m
default:
// Unknown constraint type!
return nil, fmt.Errorf("claim %s, constraint #%d: empty constraint (unsupported constraint type?)", klog.KObj(claim), i)
}
}
alloc.constraints[claimIndex] = constraints
}
// Selecting a device for a request is independent of what has been
// allocated already. Therefore the result of checking a request against
// a device instance in the pool can be cached. The pointer to both
// can serve as key because they are static for the duration of
// the Allocate call and can be compared in Go.
alloc.deviceMatchesRequest = make(map[matchKey]bool)
// Some of the existing devices are probably already allocated by
// claims...
claims, err := alloc.claimLister.ListAllAllocated()
numAllocated := 0
if err != nil {
return nil, fmt.Errorf("list allocated claims: %w", err)
}
for _, claim := range claims {
// Sanity check..
if claim.Status.Allocation == nil {
continue
}
for _, result := range claim.Status.Allocation.Devices.Results {
deviceID := DeviceID{Driver: result.Driver, Pool: result.Pool, Device: result.Device}
alloc.allocated[deviceID] = true
numAllocated++
}
}
alloc.logger.V(6).Info("Gathered information about allocated devices", "numAllocated", numAllocated)
// In practice, there aren't going to be many different CEL
// expressions. Most likely, there is going to be handful of different
// device classes that get used repeatedly. Different requests may all
// use the same selector. Therefore compiling CEL expressions on demand
// could be a useful performance enhancement. It's not implemented yet
// because the key is more complex (just the string?) and the memory
// for both key and cached content is larger than for device matches.
//
// We may also want to cache this in the shared [Allocator] instance,
// which implies adding locking.
// All errors get created such that they can be returned by Allocate
// without further wrapping.
done, err := alloc.allocateOne(deviceIndices{})
if err != nil {
return nil, err
}
if errors.Is(err, errStop) || !done {
return nil, nil
}
for claimIndex, allocationResult := range alloc.result {
claim := alloc.claimsToAllocate[claimIndex]
// Populate configs.
for requestIndex := range claim.Spec.Devices.Requests {
class := alloc.requestData[requestIndices{claimIndex: claimIndex, requestIndex: requestIndex}].class
if class != nil {
for _, config := range class.Spec.Config {
allocationResult.Devices.Config = append(allocationResult.Devices.Config, resourceapi.DeviceAllocationConfiguration{
Source: resourceapi.AllocationConfigSourceClass,
Requests: nil, // All of them...
DeviceConfiguration: config.DeviceConfiguration,
})
}
}
}
for _, config := range claim.Spec.Devices.Config {
allocationResult.Devices.Config = append(allocationResult.Devices.Config, resourceapi.DeviceAllocationConfiguration{
Source: resourceapi.AllocationConfigSourceClaim,
Requests: config.Requests,
DeviceConfiguration: config.DeviceConfiguration,
})
}
// Determine node selector.
nodeSelector, err := alloc.createNodeSelector(allocationResult)
if err != nil {
return nil, fmt.Errorf("create NodeSelector for claim %s: %w", claim.Name, err)
}
allocationResult.NodeSelector = nodeSelector
}
return alloc.result, nil
}
// errStop is a special error that gets returned by allocateOne if it detects
// that allocation cannot succeed.
var errStop = errors.New("stop allocation")
// allocator is used while an [Allocator.Allocate] is running. Only a single
// goroutine works with it, so there is no need for locking.
type allocator struct {
*Allocator
ctx context.Context
logger klog.Logger
pools []*Pool
deviceMatchesRequest map[matchKey]bool
constraints [][]constraint // one list of constraints per claim
requestData map[requestIndices]requestData // one entry per request
allocated map[DeviceID]bool
skippedUnknownDevice bool
result []*resourceapi.AllocationResult
}
// matchKey identifies a device/request pair.
type matchKey struct {
DeviceID
requestIndices
}
// requestIndices identifies one specific request by its
// claim and request index.
type requestIndices struct {
claimIndex, requestIndex int
}
// deviceIndices identifies one specific required device inside
// a request of a certain claim.
type deviceIndices struct {
claimIndex, requestIndex, deviceIndex int
}
type requestData struct {
class *resourceapi.DeviceClass
numDevices int
// pre-determined set of devices for allocating "all" devices
allDevices []deviceWithID
}
type deviceWithID struct {
DeviceID
device *resourceapi.BasicDevice
}
type constraint interface {
// add is called whenever a device is about to be allocated. It must
// check whether the device matches the constraint and if yes,
// track that it is allocated.
add(requestName string, device *resourceapi.BasicDevice, deviceID DeviceID) bool
// For every successful add there is exactly one matching removed call
// with the exact same parameters.
remove(requestName string, device *resourceapi.BasicDevice, deviceID DeviceID)
}
// matchAttributeConstraint compares an attribute value across devices.
// All devices must share the same value. When the set of devices is
// empty, any device that has the attribute can be added. After that,
// only matching devices can be added.
//
// We don't need to track *which* devices are part of the set, only
// how many.
type matchAttributeConstraint struct {
logger klog.Logger // Includes name and attribute name, so no need to repeat in log messages.
requestNames sets.Set[string]
attributeName resourceapi.FullyQualifiedName
attribute *resourceapi.DeviceAttribute
numDevices int
}
func (m *matchAttributeConstraint) add(requestName string, device *resourceapi.BasicDevice, deviceID DeviceID) bool {
if m.requestNames.Len() > 0 && !m.requestNames.Has(requestName) {
// Device not affected by constraint.
m.logger.V(7).Info("Constraint does not apply to request", "request", requestName)
return true
}
attribute := lookupAttribute(device, deviceID, m.attributeName)
if attribute == nil {
// Doesn't have the attribute.
m.logger.V(7).Info("Constraint not satisfied, attribute not set")
return false
}
if m.numDevices == 0 {
// The first device can always get picked.
m.attribute = attribute
m.numDevices = 1
m.logger.V(7).Info("First in set")
return true
}
switch {
case attribute.StringValue != nil:
if m.attribute.StringValue == nil || *attribute.StringValue != *m.attribute.StringValue {
m.logger.V(7).Info("String values different")
return false
}
case attribute.IntValue != nil:
if m.attribute.IntValue == nil || *attribute.IntValue != *m.attribute.IntValue {
m.logger.V(7).Info("Int values different")
return false
}
case attribute.BoolValue != nil:
if m.attribute.BoolValue == nil || *attribute.BoolValue != *m.attribute.BoolValue {
m.logger.V(7).Info("Bool values different")
return false
}
case attribute.VersionValue != nil:
// semver 2.0.0 requires that version strings are in their
// minimal form (in particular, no leading zeros). Therefore a
// strict "exact equal" check can do a string comparison.
if m.attribute.VersionValue == nil || *attribute.VersionValue != *m.attribute.VersionValue {
m.logger.V(7).Info("Version values different")
return false
}
default:
// Unknown value type, cannot match.
m.logger.V(7).Info("Match attribute type unknown")
return false
}
m.numDevices++
m.logger.V(7).Info("Constraint satisfied by device", "device", deviceID, "numDevices", m.numDevices)
return true
}
func (m *matchAttributeConstraint) remove(requestName string, device *resourceapi.BasicDevice, deviceID DeviceID) {
if m.requestNames.Len() > 0 && !m.requestNames.Has(requestName) {
// Device not affected by constraint.
return
}
m.numDevices--
m.logger.V(7).Info("Device removed from constraint set", "device", deviceID, "numDevices", m.numDevices)
}
func lookupAttribute(device *resourceapi.BasicDevice, deviceID DeviceID, attributeName resourceapi.FullyQualifiedName) *resourceapi.DeviceAttribute {
// Fully-qualified match?
if attr, ok := device.Attributes[resourceapi.QualifiedName(attributeName)]; ok {
return &attr
}
index := strings.Index(string(attributeName), "/")
if index < 0 {
// Should not happen for a valid fully qualified name.
return nil
}
if string(attributeName[0:index]) != deviceID.Driver {
// Not an attribute of the driver and not found above,
// so it is not available.
return nil
}
// Domain matches the driver, so let's check just the ID.
if attr, ok := device.Attributes[resourceapi.QualifiedName(attributeName[index+1:])]; ok {
return &attr
}
return nil
}
// allocateOne iterates over all eligible devices (not in use, match selector,
// satisfy constraints) for a specific required device. It returns true if
// everything got allocated, an error if allocation needs to stop.
func (alloc *allocator) allocateOne(r deviceIndices) (bool, error) {
if r.claimIndex >= len(alloc.claimsToAllocate) {
// Done! If we were doing scoring, we would compare the current allocation result
// against the previous one, keep the best, and continue. Without scoring, we stop
// and use the first solution.
alloc.logger.V(6).Info("Allocation result found")
return true, nil
}
claim := alloc.claimsToAllocate[r.claimIndex]
if r.requestIndex >= len(claim.Spec.Devices.Requests) {
// Done with the claim, continue with the next one.
return alloc.allocateOne(deviceIndices{claimIndex: r.claimIndex + 1})
}
// We already know how many devices per request are needed.
// Ready to move on to the next request?
requestData := alloc.requestData[requestIndices{claimIndex: r.claimIndex, requestIndex: r.requestIndex}]
if r.deviceIndex >= requestData.numDevices {
return alloc.allocateOne(deviceIndices{claimIndex: r.claimIndex, requestIndex: r.requestIndex + 1})
}
request := &alloc.claimsToAllocate[r.claimIndex].Spec.Devices.Requests[r.requestIndex]
doAllDevices := request.AllocationMode == resourceapi.DeviceAllocationModeAll
alloc.logger.V(6).Info("Allocating one device", "currentClaim", r.claimIndex, "totalClaims", len(alloc.claimsToAllocate), "currentRequest", r.requestIndex, "totalRequestsPerClaim", len(claim.Spec.Devices.Requests), "currentDevice", r.deviceIndex, "devicesPerRequest", requestData.numDevices, "allDevices", doAllDevices, "adminAccess", request.AdminAccess)
if doAllDevices {
// For "all" devices we already know which ones we need. We
// just need to check whether we can use them.
deviceWithID := requestData.allDevices[r.deviceIndex]
_, _, err := alloc.allocateDevice(r, deviceWithID.device, deviceWithID.DeviceID, true)
if err != nil {
return false, err
}
done, err := alloc.allocateOne(deviceIndices{claimIndex: r.claimIndex, requestIndex: r.requestIndex, deviceIndex: r.deviceIndex + 1})
if err != nil {
return false, err
}
// The order in which we allocate "all" devices doesn't matter,
// so we only try with the one which was up next. If we couldn't
// get all of them, then there is no solution and we have to stop.
if !done {
return false, errStop
}
return done, nil
}
// We need to find suitable devices.
for _, pool := range alloc.pools {
for _, slice := range pool.Slices {
for deviceIndex := range slice.Spec.Devices {
deviceID := DeviceID{Driver: pool.Driver, Pool: pool.Pool, Device: slice.Spec.Devices[deviceIndex].Name}
// Checking for "in use" is cheap and thus gets done first.
if !request.AdminAccess && alloc.allocated[deviceID] {
alloc.logger.V(7).Info("Device in use", "device", deviceID)
continue
}
// Next check selectors.
selectable, err := alloc.isSelectable(requestIndices{claimIndex: r.claimIndex, requestIndex: r.requestIndex}, slice, deviceIndex)
if err != nil {
return false, err
}
if !selectable {
alloc.logger.V(7).Info("Device not selectable", "device", deviceID)
continue
}
// Finally treat as allocated and move on to the next device.
allocated, deallocate, err := alloc.allocateDevice(r, slice.Spec.Devices[deviceIndex].Basic, deviceID, false)
if err != nil {
return false, err
}
if !allocated {
// In use or constraint violated...
alloc.logger.V(7).Info("Device not usable", "device", deviceID)
continue
}
done, err := alloc.allocateOne(deviceIndices{claimIndex: r.claimIndex, requestIndex: r.requestIndex, deviceIndex: r.deviceIndex + 1})
if err != nil {
return false, err
}
// If we found a solution, then we can stop.
if done {
return done, nil
}
// Otherwise try some other device after rolling back.
deallocate()
}
}
}
// If we get here without finding a solution, then there is none.
return false, nil
}
// isSelectable checks whether a device satisfies the request and class selectors.
func (alloc *allocator) isSelectable(r requestIndices, slice *resourceapi.ResourceSlice, deviceIndex int) (bool, error) {
// This is the only supported device type at the moment.
device := slice.Spec.Devices[deviceIndex].Basic
if device == nil {
// Must be some future, unknown device type. We cannot select it.
// If we don't find anything else, then this will get reported
// in the final result, so remember that we skipped some device.
alloc.skippedUnknownDevice = true
return false, nil
}
deviceID := DeviceID{Driver: slice.Spec.Driver, Pool: slice.Spec.Pool.Name, Device: slice.Spec.Devices[deviceIndex].Name}
matchKey := matchKey{DeviceID: deviceID, requestIndices: r}
if matches, ok := alloc.deviceMatchesRequest[matchKey]; ok {
// No need to check again.
return matches, nil
}
requestData := alloc.requestData[r]
if requestData.class != nil {
match, err := alloc.selectorsMatch(r, device, deviceID, requestData.class, requestData.class.Spec.Selectors)
if err != nil {
return false, err
}
if !match {
alloc.deviceMatchesRequest[matchKey] = false
return false, nil
}
}
request := &alloc.claimsToAllocate[r.claimIndex].Spec.Devices.Requests[r.requestIndex]
match, err := alloc.selectorsMatch(r, device, deviceID, nil, request.Selectors)
if err != nil {
return false, err
}
if !match {
alloc.deviceMatchesRequest[matchKey] = false
return false, nil
}
alloc.deviceMatchesRequest[matchKey] = true
return true, nil
}
func (alloc *allocator) selectorsMatch(r requestIndices, device *resourceapi.BasicDevice, deviceID DeviceID, class *resourceapi.DeviceClass, selectors []resourceapi.DeviceSelector) (bool, error) {
for i, selector := range selectors {
expr := cel.GetCompiler().CompileCELExpression(selector.CEL.Expression, environment.StoredExpressions)
if expr.Error != nil {
// Could happen if some future apiserver accepted some
// future expression and then got downgraded. Normally
// the "stored expression" mechanism prevents that, but
// this code here might be more than one release older
// than the cluster it runs in.
if class != nil {
return false, fmt.Errorf("class %s: selector #%d: CEL compile error: %w", class.Name, i, expr.Error)
}
return false, fmt.Errorf("claim %s: selector #%d: CEL compile error: %w", klog.KObj(alloc.claimsToAllocate[r.claimIndex]), i, expr.Error)
}
matches, err := expr.DeviceMatches(alloc.ctx, cel.Device{Driver: deviceID.Driver, Attributes: device.Attributes, Capacity: device.Capacity})
if class != nil {
alloc.logger.V(7).Info("CEL result", "device", deviceID, "class", klog.KObj(class), "selector", i, "expression", selector.CEL.Expression, "matches", matches, "err", err)
} else {
alloc.logger.V(7).Info("CEL result", "device", deviceID, "claim", klog.KObj(alloc.claimsToAllocate[r.claimIndex]), "selector", i, "expression", selector.CEL.Expression, "matches", matches, "err", err)
}
if err != nil {
// TODO (future): more detailed errors which reference class resp. claim.
if class != nil {
return false, fmt.Errorf("class %s: selector #%d: CEL runtime error: %w", class.Name, i, err)
}
return false, fmt.Errorf("claim %s: selector #%d: CEL runtime error: %w", klog.KObj(alloc.claimsToAllocate[r.claimIndex]), i, err)
}
if !matches {
return false, nil
}
}
// All of them match.
return true, nil
}
// allocateDevice checks device availability and constraints for one
// candidate. The device must be selectable.
//
// If that candidate works out okay, the shared state gets updated
// as if that candidate had been allocated. If allocation cannot continue later
// and must try something else, then the rollback function can be invoked to
// restore the previous state.
func (alloc *allocator) allocateDevice(r deviceIndices, device *resourceapi.BasicDevice, deviceID DeviceID, must bool) (bool, func(), error) {
claim := alloc.claimsToAllocate[r.claimIndex]
request := &claim.Spec.Devices.Requests[r.requestIndex]
adminAccess := request.AdminAccess
if !adminAccess && alloc.allocated[deviceID] {
alloc.logger.V(7).Info("Device in use", "device", deviceID)
return false, nil, nil
}
// It's available. Now check constraints.
for i, constraint := range alloc.constraints[r.claimIndex] {
added := constraint.add(request.Name, device, deviceID)
if !added {
if must {
// It does not make sense to declare a claim where a constraint prevents getting
// all devices. Treat this as an error.
return false, nil, fmt.Errorf("claim %s, request %s: cannot add device %s because a claim constraint would not be satisfied", klog.KObj(claim), request.Name, deviceID)
}
// Roll back for all previous constraints before we return.
for e := 0; e < i; e++ {
alloc.constraints[r.claimIndex][e].remove(request.Name, device, deviceID)
}
return false, nil, nil
}
}
// All constraints satisfied. Mark as in use (unless we do admin access)
// and record the result.
alloc.logger.V(7).Info("Device allocated", "device", deviceID)
if !adminAccess {
alloc.allocated[deviceID] = true
}
result := resourceapi.DeviceRequestAllocationResult{
Request: request.Name,
Driver: deviceID.Driver,
Pool: deviceID.Pool,
Device: deviceID.Device,
}
previousNumResults := len(alloc.result[r.claimIndex].Devices.Results)
alloc.result[r.claimIndex].Devices.Results = append(alloc.result[r.claimIndex].Devices.Results, result)
return true, func() {
for _, constraint := range alloc.constraints[r.claimIndex] {
constraint.remove(request.Name, device, deviceID)
}
if !adminAccess {
alloc.allocated[deviceID] = false
}
// Truncate, but keep the underlying slice.
alloc.result[r.claimIndex].Devices.Results = alloc.result[r.claimIndex].Devices.Results[:previousNumResults]
alloc.logger.V(7).Info("Device deallocated", "device", deviceID)
}, nil
}
// createNodeSelector constructs a node selector for the allocation, if needed,
// otherwise it returns nil.
func (alloc *allocator) createNodeSelector(allocation *resourceapi.AllocationResult) (*v1.NodeSelector, error) {
// Selector with one term. That term gets extended with additional
// requirements from the different devices.
nodeSelector := &v1.NodeSelector{
NodeSelectorTerms: []v1.NodeSelectorTerm{{}},
}
for _, deviceAllocation := range allocation.Devices.Results {
slice := alloc.findSlice(deviceAllocation)
if slice == nil {
return nil, fmt.Errorf("internal error: device %+v not found in pools", deviceAllocation)
}
if slice.Spec.NodeName != "" {
// At least one device is local to one node. This
// restricts the allocation to that node.
return &v1.NodeSelector{
NodeSelectorTerms: []v1.NodeSelectorTerm{{
MatchFields: []v1.NodeSelectorRequirement{{
Key: "metadata.name",
Operator: v1.NodeSelectorOpIn,
Values: []string{slice.Spec.NodeName},
}},
}},
}, nil
}
if slice.Spec.NodeSelector != nil {
switch len(slice.Spec.NodeSelector.NodeSelectorTerms) {
case 0:
// Nothing?
case 1:
// Add all terms if they are not present already.
addNewNodeSelectorRequirements(slice.Spec.NodeSelector.NodeSelectorTerms[0].MatchFields, &nodeSelector.NodeSelectorTerms[0].MatchFields)
addNewNodeSelectorRequirements(slice.Spec.NodeSelector.NodeSelectorTerms[0].MatchExpressions, &nodeSelector.NodeSelectorTerms[0].MatchExpressions)
default:
// This shouldn't occur, validation must prevent creation of such slices.
return nil, fmt.Errorf("unsupported ResourceSlice.NodeSelector with %d terms", len(slice.Spec.NodeSelector.NodeSelectorTerms))
}
}
}
if len(nodeSelector.NodeSelectorTerms[0].MatchFields) > 0 || len(nodeSelector.NodeSelectorTerms[0].MatchExpressions) > 0 {
// We have a valid node selector.
return nodeSelector, nil
}
// Available everywhere.
return nil, nil
}
func (alloc *allocator) findSlice(deviceAllocation resourceapi.DeviceRequestAllocationResult) *resourceapi.ResourceSlice {
for _, pool := range alloc.pools {
if pool.Driver != deviceAllocation.Driver ||
pool.Pool != deviceAllocation.Pool {
continue
}
for _, slice := range pool.Slices {
for _, device := range slice.Spec.Devices {
if device.Name == deviceAllocation.Device {
return slice
}
}
}
}
return nil
}
func addNewNodeSelectorRequirements(from []v1.NodeSelectorRequirement, to *[]v1.NodeSelectorRequirement) {
for _, requirement := range from {
if !containsNodeSelectorRequirement(*to, requirement) {
*to = append(*to, requirement)
}
}
}
func containsNodeSelectorRequirement(requirements []v1.NodeSelectorRequirement, requirement v1.NodeSelectorRequirement) bool {
values := sets.New(requirement.Values...)
for _, existingRequirement := range requirements {
if existingRequirement.Key != requirement.Key {
continue
}
if existingRequirement.Operator != requirement.Operator {
continue
}
if !sets.New(existingRequirement.Values...).Equal(values) {
continue
}
return true
}
return false
}

View File

@@ -0,0 +1,971 @@
/*
Copyright 2024 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 structured
import (
"errors"
"flag"
"fmt"
"testing"
"github.com/onsi/gomega"
"github.com/onsi/gomega/gstruct"
"github.com/onsi/gomega/types"
v1 "k8s.io/api/core/v1"
resourceapi "k8s.io/api/resource/v1alpha3"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/klog/v2/ktesting"
"k8s.io/utils/ptr"
)
const (
region1 = "region-1"
region2 = "region-2"
node1 = "node-1"
node2 = "node-2"
classA = "class-a"
classB = "class-b"
driverA = "driver-a"
driverB = "driver-b"
pool1 = "pool-1"
pool2 = "pool-2"
pool3 = "pool-3"
pool4 = "pool-4"
req0 = "req-0"
req1 = "req-1"
req2 = "req-2"
req3 = "req-3"
claim0 = "claim-0"
claim1 = "claim-1"
slice1 = "slice-1"
slice2 = "slice-2"
device1 = "device-1"
device2 = "device-2"
)
func init() {
// Bump up the default verbosity for testing. Allocate uses very
// high thresholds because it is used in the scheduler's per-node
// filter operation.
ktesting.DefaultConfig = ktesting.NewConfig(ktesting.Verbosity(7))
ktesting.DefaultConfig.AddFlags(flag.CommandLine)
}
// Test objects generators
const (
fieldNameKey = "metadata.name"
regionKey = "region"
planetKey = "planet"
planetValueEarth = "earth"
)
// generate a node object given a name and a region
func node(name, region string) *v1.Node {
return &v1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: map[string]string{
regionKey: region,
planetKey: planetValueEarth,
},
},
}
}
// generate a DeviceClass object with the given name and a driver CEL selector.
// driver name is assumed to be the same as the class name.
func class(name, driver string) *resourceapi.DeviceClass {
return &resourceapi.DeviceClass{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: resourceapi.DeviceClassSpec{
Selectors: []resourceapi.DeviceSelector{
{
CEL: &resourceapi.CELDeviceSelector{
Expression: fmt.Sprintf(`device.driver == "%s"`, driver),
},
},
},
},
}
}
// generate a DeviceConfiguration object with the given driver and attribute.
func deviceConfiguration(driver, attribute string) resourceapi.DeviceConfiguration {
return resourceapi.DeviceConfiguration{
Opaque: &resourceapi.OpaqueDeviceConfiguration{
Driver: driver,
Parameters: runtime.RawExtension{
Raw: []byte(fmt.Sprintf("{\"%s\":\"%s\"}", attribute, attribute+"Value")),
},
},
}
}
// generate a DeviceClass object with the given name and attribute.
// attribute is used to generate device configuration parameters in a form of JSON {attribute: attributeValue}.
func classWithConfig(name, driver, attribute string) *resourceapi.DeviceClass {
class := class(name, driver)
class.Spec.Config = []resourceapi.DeviceClassConfiguration{
{
DeviceConfiguration: deviceConfiguration(driver, attribute),
},
}
return class
}
// generate a DeviceClass object with the given name and the node selector
// that selects nodes with the region label set to either "west" or "east".
func classWithSuitableNodes(name, driver string) *resourceapi.DeviceClass {
class := class(name, driver)
class.Spec.SuitableNodes = &v1.NodeSelector{
NodeSelectorTerms: []v1.NodeSelectorTerm{
{
MatchExpressions: []v1.NodeSelectorRequirement{
{
Key: regionKey,
Operator: v1.NodeSelectorOpIn,
Values: []string{region1, region2},
},
},
},
},
}
return class
}
// generate a ResourceClaim object with the given name and device requests.
func claimWithRequests(name string, constraints []resourceapi.DeviceConstraint, requests ...resourceapi.DeviceRequest) *resourceapi.ResourceClaim {
return &resourceapi.ResourceClaim{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: resourceapi.ResourceClaimSpec{
Devices: resourceapi.DeviceClaim{
Requests: requests,
Constraints: constraints,
},
},
}
}
// generate a DeviceRequest object with the given name, class and selectors.
func request(name, class string, count int64, selectors ...resourceapi.DeviceSelector) resourceapi.DeviceRequest {
return resourceapi.DeviceRequest{
Name: name,
Count: count,
AllocationMode: resourceapi.DeviceAllocationModeExactCount,
DeviceClassName: class,
Selectors: selectors,
}
}
// generate a ResourceClaim object with the given name, request and class.
func claim(name, req, class string, constraints ...resourceapi.DeviceConstraint) *resourceapi.ResourceClaim {
claim := claimWithRequests(name, constraints, request(req, class, 1))
return claim
}
// generate a ResourceClaim object with the given name, request, class, and attribute.
// attribute is used to generate parameters in a form of JSON {attribute: attributeValue}.
func claimWithDeviceConfig(name, request, class, driver, attribute string) *resourceapi.ResourceClaim {
claim := claim(name, request, class)
claim.Spec.Devices.Config = []resourceapi.DeviceClaimConfiguration{
{
DeviceConfiguration: deviceConfiguration(driver, attribute),
},
}
return claim
}
// generate allocated ResourceClaim object
func allocatedClaim(name, request, class string, results ...resourceapi.DeviceRequestAllocationResult) *resourceapi.ResourceClaim {
claim := claim(name, request, class)
claim.Status.Allocation = &resourceapi.AllocationResult{
Devices: resourceapi.DeviceAllocationResult{
Results: results,
},
}
return claim
}
// generate a Device object with the given name, capacity and attributes.
func device(name string, capacity map[resourceapi.QualifiedName]resource.Quantity, attributes map[resourceapi.QualifiedName]resourceapi.DeviceAttribute) resourceapi.Device {
return resourceapi.Device{
Name: name,
Basic: &resourceapi.BasicDevice{
Attributes: attributes,
Capacity: capacity,
},
}
}
// generate a ResourceSlice object with the given name, node,
// driver and pool names, generation and a list of devices.
// The nodeSelection parameter may be a string (= node name),
// true (= all nodes), or a node selector (= specific nodes).
func slice(name string, nodeSelection any, pool, driver string, devices ...resourceapi.Device) *resourceapi.ResourceSlice {
slice := &resourceapi.ResourceSlice{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: resourceapi.ResourceSliceSpec{
Driver: driver,
Pool: resourceapi.ResourcePool{
Name: pool,
ResourceSliceCount: 1,
Generation: 1,
},
Devices: devices,
},
}
switch nodeSelection := nodeSelection.(type) {
case *v1.NodeSelector:
slice.Spec.NodeSelector = nodeSelection
case bool:
if !nodeSelection {
panic("nodeSelection == false is not valid")
}
slice.Spec.AllNodes = true
case string:
slice.Spec.NodeName = nodeSelection
default:
panic(fmt.Sprintf("unexpected nodeSelection type %T: %+v", nodeSelection, nodeSelection))
}
return slice
}
func deviceAllocationResult(request, driver, pool, device string) resourceapi.DeviceRequestAllocationResult {
return resourceapi.DeviceRequestAllocationResult{
Request: request,
Driver: driver,
Pool: pool,
Device: device,
}
}
// nodeLabelSelector creates a node selector with a label match for "key" in "values".
func nodeLabelSelector(key string, values ...string) *v1.NodeSelector {
requirements := []v1.NodeSelectorRequirement{{
Key: key,
Operator: v1.NodeSelectorOpIn,
Values: values,
}}
selector := &v1.NodeSelector{NodeSelectorTerms: []v1.NodeSelectorTerm{{MatchExpressions: requirements}}}
return selector
}
// localNodeSelector returns a node selector for a specific node.
func localNodeSelector(nodeName string) *v1.NodeSelector {
selector := nodeLabelSelector(fieldNameKey, nodeName)
// Swap the requirements: we need to select by field, not label.
selector.NodeSelectorTerms[0].MatchFields, selector.NodeSelectorTerms[0].MatchExpressions = selector.NodeSelectorTerms[0].MatchExpressions, selector.NodeSelectorTerms[0].MatchFields
return selector
}
// allocationResult returns a matcher for one AllocationResult pointer with a list of
// embedded device allocation results. The order of those results doesn't matter.
func allocationResult(selector *v1.NodeSelector, results ...resourceapi.DeviceRequestAllocationResult) types.GomegaMatcher {
return gstruct.PointTo(gstruct.MatchFields(0, gstruct.Fields{
"Devices": gstruct.MatchFields(0, gstruct.Fields{
"Results": gomega.ConsistOf(results), // Order is irrelevant.
"Config": gomega.BeNil(),
}),
"NodeSelector": matchNodeSelector(selector),
"Controller": gomega.BeEmpty(),
}))
}
// matchNodeSelector returns a matcher for a node selector. The order
// of terms, requirements, and values is irrelevant.
func matchNodeSelector(selector *v1.NodeSelector) types.GomegaMatcher {
if selector == nil {
return gomega.BeNil()
}
return gomega.HaveField("NodeSelectorTerms", matchNodeSelectorTerms(selector.NodeSelectorTerms))
}
func matchNodeSelectorTerms(terms []v1.NodeSelectorTerm) types.GomegaMatcher {
var matchTerms []types.GomegaMatcher
for _, term := range terms {
matchTerms = append(matchTerms, matchNodeSelectorTerm(term))
}
return gomega.ConsistOf(matchTerms)
}
func matchNodeSelectorTerm(term v1.NodeSelectorTerm) types.GomegaMatcher {
return gstruct.MatchFields(0, gstruct.Fields{
"MatchExpressions": matchNodeSelectorRequirements(term.MatchExpressions),
"MatchFields": matchNodeSelectorRequirements(term.MatchFields),
})
}
func matchNodeSelectorRequirements(requirements []v1.NodeSelectorRequirement) types.GomegaMatcher {
var matchRequirements []types.GomegaMatcher
for _, requirement := range requirements {
matchRequirements = append(matchRequirements, matchNodeSelectorRequirement(requirement))
}
return gomega.ConsistOf(matchRequirements)
}
func matchNodeSelectorRequirement(requirement v1.NodeSelectorRequirement) types.GomegaMatcher {
return gstruct.MatchFields(0, gstruct.Fields{
"Key": gomega.Equal(requirement.Key),
"Operator": gomega.Equal(requirement.Operator),
"Values": gomega.ConsistOf(requirement.Values),
})
}
func allocationResultWithConfig(selector *v1.NodeSelector, driver string, source resourceapi.AllocationConfigSource, attribute string, results ...resourceapi.DeviceRequestAllocationResult) *resourceapi.AllocationResult {
return &resourceapi.AllocationResult{
Devices: resourceapi.DeviceAllocationResult{
Results: results,
Config: []resourceapi.DeviceAllocationConfiguration{
{
Source: source,
DeviceConfiguration: deviceConfiguration(driver, attribute),
},
},
},
NodeSelector: selector,
}
}
// Helpers
// convert a list of objects to a slice
func objects[T any](objs ...T) []T {
return objs
}
// generate a ResourceSlice object with the given parameters and one device "device-1"
func sliceWithOneDevice(name string, nodeSelection any, pool, driver string) *resourceapi.ResourceSlice {
return slice(name, nodeSelection, pool, driver, device(device1, nil, nil))
}
func TestAllocator(t *testing.T) {
nonExistentAttribute := resourceapi.FullyQualifiedName("NonExistentAttribute")
boolAttribute := resourceapi.FullyQualifiedName("boolAttribute")
stringAttribute := resourceapi.FullyQualifiedName("stringAttribute")
versionAttribute := resourceapi.FullyQualifiedName("driverVersion")
intAttribute := resourceapi.FullyQualifiedName("numa")
testcases := map[string]struct {
claimsToAllocate []*resourceapi.ResourceClaim
allocatedClaims []*resourceapi.ResourceClaim
classes []*resourceapi.DeviceClass
slices []*resourceapi.ResourceSlice
node *v1.Node
expectResults []any
expectError types.GomegaMatcher // can be used to check for no error or match specific error types
}{
"empty": {},
"simple": {
claimsToAllocate: objects(claim(claim0, req0, classA)),
classes: objects(class(classA, driverA)),
slices: objects(sliceWithOneDevice(slice1, node1, pool1, driverA)),
node: node(node1, region1),
expectResults: []any{allocationResult(
localNodeSelector(node1),
deviceAllocationResult(req0, driverA, pool1, device1),
)},
},
"other-node": {
claimsToAllocate: objects(claim(claim0, req0, classA)),
classes: objects(class(classA, driverA)),
slices: objects(
sliceWithOneDevice(slice1, node1, pool1, driverB),
sliceWithOneDevice(slice2, node2, pool2, driverA),
),
node: node(node2, region2),
expectResults: []any{allocationResult(
localNodeSelector(node2),
deviceAllocationResult(req0, driverA, pool2, device1),
)},
},
"small-and-large": {
claimsToAllocate: objects(claimWithRequests(
claim0,
nil,
request(req0, classA, 1, resourceapi.DeviceSelector{
CEL: &resourceapi.CELDeviceSelector{
Expression: fmt.Sprintf(`device.capacity["%s"].memory.compareTo(quantity("1Gi")) >= 0`, driverA),
}}),
request(req1, classA, 1, resourceapi.DeviceSelector{
CEL: &resourceapi.CELDeviceSelector{
Expression: fmt.Sprintf(`device.capacity["%s"].memory.compareTo(quantity("2Gi")) >= 0`, driverA),
}}),
)),
classes: objects(class(classA, driverA)),
slices: objects(slice(slice1, node1, pool1, driverA,
device(device1, map[resourceapi.QualifiedName]resource.Quantity{
"memory": resource.MustParse("1Gi"),
}, nil),
device(device2, map[resourceapi.QualifiedName]resource.Quantity{
"memory": resource.MustParse("2Gi"),
}, nil),
)),
node: node(node1, region1),
expectResults: []any{allocationResult(
localNodeSelector(node1),
deviceAllocationResult(req0, driverA, pool1, device1),
deviceAllocationResult(req1, driverA, pool1, device2),
)},
},
"small-and-large-backtrack-requests": {
claimsToAllocate: objects(claimWithRequests(
claim0,
nil,
request(req0, classA, 1, resourceapi.DeviceSelector{
CEL: &resourceapi.CELDeviceSelector{
Expression: fmt.Sprintf(`device.capacity["%s"].memory.compareTo(quantity("1Gi")) >= 0`, driverA),
}}),
request(req1, classA, 1, resourceapi.DeviceSelector{
CEL: &resourceapi.CELDeviceSelector{
Expression: fmt.Sprintf(`device.capacity["%s"].memory.compareTo(quantity("2Gi")) >= 0`, driverA),
}}),
)),
classes: objects(class(classA, driverA)),
// Reversing the order in which the devices are listed causes the "large" device to
// be allocated for the "small" request, leaving the "large" request unsatisfied.
// The initial decision needs to be undone before a solution is found.
slices: objects(slice(slice1, node1, pool1, driverA,
device(device2, map[resourceapi.QualifiedName]resource.Quantity{
"memory": resource.MustParse("2Gi"),
}, nil),
device(device1, map[resourceapi.QualifiedName]resource.Quantity{
"memory": resource.MustParse("1Gi"),
}, nil),
)),
node: node(node1, region1),
expectResults: []any{allocationResult(
localNodeSelector(node1),
deviceAllocationResult(req0, driverA, pool1, device1),
deviceAllocationResult(req1, driverA, pool1, device2),
)},
},
"small-and-large-backtrack-claims": {
claimsToAllocate: objects(
claimWithRequests(
claim0,
nil,
request(req0, classA, 1, resourceapi.DeviceSelector{
CEL: &resourceapi.CELDeviceSelector{
Expression: fmt.Sprintf(`device.capacity["%s"].memory.compareTo(quantity("1Gi")) >= 0`, driverA),
}})),
claimWithRequests(
claim1,
nil,
request(req1, classA, 1, resourceapi.DeviceSelector{
CEL: &resourceapi.CELDeviceSelector{
Expression: fmt.Sprintf(`device.capacity["%s"].memory.compareTo(quantity("2Gi")) >= 0`, driverA),
}}),
)),
classes: objects(class(classA, driverA)),
// Reversing the order in which the devices are listed causes the "large" device to
// be allocated for the "small" request, leaving the "large" request unsatisfied.
// The initial decision needs to be undone before a solution is found.
slices: objects(slice(slice1, node1, pool1, driverA,
device(device2, map[resourceapi.QualifiedName]resource.Quantity{
"memory": resource.MustParse("2Gi"),
}, nil),
device(device1, map[resourceapi.QualifiedName]resource.Quantity{
"memory": resource.MustParse("1Gi"),
}, nil),
)),
node: node(node1, region1),
expectResults: []any{
allocationResult(localNodeSelector(node1), deviceAllocationResult(req0, driverA, pool1, device1)),
allocationResult(localNodeSelector(node1), deviceAllocationResult(req1, driverA, pool1, device2)),
},
},
"devices-split-across-different-slices": {
claimsToAllocate: objects(claimWithRequests(claim0, nil, resourceapi.DeviceRequest{
Name: req0,
Count: 2,
AllocationMode: resourceapi.DeviceAllocationModeExactCount,
DeviceClassName: classA,
})),
classes: objects(class(classA, driverA)),
slices: objects(
sliceWithOneDevice(slice1, node1, pool1, driverA),
sliceWithOneDevice(slice2, node1, pool2, driverA),
),
node: node(node1, region1),
expectResults: []any{allocationResult(
localNodeSelector(node1),
deviceAllocationResult(req0, driverA, pool1, device1),
deviceAllocationResult(req0, driverA, pool2, device1),
)},
},
"obsolete-slice": {
claimsToAllocate: objects(claim(claim0, req0, classA)),
classes: objects(class(classA, driverA)),
slices: objects(
sliceWithOneDevice("slice-1-obsolete", node1, pool1, driverA),
func() *resourceapi.ResourceSlice {
slice := sliceWithOneDevice(slice1, node1, pool1, driverA)
// This makes the other slice obsolete.
slice.Spec.Pool.Generation++
return slice
}(),
),
node: node(node1, region1),
expectResults: []any{allocationResult(
localNodeSelector(node1),
deviceAllocationResult(req0, driverA, pool1, device1),
)},
},
"no-slices": {
claimsToAllocate: objects(claim(claim0, req0, classA)),
classes: objects(class(classA, driverA)),
slices: nil,
node: node(node1, region1),
expectResults: nil,
},
"not-enough-suitable-devices": {
claimsToAllocate: objects(claim(claim0, req0, classA), claim(claim0, req1, classA)),
classes: objects(class(classA, driverA)),
slices: objects(sliceWithOneDevice(slice1, node1, pool1, driverA)),
node: node(node1, region1),
expectResults: nil,
},
"no-classes": {
claimsToAllocate: objects(claim(claim0, req0, classA)),
classes: nil,
slices: objects(sliceWithOneDevice(slice1, node1, pool1, driverA)),
node: node(node1, region1),
expectResults: nil,
expectError: gomega.MatchError(gomega.ContainSubstring("could not retrieve device class class-a")),
},
"unknown-class": {
claimsToAllocate: objects(claim(claim0, req0, "unknown-class")),
classes: objects(class(classA, driverA)),
slices: objects(sliceWithOneDevice(slice1, node1, pool1, driverA)),
node: node(node1, region1),
expectResults: nil,
expectError: gomega.MatchError(gomega.ContainSubstring("could not retrieve device class unknown-class")),
},
"empty-class": {
claimsToAllocate: objects(claim(claim0, req0, "")),
classes: objects(class(classA, driverA)),
slices: objects(sliceWithOneDevice(slice1, node1, pool1, driverA)),
node: node(node1, region1),
expectResults: nil,
expectError: gomega.MatchError(gomega.ContainSubstring("claim claim-0, request req-0: missing device class name (unsupported request type?)")),
},
"no-claims-to-allocate": {
claimsToAllocate: nil,
classes: objects(class(classA, driverA)),
slices: objects(sliceWithOneDevice(slice1, node1, pool1, driverA)),
node: node(node1, region1),
expectResults: nil,
},
"all-devices": {
claimsToAllocate: objects(claimWithRequests(claim0, nil, resourceapi.DeviceRequest{
Name: req0,
AllocationMode: resourceapi.DeviceAllocationModeAll,
Count: 1,
DeviceClassName: classA,
})),
classes: objects(class(classA, driverA)),
slices: objects(sliceWithOneDevice(slice1, node1, pool1, driverA)),
node: node(node1, region1),
expectResults: []any{allocationResult(
localNodeSelector(node1),
deviceAllocationResult(req0, driverA, pool1, device1),
)},
},
"all-devices-of-the-incomplete-pool": {
claimsToAllocate: objects(claimWithRequests(claim0, nil, resourceapi.DeviceRequest{
Name: req0,
AllocationMode: resourceapi.DeviceAllocationModeAll,
Count: 1,
DeviceClassName: classA,
})),
classes: objects(class(classA, driverA)),
slices: objects(
func() *resourceapi.ResourceSlice {
slice := sliceWithOneDevice(slice1, node1, pool1, driverA)
// This makes the pool incomplete, one other slice is missing.
slice.Spec.Pool.ResourceSliceCount++
return slice
}(),
),
node: node(node1, region1),
expectResults: nil,
expectError: gomega.MatchError(gomega.ContainSubstring("claim claim-0, request req-0: asks for all devices, but resource pool driver-a/pool-1 is currently being updated")),
},
"network-attached-device": {
claimsToAllocate: objects(claim(claim0, req0, classA)),
classes: objects(class(classA, driverA)),
slices: objects(sliceWithOneDevice(slice1, nodeLabelSelector(regionKey, region1), pool1, driverA)),
node: node(node1, region1),
expectResults: []any{allocationResult(
nodeLabelSelector(regionKey, region1),
deviceAllocationResult(req0, driverA, pool1, device1),
)},
},
"unsuccessful-allocation-network-attached-device": {
claimsToAllocate: objects(claim(claim0, req0, classA)),
classes: objects(class(classA, driverA)),
slices: objects(sliceWithOneDevice(slice1, nodeLabelSelector(regionKey, region1), pool1, driverA)),
// Wrong region, no devices available.
node: node(node2, region2),
expectResults: nil,
},
"many-network-attached-devices": {
claimsToAllocate: objects(claimWithRequests(claim0, nil, request(req0, classA, 4))),
classes: objects(class(classA, driverA)),
slices: objects(
sliceWithOneDevice(slice1, nodeLabelSelector(regionKey, region1), pool1, driverA),
sliceWithOneDevice(slice1, true, pool2, driverA),
sliceWithOneDevice(slice1, nodeLabelSelector(planetKey, planetValueEarth), pool3, driverA),
sliceWithOneDevice(slice1, localNodeSelector(node1), pool4, driverA),
),
node: node(node1, region1),
expectResults: []any{allocationResult(
// A union of the individual selectors.
&v1.NodeSelector{
NodeSelectorTerms: []v1.NodeSelectorTerm{{
MatchExpressions: []v1.NodeSelectorRequirement{
{Key: regionKey, Operator: v1.NodeSelectorOpIn, Values: []string{region1}},
{Key: planetKey, Operator: v1.NodeSelectorOpIn, Values: []string{planetValueEarth}},
},
MatchFields: []v1.NodeSelectorRequirement{
{Key: fieldNameKey, Operator: v1.NodeSelectorOpIn, Values: []string{node1}},
},
}},
},
deviceAllocationResult(req0, driverA, pool1, device1),
deviceAllocationResult(req0, driverA, pool2, device1),
deviceAllocationResult(req0, driverA, pool3, device1),
deviceAllocationResult(req0, driverA, pool4, device1),
)},
},
"local-and-network-attached-devices": {
claimsToAllocate: objects(claimWithRequests(claim0, nil, request(req0, classA, 2))),
classes: objects(class(classA, driverA)),
slices: objects(
sliceWithOneDevice(slice1, nodeLabelSelector(regionKey, region1), pool1, driverA),
sliceWithOneDevice(slice1, node1, pool2, driverA),
),
node: node(node1, region1),
expectResults: []any{allocationResult(
// Once there is any node-local device, the selector is for that node.
localNodeSelector(node1),
deviceAllocationResult(req0, driverA, pool1, device1),
deviceAllocationResult(req0, driverA, pool2, device1),
)},
},
"several-different-drivers": {
claimsToAllocate: objects(claim(claim0, req0, classA), claim(claim0, req0, classB)),
classes: objects(class(classA, driverA), class(classB, driverB)),
slices: objects(
slice(slice1, node1, pool1, driverA,
device(device1, nil, nil),
device(device2, nil, nil),
),
sliceWithOneDevice(slice1, node1, pool1, driverB),
),
node: node(node1, region1),
expectResults: []any{
allocationResult(localNodeSelector(node1), deviceAllocationResult(req0, driverA, pool1, device1)),
allocationResult(localNodeSelector(node1), deviceAllocationResult(req0, driverB, pool1, device1)),
},
},
"already-allocated-devices": {
claimsToAllocate: objects(claim(claim0, req0, classA)),
allocatedClaims: objects(
allocatedClaim(claim0, req0, classA,
deviceAllocationResult(req0, driverA, pool1, device1),
deviceAllocationResult(req1, driverA, pool1, device2),
),
),
classes: objects(class(classA, driverA)),
slices: objects(sliceWithOneDevice(slice1, node1, pool1, driverA)),
node: node(node1, region1),
expectResults: nil,
},
"with-constraint": {
claimsToAllocate: objects(claimWithRequests(
claim0,
[]resourceapi.DeviceConstraint{
{MatchAttribute: &intAttribute},
{MatchAttribute: &versionAttribute},
{MatchAttribute: &stringAttribute},
{MatchAttribute: &boolAttribute},
},
request(req0, classA, 1),
request(req1, classA, 1),
),
),
classes: objects(class(classA, driverA)),
slices: objects(slice(slice1, node1, pool1, driverA,
device(device1, nil, map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
"driverVersion": {VersionValue: ptr.To("1.0.0")},
"numa": {IntValue: ptr.To(int64(1))},
"stringAttribute": {StringValue: ptr.To("stringAttributeValue")},
"boolAttribute": {BoolValue: ptr.To(true)},
}),
device(device2, nil, map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
"driverVersion": {VersionValue: ptr.To("1.0.0")},
"numa": {IntValue: ptr.To(int64(1))},
"stringAttribute": {StringValue: ptr.To("stringAttributeValue")},
"boolAttribute": {BoolValue: ptr.To(true)},
}),
)),
node: node(node1, region1),
expectResults: []any{allocationResult(
localNodeSelector(node1),
deviceAllocationResult(req0, driverA, pool1, device1),
deviceAllocationResult(req1, driverA, pool1, device2),
)},
},
"with-constraint-non-existent-attribute": {
claimsToAllocate: objects(claim(claim0, req0, classA, resourceapi.DeviceConstraint{
MatchAttribute: &nonExistentAttribute,
})),
classes: objects(class(classA, driverA)),
slices: objects(sliceWithOneDevice(slice1, node1, pool1, driverA)),
node: node(node1, region1),
expectResults: nil,
},
"with-constraint-not-matching-int-attribute": {
claimsToAllocate: objects(claimWithRequests(
claim0,
[]resourceapi.DeviceConstraint{{MatchAttribute: &intAttribute}},
request(req0, classA, 3)),
),
classes: objects(class(classA, driverA), class(classB, driverB)),
slices: objects(slice(slice1, node1, pool1, driverA,
device(device1, nil, map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
"numa": {IntValue: ptr.To(int64(1))},
}),
device(device2, nil, map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
"numa": {IntValue: ptr.To(int64(2))},
}),
)),
node: node(node1, region1),
expectResults: nil,
},
"with-constraint-not-matching-version-attribute": {
claimsToAllocate: objects(claimWithRequests(
claim0,
[]resourceapi.DeviceConstraint{{MatchAttribute: &versionAttribute}},
request(req0, classA, 3)),
),
classes: objects(class(classA, driverA), class(classB, driverB)),
slices: objects(slice(slice1, node1, pool1, driverA,
device(device1, nil, map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
"driverVersion": {VersionValue: ptr.To("1.0.0")},
}),
device(device2, nil, map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
"driverVersion": {VersionValue: ptr.To("2.0.0")},
}),
)),
node: node(node1, region1),
expectResults: nil,
},
"with-constraint-not-matching-string-attribute": {
claimsToAllocate: objects(claimWithRequests(
claim0,
[]resourceapi.DeviceConstraint{{MatchAttribute: &stringAttribute}},
request(req0, classA, 3)),
),
classes: objects(class(classA, driverA), class(classB, driverB)),
slices: objects(slice(slice1, node1, pool1, driverA,
device(device1, nil, map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
"stringAttribute": {StringValue: ptr.To("stringAttributeValue")},
}),
device(device2, nil, map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
"stringAttribute": {StringValue: ptr.To("stringAttributeValue2")},
}),
)),
node: node(node1, region1),
expectResults: nil,
},
"with-constraint-not-matching-bool-attribute": {
claimsToAllocate: objects(claimWithRequests(
claim0,
[]resourceapi.DeviceConstraint{{MatchAttribute: &boolAttribute}},
request(req0, classA, 3)),
),
classes: objects(class(classA, driverA), class(classB, driverB)),
slices: objects(slice(slice1, node1, pool1, driverA,
device(device1, nil, map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
"boolAttribute": {BoolValue: ptr.To(true)},
}),
device(device2, nil, map[resourceapi.QualifiedName]resourceapi.DeviceAttribute{
"boolAttribute": {BoolValue: ptr.To(false)},
}),
)),
node: node(node1, region1),
expectResults: nil,
},
"with-class-device-config": {
claimsToAllocate: objects(claim(claim0, req0, classA)),
classes: objects(classWithConfig(classA, driverA, "classAttribute")),
slices: objects(sliceWithOneDevice(slice1, node1, pool1, driverA)),
node: node(node1, region1),
expectResults: []any{
allocationResultWithConfig(
localNodeSelector(node1),
driverA,
resourceapi.AllocationConfigSourceClass,
"classAttribute",
deviceAllocationResult(req0, driverA, pool1, device1),
),
},
},
"claim-with-device-config": {
claimsToAllocate: objects(claimWithDeviceConfig(claim0, req0, classA, driverA, "deviceAttribute")),
classes: objects(class(classA, driverA)),
slices: objects(sliceWithOneDevice(slice1, node1, pool1, driverA)),
node: node(node1, region1),
expectResults: []any{
allocationResultWithConfig(
localNodeSelector(node1),
driverA,
resourceapi.AllocationConfigSourceClaim,
"deviceAttribute",
deviceAllocationResult(req0, driverA, pool1, device1),
),
},
},
}
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
_, ctx := ktesting.NewTestContext(t)
g := gomega.NewWithT(t)
// Listing objects is deterministic and returns them in the same
// order as in the test case. That makes the allocation result
// also deterministic.
var allocated, toAllocate claimLister
var classLister informerLister[resourceapi.DeviceClass]
var sliceLister informerLister[resourceapi.ResourceSlice]
for _, claim := range tc.claimsToAllocate {
toAllocate.claims = append(toAllocate.claims, claim.DeepCopy())
}
for _, claim := range tc.allocatedClaims {
allocated.claims = append(allocated.claims, claim.DeepCopy())
}
for _, slice := range tc.slices {
sliceLister.objs = append(sliceLister.objs, slice.DeepCopy())
}
for _, class := range tc.classes {
classLister.objs = append(classLister.objs, class.DeepCopy())
}
allocator, err := NewAllocator(ctx, toAllocate.claims, allocated, classLister, sliceLister)
g.Expect(err).ToNot(gomega.HaveOccurred())
results, err := allocator.Allocate(ctx, tc.node)
matchError := tc.expectError
if matchError == nil {
matchError = gomega.Not(gomega.HaveOccurred())
}
g.Expect(err).To(matchError)
g.Expect(results).To(gomega.ConsistOf(tc.expectResults...))
// Objects that the allocator had access to should not have been modified.
g.Expect(toAllocate.claims).To(gomega.HaveExactElements(tc.claimsToAllocate))
g.Expect(allocated.claims).To(gomega.HaveExactElements(tc.allocatedClaims))
g.Expect(sliceLister.objs).To(gomega.ConsistOf(tc.slices))
g.Expect(classLister.objs).To(gomega.ConsistOf(tc.classes))
})
}
}
type claimLister struct {
claims []*resourceapi.ResourceClaim
err error
}
func (l claimLister) ListAllAllocated() ([]*resourceapi.ResourceClaim, error) {
return l.claims, l.err
}
type informerLister[T any] struct {
objs []*T
err error
}
func (l informerLister[T]) List(selector labels.Selector) (ret []*T, err error) {
if selector.String() != labels.Everything().String() {
return nil, errors.New("labels selector not implemented")
}
return l.objs, l.err
}
func (l informerLister[T]) Get(name string) (*T, error) {
for _, obj := range l.objs {
accessor, err := meta.Accessor(obj)
if err != nil {
return nil, err
}
if accessor.GetName() == name {
return obj, nil
}
}
return nil, apierrors.NewNotFound(schema.GroupResource{}, "not found")
}

View File

@@ -0,0 +1,18 @@
/*
Copyright 2024 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 structured contains code for working with structured parameters.
package structured

View File

@@ -0,0 +1,131 @@
/*
Copyright 2024 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 structured
import (
"context"
"fmt"
v1 "k8s.io/api/core/v1"
resourceapi "k8s.io/api/resource/v1alpha3"
"k8s.io/apimachinery/pkg/labels"
resourcelisters "k8s.io/client-go/listers/resource/v1alpha3"
"k8s.io/component-helpers/scheduling/corev1/nodeaffinity"
)
// GatherPools collects information about all resource pools which provide
// devices that are accessible from the given node.
//
// Out-dated slices are silently ignored. Pools may be incomplete, which is
// recorded in the result.
func GatherPools(ctx context.Context, sliceLister resourcelisters.ResourceSliceLister, node *v1.Node) ([]*Pool, error) {
pools := make(map[PoolID]*Pool)
// TODO (future): use a custom lister interface and implement it with
// and indexer on the node name field. Then here we can ask for only
// slices with the right node name and those with no node name.
slices, err := sliceLister.List(labels.Everything())
if err != nil {
return nil, fmt.Errorf("list resource slices: %w", err)
}
for _, slice := range slices {
switch {
case slice.Spec.NodeName != "":
if slice.Spec.NodeName == node.Name {
addSlice(pools, slice)
}
case slice.Spec.AllNodes:
addSlice(pools, slice)
case slice.Spec.NodeSelector != nil:
selector, err := nodeaffinity.NewNodeSelector(slice.Spec.NodeSelector)
if err != nil {
return nil, fmt.Errorf("node selector in resource slice %s: %w", slice.Name, err)
}
if selector.Match(node) {
addSlice(pools, slice)
}
default:
// Nothing known was set. This must be some future, unknown extension,
// so we don't know how to handle it. We may still be able to allocated from
// other pools, so we continue.
//
// TODO (eventually): let caller decide how to report this to the user. Warning
// about it for every slice on each scheduling attempt would be too noisy, but
// perhaps once per run would be useful?
continue
}
}
// Find incomplete pools and flatten into a single slice.
result := make([]*Pool, 0, len(pools))
for _, pool := range pools {
pool.IsIncomplete = int64(len(pool.Slices)) != pool.Slices[0].Spec.Pool.ResourceSliceCount
result = append(result, pool)
}
return result, nil
}
func addSlice(pools map[PoolID]*Pool, slice *resourceapi.ResourceSlice) {
id := PoolID{Driver: slice.Spec.Driver, Pool: slice.Spec.Pool.Name}
pool := pools[id]
if pool == nil {
// New pool.
pool = &Pool{
PoolID: id,
Slices: []*resourceapi.ResourceSlice{slice},
}
pools[id] = pool
return
}
if slice.Spec.Pool.Generation < pool.Slices[0].Spec.Pool.Generation {
// Out-dated.
return
}
if slice.Spec.Pool.Generation > pool.Slices[0].Spec.Pool.Generation {
// Newer, replaces all old slices.
pool.Slices = []*resourceapi.ResourceSlice{slice}
}
// Add to pool.
pool.Slices = append(pool.Slices, slice)
}
type Pool struct {
PoolID
IsIncomplete bool
Slices []*resourceapi.ResourceSlice
}
type PoolID struct {
Driver, Pool string
}
func (p PoolID) String() string {
return p.Driver + "/" + p.Pool
}
type DeviceID struct {
Driver, Pool, Device string
}
func (d DeviceID) String() string {
return d.Driver + "/" + d.Pool + "/" + d.Device
}

View File

@@ -772,7 +772,20 @@ var _ = framework.SIGDescribe("node")("DRA", feature.DynamicResourceAllocation,
)
b.create(ctx, pod, template)
framework.ExpectNoError(e2epod.WaitForPodNameUnschedulableInNamespace(ctx, f.ClientSet, pod.Name, pod.Namespace), "pod must not get scheduled because of a CEL runtime error")
framework.ExpectNoError(e2epod.WaitForPodCondition(ctx, f.ClientSet, pod.Namespace, pod.Name, "scheduling failure", f.Timeouts.PodStartShort, func(pod *v1.Pod) (bool, error) {
for _, condition := range pod.Status.Conditions {
if condition.Type == "PodScheduled" {
if condition.Status != "False" {
gomega.StopTrying("pod got scheduled unexpectedly").Now()
}
if strings.Contains(condition.Message, "CEL runtime error") {
// This is what we are waiting for.
return true, nil
}
}
}
return false, nil
}), "pod must not get scheduled because of a CEL runtime error")
})
})
case parameterModeClassicDRA:

View File

@@ -678,28 +678,13 @@ func TestPodSchedulingContextSSA(t *testing.T) {
}
}
defer func() {
if err := testCtx.ClientSet.ResourceV1alpha3().ResourceClasses().DeleteCollection(testCtx.Ctx, metav1.DeleteOptions{}, metav1.ListOptions{}); err != nil {
t.Errorf("Unexpected error deleting ResourceClasses: %v", err)
}
}()
class := &resourceapi.ResourceClass{
ObjectMeta: metav1.ObjectMeta{
Name: "my-class",
},
DriverName: "does-not-matter",
}
if _, err := testCtx.ClientSet.ResourceV1alpha3().ResourceClasses().Create(testCtx.Ctx, class, metav1.CreateOptions{}); err != nil {
t.Fatalf("Failed to create class: %v", err)
}
claim := &resourceapi.ResourceClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "my-claim",
Namespace: testCtx.NS.Name,
},
Spec: resourceapi.ResourceClaimSpec{
ResourceClassName: class.Name,
Controller: "dra.example.com",
},
}
if _, err := testCtx.ClientSet.ResourceV1alpha3().ResourceClaims(claim.Namespace).Create(testCtx.Ctx, claim, metav1.CreateOptions{}); err != nil {

View File

@@ -1,7 +1,11 @@
apiVersion: resource.k8s.io/v1alpha1
apiVersion: resource.k8s.io/v1alpha3
kind: ResourceClaimTemplate
metadata:
name: another-test-claim-template
spec:
spec:
resourceClassName: another-test-class
controller: another-test-driver.cdi.k8s.io
devices:
requests:
- name: req-0
deviceClassName: test-class

View File

@@ -1,5 +0,0 @@
apiVersion: resource.k8s.io/v1alpha1
kind: ResourceClass
metadata:
name: another-test-class
driverName: another-test-driver.cdi.k8s.io

View File

@@ -1,7 +0,0 @@
apiVersion: resource.k8s.io/v1alpha1
kind: ResourceClaimTemplate
metadata:
name: claim-template
spec:
spec:
resourceClassName: scheduler-performance

View File

@@ -0,0 +1,8 @@
apiVersion: resource.k8s.io/v1alpha3
kind: DeviceClass
metadata:
name: test-class
spec:
selectors:
- cel:
expression: device.driver == "test-driver.cdi.k8s.io"

View File

@@ -1,5 +1,4 @@
apiVersion: resource.k8s.io/v1alpha3
kind: ResourceClass
kind: DeviceClass
metadata:
name: test-class
driverName: test-driver.cdi.k8s.io

View File

@@ -3,8 +3,7 @@ kind: ResourceClaim
metadata:
name: test-claim-{{.Index}}
spec:
resourceClassName: test-class
parametersRef:
apiGroup: resource.k8s.io
kind: ResourceClaimParameters
name: test-claim-parameters
devices:
requests:
- name: req-0
deviceClassName: test-class

View File

@@ -3,4 +3,8 @@ kind: ResourceClaim
metadata:
name: test-claim-{{.Index}}
spec:
resourceClassName: test-class
controller: test-driver.cdi.k8s.io
devices:
requests:
- name: req-0
deviceClassName: test-class

View File

@@ -1,9 +0,0 @@
apiVersion: resource.k8s.io/v1alpha3
kind: ResourceClaimParameters
metadata:
name: test-claim-parameters
driverRequests:
- driverName: test-driver.cdi.k8s.io
requests:
- namedResources:
selector: "true"

View File

@@ -4,8 +4,7 @@ metadata:
name: test-claim-template
spec:
spec:
resourceClassName: test-class
parametersRef:
apiGroup: resource.k8s.io
kind: ResourceClaimParameters
name: test-claim-parameters
devices:
requests:
- name: req-0
deviceClassName: test-class

View File

@@ -4,4 +4,8 @@ metadata:
name: test-claim-template
spec:
spec:
resourceClassName: test-class
controller: test-driver.cdi.k8s.io
devices:
requests:
- name: req-0
deviceClassName: test-class

View File

@@ -1,6 +0,0 @@
apiVersion: resource.k8s.io/v1alpha3
kind: ResourceClass
metadata:
name: test-class
driverName: test-driver.cdi.k8s.io
structuredParameters: true

View File

@@ -758,7 +758,7 @@
nodes: scheduler-perf-dra-*
maxClaimsPerNodeParam: $maxClaimsPerNode
- opcode: createAny
templatePath: config/dra/resourceclass.yaml
templatePath: config/dra/deviceclass.yaml
- opcode: createAny
templatePath: config/dra/resourceclaimtemplate.yaml
namespace: init
@@ -829,9 +829,7 @@
nodes: scheduler-perf-dra-*
maxClaimsPerNodeParam: $maxClaimsPerNode
- opcode: createAny
templatePath: config/dra/resourceclass.yaml
- opcode: createAny
templatePath: config/dra/another-resourceclass.yaml
templatePath: config/dra/deviceclass.yaml
- opcode: createAny
templatePath: config/dra/resourceclaimtemplate.yaml
namespace: init
@@ -855,6 +853,7 @@
collectMetrics: true
workloads:
- name: fast
labels: [integration-test, fast]
params:
# This testcase runs through all code paths without
# taking too long overall.
@@ -902,10 +901,7 @@
maxClaimsPerNodeParam: $maxClaimsPerNode
structuredParameters: true
- opcode: createAny
templatePath: config/dra/resourceclass-structured.yaml
- opcode: createAny
templatePath: config/dra/resourceclaimparameters.yaml
namespace: init
templatePath: config/dra/deviceclass-structured.yaml
- opcode: createAny
templatePath: config/dra/resourceclaimtemplate-structured.yaml
namespace: init
@@ -913,9 +909,6 @@
namespace: init
countParam: $initPods
podTemplatePath: config/dra/pod-with-claim-template.yaml
- opcode: createAny
templatePath: config/dra/resourceclaimparameters.yaml
namespace: test
- opcode: createAny
templatePath: config/dra/resourceclaimtemplate-structured.yaml
namespace: test
@@ -979,10 +972,7 @@
maxClaimsPerNodeParam: $maxClaimsPerNode
structuredParameters: true
- opcode: createAny
templatePath: config/dra/resourceclass-structured.yaml
- opcode: createAny
templatePath: config/dra/resourceclaimparameters.yaml
namespace: init
templatePath: config/dra/deviceclass-structured.yaml
- opcode: createAny
templatePath: config/dra/resourceclaim-structured.yaml
namespace: init
@@ -991,9 +981,6 @@
namespace: init
countParam: $initPods
podTemplatePath: config/dra/pod-with-claim-ref.yaml
- opcode: createAny
templatePath: config/dra/resourceclaimparameters.yaml
namespace: test
- opcode: createAny
templatePath: config/dra/resourceclaim-structured.yaml
namespace: test
@@ -1100,4 +1087,4 @@
labels: [performance, fast]
params:
gatedPods: 10000
measurePods: 20000
measurePods: 20000

View File

@@ -202,7 +202,7 @@ func (op *createResourceDriverOp) run(tCtx ktesting.TContext) {
tCtx.CleanupCtx(func(tCtx ktesting.TContext) {
err := tCtx.Client().ResourceV1alpha3().ResourceSlices().DeleteCollection(tCtx,
metav1.DeleteOptions{},
metav1.ListOptions{FieldSelector: "driverName=" + op.DriverName},
metav1.ListOptions{FieldSelector: resourceapi.ResourceSliceSelectorDriver + "=" + op.DriverName},
)
tCtx.ExpectNoError(err, "delete node resource slices")
})
@@ -234,18 +234,21 @@ func resourceSlice(driverName, nodeName string, capacity int) *resourceapi.Resou
Name: nodeName,
},
NodeName: nodeName,
DriverName: driverName,
ResourceModel: resourceapi.ResourceModel{
NamedResources: &resourceapi.NamedResourcesResources{},
Spec: resourceapi.ResourceSliceSpec{
Driver: driverName,
NodeName: nodeName,
Pool: resourceapi.ResourcePool{
Name: nodeName,
ResourceSliceCount: 1,
},
},
}
for i := 0; i < capacity; i++ {
slice.ResourceModel.NamedResources.Instances = append(slice.ResourceModel.NamedResources.Instances,
resourceapi.NamedResourcesInstance{
Name: fmt.Sprintf("instance-%d", i),
slice.Spec.Devices = append(slice.Spec.Devices,
resourceapi.Device{
Name: fmt.Sprintf("instance-%d", i),
Basic: &resourceapi.BasicDevice{},
},
)
}