Merge pull request #136589 from tosi3k/preemption-mode

Add Workload-Aware Preemption fields to Workload and PodGroup APIs
This commit is contained in:
Kubernetes Prow Robot 2026-03-24 15:14:16 +05:30 committed by GitHub
commit c1ebc2c3e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 3312 additions and 697 deletions

View file

@ -20008,10 +20008,23 @@
"io.k8s.api.scheduling.v1alpha2.PodGroupSpec": {
"description": "PodGroupSpec defines the desired state of a PodGroup.",
"properties": {
"disruptionMode": {
"description": "DisruptionMode defines the mode in which a given PodGroup can be disrupted. Controllers are expected to fill this field by copying it from a PodGroupTemplate. One of Pod, PodGroup. Defaults to Pod if unset. This field is immutable. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
"type": "string"
},
"podGroupTemplateRef": {
"$ref": "#/definitions/io.k8s.api.scheduling.v1alpha2.PodGroupTemplateReference",
"description": "PodGroupTemplateRef references an optional PodGroup template within other object (e.g. Workload) that was used to create the PodGroup. This field is immutable."
},
"priority": {
"description": "Priority is the value of priority of this pod group. Various system components use this field to find the priority of the pod group. When Priority Admission Controller is enabled, it prevents users from setting this field. The admission controller populates this field from PriorityClassName. The higher the value, the higher the priority. This field is immutable. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
"format": "int32",
"type": "integer"
},
"priorityClassName": {
"description": "PriorityClassName defines the priority that should be considered when scheduling this pod group. Controllers are expected to fill this field by copying it from a PodGroupTemplate. Otherwise, it is validated and resolved similarly to the PriorityClassName on PodGroupTemplate (i.e. if no priority class is specified, admission control can set this to the global default priority class if it exists. Otherwise, the pod group's priority will be zero). This field is immutable. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
"type": "string"
},
"resourceClaims": {
"description": "ResourceClaims defines which ResourceClaims may be shared among Pods in the group. Pods consume the devices allocated to a PodGroup's claim by defining a claim in its own Spec.ResourceClaims that matches the PodGroup's claim exactly. The claim must have the same name and refer to the same ResourceClaim or ResourceClaimTemplate.\n\nThis is an alpha-level field and requires that the DRAWorkloadResourceClaims feature gate is enabled.\n\nThis field is immutable.",
"items": {
@ -20074,10 +20087,23 @@
"io.k8s.api.scheduling.v1alpha2.PodGroupTemplate": {
"description": "PodGroupTemplate represents a template for a set of pods with a scheduling policy.",
"properties": {
"disruptionMode": {
"description": "DisruptionMode defines the mode in which a given PodGroup can be disrupted. One of Pod, PodGroup. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
"type": "string"
},
"name": {
"description": "Name is a unique identifier for the PodGroupTemplate within the Workload. It must be a DNS label. This field is immutable.",
"type": "string"
},
"priority": {
"description": "Priority is the value of priority of pod groups created from this template. Various system components use this field to find the priority of the pod group. When Priority Admission Controller is enabled, it prevents users from setting this field. The admission controller populates this field from PriorityClassName. The higher the value, the higher the priority. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
"format": "int32",
"type": "integer"
},
"priorityClassName": {
"description": "PriorityClassName indicates the priority that should be considered when scheduling a pod group created from this template. If no priority class is specified, admission control can set this to the global default priority class if it exists. Otherwise, pod groups created from this template will have the priority set to zero. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
"type": "string"
},
"resourceClaims": {
"description": "ResourceClaims defines which ResourceClaims may be shared among Pods in the group. Pods consume the devices allocated to a PodGroup's claim by defining a claim in its own Spec.ResourceClaims that matches the PodGroup's claim exactly. The claim must have the same name and refer to the same ResourceClaim or ResourceClaimTemplate.\n\nThis is an alpha-level field and requires that the DRAWorkloadResourceClaims feature gate is enabled.\n\nThis field is immutable.",
"items": {

View file

@ -208,6 +208,11 @@
"io.k8s.api.scheduling.v1alpha2.PodGroupSpec": {
"description": "PodGroupSpec defines the desired state of a PodGroup.",
"properties": {
"disruptionMode": {
"default": "Pod",
"description": "DisruptionMode defines the mode in which a given PodGroup can be disrupted. Controllers are expected to fill this field by copying it from a PodGroupTemplate. One of Pod, PodGroup. Defaults to Pod if unset. This field is immutable. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
"type": "string"
},
"podGroupTemplateRef": {
"allOf": [
{
@ -216,6 +221,15 @@
],
"description": "PodGroupTemplateRef references an optional PodGroup template within other object (e.g. Workload) that was used to create the PodGroup. This field is immutable."
},
"priority": {
"description": "Priority is the value of priority of this pod group. Various system components use this field to find the priority of the pod group. When Priority Admission Controller is enabled, it prevents users from setting this field. The admission controller populates this field from PriorityClassName. The higher the value, the higher the priority. This field is immutable. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
"format": "int32",
"type": "integer"
},
"priorityClassName": {
"description": "PriorityClassName defines the priority that should be considered when scheduling this pod group. Controllers are expected to fill this field by copying it from a PodGroupTemplate. Otherwise, it is validated and resolved similarly to the PriorityClassName on PodGroupTemplate (i.e. if no priority class is specified, admission control can set this to the global default priority class if it exists. Otherwise, the pod group's priority will be zero). This field is immutable. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
"type": "string"
},
"resourceClaims": {
"description": "ResourceClaims defines which ResourceClaims may be shared among Pods in the group. Pods consume the devices allocated to a PodGroup's claim by defining a claim in its own Spec.ResourceClaims that matches the PodGroup's claim exactly. The claim must have the same name and refer to the same ResourceClaim or ResourceClaimTemplate.\n\nThis is an alpha-level field and requires that the DRAWorkloadResourceClaims feature gate is enabled.\n\nThis field is immutable.",
"items": {
@ -302,11 +316,24 @@
"io.k8s.api.scheduling.v1alpha2.PodGroupTemplate": {
"description": "PodGroupTemplate represents a template for a set of pods with a scheduling policy.",
"properties": {
"disruptionMode": {
"description": "DisruptionMode defines the mode in which a given PodGroup can be disrupted. One of Pod, PodGroup. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
"type": "string"
},
"name": {
"default": "",
"description": "Name is a unique identifier for the PodGroupTemplate within the Workload. It must be a DNS label. This field is immutable.",
"type": "string"
},
"priority": {
"description": "Priority is the value of priority of pod groups created from this template. Various system components use this field to find the priority of the pod group. When Priority Admission Controller is enabled, it prevents users from setting this field. The admission controller populates this field from PriorityClassName. The higher the value, the higher the priority. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
"format": "int32",
"type": "integer"
},
"priorityClassName": {
"description": "PriorityClassName indicates the priority that should be considered when scheduling a pod group created from this template. If no priority class is specified, admission control can set this to the global default priority class if it exists. Otherwise, pod groups created from this template will have the priority set to zero. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
"type": "string"
},
"resourceClaims": {
"description": "ResourceClaims defines which ResourceClaims may be shared among Pods in the group. Pods consume the devices allocated to a PodGroup's claim by defining a claim in its own Spec.ResourceClaims that matches the PodGroup's claim exactly. The claim must have the same name and refer to the same ResourceClaim or ResourceClaimTemplate.\n\nThis is an alpha-level field and requires that the DRAWorkloadResourceClaims feature gate is enabled.\n\nThis field is immutable.",
"items": {

View file

@ -219,6 +219,8 @@ func TestDefaulting(t *testing.T) {
{Group: "storage.k8s.io", Version: "v1beta1", Kind: "VolumeAttachment"}: {},
{Group: "storage.k8s.io", Version: "v1beta1", Kind: "VolumeAttachmentList"}: {},
{Group: "authentication.k8s.io", Version: "v1", Kind: "TokenRequest"}: {},
{Group: "scheduling.k8s.io", Version: "v1alpha2", Kind: "PodGroup"}: {},
{Group: "scheduling.k8s.io", Version: "v1alpha2", Kind: "PodGroupList"}: {},
{Group: "scheduling.k8s.io", Version: "v1beta1", Kind: "PriorityClass"}: {},
{Group: "scheduling.k8s.io", Version: "v1", Kind: "PriorityClass"}: {},
{Group: "scheduling.k8s.io", Version: "v1beta1", Kind: "PriorityClassList"}: {},

View file

@ -33,5 +33,11 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
s.PreemptionPolicy = &preemptLowerPriority
}
},
func(s *scheduling.PodGroup, c randfill.Continue) {
c.FillNoCustom(s)
if s.Spec.DisruptionMode == nil {
s.Spec.DisruptionMode = new(scheduling.DisruptionModePod)
}
},
}
}

View file

@ -212,6 +212,38 @@ type PodGroupTemplate struct {
// +listMapKey=name
// +featureGate=DRAWorkloadResourceClaims
ResourceClaims []PodGroupResourceClaim
// DisruptionMode defines the mode in which a given PodGroup can be disrupted.
// One of Pod, PodGroup.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
//
// +featureGate=WorkloadAwarePreemption
// +optional
DisruptionMode *DisruptionMode
// PriorityClassName indicates the priority that should be considered when scheduling
// a pod group created from this template. If no priority class is specified, admission
// control can set this to the global default priority class if it exists. Otherwise,
// pod groups created from this template will have the priority set to zero.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
//
// +featureGate=WorkloadAwarePreemption
// +optional
PriorityClassName string
// Priority is the value of priority of pod groups created from this template. Various
// system components use this field to find the priority of the pod group. When
// Priority Admission Controller is enabled, it prevents users from setting this field.
// The admission controller populates this field from PriorityClassName.
// The higher the value, the higher the priority.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
//
// +featureGate=WorkloadAwarePreemption
// +optional
Priority *int32
}
// PodGroupSchedulingPolicy defines the scheduling configuration for a PodGroup.
@ -295,6 +327,19 @@ type PodGroupResourceClaim struct {
ResourceClaimTemplateName *string
}
// DisruptionMode describes the mode in which a PodGroup can be disrupted (e.g. preempted).
// +enum
type DisruptionMode string
const (
// DisruptionModePod means that individual pods can be disrupted or preempted independently.
// It doesn't depend on exact set of pods currently running in this PodGroup.
DisruptionModePod DisruptionMode = "Pod"
// DisruptionModePodGroup means that the whole PodGroup needs to be disrupted
// or preempted together.
DisruptionModePodGroup DisruptionMode = "PodGroup"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// PodGroup represents a runtime instance of pods grouped together.
@ -374,6 +419,43 @@ type PodGroupSpec struct {
// +listMapKey=name
// +featureGate=DRAWorkloadResourceClaims
ResourceClaims []PodGroupResourceClaim
// DisruptionMode defines the mode in which a given PodGroup can be disrupted.
// Controllers are expected to fill this field by copying it from a PodGroupTemplate.
// One of Pod, PodGroup. Defaults to Pod if unset.
// This field is immutable.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
//
// +featureGate=WorkloadAwarePreemption
// +optional
DisruptionMode *DisruptionMode
// PriorityClassName defines the priority that should be considered when scheduling this pod group.
// Controllers are expected to fill this field by copying it from a PodGroupTemplate.
// Otherwise, it is validated and resolved similarly to the PriorityClassName on PodGroupTemplate
// (i.e. if no priority class is specified, admission control can set this to the global default
// priority class if it exists. Otherwise, the pod group's priority will be zero).
// This field is immutable.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
//
// +featureGate=WorkloadAwarePreemption
// +optional
PriorityClassName string
// Priority is the value of priority of this pod group. Various system components
// use this field to find the priority of the pod group. When Priority Admission
// Controller is enabled, it prevents users from setting this field. The admission
// controller populates this field from PriorityClassName.
// The higher the value, the higher the priority.
// This field is immutable.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
//
// +featureGate=WorkloadAwarePreemption
// +optional
Priority *int32
}
// PodGroupStatus represents information about the status of a pod group.

View file

@ -408,6 +408,9 @@ func autoConvert_v1alpha2_PodGroupSpec_To_scheduling_PodGroupSpec(in *scheduling
}
out.SchedulingConstraints = (*scheduling.PodGroupSchedulingConstraints)(unsafe.Pointer(in.SchedulingConstraints))
out.ResourceClaims = *(*[]scheduling.PodGroupResourceClaim)(unsafe.Pointer(&in.ResourceClaims))
out.DisruptionMode = (*scheduling.DisruptionMode)(unsafe.Pointer(in.DisruptionMode))
out.PriorityClassName = in.PriorityClassName
out.Priority = (*int32)(unsafe.Pointer(in.Priority))
return nil
}
@ -423,6 +426,9 @@ func autoConvert_scheduling_PodGroupSpec_To_v1alpha2_PodGroupSpec(in *scheduling
}
out.SchedulingConstraints = (*schedulingv1alpha2.PodGroupSchedulingConstraints)(unsafe.Pointer(in.SchedulingConstraints))
out.ResourceClaims = *(*[]schedulingv1alpha2.PodGroupResourceClaim)(unsafe.Pointer(&in.ResourceClaims))
out.DisruptionMode = (*schedulingv1alpha2.DisruptionMode)(unsafe.Pointer(in.DisruptionMode))
out.PriorityClassName = in.PriorityClassName
out.Priority = (*int32)(unsafe.Pointer(in.Priority))
return nil
}
@ -460,6 +466,9 @@ func autoConvert_v1alpha2_PodGroupTemplate_To_scheduling_PodGroupTemplate(in *sc
}
out.SchedulingConstraints = (*scheduling.PodGroupSchedulingConstraints)(unsafe.Pointer(in.SchedulingConstraints))
out.ResourceClaims = *(*[]scheduling.PodGroupResourceClaim)(unsafe.Pointer(&in.ResourceClaims))
out.DisruptionMode = (*scheduling.DisruptionMode)(unsafe.Pointer(in.DisruptionMode))
out.PriorityClassName = in.PriorityClassName
out.Priority = (*int32)(unsafe.Pointer(in.Priority))
return nil
}
@ -475,6 +484,9 @@ func autoConvert_scheduling_PodGroupTemplate_To_v1alpha2_PodGroupTemplate(in *sc
}
out.SchedulingConstraints = (*schedulingv1alpha2.PodGroupSchedulingConstraints)(unsafe.Pointer(in.SchedulingConstraints))
out.ResourceClaims = *(*[]schedulingv1alpha2.PodGroupResourceClaim)(unsafe.Pointer(&in.ResourceClaims))
out.DisruptionMode = (*schedulingv1alpha2.DisruptionMode)(unsafe.Pointer(in.DisruptionMode))
out.PriorityClassName = in.PriorityClassName
out.Priority = (*int32)(unsafe.Pointer(in.Priority))
return nil
}

View file

@ -22,6 +22,7 @@ limitations under the License.
package v1alpha2
import (
schedulingv1alpha2 "k8s.io/api/scheduling/v1alpha2"
runtime "k8s.io/apimachinery/pkg/runtime"
)
@ -29,5 +30,21 @@ import (
// Public to allow building arbitrary schemes.
// All generated defaulters are covering - they call all nested defaulters.
func RegisterDefaults(scheme *runtime.Scheme) error {
scheme.AddTypeDefaultingFunc(&schedulingv1alpha2.PodGroup{}, func(obj interface{}) { SetObjectDefaults_PodGroup(obj.(*schedulingv1alpha2.PodGroup)) })
scheme.AddTypeDefaultingFunc(&schedulingv1alpha2.PodGroupList{}, func(obj interface{}) { SetObjectDefaults_PodGroupList(obj.(*schedulingv1alpha2.PodGroupList)) })
return nil
}
func SetObjectDefaults_PodGroup(in *schedulingv1alpha2.PodGroup) {
if in.Spec.DisruptionMode == nil {
var ptrVar1 schedulingv1alpha2.DisruptionMode = "Pod"
in.Spec.DisruptionMode = &ptrVar1
}
}
func SetObjectDefaults_PodGroupList(in *schedulingv1alpha2.PodGroupList) {
for i := range in.Items {
a := &in.Items[i]
SetObjectDefaults_PodGroup(a)
}
}

View file

@ -31,6 +31,7 @@ import (
safe "k8s.io/apimachinery/pkg/api/safe"
validate "k8s.io/apimachinery/pkg/api/validate"
runtime "k8s.io/apimachinery/pkg/runtime"
sets "k8s.io/apimachinery/pkg/util/sets"
field "k8s.io/apimachinery/pkg/util/validation/field"
)
@ -58,6 +59,16 @@ func RegisterValidations(scheme *runtime.Scheme) error {
return nil
}
var symbolsForDisruptionMode = sets.New(schedulingv1alpha2.DisruptionModePod, schedulingv1alpha2.DisruptionModePodGroup)
// Validate_DisruptionMode validates an instance of DisruptionMode according
// to declarative validation rules in the API schema.
func Validate_DisruptionMode(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *schedulingv1alpha2.DisruptionMode) (errs field.ErrorList) {
errs = append(errs, validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForDisruptionMode, nil)...)
return errs
}
// Validate_GangSchedulingPolicy validates an instance of GangSchedulingPolicy according
// to declarative validation rules in the API schema.
func Validate_GangSchedulingPolicy(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *schedulingv1alpha2.GangSchedulingPolicy) (errs field.ErrorList) {
@ -330,11 +341,11 @@ func Validate_PodGroupSpec(ctx context.Context, op operation.Operation, fldPath
}
// call field-attached validations
earlyReturn := false
if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
if e := validate.Immutable(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if e := validate.Immutable(ctx, op, fldPath, obj, oldObj).MarkAlpha(); len(e) != 0 {
errs = append(errs, e...)
if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
earlyReturn = true
}
if earlyReturn {
@ -356,7 +367,7 @@ func Validate_PodGroupSpec(ctx context.Context, op operation.Operation, fldPath
}
// call field-attached validations
earlyReturn := false
if e := validate.Immutable(ctx, op, fldPath, obj, oldObj).MarkAlpha(); len(e) != 0 {
if e := validate.Immutable(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
@ -412,6 +423,10 @@ func Validate_PodGroupSpec(ctx context.Context, op operation.Operation, fldPath
}
// call field-attached validations
earlyReturn := false
if e := validate.Immutable(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 4); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
@ -419,10 +434,6 @@ func Validate_PodGroupSpec(ctx context.Context, op operation.Operation, fldPath
if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
earlyReturn = true
}
if e := validate.Immutable(ctx, op, fldPath, obj, oldObj).MarkAlpha(); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
@ -439,6 +450,106 @@ func Validate_PodGroupSpec(ctx context.Context, op operation.Operation, fldPath
return oldObj.ResourceClaims
}), oldObj != nil)...)
// field schedulingv1alpha2.PodGroupSpec.DisruptionMode
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *schedulingv1alpha2.DisruptionMode, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", false, validate.ForbiddenPointer); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", false, validate.OptionalPointer); len(e) != 0 {
earlyReturn = true
}
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", true, // optional fields with default values are effectively required
validate.RequiredPointer); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", true, validate.Immutable); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
// call the type's validation function
errs = append(errs, Validate_DisruptionMode(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("disruptionMode"), obj.DisruptionMode, safe.Field(oldObj, func(oldObj *schedulingv1alpha2.PodGroupSpec) *schedulingv1alpha2.DisruptionMode {
return oldObj.DisruptionMode
}), oldObj != nil)...)
// field schedulingv1alpha2.PodGroupSpec.PriorityClassName
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", false, validate.ForbiddenValue); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", false, validate.OptionalValue); len(e) != 0 {
earlyReturn = true
}
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", true, validate.OptionalValue); len(e) != 0 {
earlyReturn = true
}
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", true, validate.Immutable); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", true, validate.LongName)...)
return
}(fldPath.Child("priorityClassName"), &obj.PriorityClassName, safe.Field(oldObj, func(oldObj *schedulingv1alpha2.PodGroupSpec) *string { return &oldObj.PriorityClassName }), oldObj != nil)...)
// field schedulingv1alpha2.PodGroupSpec.Priority
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int32, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", false, validate.ForbiddenPointer); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", false, validate.OptionalPointer); len(e) != 0 {
earlyReturn = true
}
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", true, validate.OptionalPointer); len(e) != 0 {
earlyReturn = true
}
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", true, validate.Immutable); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", true, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int32) field.ErrorList {
return validate.Maximum(ctx, op, fldPath, obj, oldObj, 1000000000)
})...)
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", true, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int32) field.ErrorList {
return validate.Minimum(ctx, op, fldPath, obj, oldObj, -2147483648)
})...)
return
}(fldPath.Child("priority"), obj.Priority, safe.Field(oldObj, func(oldObj *schedulingv1alpha2.PodGroupSpec) *int32 { return oldObj.Priority }), oldObj != nil)...)
return errs
}
@ -557,6 +668,10 @@ func Validate_PodGroupTemplate(ctx context.Context, op operation.Operation, fldP
}
// call field-attached validations
earlyReturn := false
if e := validate.Immutable(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 4); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
@ -564,10 +679,6 @@ func Validate_PodGroupTemplate(ctx context.Context, op operation.Operation, fldP
if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
earlyReturn = true
}
if e := validate.Immutable(ctx, op, fldPath, obj, oldObj).MarkAlpha(); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
@ -584,6 +695,92 @@ func Validate_PodGroupTemplate(ctx context.Context, op operation.Operation, fldP
return oldObj.ResourceClaims
}), oldObj != nil)...)
// field schedulingv1alpha2.PodGroupTemplate.DisruptionMode
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *schedulingv1alpha2.DisruptionMode, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", false, validate.ForbiddenPointer); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", false, validate.OptionalPointer); len(e) != 0 {
earlyReturn = true
}
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", true, validate.OptionalPointer); len(e) != 0 {
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
// call the type's validation function
errs = append(errs, Validate_DisruptionMode(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("disruptionMode"), obj.DisruptionMode, safe.Field(oldObj, func(oldObj *schedulingv1alpha2.PodGroupTemplate) *schedulingv1alpha2.DisruptionMode {
return oldObj.DisruptionMode
}), oldObj != nil)...)
// field schedulingv1alpha2.PodGroupTemplate.PriorityClassName
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", false, validate.ForbiddenValue); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", false, validate.OptionalValue); len(e) != 0 {
earlyReturn = true
}
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", true, validate.OptionalValue); len(e) != 0 {
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", true, validate.LongName)...)
return
}(fldPath.Child("priorityClassName"), &obj.PriorityClassName, safe.Field(oldObj, func(oldObj *schedulingv1alpha2.PodGroupTemplate) *string { return &oldObj.PriorityClassName }), oldObj != nil)...)
// field schedulingv1alpha2.PodGroupTemplate.Priority
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int32, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", false, validate.ForbiddenPointer); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", false, validate.OptionalPointer); len(e) != 0 {
earlyReturn = true
}
if e := validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", true, validate.OptionalPointer); len(e) != 0 {
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", true, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int32) field.ErrorList {
return validate.Maximum(ctx, op, fldPath, obj, oldObj, 1000000000)
})...)
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "WorkloadAwarePreemption", true, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int32) field.ErrorList {
return validate.Minimum(ctx, op, fldPath, obj, oldObj, -2147483648)
})...)
return
}(fldPath.Child("priority"), obj.Priority, safe.Field(oldObj, func(oldObj *schedulingv1alpha2.PodGroupTemplate) *int32 { return oldObj.Priority }), oldObj != nil)...)
return errs
}
@ -798,11 +995,11 @@ func Validate_WorkloadSpec(ctx context.Context, op operation.Operation, fldPath
}
// call field-attached validations
earlyReturn := false
if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
if e := validate.Immutable(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if e := validate.Immutable(ctx, op, fldPath, obj, oldObj).MarkAlpha(); len(e) != 0 {
errs = append(errs, e...)
if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
earlyReturn = true
}
if earlyReturn {
@ -824,6 +1021,10 @@ func Validate_WorkloadSpec(ctx context.Context, op operation.Operation, fldPath
}
// call field-attached validations
earlyReturn := false
if e := validate.Immutable(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 8); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
@ -832,10 +1033,6 @@ func Validate_WorkloadSpec(ctx context.Context, op operation.Operation, fldPath
errs = append(errs, e...)
earlyReturn = true
}
if e := validate.Immutable(ctx, op, fldPath, obj, oldObj).MarkAlpha(); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}

View file

@ -75,16 +75,7 @@ func ValidatePodGroup(podGroup *scheduling.PodGroup) field.ErrorList {
// ValidatePodGroupUpdate tests if an update to PodGroup is valid.
func ValidatePodGroupUpdate(podGroup, oldPodGroup *scheduling.PodGroup) field.ErrorList {
allErrs := apivalidation.ValidateObjectMetaUpdate(&podGroup.ObjectMeta, &oldPodGroup.ObjectMeta, field.NewPath("metadata"))
allErrs = append(allErrs, validatePodGroupSpecUpdate(&podGroup.Spec, &oldPodGroup.Spec, field.NewPath("spec"))...)
return allErrs
}
func validatePodGroupSpecUpdate(spec, oldSpec *scheduling.PodGroupSpec, fldPath *field.Path) field.ErrorList {
allErrs := apivalidation.ValidateImmutableField(spec.PodGroupTemplateRef, oldSpec.PodGroupTemplateRef, fldPath.Child("podGroupTemplateRef")).WithOrigin("immutable").MarkCoveredByDeclarative()
allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.SchedulingPolicy, oldSpec.SchedulingPolicy, fldPath.Child("schedulingPolicy")).WithOrigin("immutable").MarkCoveredByDeclarative()...)
allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.ResourceClaims, oldSpec.ResourceClaims, fldPath.Child("resourceClaims")).WithOrigin("immutable").MarkCoveredByDeclarative()...)
return allErrs
return apivalidation.ValidateObjectMetaUpdate(&podGroup.ObjectMeta, &oldPodGroup.ObjectMeta, field.NewPath("metadata"))
}
// ValidateWorkload tests if all fields in a Workload are set correctly.
@ -94,16 +85,7 @@ func ValidateWorkload(workload *scheduling.Workload) field.ErrorList {
// ValidateWorkloadUpdate tests if an update to Workload is valid.
func ValidateWorkloadUpdate(workload, oldWorkload *scheduling.Workload) field.ErrorList {
allErrs := apivalidation.ValidateObjectMetaUpdate(&workload.ObjectMeta, &oldWorkload.ObjectMeta, field.NewPath("metadata"))
allErrs = append(allErrs, validateWorkloadSpecUpdate(&workload.Spec, &oldWorkload.Spec, field.NewPath("spec"))...)
return allErrs
}
func validateWorkloadSpecUpdate(spec, oldSpec *scheduling.WorkloadSpec, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
allErrs = append(allErrs, apimachineryvalidation.ValidateImmutableField(spec.ControllerRef, oldSpec.ControllerRef, fldPath.Child("controllerRef")).WithOrigin("immutable").MarkCoveredByDeclarative()...)
allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.PodGroupTemplates, oldSpec.PodGroupTemplates, fldPath.Child("podGroupTemplates")).WithOrigin("immutable").MarkCoveredByDeclarative()...)
return allErrs
return apivalidation.ValidateObjectMetaUpdate(&workload.ObjectMeta, &oldWorkload.ObjectMeta, field.NewPath("metadata"))
}
// ValidatePodGroupStatusUpdate tests if an update to the status of a PodGroup is valid.

View file

@ -305,91 +305,6 @@ func TestValidateWorkloadUpdate(t *testing.T) {
w.Namespace += "bar"
}),
},
"set controller ref": {
old: mkWorkload(func(w *scheduling.Workload) {
w.Spec.ControllerRef = nil
}),
update: mkWorkload(),
},
"unset controller ref": {
old: mkWorkload(),
update: mkWorkload(func(w *scheduling.Workload) {
w.Spec.ControllerRef = nil
}),
},
"change pod group name": {
old: mkWorkload(),
update: mkWorkload(func(w *scheduling.Workload) {
w.Spec.PodGroupTemplates[0].Name += "bar"
}),
},
"add pod group": {
old: mkWorkload(),
update: mkWorkload(func(w *scheduling.Workload) {
w.Spec.PodGroupTemplates = append(w.Spec.PodGroupTemplates, scheduling.PodGroupTemplate{
Name: "group3",
SchedulingPolicy: scheduling.PodGroupSchedulingPolicy{
Basic: &scheduling.BasicSchedulingPolicy{},
},
})
}),
},
"delete pod group": {
old: mkWorkload(),
update: mkWorkload(func(w *scheduling.Workload) {
w.Spec.PodGroupTemplates = w.Spec.PodGroupTemplates[:1]
}),
},
"change gang min count": {
old: mkWorkload(),
update: mkWorkload(func(w *scheduling.Workload) {
w.Spec.PodGroupTemplates[1].SchedulingPolicy.Gang.MinCount = 5
}),
},
"change controllerRef": {
old: mkWorkload(),
update: mkWorkload(func(w *scheduling.Workload) {
w.Spec.ControllerRef.Kind += "bar"
}),
},
"delete controllerRef": {
old: mkWorkload(),
update: mkWorkload(func(w *scheduling.Workload) {
w.Spec.ControllerRef = nil
}),
},
"add resource claim": {
old: mkWorkload(),
update: mkWorkload(func(w *scheduling.Workload) {
w.Spec.PodGroupTemplates[0].ResourceClaims = append(w.Spec.PodGroupTemplates[0].ResourceClaims, scheduling.PodGroupResourceClaim{
Name: "my-claim",
ResourceClaimName: new("my-claim-name"),
})
}),
},
"remove resource claim": {
old: mkWorkload(func(w *scheduling.Workload) {
w.Spec.PodGroupTemplates[0].ResourceClaims = append(w.Spec.PodGroupTemplates[0].ResourceClaims, scheduling.PodGroupResourceClaim{
Name: "my-claim",
ResourceClaimName: new("my-claim-name"),
})
}),
update: mkWorkload(),
},
"change resource claim": {
old: mkWorkload(func(w *scheduling.Workload) {
w.Spec.PodGroupTemplates[0].ResourceClaims = append(w.Spec.PodGroupTemplates[0].ResourceClaims, scheduling.PodGroupResourceClaim{
Name: "my-claim",
ResourceClaimName: new("my-claim-name"),
})
}),
update: mkWorkload(func(w *scheduling.Workload) {
w.Spec.PodGroupTemplates[0].ResourceClaims = append(w.Spec.PodGroupTemplates[0].ResourceClaims, scheduling.PodGroupResourceClaim{
Name: "my-claim",
ResourceClaimName: new("my-otherclaim-name"),
})
}),
},
}
for name, tc := range failureCases {
tc.old.ResourceVersion = "0"
@ -574,53 +489,6 @@ func TestValidatePodGroupUpdate(t *testing.T) {
pg.Namespace += "bar"
}),
},
"change podGroup template ref name": {
old: mkPodGroup(),
update: mkPodGroup(func(pg *scheduling.PodGroup) {
pg.Spec.PodGroupTemplateRef.Workload.PodGroupTemplateName = "new-template"
}),
},
"change podGroup template ref workload name": {
old: mkPodGroup(),
update: mkPodGroup(func(pg *scheduling.PodGroup) {
pg.Spec.PodGroupTemplateRef.Workload.WorkloadName = "new-workload"
}),
},
"delete podGroup template ref": {
old: mkPodGroup(),
update: mkPodGroup(func(pg *scheduling.PodGroup) {
pg.Spec.PodGroupTemplateRef = nil
}),
},
"change gang min count": {
old: mkPodGroup(),
update: mkPodGroup(func(pg *scheduling.PodGroup) {
pg.Spec.SchedulingPolicy.Gang.MinCount = 10
}),
},
"change scheduling policy": {
old: mkPodGroup(),
update: mkPodGroup(func(pg *scheduling.PodGroup) {
pg.Spec.SchedulingPolicy = scheduling.PodGroupSchedulingPolicy{
Basic: &scheduling.BasicSchedulingPolicy{},
}
}),
},
"multiple scheduling policies": {
old: mkPodGroup(),
update: mkPodGroup(func(pg *scheduling.PodGroup) {
pg.Spec.SchedulingPolicy.Basic = &scheduling.BasicSchedulingPolicy{}
}),
},
"change resource claims": {
old: mkPodGroup(),
update: mkPodGroup(func(pg *scheduling.PodGroup) {
pg.Spec.ResourceClaims = append(pg.Spec.ResourceClaims, scheduling.PodGroupResourceClaim{
Name: "new-claim",
ResourceClaimName: new("resource-claim"),
})
}),
},
}
for name, tc := range failureCases {
tc.old.ResourceVersion = "0"

View file

@ -235,6 +235,16 @@ func (in *PodGroupSpec) DeepCopyInto(out *PodGroupSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.DisruptionMode != nil {
in, out := &in.DisruptionMode, &out.DisruptionMode
*out = new(DisruptionMode)
**out = **in
}
if in.Priority != nil {
in, out := &in.Priority, &out.Priority
*out = new(int32)
**out = **in
}
return
}
@ -294,6 +304,16 @@ func (in *PodGroupTemplate) DeepCopyInto(out *PodGroupTemplate) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.DisruptionMode != nil {
in, out := &in.DisruptionMode, &out.DisruptionMode
*out = new(DisruptionMode)
**out = **in
}
if in.Priority != nil {
in, out := &in.Priority, &out.Priority
*out = new(int32)
**out = **in
}
return
}

View file

@ -54984,6 +54984,29 @@ func schema_k8sio_api_scheduling_v1alpha2_PodGroupSpec(ref common.ReferenceCallb
},
},
},
"disruptionMode": {
SchemaProps: spec.SchemaProps{
Description: "DisruptionMode defines the mode in which a given PodGroup can be disrupted. Controllers are expected to fill this field by copying it from a PodGroupTemplate. One of Pod, PodGroup. Defaults to Pod if unset. This field is immutable. This field is available only when the WorkloadAwarePreemption feature gate is enabled.\n\n\nPossible enum values:\n - `\"Pod\"` means that individual pods can be disrupted or preempted independently. It doesn't depend on exact set of pods currently running in this PodGroup.\n - `\"PodGroup\"` means that the whole PodGroup needs to be disrupted or preempted together.",
Default: "Pod",
Type: []string{"string"},
Format: "",
Enum: []interface{}{"Pod", "PodGroup"},
},
},
"priorityClassName": {
SchemaProps: spec.SchemaProps{
Description: "PriorityClassName defines the priority that should be considered when scheduling this pod group. Controllers are expected to fill this field by copying it from a PodGroupTemplate. Otherwise, it is validated and resolved similarly to the PriorityClassName on PodGroupTemplate (i.e. if no priority class is specified, admission control can set this to the global default priority class if it exists. Otherwise, the pod group's priority will be zero). This field is immutable. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
Type: []string{"string"},
Format: "",
},
},
"priority": {
SchemaProps: spec.SchemaProps{
Description: "Priority is the value of priority of this pod group. Various system components use this field to find the priority of the pod group. When Priority Admission Controller is enabled, it prevents users from setting this field. The admission controller populates this field from PriorityClassName. The higher the value, the higher the priority. This field is immutable. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
Type: []string{"integer"},
Format: "int32",
},
},
},
Required: []string{"schedulingPolicy"},
},
@ -55108,6 +55131,28 @@ func schema_k8sio_api_scheduling_v1alpha2_PodGroupTemplate(ref common.ReferenceC
},
},
},
"disruptionMode": {
SchemaProps: spec.SchemaProps{
Description: "DisruptionMode defines the mode in which a given PodGroup can be disrupted. One of Pod, PodGroup. This field is available only when the WorkloadAwarePreemption feature gate is enabled.\n\n\nPossible enum values:\n - `\"Pod\"` means that individual pods can be disrupted or preempted independently. It doesn't depend on exact set of pods currently running in this PodGroup.\n - `\"PodGroup\"` means that the whole PodGroup needs to be disrupted or preempted together.",
Type: []string{"string"},
Format: "",
Enum: []interface{}{"Pod", "PodGroup"},
},
},
"priorityClassName": {
SchemaProps: spec.SchemaProps{
Description: "PriorityClassName indicates the priority that should be considered when scheduling a pod group created from this template. If no priority class is specified, admission control can set this to the global default priority class if it exists. Otherwise, pod groups created from this template will have the priority set to zero. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
Type: []string{"string"},
Format: "",
},
},
"priority": {
SchemaProps: spec.SchemaProps{
Description: "Priority is the value of priority of pod groups created from this template. Various system components use this field to find the priority of the pod group. When Priority Admission Controller is enabled, it prevents users from setting this field. The admission controller populates this field from PriorityClassName. The higher the value, the higher the priority. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
Type: []string{"integer"},
Format: "int32",
},
},
},
Required: []string{"name", "schedulingPolicy"},
},

View file

@ -22,6 +22,7 @@ import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apimachinery/pkg/util/version"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
@ -35,6 +36,11 @@ import (
_ "k8s.io/kubernetes/pkg/apis/scheduling/install"
)
var allowedDisruptionModes = sets.New(
scheduling.DisruptionModePod,
scheduling.DisruptionModePodGroup,
)
func TestDeclarativeValidate(t *testing.T) {
apiVersions := []string{"v1alpha2"} // PodGroup is currently only in v1alpha2
for _, apiVersion := range apiVersions {
@ -55,9 +61,11 @@ func testDeclarativeValidate(t *testing.T, apiVersion string) {
strategy := NewStrategy()
testCases := map[string]struct {
input scheduling.PodGroup
expectedErrs field.ErrorList
tasEnabled bool
input scheduling.PodGroup
enableTopologyAwareScheduling bool
enableDRAWorkloadResourceClaims bool
enableWorkloadAwarePreemption bool
expectedErrs field.ErrorList
}{
"valid": {
input: mkValidPodGroup(),
@ -73,14 +81,14 @@ func testDeclarativeValidate(t *testing.T, apiVersion string) {
},
"gang minCount negative": {
input: mkValidPodGroup(setPodGroupMinCount(-1)),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingPolicy", "gang", "minCount"), -1, "").WithOrigin("minimum")},
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingPolicy", "gang", "minCount"), nil, "").WithOrigin("minimum")},
},
"no podGroupTemplateRef": {
input: mkValidPodGroup(unsetPodGroupTemplateRef()),
},
"empty podGroupTemplateRef": {
input: mkValidPodGroup(setEmptyPodGroupTemplateRef()),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplateRef"), "", "must specify one of: `workload`").WithOrigin("union")},
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplateRef"), nil, "").WithOrigin("union")},
},
"podGroupTemplateRef with empty template name": {
input: mkValidPodGroup(setPodGroupTemplateRef("", "workload")),
@ -119,50 +127,126 @@ func testDeclarativeValidate(t *testing.T, apiVersion string) {
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "schedulingConstraints"), "")},
},
"valid with schedulingConstraints": {
input: mkValidPodGroup(addTopologyConstraint("foo")),
tasEnabled: true,
input: mkValidPodGroup(addTopologyConstraint("foo")),
enableTopologyAwareScheduling: true,
},
"schedulingConstraints with multiple topology constraints": {
input: mkValidPodGroup(addTopologyConstraint("foo"), addTopologyConstraint("bar")),
expectedErrs: field.ErrorList{field.TooMany(field.NewPath("spec", "schedulingConstraints", "topology"), 2, 1).WithOrigin("maxItems")},
tasEnabled: true,
input: mkValidPodGroup(addTopologyConstraint("foo"), addTopologyConstraint("bar")),
enableTopologyAwareScheduling: true,
expectedErrs: field.ErrorList{field.TooMany(field.NewPath("spec", "schedulingConstraints", "topology"), 2, 1).WithOrigin("maxItems")},
},
"valid with empty schedulingConstraints": {
input: mkValidPodGroup(setSchedulingConstraints()),
tasEnabled: true,
input: mkValidPodGroup(setSchedulingConstraints()),
enableTopologyAwareScheduling: true,
},
"topologyConstraint with empty topology key": {
input: mkValidPodGroup(addTopologyConstraint("")),
expectedErrs: field.ErrorList{field.Required(field.NewPath("spec", "schedulingConstraints", "topology").Index(0).Child("key"), "")},
tasEnabled: true,
input: mkValidPodGroup(addTopologyConstraint("")),
enableTopologyAwareScheduling: true,
expectedErrs: field.ErrorList{field.Required(field.NewPath("spec", "schedulingConstraints", "topology").Index(0).Child("key"), "")},
},
"valid with topology key with DNS prefix": {
input: mkValidPodGroup(addTopologyConstraint("example.com/Foo")),
tasEnabled: true,
input: mkValidPodGroup(addTopologyConstraint("example.com/Foo")),
enableTopologyAwareScheduling: true,
},
"valid with topology key with prefix with max length": {
input: mkValidPodGroup(addTopologyConstraint(strings.Repeat("a", 253) + "/" + strings.Repeat("b", 63))),
tasEnabled: true,
input: mkValidPodGroup(addTopologyConstraint(strings.Repeat("a", 253) + "/" + strings.Repeat("b", 63))),
enableTopologyAwareScheduling: true,
},
"with topology key with prefix exceending max prefix length": {
input: mkValidPodGroup(addTopologyConstraint(strings.Repeat("a", 254) + "/foo")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingConstraints", "topology").Index(0).Child("key"), nil, "").WithOrigin("format=k8s-label-key")},
tasEnabled: true,
input: mkValidPodGroup(addTopologyConstraint(strings.Repeat("a", 254) + "/foo")),
enableTopologyAwareScheduling: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingConstraints", "topology").Index(0).Child("key"), nil, "").WithOrigin("format=k8s-label-key")},
},
"with topology key with prefix exceending max name length": {
input: mkValidPodGroup(addTopologyConstraint("foo/" + strings.Repeat("b", 64))),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingConstraints", "topology").Index(0).Child("key"), nil, "").WithOrigin("format=k8s-label-key")},
tasEnabled: true,
input: mkValidPodGroup(addTopologyConstraint("foo/" + strings.Repeat("b", 64))),
enableTopologyAwareScheduling: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingConstraints", "topology").Index(0).Child("key"), nil, "").WithOrigin("format=k8s-label-key")},
},
"with topology key without prefix exceeding max length": {
input: mkValidPodGroup(addTopologyConstraint(strings.Repeat("b", 64))),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingConstraints", "topology").Index(0).Child("key"), nil, "").WithOrigin("format=k8s-label-key")},
tasEnabled: true,
input: mkValidPodGroup(addTopologyConstraint(strings.Repeat("b", 64))),
enableTopologyAwareScheduling: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingConstraints", "topology").Index(0).Child("key"), nil, "").WithOrigin("format=k8s-label-key")},
},
"with topology key with invalid characters": {
input: mkValidPodGroup(addTopologyConstraint("Example.com/Foo")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingConstraints", "topology").Index(0).Child("key"), nil, "").WithOrigin("format=k8s-label-key")},
tasEnabled: true,
input: mkValidPodGroup(addTopologyConstraint("Example.com/Foo")),
enableTopologyAwareScheduling: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingConstraints", "topology").Index(0).Child("key"), nil, "").WithOrigin("format=k8s-label-key")},
},
"pod disruption mode, workload aware preemption enabled": {
input: mkValidPodGroup(setDisruptionMode(scheduling.DisruptionModePod)),
enableWorkloadAwarePreemption: true,
},
"pod disruption mode, workload aware preemption disabled": {
input: mkValidPodGroup(setDisruptionMode(scheduling.DisruptionModePod)),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "disruptionMode"), "")},
},
"pod group disruption mode, workload aware preemption enabled": {
input: mkValidPodGroup(setDisruptionMode(scheduling.DisruptionModePodGroup)),
enableWorkloadAwarePreemption: true,
},
"pod group disruption mode, workload aware preemption disabled": {
input: mkValidPodGroup(setDisruptionMode(scheduling.DisruptionModePodGroup)),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "disruptionMode"), "")},
},
"invalid disruption mode, workload aware preemption enabled": {
input: mkValidPodGroup(setDisruptionMode("Invalid")),
enableWorkloadAwarePreemption: true,
expectedErrs: field.ErrorList{field.NotSupported(field.NewPath("spec", "disruptionMode"), "Invalid", sets.List(allowedDisruptionModes))},
},
"invalid disruption mode, workload aware preemption disabled": {
input: mkValidPodGroup(setDisruptionMode("Invalid")),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "disruptionMode"), "")},
},
"valid pod group without disruption mode, workload aware preemption enabled": {
input: mkValidPodGroup(),
enableWorkloadAwarePreemption: true,
expectedErrs: field.ErrorList{field.Required(field.NewPath("spec", "disruptionMode"), "")},
},
"valid priority class name, workload aware preemption enabled": {
input: mkValidPodGroup(setDisruptionMode(scheduling.DisruptionModePod), setPriorityClassName("high-priority")),
enableWorkloadAwarePreemption: true,
},
"valid priority class name, workload aware preemption disabled": {
input: mkValidPodGroup(setPriorityClassName("high-priority")),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "priorityClassName"), "")},
},
"invalid priority class name, workload aware preemption enabled": {
input: mkValidPodGroup(setDisruptionMode(scheduling.DisruptionModePod), setPriorityClassName("high/priority")),
enableWorkloadAwarePreemption: true,
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "priorityClassName"), nil, "").WithOrigin("format=k8s-long-name"),
},
},
"invalid priority class name, workload aware preemption disabled": {
input: mkValidPodGroup(setPriorityClassName("high/priority")),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "priorityClassName"), "")},
},
"valid priority, workload aware preemption enabled": {
input: mkValidPodGroup(setDisruptionMode(scheduling.DisruptionModePod), setPriority(1000)),
enableWorkloadAwarePreemption: true,
},
"valid priority, workload aware preemption disabled": {
input: mkValidPodGroup(setPriority(1000)),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "priority"), "")},
},
"valid negative priority, workload aware preemption enabled": {
input: mkValidPodGroup(setDisruptionMode(scheduling.DisruptionModePod), setPriority(-2147483648)),
enableWorkloadAwarePreemption: true,
},
"valid negative priority, workload aware preemption disabled": {
input: mkValidPodGroup(setPriority(-2147483648)),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "priority"), "")},
},
"too high priority, workload aware preemption enabled": {
input: mkValidPodGroup(setDisruptionMode(scheduling.DisruptionModePod), setPriority(scheduling.HighestUserDefinablePriority+1)),
enableWorkloadAwarePreemption: true,
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "priority"), nil, "").WithOrigin("maximum"),
},
},
"too high priority, workload aware preemption disabled": {
input: mkValidPodGroup(setPriority(scheduling.HighestUserDefinablePriority + 1)),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "priority"), "")},
},
"ok resourceClaimName reference": {
input: mkValidPodGroup(addResourceClaims(scheduling.PodGroupResourceClaim{Name: "claim", ResourceClaimName: new("resource-claim")})),
@ -251,8 +335,11 @@ func testDeclarativeValidate(t *testing.T, apiVersion string) {
for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
features.GenericWorkload: tc.tasEnabled,
features.TopologyAwareWorkloadScheduling: tc.tasEnabled,
features.GenericWorkload: true,
features.TopologyAwareWorkloadScheduling: tc.enableTopologyAwareScheduling,
features.DRAWorkloadResourceClaims: tc.enableDRAWorkloadResourceClaims,
features.GangScheduling: tc.enableWorkloadAwarePreemption,
features.WorkloadAwarePreemption: tc.enableWorkloadAwarePreemption,
})
apitesting.VerifyValidationEquivalence(t, ctx, &tc.input, strategy.Validate, tc.expectedErrs, apitesting.WithMinEmulationVersion(version.MustParse("1.36")))
})
@ -279,10 +366,12 @@ func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) {
Verb: "update",
})
testCases := map[string]struct {
oldObj scheduling.PodGroup
updateObj scheduling.PodGroup
expectedErrs field.ErrorList
tasEnabled bool
oldObj scheduling.PodGroup
updateObj scheduling.PodGroup
enableTopologyAwareScheduling bool
enableDRAWorkloadResourceClaims bool
enableWorkloadAwarePreemption bool
expectedErrs field.ErrorList
}{
"valid update": {
oldObj: mkValidPodGroup(setResourceVersion("1")),
@ -292,90 +381,189 @@ func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) {
oldObj: mkValidPodGroup(setResourceVersion("1")),
updateObj: mkValidPodGroup(setResourceVersion("1"), setEmptyPodGroupTemplateRef()),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "podGroupTemplateRef"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha(),
field.Invalid(field.NewPath("spec", "podGroupTemplateRef"), nil, "").WithOrigin("immutable"),
},
},
"invalid update unset podGroupTemplateRef": {
oldObj: mkValidPodGroup(setResourceVersion("1")),
updateObj: mkValidPodGroup(setResourceVersion("1"), unsetPodGroupTemplateRef()),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "podGroupTemplateRef"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha(),
field.Invalid(field.NewPath("spec", "podGroupTemplateRef"), nil, "").WithOrigin("immutable"),
},
},
"invalid update setPodGroupTemplateRef": {
"invalid update change podGroupTemplateRef template name": {
oldObj: mkValidPodGroup(setResourceVersion("1")),
updateObj: mkValidPodGroup(setResourceVersion("1"), setPodGroupTemplateRef("other-template", "other-workload")),
updateObj: mkValidPodGroup(setResourceVersion("1"), setPodGroupTemplateRef("other-template", "workload")),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "podGroupTemplateRef"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha(),
field.Invalid(field.NewPath("spec", "podGroupTemplateRef"), nil, "").WithOrigin("immutable"),
},
},
"invalid update change podGroupTemplateRef workload name": {
oldObj: mkValidPodGroup(setResourceVersion("1")),
updateObj: mkValidPodGroup(setResourceVersion("1"), setPodGroupTemplateRef("template", "other-workload")),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "podGroupTemplateRef"), nil, "").WithOrigin("immutable"),
},
},
"invalid update with neither basic nor gang": {
oldObj: mkValidPodGroup(setResourceVersion("1")),
updateObj: mkValidPodGroup(setResourceVersion("1"), clearPodGroupPolicy()),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "schedulingPolicy"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha(),
field.Invalid(field.NewPath("spec", "schedulingPolicy"), nil, "").WithOrigin("immutable"),
},
},
"invalid update with both basic and gang": {
oldObj: mkValidPodGroup(setResourceVersion("1")),
updateObj: mkValidPodGroup(setResourceVersion("1"), setBothPolicies()),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "schedulingPolicy"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha(),
field.Invalid(field.NewPath("spec", "schedulingPolicy"), nil, "").WithOrigin("immutable"),
},
},
"invalid update of gang minCount": {
oldObj: mkValidPodGroup(setResourceVersion("1")),
updateObj: mkValidPodGroup(setResourceVersion("1"), setPodGroupMinCount(10)),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "schedulingPolicy"), nil, "").WithOrigin("immutable"),
},
},
"invalid update from gang to basic policy": {
oldObj: mkValidPodGroup(setResourceVersion("1")),
updateObj: mkValidPodGroup(setResourceVersion("1"), setBasicPolicy()),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingPolicy"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha()},
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingPolicy"), nil, "").WithOrigin("immutable")},
},
"valid update with unchanged scheduling constraints and TAS disabled": {
oldObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("foo")),
updateObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("foo")),
},
"valid update with unchanged scheduling constraints": {
oldObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("foo")),
updateObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("foo")),
tasEnabled: true,
oldObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("foo")),
updateObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("foo")),
enableTopologyAwareScheduling: true,
},
"invalid update to scheduling constraints": {
oldObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("foo")),
updateObj: mkValidPodGroup(setResourceVersion("1"), setSchedulingConstraints()),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingConstraints"), nil, "field is immutable").WithOrigin("immutable")},
tasEnabled: true,
oldObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("foo")),
updateObj: mkValidPodGroup(setResourceVersion("1"), setSchedulingConstraints()),
enableTopologyAwareScheduling: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingConstraints"), nil, "field is immutable").WithOrigin("immutable")},
},
"invalid update to topology constraints": {
oldObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("foo")),
updateObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("foo"), addTopologyConstraint("bar")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingConstraints"), nil, "field is immutable").WithOrigin("immutable")},
tasEnabled: true,
oldObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("foo")),
updateObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("foo"), addTopologyConstraint("bar")),
enableTopologyAwareScheduling: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingConstraints"), nil, "field is immutable").WithOrigin("immutable")},
},
"invalid update to topology key": {
oldObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("foo")),
updateObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("bar")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingConstraints"), nil, "field is immutable").WithOrigin("immutable")},
tasEnabled: true,
oldObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("foo")),
updateObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("bar")),
enableTopologyAwareScheduling: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingConstraints"), nil, "field is immutable").WithOrigin("immutable")},
},
"invalid update to scheduling constraints with TAS disabled": {
oldObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("foo")),
updateObj: mkValidPodGroup(setResourceVersion("1"), setSchedulingConstraints()),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingConstraints"), nil, "field is immutable").WithOrigin("immutable")},
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "schedulingConstraints"), "")},
},
"invalid update to topology constraints with TAS disabled": {
oldObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("foo")),
updateObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("foo"), addTopologyConstraint("bar")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingConstraints"), nil, "field is immutable").WithOrigin("immutable")},
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "schedulingConstraints"), "")},
},
"invalid update to topology key with TAS disabled": {
oldObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("foo")),
updateObj: mkValidPodGroup(setResourceVersion("1"), addTopologyConstraint("bar")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "schedulingConstraints"), nil, "field is immutable").WithOrigin("immutable")},
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "schedulingConstraints"), "")},
},
"invalid add of resource claims, DRA workload resource claims disabled": {
oldObj: mkValidPodGroup(setResourceVersion("1")),
updateObj: mkValidPodGroup(setResourceVersion("1"), addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-claim", ResourceClaimTemplateName: new("my-template")},
)),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "resourceClaims"), nil, "").WithOrigin("immutable")},
},
"invalid add of resource claims, DRA workload resource claims enabled": {
oldObj: mkValidPodGroup(setResourceVersion("1")),
updateObj: mkValidPodGroup(setResourceVersion("1"), addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-claim", ResourceClaimTemplateName: new("my-template")},
)),
enableDRAWorkloadResourceClaims: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "resourceClaims"), nil, "").WithOrigin("immutable")},
},
"invalid update of resource claims, DRA workload resource claims disabled": {
oldObj: mkValidPodGroup(setResourceVersion("1"), addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-claim", ResourceClaimTemplateName: new("my-template")},
)),
updateObj: mkValidPodGroup(setResourceVersion("1"), addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-other-claim", ResourceClaimTemplateName: new("my-template")},
)),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "resourceClaims"), nil, "").WithOrigin("immutable")},
},
"invalid update of resource claims, DRA workload resource claims enabled": {
oldObj: mkValidPodGroup(setResourceVersion("1"), addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-claim", ResourceClaimTemplateName: new("my-template")},
)),
updateObj: mkValidPodGroup(setResourceVersion("1"), addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-other-claim", ResourceClaimTemplateName: new("my-template")},
)),
enableDRAWorkloadResourceClaims: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "resourceClaims"), nil, "").WithOrigin("immutable")},
},
"invalid remove of resource claims, DRA workload resource claims disabled": {
oldObj: mkValidPodGroup(setResourceVersion("1"), addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-claim", ResourceClaimTemplateName: new("my-template")},
)),
updateObj: mkValidPodGroup(setResourceVersion("1")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "resourceClaims"), nil, "").WithOrigin("immutable")},
},
"invalid remove of resource claims, DRA workload resource claims enabled": {
oldObj: mkValidPodGroup(setResourceVersion("1"), addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-claim", ResourceClaimTemplateName: new("my-template")},
)),
updateObj: mkValidPodGroup(setResourceVersion("1")),
enableDRAWorkloadResourceClaims: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "resourceClaims"), nil, "").WithOrigin("immutable")},
},
"invalid update of disruption mode, workload aware preemption enabled": {
oldObj: mkValidPodGroup(setResourceVersion("1"), setDisruptionMode(scheduling.DisruptionModePod)),
updateObj: mkValidPodGroup(setResourceVersion("1"), setDisruptionMode(scheduling.DisruptionModePodGroup)),
enableWorkloadAwarePreemption: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "disruptionMode"), nil, "").WithOrigin("immutable")},
},
"invalid update of disruption mode, workload aware preemption disabled": {
oldObj: mkValidPodGroup(setResourceVersion("1"), setDisruptionMode(scheduling.DisruptionModePod)),
updateObj: mkValidPodGroup(setResourceVersion("1"), setDisruptionMode(scheduling.DisruptionModePodGroup)),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "disruptionMode"), "")},
},
"invalid update of priority class name, workload aware preemption enabled": {
oldObj: mkValidPodGroup(setResourceVersion("1"), setPriorityClassName("low-priority")),
updateObj: mkValidPodGroup(setResourceVersion("1"), setPriorityClassName("high-priority")),
enableWorkloadAwarePreemption: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "priorityClassName"), nil, "").WithOrigin("immutable")},
},
"invalid update of priority class name, workload aware preemption disabled": {
oldObj: mkValidPodGroup(setResourceVersion("1"), setPriorityClassName("low-priority")),
updateObj: mkValidPodGroup(setResourceVersion("1"), setPriorityClassName("high-priority")),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "priorityClassName"), "")},
},
"invalid update of priority, workload aware preemption enabled": {
oldObj: mkValidPodGroup(setResourceVersion("1"), setPriority(1000)),
updateObj: mkValidPodGroup(setResourceVersion("1"), setPriority(2000)),
enableWorkloadAwarePreemption: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "priority"), nil, "").WithOrigin("immutable")},
},
"invalid update of priority, workload aware preemption disabled": {
oldObj: mkValidPodGroup(setResourceVersion("1"), setPriority(1000)),
updateObj: mkValidPodGroup(setResourceVersion("1"), setPriority(2000)),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "priority"), "")},
},
}
for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
features.GenericWorkload: tc.tasEnabled,
features.TopologyAwareWorkloadScheduling: tc.tasEnabled,
features.GenericWorkload: true,
features.TopologyAwareWorkloadScheduling: tc.enableTopologyAwareScheduling,
features.DRAWorkloadResourceClaims: tc.enableDRAWorkloadResourceClaims,
features.GangScheduling: tc.enableWorkloadAwarePreemption,
features.WorkloadAwarePreemption: tc.enableWorkloadAwarePreemption,
})
strategy := NewStrategy()
apitesting.VerifyUpdateValidationEquivalence(t, ctx, &tc.updateObj, &tc.oldObj, strategy.ValidateUpdate, tc.expectedErrs, apitesting.WithMinEmulationVersion(version.MustParse("1.36")))
@ -626,3 +814,21 @@ func addTopologyConstraint(value string) func(obj *scheduling.PodGroup) {
obj.Spec.SchedulingConstraints.Topology = append(obj.Spec.SchedulingConstraints.Topology, scheduling.TopologyConstraint{Key: value})
}
}
func setDisruptionMode(mode scheduling.DisruptionMode) func(obj *scheduling.PodGroup) {
return func(obj *scheduling.PodGroup) {
obj.Spec.DisruptionMode = &mode
}
}
func setPriorityClassName(priorityClassName string) func(obj *scheduling.PodGroup) {
return func(obj *scheduling.PodGroup) {
obj.Spec.PriorityClassName = priorityClassName
}
}
func setPriority(priority int32) func(obj *scheduling.PodGroup) {
return func(obj *scheduling.PodGroup) {
obj.Spec.Priority = new(priority)
}
}

View file

@ -75,9 +75,15 @@ func (*podGroupStrategy) Validate(ctx context.Context, obj runtime.Object) field
podGroup := obj.(*scheduling.PodGroup)
allErrs := validation.ValidatePodGroup(podGroup)
opts := []string{}
if schedulingConstraintsInUse(nil) {
if utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareWorkloadScheduling) {
opts = append(opts, string(features.TopologyAwareWorkloadScheduling))
}
if utilfeature.DefaultFeatureGate.Enabled(features.DRAWorkloadResourceClaims) {
opts = append(opts, string(features.DRAWorkloadResourceClaims))
}
if utilfeature.DefaultFeatureGate.Enabled(features.WorkloadAwarePreemption) {
opts = append(opts, string(features.WorkloadAwarePreemption))
}
return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, obj, nil, allErrs, operation.Create, rest.WithDeclarativeEnforcement(), rest.WithOptions(opts))
}
@ -103,9 +109,19 @@ func (*podGroupStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Ob
oldPodGroup := old.(*scheduling.PodGroup)
allErrs := validation.ValidatePodGroupUpdate(newPodGroup, oldPodGroup)
opts := []string{}
if schedulingConstraintsInUse(oldPodGroup) {
// Declarative validation will always allow fields to remain unchanged, so if any
// of the fields which are covered by these gates are set, we will not re-validate them
// (even if the gates are disabled) as long as they do not change values. If a gate
// is disabled, they will not be allowed to change values.
if utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareWorkloadScheduling) {
opts = append(opts, string(features.TopologyAwareWorkloadScheduling))
}
if utilfeature.DefaultFeatureGate.Enabled(features.DRAWorkloadResourceClaims) {
opts = append(opts, string(features.DRAWorkloadResourceClaims))
}
if utilfeature.DefaultFeatureGate.Enabled(features.WorkloadAwarePreemption) {
opts = append(opts, string(features.WorkloadAwarePreemption))
}
return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newPodGroup, oldPodGroup, allErrs, operation.Update, rest.WithDeclarativeEnforcement(), rest.WithOptions(opts))
}
@ -160,46 +176,104 @@ func (*podGroupStatusStrategy) WarningsOnUpdate(ctx context.Context, obj, old ru
}
// dropDisabledPodGroupFields removes fields which are covered by a feature gate.
func dropDisabledPodGroupFields(newPodGroup, oldPodGroup *scheduling.PodGroup) {
dropDisabledSchedulingConstraintsFields(newPodGroup, oldPodGroup)
dropDisabledDRAWorkloadResourceClaimsFields(newPodGroup, oldPodGroup)
func dropDisabledPodGroupFields(podGroup, oldPodGroup *scheduling.PodGroup) {
var podGroupSpec, oldPodGroupSpec *scheduling.PodGroupSpec
if podGroup != nil {
podGroupSpec = &podGroup.Spec
}
if oldPodGroup != nil {
oldPodGroupSpec = &oldPodGroup.Spec
}
dropDisabledPodGroupSpecFields(podGroupSpec, oldPodGroupSpec)
}
func dropDisabledPodGroupSpecFields(podGroupSpec, oldPodGroupSpec *scheduling.PodGroupSpec) {
dropDisabledSchedulingConstraintsFields(podGroupSpec, oldPodGroupSpec)
dropDisabledDRAWorkloadResourceClaimsFields(podGroupSpec, oldPodGroupSpec)
dropDisabledDisruptionModeField(podGroupSpec, oldPodGroupSpec)
dropDisabledPriorityClassNameField(podGroupSpec, oldPodGroupSpec)
dropDisabledPriorityField(podGroupSpec, oldPodGroupSpec)
}
func dropDisabledPodGroupStatusFields(newPodGroup, oldPodGroup *scheduling.PodGroup) {
if newPodGroup == nil {
var oldPodGroupSpec *scheduling.PodGroupSpec
if oldPodGroup != nil {
oldPodGroupSpec = &oldPodGroup.Spec
}
if utilfeature.DefaultFeatureGate.Enabled(features.DRAWorkloadResourceClaims) || draWorkloadResourceClaimsInUse(oldPodGroupSpec) {
// No need to drop anything.
return
}
if !utilfeature.DefaultFeatureGate.Enabled(features.DRAWorkloadResourceClaims) && !draWorkloadResourceClaimsInUse(oldPodGroup) {
newPodGroup.Status.ResourceClaimStatuses = nil
}
newPodGroup.Status.ResourceClaimStatuses = nil
}
// dropDisabledSchedulingConstraintsFields drops the SchedulingConstraints field
// from the new PodGroup if the TopologyAwareWorkloadScheduling feature gate is disabled
// and it was not used in the old PodGroup.
func dropDisabledSchedulingConstraintsFields(newPodGroup, oldPodGroup *scheduling.PodGroup) {
if schedulingConstraintsInUse(oldPodGroup) {
func dropDisabledSchedulingConstraintsFields(podGroupSpec, oldPodGroupSpec *scheduling.PodGroupSpec) {
if utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareWorkloadScheduling) || schedulingConstraintsInUse(oldPodGroupSpec) {
// No need to drop anything.
return
}
newPodGroup.Spec.SchedulingConstraints = nil
}
func schedulingConstraintsInUse(pg *scheduling.PodGroup) bool {
return utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareWorkloadScheduling) || (pg != nil && pg.Spec.SchedulingConstraints != nil)
podGroupSpec.SchedulingConstraints = nil
}
// dropDisabledDRAWorkloadResourceClaimsFields removes resource claim references
// unless they are already used by the old PodGroup spec.
func dropDisabledDRAWorkloadResourceClaimsFields(podGroup, oldPodGroup *scheduling.PodGroup) {
if draWorkloadResourceClaimsInUse(oldPodGroup) {
func dropDisabledDRAWorkloadResourceClaimsFields(podGroupSpec, oldPodGroupSpec *scheduling.PodGroupSpec) {
if utilfeature.DefaultFeatureGate.Enabled(features.DRAWorkloadResourceClaims) || draWorkloadResourceClaimsInUse(oldPodGroupSpec) {
// No need to drop anything.
return
}
podGroup.Spec.ResourceClaims = nil
podGroupSpec.ResourceClaims = nil
}
func draWorkloadResourceClaimsInUse(pg *scheduling.PodGroup) bool {
return utilfeature.DefaultFeatureGate.Enabled(features.DRAWorkloadResourceClaims) || (pg != nil && len(pg.Spec.ResourceClaims) > 0)
// dropDisabledDisruptionModeField removes the DisruptionMode field unless it is
// already used in the old PodGroup spec.
func dropDisabledDisruptionModeField(podGroupSpec, oldPodGroupSpec *scheduling.PodGroupSpec) {
if utilfeature.DefaultFeatureGate.Enabled(features.WorkloadAwarePreemption) || disruptionModeInUse(oldPodGroupSpec) {
// No need to drop anything.
return
}
podGroupSpec.DisruptionMode = nil
}
// dropDisabledPriorityClassNameField removes the PriorityClassName field unless
// it is already used in the old PodGroup spec.
func dropDisabledPriorityClassNameField(podGroupSpec, oldPodGroupSpec *scheduling.PodGroupSpec) {
if utilfeature.DefaultFeatureGate.Enabled(features.WorkloadAwarePreemption) || priorityClassNameInUse(oldPodGroupSpec) {
// No need to drop anything.
return
}
podGroupSpec.PriorityClassName = ""
}
// dropDisabledPriorityField removes the Priority field unless it is already used
// in the old PodGroup spec.
func dropDisabledPriorityField(podGroupSpec, oldPodGroupSpec *scheduling.PodGroupSpec) {
if utilfeature.DefaultFeatureGate.Enabled(features.WorkloadAwarePreemption) || priorityInUse(oldPodGroupSpec) {
// No need to drop anything.
return
}
podGroupSpec.Priority = nil
}
func schedulingConstraintsInUse(podGroupSpec *scheduling.PodGroupSpec) bool {
return podGroupSpec != nil && podGroupSpec.SchedulingConstraints != nil
}
func draWorkloadResourceClaimsInUse(podGroupSpec *scheduling.PodGroupSpec) bool {
return podGroupSpec != nil && len(podGroupSpec.ResourceClaims) > 0
}
func disruptionModeInUse(podGroupSpec *scheduling.PodGroupSpec) bool {
return podGroupSpec != nil && podGroupSpec.DisruptionMode != nil
}
func priorityClassNameInUse(podGroupSpec *scheduling.PodGroupSpec) bool {
return podGroupSpec != nil && podGroupSpec.PriorityClassName != ""
}
func priorityInUse(podGroupSpec *scheduling.PodGroupSpec) bool {
return podGroupSpec != nil && podGroupSpec.Priority != nil
}

View file

@ -18,12 +18,10 @@ package podgroup
import (
"context"
"fmt"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
utilfeature "k8s.io/apiserver/pkg/util/feature"
@ -52,29 +50,22 @@ var podGroup = &scheduling.PodGroup{
},
}
var podGroupWithSchedulingConstraints = &scheduling.PodGroup{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: metav1.NamespaceDefault,
},
Spec: scheduling.PodGroupSpec{
PodGroupTemplateRef: &scheduling.PodGroupTemplateReference{
Workload: &scheduling.WorkloadPodGroupTemplateReference{
WorkloadName: "w",
PodGroupTemplateName: "t",
},
},
SchedulingPolicy: scheduling.PodGroupSchedulingPolicy{
Gang: &scheduling.GangSchedulingPolicy{
MinCount: 5,
},
},
SchedulingConstraints: &scheduling.PodGroupSchedulingConstraints{
Topology: []scheduling.TopologyConstraint{
{Key: "foo"},
},
},
},
func podGroupWithSchedulingConstraints(keys ...string) *scheduling.PodGroup {
pg := podGroup.DeepCopy()
pg.Spec.SchedulingConstraints = &scheduling.PodGroupSchedulingConstraints{
Topology: []scheduling.TopologyConstraint{},
}
for _, key := range keys {
constraint := scheduling.TopologyConstraint{Key: key}
pg.Spec.SchedulingConstraints.Topology = append(pg.Spec.SchedulingConstraints.Topology, constraint)
}
return pg
}
func podGroupWithDisruptionMode(mode scheduling.DisruptionMode) *scheduling.PodGroup {
pg := podGroup.DeepCopy()
pg.Spec.DisruptionMode = &mode
return pg
}
var (
@ -82,6 +73,11 @@ var (
minCountError = "must be greater than or equal to 1"
oneOfError = "must specify one of: `basic`, `gang`"
multipleFieldsSetError = "must specify exactly one of: `basic`, `gang`"
tooManyItemsError = "must have at most 1 item"
maximumError = "must be less than or equal to 1000000000"
subdomainNameError = "lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters"
forbiddenError = "Forbidden"
supportedModesError = `supported values: "Pod", "PodGroup"`
)
func TestStrategy(t *testing.T) {
@ -106,13 +102,13 @@ func ctxWithRequestInfo() context.Context {
func TestStrategyCreate(t *testing.T) {
ctx := ctxWithRequestInfo()
now := metav1.Now()
type testCase struct {
obj *scheduling.PodGroup
expectObj *scheduling.PodGroup
expectValidationError string
tasEnabled bool
}
testCases := map[string]testCase{
testCases := map[string]struct {
obj *scheduling.PodGroup
expectObj *scheduling.PodGroup
enableTopologyAwareScheduling bool
enableWorkloadAwarePreemption bool
expectValidationError string
}{
"simple": {
obj: podGroup,
expectObj: podGroup,
@ -157,51 +153,116 @@ func TestStrategyCreate(t *testing.T) {
}(),
expectObj: podGroup,
},
"multiple topology constraints": {
obj: func() *scheduling.PodGroup {
newPodGroup := podGroupWithSchedulingConstraints.DeepCopy()
newPodGroup.Spec.SchedulingConstraints.Topology = []scheduling.TopologyConstraint{
{Key: "foo"},
{Key: "bar"},
}
return newPodGroup
}(),
expectValidationError: "must have at most 1 item",
tasEnabled: true,
"multiple topology constraints, topology aware scheduling enabled": {
obj: podGroupWithSchedulingConstraints("foo", "bar"),
enableTopologyAwareScheduling: true,
expectValidationError: tooManyItemsError,
},
"invalid topology key": {
obj: func() *scheduling.PodGroup {
newPodGroup := podGroupWithSchedulingConstraints.DeepCopy()
newPodGroup.Spec.SchedulingConstraints.Topology[0].Key = "foo-"
return newPodGroup
}(),
expectValidationError: "Invalid value: \"foo-\"",
tasEnabled: true,
"multiple topology constraints, topology aware scheduling disabled": {
obj: podGroupWithSchedulingConstraints("foo", "bar"),
expectObj: podGroup,
},
"invalid topology key, topology aware scheduling enabled": {
obj: podGroupWithSchedulingConstraints("foo-"),
enableTopologyAwareScheduling: true,
expectValidationError: "Invalid value: \"foo-\"",
},
"invalid topology key, topology aware scheduling disabled": {
obj: podGroupWithSchedulingConstraints("foo-"),
expectObj: podGroup,
},
"with TAS feature gate disabled, drop scheduling constraints on creation": {
obj: podGroupWithSchedulingConstraints.DeepCopy(),
expectObj: podGroup,
tasEnabled: false,
obj: podGroupWithSchedulingConstraints("foo-"),
expectObj: podGroup,
},
"workload aware preemption disabled - drop disruption mode": {
obj: podGroupWithDisruptionMode(scheduling.DisruptionModePod),
expectObj: podGroup,
},
"workload aware preemption enabled - preserve disruption mode (pod)": {
obj: podGroupWithDisruptionMode(scheduling.DisruptionModePod),
expectObj: podGroupWithDisruptionMode(scheduling.DisruptionModePod),
enableWorkloadAwarePreemption: true,
},
"workload aware preemption enabled - preserve disruption mode (pod group)": {
obj: podGroupWithDisruptionMode(scheduling.DisruptionModePodGroup),
expectObj: podGroupWithDisruptionMode(scheduling.DisruptionModePodGroup),
enableWorkloadAwarePreemption: true,
},
"workload aware preemption enabled - unknown disruption mode": {
obj: podGroupWithDisruptionMode(scheduling.DisruptionMode("Invalid")),
enableWorkloadAwarePreemption: true,
expectValidationError: supportedModesError,
},
"workload aware preemption disabled - drop priorityClassName": {
obj: func() *scheduling.PodGroup {
pg := podGroupWithDisruptionMode(scheduling.DisruptionModePod)
pg.Spec.PriorityClassName = "high-priority"
return pg
}(),
expectObj: podGroup,
},
"workload aware preemption enabled - invalid priorityClassName": {
obj: func() *scheduling.PodGroup {
pg := podGroupWithDisruptionMode(scheduling.DisruptionModePod)
pg.Spec.PriorityClassName = "invalid/priority/class/name"
return pg
}(),
enableWorkloadAwarePreemption: true,
expectValidationError: subdomainNameError,
},
"workload aware preemption enabled - preserve priorityClassName": {
obj: func() *scheduling.PodGroup {
pg := podGroupWithDisruptionMode(scheduling.DisruptionModePod)
pg.Spec.PriorityClassName = "high-priority"
return pg
}(),
expectObj: func() *scheduling.PodGroup {
pg := podGroupWithDisruptionMode(scheduling.DisruptionModePod)
pg.Spec.PriorityClassName = "high-priority"
return pg
}(),
enableWorkloadAwarePreemption: true,
},
"workload aware preemption disabled - drop priority": {
obj: func() *scheduling.PodGroup {
pg := podGroupWithDisruptionMode(scheduling.DisruptionModePod)
pg.Spec.Priority = new(int32(1000))
return pg
}(),
expectObj: podGroup,
},
"workload aware preemption enabled - preserve priority": {
obj: func() *scheduling.PodGroup {
pg := podGroupWithDisruptionMode(scheduling.DisruptionModePod)
pg.Spec.Priority = new(int32(1000))
return pg
}(),
expectObj: func() *scheduling.PodGroup {
pg := podGroupWithDisruptionMode(scheduling.DisruptionModePod)
pg.Spec.Priority = new(int32(1000))
return pg
}(),
enableWorkloadAwarePreemption: true,
},
"workload aware preemption enabled - too high priority": {
obj: func() *scheduling.PodGroup {
pg := podGroupWithDisruptionMode(scheduling.DisruptionModePod)
pg.Spec.Priority = new(int32(scheduling.HighestUserDefinablePriority + 1))
return pg
}(),
enableWorkloadAwarePreemption: true,
expectValidationError: maximumError,
},
}
allTestCases := make(map[string]testCase)
for name, tc := range testCases {
allTestCases[name] = tc
if tc.tasEnabled {
newTc := testCase{
obj: tc.obj,
expectObj: podGroup,
}
allTestCases[fmt.Sprintf("drops scheduling constraints, originally %s", name)] = newTc
}
}
for name, tc := range allTestCases {
t.Run(name, func(t *testing.T) {
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
features.GenericWorkload: tc.tasEnabled,
features.TopologyAwareWorkloadScheduling: tc.tasEnabled,
features.GenericWorkload: true,
features.TopologyAwareWorkloadScheduling: tc.enableTopologyAwareScheduling,
features.GangScheduling: tc.enableWorkloadAwarePreemption,
features.WorkloadAwarePreemption: tc.enableWorkloadAwarePreemption,
})
podGroup := tc.obj.DeepCopy()
@ -225,6 +286,7 @@ func TestStrategyCreate(t *testing.T) {
if warnings := strategy.WarningsOnCreate(ctx, podGroup); len(warnings) != 0 {
t.Fatalf("unexpected warnings: %q", warnings)
}
strategy.Canonicalize(podGroup)
if tc.expectObj != nil {
if diff := cmp.Diff(tc.expectObj, podGroup); diff != "" {
t.Errorf("got unexpected podGroup object (-want, +got): %s", diff)
@ -237,10 +299,11 @@ func TestStrategyCreate(t *testing.T) {
func TestStrategyUpdate(t *testing.T) {
ctx := ctxWithRequestInfo()
testCases := map[string]struct {
oldObj *scheduling.PodGroup
newObj *scheduling.PodGroup
expectValidationError string
tasEnabled bool
oldObj *scheduling.PodGroup
newObj *scheduling.PodGroup
enableTopologyAwareScheduling bool
enableWorkloadAwarePreemption bool
expectValidationError string
}{
"no changes": {
oldObj: podGroup,
@ -290,69 +353,104 @@ func TestStrategyUpdate(t *testing.T) {
expectValidationError: fieldImmutableError,
},
"changing scheduling constraints not allowed": {
oldObj: podGroupWithSchedulingConstraints,
newObj: func() *scheduling.PodGroup {
newPodGroup := podGroupWithSchedulingConstraints.DeepCopy()
newPodGroup.Spec.SchedulingConstraints = &scheduling.PodGroupSchedulingConstraints{}
return newPodGroup
}(),
expectValidationError: fieldImmutableError,
tasEnabled: true,
oldObj: podGroupWithSchedulingConstraints("foo"),
newObj: podGroupWithSchedulingConstraints(),
enableTopologyAwareScheduling: true,
expectValidationError: fieldImmutableError,
},
"changing topology constraints not allowed": {
oldObj: podGroupWithSchedulingConstraints,
newObj: func() *scheduling.PodGroup {
newPodGroup := podGroupWithSchedulingConstraints.DeepCopy()
newPodGroup.Spec.SchedulingConstraints.Topology = []scheduling.TopologyConstraint{}
return newPodGroup
}(),
expectValidationError: fieldImmutableError,
tasEnabled: true,
oldObj: podGroupWithSchedulingConstraints("foo"),
newObj: podGroupWithSchedulingConstraints(),
enableTopologyAwareScheduling: true,
expectValidationError: fieldImmutableError,
},
"changing topology key not allowed": {
oldObj: podGroupWithSchedulingConstraints,
newObj: func() *scheduling.PodGroup {
newPodGroup := podGroupWithSchedulingConstraints.DeepCopy()
newPodGroup.Spec.SchedulingConstraints.Topology[0].Key = "foobar"
return newPodGroup
}(),
expectValidationError: fieldImmutableError,
tasEnabled: true,
oldObj: podGroupWithSchedulingConstraints("foo"),
newObj: podGroupWithSchedulingConstraints("foobar"),
enableTopologyAwareScheduling: true,
expectValidationError: fieldImmutableError,
},
"changing scheduling constraints not allowed with TAS disabled": {
oldObj: podGroupWithSchedulingConstraints,
newObj: func() *scheduling.PodGroup {
newPodGroup := podGroupWithSchedulingConstraints.DeepCopy()
newPodGroup.Spec.SchedulingConstraints = &scheduling.PodGroupSchedulingConstraints{}
return newPodGroup
}(),
expectValidationError: fieldImmutableError,
oldObj: podGroupWithSchedulingConstraints("foo"),
newObj: podGroupWithSchedulingConstraints(),
expectValidationError: forbiddenError,
},
"changing topology constraints not allowed with TAS disabled": {
oldObj: podGroupWithSchedulingConstraints,
newObj: func() *scheduling.PodGroup {
newPodGroup := podGroupWithSchedulingConstraints.DeepCopy()
newPodGroup.Spec.SchedulingConstraints.Topology = []scheduling.TopologyConstraint{}
return newPodGroup
}(),
expectValidationError: fieldImmutableError,
oldObj: podGroupWithSchedulingConstraints("foo"),
newObj: podGroupWithSchedulingConstraints(),
expectValidationError: forbiddenError,
},
"changing topology key not allowed with TAS disabled": {
oldObj: podGroupWithSchedulingConstraints,
newObj: func() *scheduling.PodGroup {
newPodGroup := podGroupWithSchedulingConstraints.DeepCopy()
newPodGroup.Spec.SchedulingConstraints.Topology[0].Key = "foobar"
return newPodGroup
oldObj: podGroupWithSchedulingConstraints("foo"),
newObj: podGroupWithSchedulingConstraints("foobar"),
expectValidationError: forbiddenError,
},
"disruption mode update, workload aware preemption disabled": {
oldObj: podGroupWithDisruptionMode(scheduling.DisruptionModePod),
newObj: podGroupWithDisruptionMode(scheduling.DisruptionModePodGroup),
expectValidationError: forbiddenError,
},
"disruption mode update, workload aware preemption enabled": {
oldObj: podGroupWithDisruptionMode(scheduling.DisruptionModePod),
newObj: podGroupWithDisruptionMode(scheduling.DisruptionModePodGroup),
enableWorkloadAwarePreemption: true,
expectValidationError: fieldImmutableError,
},
"priority class name update, workload aware preemption disabled": {
oldObj: func() *scheduling.PodGroup {
pg := podGroupWithDisruptionMode(scheduling.DisruptionModePod)
pg.Spec.PriorityClassName = "low-priority"
return pg
}(),
expectValidationError: fieldImmutableError,
newObj: func() *scheduling.PodGroup {
pg := podGroupWithDisruptionMode(scheduling.DisruptionModePod)
pg.Spec.PriorityClassName = "high-priority"
return pg
}(),
expectValidationError: forbiddenError,
},
"priority class name update, workload aware preemption enabled": {
oldObj: podGroupWithDisruptionMode(scheduling.DisruptionModePod),
newObj: func() *scheduling.PodGroup {
pg := podGroupWithDisruptionMode(scheduling.DisruptionModePod)
pg.Spec.PriorityClassName = "high-priority"
return pg
}(),
enableWorkloadAwarePreemption: true,
expectValidationError: fieldImmutableError,
},
"priority update, workload aware preemption disabled": {
oldObj: func() *scheduling.PodGroup {
pg := podGroupWithDisruptionMode(scheduling.DisruptionModePod)
pg.Spec.Priority = new(int32(1000))
return pg
}(),
newObj: func() *scheduling.PodGroup {
pg := podGroupWithDisruptionMode(scheduling.DisruptionModePod)
pg.Spec.Priority = new(int32(2000))
return pg
}(),
expectValidationError: forbiddenError,
},
"priority update, workload aware preemption enabled": {
oldObj: podGroupWithDisruptionMode(scheduling.DisruptionModePod),
newObj: func() *scheduling.PodGroup {
pg := podGroupWithDisruptionMode(scheduling.DisruptionModePod)
pg.Spec.Priority = new(int32(2000))
return pg
}(),
enableWorkloadAwarePreemption: true,
expectValidationError: fieldImmutableError,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
features.GenericWorkload: tc.tasEnabled,
features.TopologyAwareWorkloadScheduling: tc.tasEnabled,
features.GenericWorkload: true,
features.TopologyAwareWorkloadScheduling: tc.enableTopologyAwareScheduling,
features.GangScheduling: tc.enableWorkloadAwarePreemption,
features.WorkloadAwarePreemption: tc.enableWorkloadAwarePreemption,
})
podGroup := tc.oldObj.DeepCopy()
newPodGroup := tc.newObj.DeepCopy()
@ -478,7 +576,9 @@ func TestStatusStrategyUpdate(t *testing.T) {
expectObj := tc.expectObj.DeepCopy()
expectObj.ResourceVersion = "4"
assert.Equal(t, expectObj, newObj)
if diff := cmp.Diff(expectObj, newObj); diff != "" {
t.Errorf("PodGroup mismatch (-want +got):\n%s", diff)
}
})
}
}

View file

@ -23,6 +23,7 @@ import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apimachinery/pkg/util/version"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
@ -36,6 +37,11 @@ import (
_ "k8s.io/kubernetes/pkg/apis/scheduling/install"
)
var allowedDisruptionModes = sets.New(
scheduling.DisruptionModePod,
scheduling.DisruptionModePodGroup,
)
func TestDeclarativeValidate(t *testing.T) {
apiVersions := []string{"v1alpha2"} // Workload is currently only in v1alpha2
for _, apiVersion := range apiVersions {
@ -55,9 +61,11 @@ func testDeclarativeValidate(t *testing.T, apiVersion string) {
})
testCases := map[string]struct {
input scheduling.Workload
expectedErrs field.ErrorList
tasEnabled bool
input scheduling.Workload
enableTopologyAwareScheduling bool
enableDRAWorkloadResourceClaims bool
enableWorkloadAwarePreemption bool
expectedErrs field.ErrorList
}{
"valid": {
input: mkValidWorkload(),
@ -142,50 +150,122 @@ func testDeclarativeValidate(t *testing.T, apiVersion string) {
input: mkValidWorkload(setBasicPolicy(0)),
},
"valid with schedulingConstraints": {
input: mkValidWorkload(addTopologyConstraint(0, "foo")),
tasEnabled: true,
input: mkValidWorkload(addTopologyConstraint(0, "foo")),
enableTopologyAwareScheduling: true,
},
"valid with empty schedulingConstraints": {
input: mkValidWorkload(setSchedulingConstraints(0)),
tasEnabled: true,
input: mkValidWorkload(setSchedulingConstraints(0)),
enableTopologyAwareScheduling: true,
},
"with multiple topology constraints": {
input: mkValidWorkload(addTopologyConstraint(0, "foo"), addTopologyConstraint(0, "bar")),
expectedErrs: field.ErrorList{field.TooMany(field.NewPath("spec", "podGroupTemplates").Index(0).Child("schedulingConstraints", "topology"), 2, 1).WithOrigin("maxItems")},
tasEnabled: true,
input: mkValidWorkload(addTopologyConstraint(0, "foo"), addTopologyConstraint(0, "bar")),
enableTopologyAwareScheduling: true,
expectedErrs: field.ErrorList{field.TooMany(field.NewPath("spec", "podGroupTemplates").Index(0).Child("schedulingConstraints", "topology"), 2, 1).WithOrigin("maxItems")},
},
"with empty topology key": {
input: mkValidWorkload(addTopologyConstraint(0, "")),
expectedErrs: field.ErrorList{field.Required(field.NewPath("spec", "podGroupTemplates").Index(0).Child("schedulingConstraints", "topology").Index(0).Child("key"), "")},
tasEnabled: true,
input: mkValidWorkload(addTopologyConstraint(0, "")),
enableTopologyAwareScheduling: true,
expectedErrs: field.ErrorList{field.Required(field.NewPath("spec", "podGroupTemplates").Index(0).Child("schedulingConstraints", "topology").Index(0).Child("key"), "")},
},
"valid with topology key with DNS prefix": {
input: mkValidWorkload(addTopologyConstraint(0, "example.com/Foo")),
tasEnabled: true,
input: mkValidWorkload(addTopologyConstraint(0, "example.com/Foo")),
enableTopologyAwareScheduling: true,
},
"valid with topology key with prefix with max length": {
input: mkValidWorkload(addTopologyConstraint(0, strings.Repeat("a", 253)+"/"+strings.Repeat("b", 63))),
tasEnabled: true,
input: mkValidWorkload(addTopologyConstraint(0, strings.Repeat("a", 253)+"/"+strings.Repeat("b", 63))),
enableTopologyAwareScheduling: true,
},
"with topology key with prefix exceending max prefix length": {
input: mkValidWorkload(addTopologyConstraint(0, strings.Repeat("a", 254)+"/foo")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("schedulingConstraints", "topology").Index(0).Child("key"), nil, "").WithOrigin("format=k8s-label-key")},
tasEnabled: true,
input: mkValidWorkload(addTopologyConstraint(0, strings.Repeat("a", 254)+"/foo")),
enableTopologyAwareScheduling: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("schedulingConstraints", "topology").Index(0).Child("key"), nil, "").WithOrigin("format=k8s-label-key")},
},
"with topology key with prefix exceending max name length": {
input: mkValidWorkload(addTopologyConstraint(0, "foo/"+strings.Repeat("b", 64))),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("schedulingConstraints", "topology").Index(0).Child("key"), nil, "").WithOrigin("format=k8s-label-key")},
tasEnabled: true,
input: mkValidWorkload(addTopologyConstraint(0, "foo/"+strings.Repeat("b", 64))),
enableTopologyAwareScheduling: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("schedulingConstraints", "topology").Index(0).Child("key"), nil, "").WithOrigin("format=k8s-label-key")},
},
"with topology key without prefix exceeding max length": {
input: mkValidWorkload(addTopologyConstraint(0, strings.Repeat("b", 64))),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("schedulingConstraints", "topology").Index(0).Child("key"), nil, "").WithOrigin("format=k8s-label-key")},
tasEnabled: true,
input: mkValidWorkload(addTopologyConstraint(0, strings.Repeat("b", 64))),
enableTopologyAwareScheduling: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("schedulingConstraints", "topology").Index(0).Child("key"), nil, "").WithOrigin("format=k8s-label-key")},
},
"with topology key with invalid characters": {
input: mkValidWorkload(addTopologyConstraint(0, "Example.com/Foo")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("schedulingConstraints", "topology").Index(0).Child("key"), nil, "").WithOrigin("format=k8s-label-key")},
tasEnabled: true,
input: mkValidWorkload(addTopologyConstraint(0, "Example.com/Foo")),
enableTopologyAwareScheduling: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("schedulingConstraints", "topology").Index(0).Child("key"), nil, "").WithOrigin("format=k8s-label-key")},
},
"pod disruption mode, workload aware preemption enabled": {
input: mkValidWorkload(setDisruptionMode(0, podDisruptionMode)),
enableWorkloadAwarePreemption: true,
},
"pod disruption mode, workload aware preemption disabled": {
input: mkValidWorkload(setDisruptionMode(0, podDisruptionMode)),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "podGroupTemplates").Index(0).Child("disruptionMode"), "")},
},
"pod group disruption mode, workload aware preemption enabled": {
input: mkValidWorkload(setDisruptionMode(0, podGroupDisruptionMode)),
enableWorkloadAwarePreemption: true,
},
"pod group disruption mode, workload aware preemption disabled": {
input: mkValidWorkload(setDisruptionMode(0, podGroupDisruptionMode)),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "podGroupTemplates").Index(0).Child("disruptionMode"), "")},
},
"invalid disruption mode, workload aware preemption enabled": {
input: mkValidWorkload(setDisruptionMode(0, invalidDisruptionMode)),
enableWorkloadAwarePreemption: true,
expectedErrs: field.ErrorList{field.NotSupported(field.NewPath("spec", "podGroupTemplates").Index(0).Child("disruptionMode"), invalidDisruptionMode, sets.List(allowedDisruptionModes))},
},
"invalid disruption mode, workload aware preemption disabled": {
input: mkValidWorkload(setDisruptionMode(0, invalidDisruptionMode)),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "podGroupTemplates").Index(0).Child("disruptionMode"), "")},
},
"valid priorityClassName, workload aware preemption enabled": {
input: mkValidWorkload(setPriorityClassName(0, "high-priority")),
enableWorkloadAwarePreemption: true,
},
"valid priorityClassName, workload aware preemption disabled": {
input: mkValidWorkload(setPriorityClassName(0, "high-priority")),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "podGroupTemplates").Index(0).Child("priorityClassName"), "")},
},
"invalid priorityClassName, workload aware preemption enabled": {
input: mkValidWorkload(setPriorityClassName(0, "high/priority")),
enableWorkloadAwarePreemption: true,
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("priorityClassName"), nil, "").WithOrigin("format=k8s-long-name"),
},
},
"invalid priorityClassName, workload aware preemption disabled": {
input: mkValidWorkload(setPriorityClassName(0, "high/priority")),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "podGroupTemplates").Index(0).Child("priorityClassName"), "")},
},
"valid priority, workload aware preemption enabled": {
input: mkValidWorkload(setPriority(0, 1000)),
enableWorkloadAwarePreemption: true,
},
"valid priority, workload aware preemption disabled": {
input: mkValidWorkload(setPriority(0, 1000)),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "podGroupTemplates").Index(0).Child("priority"), "")},
},
"valid negative priority, workload aware preemption enabled": {
input: mkValidWorkload(setPriority(0, -2147483648)),
enableWorkloadAwarePreemption: true,
},
"valid negative priority, workload aware preemption disabled": {
input: mkValidWorkload(setPriority(0, -2147483648)),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "podGroupTemplates").Index(0).Child("priority"), "")},
},
"too high priority, workload aware preemption enabled": {
input: mkValidWorkload(setPriority(0, scheduling.HighestUserDefinablePriority+1)),
enableWorkloadAwarePreemption: true,
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("priority"), nil, "").WithOrigin("maximum"),
},
},
"too high priority, workload aware preemption disabled": {
input: mkValidWorkload(setPriority(0, scheduling.HighestUserDefinablePriority+1)),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "podGroupTemplates").Index(0).Child("priority"), "")},
},
"ok resourceClaimName reference": {
input: mkValidWorkload(addResourceClaims(scheduling.PodGroupResourceClaim{Name: "claim", ResourceClaimName: new("resource-claim")})),
@ -273,8 +353,11 @@ func testDeclarativeValidate(t *testing.T, apiVersion string) {
for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
features.GenericWorkload: tc.tasEnabled,
features.TopologyAwareWorkloadScheduling: tc.tasEnabled,
features.GenericWorkload: true,
features.TopologyAwareWorkloadScheduling: tc.enableTopologyAwareScheduling,
features.DRAWorkloadResourceClaims: tc.enableDRAWorkloadResourceClaims,
features.GangScheduling: tc.enableWorkloadAwarePreemption,
features.WorkloadAwarePreemption: tc.enableWorkloadAwarePreemption,
})
apitesting.VerifyValidationEquivalence(t, ctx, &tc.input, Strategy.Validate, tc.expectedErrs, apitesting.WithMinEmulationVersion(version.MustParse("1.36")))
})
@ -292,10 +375,12 @@ func TestDeclarativeValidateUpdate(t *testing.T) {
func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) {
testCases := map[string]struct {
oldObj scheduling.Workload
updateObj scheduling.Workload
tasEnabled bool
expectedErrs field.ErrorList
oldObj scheduling.Workload
updateObj scheduling.Workload
enableTopologyAwareScheduling bool
enableDRAWorkloadResourceClaims bool
enableWorkloadAwarePreemption bool
expectedErrs field.ErrorList
}{
"valid update": {
oldObj: mkValidWorkload(setResourceVersion("1")),
@ -308,24 +393,31 @@ func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) {
"set controllerRef": {
oldObj: mkValidWorkload(setResourceVersion("1")),
updateObj: mkValidWorkload(setResourceVersion("1"), setControllerRef("apps", "Deployment", "different-deployment")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "controllerRef"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha()},
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "controllerRef"), nil, "").WithOrigin("immutable")},
},
"invalid update controllerRef": {
oldObj: mkValidWorkload(setResourceVersion("1"), setControllerRef("apps", "Deployment", "my-deployment")),
updateObj: mkValidWorkload(setResourceVersion("1"), setControllerRef("apps", "Deployment", "different-deployment")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "controllerRef"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha()},
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "controllerRef"), nil, "").WithOrigin("immutable")},
},
"unset controllerRef": {
oldObj: mkValidWorkload(setResourceVersion("1"), setControllerRef("apps", "Deployment", "different-deployment")),
updateObj: mkValidWorkload(setResourceVersion("1")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "controllerRef"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha()},
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "controllerRef"), nil, "").WithOrigin("immutable")},
},
"invalid update empty podGroupTemplates": {
oldObj: mkValidWorkload(setResourceVersion("1")),
updateObj: mkValidWorkload(setResourceVersion("1"), setEmptyPodGroupTemplates()),
expectedErrs: field.ErrorList{
field.Required(field.NewPath("spec", "podGroupTemplates"), "must have at least one item"),
field.Invalid(field.NewPath("spec", "podGroupTemplates"), []scheduling.PodGroupTemplate{}, "field is immutable").WithOrigin("immutable").MarkAlpha(),
field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable"),
},
},
"change podGroupTemplate name": {
oldObj: mkValidWorkload(setResourceVersion("1"), addPodGroupTemplate("worker1")),
updateObj: mkValidWorkload(setResourceVersion("1"), addPodGroupTemplate("worker2")),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable"),
},
},
"invalid update too many podGroupTemplates": {
@ -333,55 +425,65 @@ func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) {
updateObj: mkValidWorkload(setResourceVersion("1"), setManyPodGroupTemplates(scheduling.WorkloadMaxPodGroupTemplates+1)),
expectedErrs: field.ErrorList{
field.TooMany(field.NewPath("spec", "podGroupTemplates"), scheduling.WorkloadMaxPodGroupTemplates+1, scheduling.WorkloadMaxPodGroupTemplates).WithOrigin("maxItems"),
field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha(),
field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable"),
},
},
"invalid update podGroupTemplate": {
"add podGroupTemplate": {
oldObj: mkValidWorkload(setResourceVersion("1")),
updateObj: mkValidWorkload(setResourceVersion("1"), addPodGroupTemplate("worker1")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha()},
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable")},
},
"remove podGroupTemplate": {
oldObj: mkValidWorkload(setResourceVersion("1"), addPodGroupTemplate("worker1")),
updateObj: mkValidWorkload(setResourceVersion("1")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable")},
},
"invalid update with neither basic nor gang": {
oldObj: mkValidWorkload(setResourceVersion("1")),
updateObj: mkValidWorkload(setResourceVersion("1"), clearPodGroupPolicy(0)),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha(),
field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable"),
},
},
"invalid update with both basic and gang": {
oldObj: mkValidWorkload(setResourceVersion("1")),
updateObj: mkValidWorkload(setResourceVersion("1"), setBothPolicies(0)),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha(),
field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable"),
},
},
"invalid update of gang minCount": {
oldObj: mkValidWorkload(setResourceVersion("1")),
updateObj: mkValidWorkload(setResourceVersion("1"), setPodGroupMinCount(0, 10)),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable")},
},
"valid update from gang to basic policy": {
oldObj: mkValidWorkload(setResourceVersion("1")),
updateObj: mkValidWorkload(setResourceVersion("1"), setBasicPolicy(0)),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha()},
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable")},
},
"valid update with unchanged scheduling constraints": {
oldObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "foo")),
updateObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "foo")),
tasEnabled: true,
oldObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "foo")),
updateObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "foo")),
enableTopologyAwareScheduling: true,
},
"invalid update to scheduling constraints": {
oldObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "foo")),
updateObj: mkValidWorkload(setResourceVersion("1"), setSchedulingConstraints(0)),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha()},
tasEnabled: true,
oldObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "foo")),
updateObj: mkValidWorkload(setResourceVersion("1"), setSchedulingConstraints(0)),
enableTopologyAwareScheduling: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "field is immutable").WithOrigin("immutable")},
},
"invalid update to topology constraints": {
oldObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "foo")),
updateObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "foo"), addTopologyConstraint(0, "bar")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha()},
tasEnabled: true,
oldObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "foo")),
updateObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "foo"), addTopologyConstraint(0, "bar")),
enableTopologyAwareScheduling: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "field is immutable").WithOrigin("immutable")},
},
"invalid update to topology key": {
oldObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "foo")),
updateObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "bar")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha()},
tasEnabled: true,
oldObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "foo")),
updateObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "bar")),
enableTopologyAwareScheduling: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "field is immutable").WithOrigin("immutable")},
},
"valid update with unchanged scheduling constraints with TAS disabled": {
oldObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "foo")),
@ -390,24 +492,109 @@ func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) {
"invalid update to scheduling constraints with TAS disabled": {
oldObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "foo")),
updateObj: mkValidWorkload(setResourceVersion("1"), setSchedulingConstraints(0)),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha()},
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "field is immutable").WithOrigin("immutable")},
},
"invalid update to topology constraints with TAS disabled": {
oldObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "foo")),
updateObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "foo"), addTopologyConstraint(0, "bar")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha()},
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "field is immutable").WithOrigin("immutable")},
},
"invalid update to topology key with TAS disabled": {
oldObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "foo")),
updateObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "bar")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "field is immutable").WithOrigin("immutable").MarkAlpha()},
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "field is immutable").WithOrigin("immutable")},
},
"invalid add of resource claims, DRA workload resource claims disabled": {
oldObj: mkValidWorkload(setResourceVersion("1")),
updateObj: mkValidWorkload(setResourceVersion("1"), addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-claim", ResourceClaimTemplateName: new("my-template")},
)),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable")},
},
"invalid add of resource claims, DRA workload resource claims enabled": {
oldObj: mkValidWorkload(setResourceVersion("1")),
updateObj: mkValidWorkload(setResourceVersion("1"), addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-claim", ResourceClaimTemplateName: new("my-template")},
)),
enableDRAWorkloadResourceClaims: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable")},
},
"invalid update of resource claims, DRA workload resource claims disabled": {
oldObj: mkValidWorkload(setResourceVersion("1"), addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-claim", ResourceClaimTemplateName: new("my-template")},
)),
updateObj: mkValidWorkload(setResourceVersion("1"), addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-other-claim", ResourceClaimTemplateName: new("my-template")},
)),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable")},
},
"invalid update of resource claims, DRA workload resource claims enabled": {
oldObj: mkValidWorkload(setResourceVersion("1"), addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-claim", ResourceClaimTemplateName: new("my-template")},
)),
updateObj: mkValidWorkload(setResourceVersion("1"), addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-other-claim", ResourceClaimTemplateName: new("my-template")},
)),
enableDRAWorkloadResourceClaims: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable")},
},
"invalid remove of resource claims, DRA workload resource claims disabled": {
oldObj: mkValidWorkload(setResourceVersion("1"), addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-claim", ResourceClaimTemplateName: new("my-template")},
)),
updateObj: mkValidWorkload(setResourceVersion("1")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable")},
},
"invalid remove of resource claims, DRA workload resource claims enabled": {
oldObj: mkValidWorkload(setResourceVersion("1"), addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-claim", ResourceClaimTemplateName: new("my-template")},
)),
updateObj: mkValidWorkload(setResourceVersion("1")),
enableDRAWorkloadResourceClaims: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable")},
},
"invalid update of disruption mode, workload aware preemption enabled": {
oldObj: mkValidWorkload(setResourceVersion("1"), setDisruptionMode(0, podDisruptionMode)),
updateObj: mkValidWorkload(setResourceVersion("1"), setDisruptionMode(0, podGroupDisruptionMode)),
enableWorkloadAwarePreemption: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable")},
},
"invalid update of disruption mode, workload aware preemption disabled": {
oldObj: mkValidWorkload(setResourceVersion("1"), setDisruptionMode(0, podDisruptionMode)),
updateObj: mkValidWorkload(setResourceVersion("1"), setDisruptionMode(0, podGroupDisruptionMode)),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable")},
},
"invalid update of priority class name, workload aware preemption enabled": {
oldObj: mkValidWorkload(setResourceVersion("1"), setPriorityClassName(0, "low-priority")),
updateObj: mkValidWorkload(setResourceVersion("1"), setPriorityClassName(0, "high-priority")),
enableWorkloadAwarePreemption: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable")},
},
"invalid update of priority class name, workload aware preemption disabled": {
oldObj: mkValidWorkload(setResourceVersion("1"), setPriorityClassName(0, "low-priority")),
updateObj: mkValidWorkload(setResourceVersion("1"), setPriorityClassName(0, "high-priority")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable")},
},
"invalid update of priority, workload aware preemption enabled": {
oldObj: mkValidWorkload(setResourceVersion("1"), setPriority(0, 1000)),
updateObj: mkValidWorkload(setResourceVersion("1"), setPriority(0, 2000)),
enableWorkloadAwarePreemption: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable")},
},
"invalid update of priority, workload aware preemption disabled": {
oldObj: mkValidWorkload(setResourceVersion("1"), setPriority(0, 1000)),
updateObj: mkValidWorkload(setResourceVersion("1"), setPriority(0, 2000)),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates"), nil, "").WithOrigin("immutable")},
},
}
for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
features.GenericWorkload: tc.tasEnabled,
features.TopologyAwareWorkloadScheduling: tc.tasEnabled,
features.GenericWorkload: true,
features.TopologyAwareWorkloadScheduling: tc.enableTopologyAwareScheduling,
features.DRAWorkloadResourceClaims: tc.enableDRAWorkloadResourceClaims,
features.GangScheduling: tc.enableWorkloadAwarePreemption,
features.WorkloadAwarePreemption: tc.enableWorkloadAwarePreemption,
})
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIPrefix: "apis",
@ -561,3 +748,21 @@ func addResourceClaims(claims ...scheduling.PodGroupResourceClaim) func(obj *sch
obj.Spec.PodGroupTemplates[0].ResourceClaims = append(obj.Spec.PodGroupTemplates[0].ResourceClaims, claims...)
}
}
func setDisruptionMode(pgIdx int, mode scheduling.DisruptionMode) func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.Spec.PodGroupTemplates[pgIdx].DisruptionMode = &mode
}
}
func setPriorityClassName(pgIdx int, priorityClassName string) func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.Spec.PodGroupTemplates[pgIdx].PriorityClassName = priorityClassName
}
}
func setPriority(pgIdx int, priority int32) func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.Spec.PodGroupTemplates[pgIdx].Priority = new(priority)
}
}

View file

@ -52,9 +52,15 @@ func (workloadStrategy) Validate(ctx context.Context, obj runtime.Object) field.
workloadScheduling := obj.(*scheduling.Workload)
allErrs := validation.ValidateWorkload(workloadScheduling)
opts := []string{}
if utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareWorkloadScheduling) || anySchedulingConstraintsInUse(nil) {
if utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareWorkloadScheduling) {
opts = append(opts, string(features.TopologyAwareWorkloadScheduling))
}
if utilfeature.DefaultFeatureGate.Enabled(features.DRAWorkloadResourceClaims) {
opts = append(opts, string(features.DRAWorkloadResourceClaims))
}
if utilfeature.DefaultFeatureGate.Enabled(features.WorkloadAwarePreemption) {
opts = append(opts, string(features.WorkloadAwarePreemption))
}
return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, obj, nil, allErrs, operation.Create, rest.WithDeclarativeEnforcement(), rest.WithOptions(opts))
}
@ -75,9 +81,19 @@ func (workloadStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.O
func (workloadStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
allErrs := validation.ValidateWorkloadUpdate(obj.(*scheduling.Workload), old.(*scheduling.Workload))
opts := []string{}
if utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareWorkloadScheduling) || anySchedulingConstraintsInUse(old.(*scheduling.Workload)) {
// Declarative validation will always allow fields to remain unchanged, so if any
// of the fields which are covered by these gates are set, we will not re-validate them
// (even if the gates are disabled) as long as they do not change values. If a gate
// is disabled, they will not be allowed to change values.
if utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareWorkloadScheduling) {
opts = append(opts, string(features.TopologyAwareWorkloadScheduling))
}
if utilfeature.DefaultFeatureGate.Enabled(features.DRAWorkloadResourceClaims) {
opts = append(opts, string(features.DRAWorkloadResourceClaims))
}
if utilfeature.DefaultFeatureGate.Enabled(features.WorkloadAwarePreemption) {
opts = append(opts, string(features.WorkloadAwarePreemption))
}
return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, obj, old, allErrs, operation.Update, rest.WithDeclarativeEnforcement(), rest.WithOptions(opts))
}
@ -122,6 +138,9 @@ func dropDisabledPodGroupTemplatesFields(templates, oldTemplates []scheduling.Po
template := &templates[i]
dropDisabledSchedulingConstraintsFields(template, oldTemplate)
dropDisabledDRAWorkloadResourceClaimsFields(template, oldTemplate)
dropDisabledDisruptionModeField(template, oldTemplate)
dropDisabledPriorityClassNameField(template, oldTemplate)
dropDisabledPriorityField(template, oldTemplate)
}
}
@ -131,27 +150,9 @@ func dropDisabledSchedulingConstraintsFields(template, oldTemplate *scheduling.P
if utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareWorkloadScheduling) || schedulingConstraintsInUse(oldTemplate) {
return
}
template.SchedulingConstraints = nil
}
func anySchedulingConstraintsInUse(workload *scheduling.Workload) bool {
if workload == nil {
return false
}
for i := range workload.Spec.PodGroupTemplates {
if schedulingConstraintsInUse(&workload.Spec.PodGroupTemplates[i]) {
return true
}
}
return false
}
func schedulingConstraintsInUse(pgt *scheduling.PodGroupTemplate) bool {
return pgt != nil && pgt.SchedulingConstraints != nil
}
// dropDisabledDRAWorkloadResourceClaimsFields removes resource claim references from
// podGroupTemplates unless they are already used by the old Workload spec.
func dropDisabledDRAWorkloadResourceClaimsFields(template, oldTemplate *scheduling.PodGroupTemplate) {
@ -161,6 +162,52 @@ func dropDisabledDRAWorkloadResourceClaimsFields(template, oldTemplate *scheduli
template.ResourceClaims = nil
}
// dropDisabledDisruptionModeField removes the DisruptionMode field from a template
// unless it is already used in the old template.
func dropDisabledDisruptionModeField(template, oldTemplate *scheduling.PodGroupTemplate) {
if utilfeature.DefaultFeatureGate.Enabled(features.WorkloadAwarePreemption) || disruptionModeInUse(oldTemplate) {
// No need to drop anything.
return
}
template.DisruptionMode = nil
}
// dropDisabledPriorityClassNameField removes the PriorityClassName field from a template
// unless it is already used in the old template.
func dropDisabledPriorityClassNameField(template, oldTemplate *scheduling.PodGroupTemplate) {
if utilfeature.DefaultFeatureGate.Enabled(features.WorkloadAwarePreemption) || priorityClassNameInUse(oldTemplate) {
// No need to drop anything.
return
}
template.PriorityClassName = ""
}
// dropDisabledPriorityField removes the Priority field from a template unless it is
// already used in the old template.
func dropDisabledPriorityField(template, oldTemplate *scheduling.PodGroupTemplate) {
if utilfeature.DefaultFeatureGate.Enabled(features.WorkloadAwarePreemption) || priorityInUse(oldTemplate) {
// No need to drop anything.
return
}
template.Priority = nil
}
func schedulingConstraintsInUse(pgt *scheduling.PodGroupTemplate) bool {
return pgt != nil && pgt.SchedulingConstraints != nil
}
func draWorkloadResourceClaimsInUse(pgt *scheduling.PodGroupTemplate) bool {
return pgt != nil && len(pgt.ResourceClaims) > 0
}
func disruptionModeInUse(pgt *scheduling.PodGroupTemplate) bool {
return pgt != nil && pgt.DisruptionMode != nil
}
func priorityClassNameInUse(pgt *scheduling.PodGroupTemplate) bool {
return pgt != nil && pgt.PriorityClassName != ""
}
func priorityInUse(pgt *scheduling.PodGroupTemplate) bool {
return pgt != nil && pgt.Priority != nil
}

View file

@ -30,24 +30,37 @@ import (
"k8s.io/kubernetes/pkg/features"
)
var workload = &scheduling.Workload{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: metav1.NamespaceDefault,
},
Spec: scheduling.WorkloadSpec{
PodGroupTemplates: []scheduling.PodGroupTemplate{
{
Name: "bar",
SchedulingPolicy: scheduling.PodGroupSchedulingPolicy{
Gang: &scheduling.GangSchedulingPolicy{
MinCount: 5,
var (
workload = &scheduling.Workload{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: metav1.NamespaceDefault,
},
Spec: scheduling.WorkloadSpec{
PodGroupTemplates: []scheduling.PodGroupTemplate{
{
Name: "bar",
SchedulingPolicy: scheduling.PodGroupSchedulingPolicy{
Gang: &scheduling.GangSchedulingPolicy{
MinCount: 5,
},
},
},
},
},
},
}
}
podDisruptionMode = scheduling.DisruptionModePod
podGroupDisruptionMode = scheduling.DisruptionModePodGroup
invalidDisruptionMode = scheduling.DisruptionMode("Invalid")
fieldImmutableError = "field is immutable"
minCountError = "must be greater than or equal to 1"
tooManyItemsError = "must have at most 1 item"
requiredError = "Required value"
subdomainNameError = "lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters"
supportedModesError = `supported values: "Pod", "PodGroup"`
)
func TestWorkloadStrategy(t *testing.T) {
if !Strategy.NamespaceScoped() {
@ -67,38 +80,28 @@ func ctxWithRequestInfo() context.Context {
})
}
func TestPodSchedulingStrategyCreate(t *testing.T) {
t.Run("simple", func(t *testing.T) {
ctx := ctxWithRequestInfo()
workload := workload.DeepCopy()
func TestStrategyCreate(t *testing.T) {
ctx := ctxWithRequestInfo()
Strategy.PrepareForCreate(ctx, workload)
errs := Strategy.Validate(ctx, workload)
if len(errs) != 0 {
t.Errorf("Unexpected validation error: %v", errs)
}
})
t.Run("failed validation", func(t *testing.T) {
ctx := ctxWithRequestInfo()
workload := workload.DeepCopy()
workload.Spec.PodGroupTemplates[0].SchedulingPolicy.Gang.MinCount = -1
Strategy.PrepareForCreate(ctx, workload)
errs := Strategy.Validate(ctx, workload)
if len(errs) == 0 {
t.Errorf("Expected validation error")
}
})
}
func TestPodSchedulingStrategyCreate_SchedulingConstraints(t *testing.T) {
testCases := map[string]struct {
obj *scheduling.Workload
expectObj *scheduling.Workload
expectValidationError string
tasEnabled bool
obj *scheduling.Workload
expectObj *scheduling.Workload
enableTopologyAwareScheduling bool
enableWorkloadAwarePreemption bool
expectValidationError string
}{
"simple": {
obj: workload,
expectObj: workload,
},
"failed validation": {
obj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.PodGroupTemplates[0].SchedulingPolicy.Gang.MinCount = -1
return w
}(),
expectValidationError: minCountError,
},
"drops field with SchedulingConstraints set and TAS disabled": {
obj: func() *scheduling.Workload {
workload := workload.DeepCopy()
@ -107,8 +110,7 @@ func TestPodSchedulingStrategyCreate_SchedulingConstraints(t *testing.T) {
}
return workload
}(),
expectObj: workload,
tasEnabled: false,
expectObj: workload,
},
"valid with SchedulingConstraints set and TAS enabled": {
obj: func() *scheduling.Workload {
@ -125,7 +127,7 @@ func TestPodSchedulingStrategyCreate_SchedulingConstraints(t *testing.T) {
}
return workload
}(),
tasEnabled: true,
enableTopologyAwareScheduling: true,
},
"invalid with multiple topology constraints": {
obj: func() *scheduling.Workload {
@ -138,8 +140,8 @@ func TestPodSchedulingStrategyCreate_SchedulingConstraints(t *testing.T) {
}
return workload
}(),
expectValidationError: "must have at most 1 item",
tasEnabled: true,
enableTopologyAwareScheduling: true,
expectValidationError: tooManyItemsError,
},
"invalid with invalid topology key": {
obj: func() *scheduling.Workload {
@ -151,20 +153,95 @@ func TestPodSchedulingStrategyCreate_SchedulingConstraints(t *testing.T) {
}
return workload
}(),
expectValidationError: "Required value",
tasEnabled: true,
enableTopologyAwareScheduling: true,
expectValidationError: requiredError,
},
"workload aware preemption disabled - drop disruption mode": {
obj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.PodGroupTemplates[0].DisruptionMode = &podDisruptionMode
return w
}(),
expectObj: workload,
},
"workload aware preemption enabled - preserve disruption mode (pod)": {
obj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.PodGroupTemplates[0].DisruptionMode = &podDisruptionMode
return w
}(),
enableWorkloadAwarePreemption: true,
expectObj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.PodGroupTemplates[0].DisruptionMode = &podDisruptionMode
return w
}(),
},
"workload aware preemption enabled - preserve disruption mode (pod group)": {
obj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.PodGroupTemplates[0].DisruptionMode = &podGroupDisruptionMode
return w
}(),
enableWorkloadAwarePreemption: true,
expectObj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.PodGroupTemplates[0].DisruptionMode = &podGroupDisruptionMode
return w
}(),
},
"workload aware preemption enabled - unknown disruption mode": {
obj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.PodGroupTemplates[0].DisruptionMode = &invalidDisruptionMode
return w
}(),
enableWorkloadAwarePreemption: true,
expectValidationError: supportedModesError,
},
"workload aware preemption enabled - preserve priorityClassName": {
obj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.PodGroupTemplates[0].PriorityClassName = "high-priority"
return w
}(),
enableWorkloadAwarePreemption: true,
expectObj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.PodGroupTemplates[0].PriorityClassName = "high-priority"
return w
}(),
},
"workload aware preemption disabled - drop priorityClassName": {
obj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.PodGroupTemplates[0].PriorityClassName = "high-priority"
return w
}(),
expectObj: workload,
},
"workload aware preemption enabled - invalid priorityClassName": {
obj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.PodGroupTemplates[0].PriorityClassName = "invalid/priority/class/name"
return w
}(),
enableWorkloadAwarePreemption: true,
expectValidationError: subdomainNameError,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
features.GenericWorkload: tc.tasEnabled,
features.TopologyAwareWorkloadScheduling: tc.tasEnabled,
})
ctx := ctxWithRequestInfo()
workload := tc.obj.DeepCopy()
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
features.GenericWorkload: true,
features.TopologyAwareWorkloadScheduling: tc.enableTopologyAwareScheduling,
features.GangScheduling: tc.enableWorkloadAwarePreemption,
features.WorkloadAwarePreemption: tc.enableWorkloadAwarePreemption,
})
Strategy.PrepareForCreate(ctx, workload)
if errs := Strategy.Validate(ctx, workload); len(errs) != 0 {
if tc.expectValidationError == "" {
@ -186,74 +263,52 @@ func TestPodSchedulingStrategyCreate_SchedulingConstraints(t *testing.T) {
}
}
func TestPodSchedulingStrategyUpdate(t *testing.T) {
t.Run("no changes", func(t *testing.T) {
ctx := ctxWithRequestInfo()
workload := workload.DeepCopy()
newWorkload := workload.DeepCopy()
newWorkload.ResourceVersion = "4"
func TestStrategyUpdate(t *testing.T) {
ctx := ctxWithRequestInfo()
Strategy.PrepareForUpdate(ctx, newWorkload, workload)
errs := Strategy.ValidateUpdate(ctx, newWorkload, workload)
if len(errs) != 0 {
t.Errorf("Unexpected validation error: %v", errs)
}
})
t.Run("name update", func(t *testing.T) {
ctx := ctxWithRequestInfo()
workload := workload.DeepCopy()
newWorkload := workload.DeepCopy()
newWorkload.Name += "bar"
newWorkload.ResourceVersion = "4"
Strategy.PrepareForUpdate(ctx, newWorkload, workload)
errs := Strategy.ValidateUpdate(ctx, newWorkload, workload)
if len(errs) == 0 {
t.Errorf("Expected validation error")
}
})
t.Run("invalid spec update - controllerRef", func(t *testing.T) {
ctx := ctxWithRequestInfo()
workload := workload.DeepCopy()
newWorkload := workload.DeepCopy()
newWorkload.Spec.ControllerRef = &scheduling.TypedLocalObjectReference{
Kind: "foo",
Name: "baz",
}
newWorkload.ResourceVersion = "4"
Strategy.PrepareForUpdate(ctx, newWorkload, workload)
errs := Strategy.ValidateUpdate(ctx, newWorkload, workload)
if len(errs) == 0 {
t.Errorf("Expected validation error")
}
})
t.Run("invalid spec update - podGroupTemplates", func(t *testing.T) {
ctx := ctxWithRequestInfo()
workload := workload.DeepCopy()
newWorkload := workload.DeepCopy()
newWorkload.Spec.PodGroupTemplates[0].SchedulingPolicy.Gang.MinCount = 4
newWorkload.ResourceVersion = "4"
Strategy.PrepareForUpdate(ctx, newWorkload, workload)
errs := Strategy.ValidateUpdate(ctx, newWorkload, workload)
if len(errs) == 0 {
t.Errorf("Expected validation error")
}
})
}
func TestPodSchedulingStrategyUpdate_SchedulingConstraints(t *testing.T) {
testCases := map[string]struct {
oldObj *scheduling.Workload
newObj *scheduling.Workload
expectObj *scheduling.Workload
expectValidationError string
tasEnabled bool
oldObj *scheduling.Workload
newObj *scheduling.Workload
enableTopologyAwareScheduling bool
enableWorkloadAwarePreemption bool
expectValidationError string
expectWorkload *scheduling.Workload
}{
"no changes": {
oldObj: workload,
newObj: workload,
expectWorkload: workload,
},
"name update": {
oldObj: workload,
newObj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Name += "bar"
return w
}(),
expectValidationError: fieldImmutableError,
},
"invalid spec update - controllerRef": {
oldObj: workload,
newObj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.ControllerRef = &scheduling.TypedLocalObjectReference{
Kind: "foo",
Name: "baz",
}
return w
}(),
expectValidationError: fieldImmutableError,
},
"invalid spec update - podGroupTemplates": {
oldObj: workload,
newObj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.PodGroupTemplates[0].SchedulingPolicy.Gang.MinCount = 4
return w
}(),
expectValidationError: fieldImmutableError,
},
"valid update with scheduling constraints unchanged and TAS disabled": {
oldObj: func() *scheduling.Workload {
workload := workload.DeepCopy()
@ -271,7 +326,7 @@ func TestPodSchedulingStrategyUpdate_SchedulingConstraints(t *testing.T) {
}
return workload
}(),
expectObj: func() *scheduling.Workload {
expectWorkload: func() *scheduling.Workload {
workload := workload.DeepCopy()
workload.Spec.PodGroupTemplates = append(workload.Spec.PodGroupTemplates, *workload.Spec.PodGroupTemplates[0].DeepCopy())
workload.Spec.PodGroupTemplates[0].SchedulingConstraints = &scheduling.PodGroupSchedulingConstraints{
@ -279,7 +334,6 @@ func TestPodSchedulingStrategyUpdate_SchedulingConstraints(t *testing.T) {
}
return workload
}(),
tasEnabled: false,
},
"valid update with scheduling constraints unchanged and TAS enabled": {
oldObj: func() *scheduling.Workload {
@ -298,7 +352,7 @@ func TestPodSchedulingStrategyUpdate_SchedulingConstraints(t *testing.T) {
}
return workload
}(),
expectObj: func() *scheduling.Workload {
expectWorkload: func() *scheduling.Workload {
workload := workload.DeepCopy()
workload.Spec.PodGroupTemplates = append(workload.Spec.PodGroupTemplates, *workload.Spec.PodGroupTemplates[0].DeepCopy())
workload.Spec.PodGroupTemplates[0].SchedulingConstraints = &scheduling.PodGroupSchedulingConstraints{
@ -306,7 +360,7 @@ func TestPodSchedulingStrategyUpdate_SchedulingConstraints(t *testing.T) {
}
return workload
}(),
tasEnabled: true,
enableTopologyAwareScheduling: true,
},
"changing topology key not allowed with TAS disabled": {
oldObj: func() *scheduling.Workload {
@ -323,8 +377,7 @@ func TestPodSchedulingStrategyUpdate_SchedulingConstraints(t *testing.T) {
}
return workload
}(),
expectValidationError: "field is immutable",
tasEnabled: false,
expectValidationError: fieldImmutableError,
},
"changing topology key not allowed with TAS enabled": {
oldObj: func() *scheduling.Workload {
@ -341,31 +394,19 @@ func TestPodSchedulingStrategyUpdate_SchedulingConstraints(t *testing.T) {
}
return workload
}(),
expectValidationError: "field is immutable",
tasEnabled: true,
enableTopologyAwareScheduling: true,
expectValidationError: fieldImmutableError,
},
"changing topology constraints not allowed with TAS disabled": {
oldObj: func() *scheduling.Workload {
workload := workload.DeepCopy()
workload.Spec.PodGroupTemplates = append(workload.Spec.PodGroupTemplates, *workload.Spec.PodGroupTemplates[0].DeepCopy())
workload.Spec.PodGroupTemplates[0].SchedulingConstraints = &scheduling.PodGroupSchedulingConstraints{
Topology: []scheduling.TopologyConstraint{{Key: "foo"}},
}
return workload
}(),
"topology constraint addition is dropped with TAS disabled": {
oldObj: workload,
newObj: func() *scheduling.Workload {
workload := workload.DeepCopy()
workload.Spec.PodGroupTemplates = append(workload.Spec.PodGroupTemplates, *workload.Spec.PodGroupTemplates[0].DeepCopy())
workload.Spec.PodGroupTemplates[0].SchedulingConstraints = &scheduling.PodGroupSchedulingConstraints{
Topology: []scheduling.TopologyConstraint{{Key: "foo"}},
}
workload.Spec.PodGroupTemplates[1].SchedulingConstraints = &scheduling.PodGroupSchedulingConstraints{
Topology: []scheduling.TopologyConstraint{{Key: "foo"}},
}
return workload
}(),
expectValidationError: "field is immutable",
tasEnabled: false,
expectWorkload: workload,
},
"changing topology constraints not allowed with TAS enabled": {
oldObj: func() *scheduling.Workload {
@ -387,8 +428,8 @@ func TestPodSchedulingStrategyUpdate_SchedulingConstraints(t *testing.T) {
}
return workload
}(),
expectValidationError: "field is immutable",
tasEnabled: true,
enableTopologyAwareScheduling: true,
expectValidationError: fieldImmutableError,
},
"adding scheduling constraints not allowed with TAS disabled": {
oldObj: func() *scheduling.Workload {
@ -409,8 +450,7 @@ func TestPodSchedulingStrategyUpdate_SchedulingConstraints(t *testing.T) {
}
return workload
}(),
expectValidationError: "field is immutable",
tasEnabled: false,
expectValidationError: fieldImmutableError,
},
"adding scheduling constraints not allowed with TAS enabled": {
oldObj: func() *scheduling.Workload {
@ -431,22 +471,72 @@ func TestPodSchedulingStrategyUpdate_SchedulingConstraints(t *testing.T) {
}
return workload
}(),
expectValidationError: "field is immutable",
tasEnabled: true,
enableTopologyAwareScheduling: true,
expectValidationError: fieldImmutableError,
},
"disruption mode update, workload aware preemption disabled": {
oldObj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.PodGroupTemplates[0].DisruptionMode = &podGroupDisruptionMode
return w
}(),
newObj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.PodGroupTemplates[0].DisruptionMode = &podDisruptionMode
return w
}(),
expectValidationError: fieldImmutableError,
},
"disruption mode update, workload aware preemption enabled": {
oldObj: workload,
newObj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.PodGroupTemplates[0].DisruptionMode = &podDisruptionMode
return w
}(),
enableWorkloadAwarePreemption: true,
expectValidationError: fieldImmutableError,
},
"priorityClassName update, workload aware preemption disabled": {
oldObj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.PodGroupTemplates[0].PriorityClassName = "high-priority"
return w
}(),
newObj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.PodGroupTemplates[0].PriorityClassName = "low-priority"
return w
}(),
expectValidationError: fieldImmutableError,
},
"priorityClassName update, workload aware preemption enabled": {
oldObj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.PodGroupTemplates[0].PriorityClassName = "high-priority"
return w
}(),
newObj: func() *scheduling.Workload {
w := workload.DeepCopy()
w.Spec.PodGroupTemplates[0].PriorityClassName = "low-priority"
return w
}(),
enableWorkloadAwarePreemption: true,
expectValidationError: fieldImmutableError,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
features.GenericWorkload: tc.tasEnabled,
features.TopologyAwareWorkloadScheduling: tc.tasEnabled,
features.GenericWorkload: true,
features.TopologyAwareWorkloadScheduling: tc.enableTopologyAwareScheduling,
features.GangScheduling: tc.enableWorkloadAwarePreemption,
features.WorkloadAwarePreemption: tc.enableWorkloadAwarePreemption,
})
ctx := ctxWithRequestInfo()
oldWorkload := tc.oldObj.DeepCopy()
oldWorkload.ResourceVersion = "1"
newWorkload := tc.newObj.DeepCopy()
newWorkload.ResourceVersion = "2"
newWorkload.ResourceVersion = "4"
Strategy.PrepareForUpdate(ctx, newWorkload, oldWorkload)
if errs := Strategy.ValidateUpdate(ctx, newWorkload, oldWorkload); len(errs) != 0 {
@ -459,12 +549,20 @@ func TestPodSchedulingStrategyUpdate_SchedulingConstraints(t *testing.T) {
if errMsg := errs[0].Error(); !strings.Contains(errMsg, tc.expectValidationError) {
t.Fatalf("error %#v does not contain the expected message %q", errMsg, tc.expectValidationError)
}
return
}
if tc.expectObj != nil {
tc.expectObj.ResourceVersion = newWorkload.ResourceVersion
if diff := cmp.Diff(tc.expectObj, newWorkload); diff != "" {
t.Errorf("got unexpected workload object (-want, +got): %s", diff)
}
if tc.expectValidationError != "" {
t.Fatal("expected validation error(s), got none")
}
if warnings := Strategy.WarningsOnUpdate(ctx, newWorkload, oldWorkload); len(warnings) != 0 {
t.Fatalf("unexpected warnings: %q", warnings)
}
Strategy.Canonicalize(newWorkload)
expectWorkload := tc.expectWorkload.DeepCopy()
expectWorkload.ResourceVersion = "4"
if diff := cmp.Diff(expectWorkload, newWorkload); diff != "" {
t.Errorf("Workload mismatch (-want +got):\n%s", diff)
}
})
}

View file

@ -2499,7 +2499,7 @@ func TestPreEnqueue(t *testing.T) {
podToTriggerPreemption: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).PodGroupName("pg1").Priority(highPriority).Obj(),
podToCheck: st.MakePod().Name("p_other").UID("p_other").Namespace(v1.NamespaceDefault).PodGroupName("pg1").Priority(highPriority).Obj(),
pgs: []*v1alpha2.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").Namespace(v1.NamespaceDefault).Obj(),
st.MakePodGroup().Name("pg1").UID("pg1").Namespace(v1.NamespaceDefault).Priority(highPriority).Obj(),
},
features: feature.Features{EnableAsyncPreemption: true, EnableWorkloadAwarePreemption: true},
expectPreemption: true,
@ -2521,8 +2521,8 @@ func TestPreEnqueue(t *testing.T) {
podToTriggerPreemption: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).PodGroupName("pg1").Priority(highPriority).Obj(),
podToCheck: st.MakePod().Name("p_other").UID("p_other").Namespace(v1.NamespaceDefault).PodGroupName("pg2").Priority(highPriority).Obj(),
pgs: []*v1alpha2.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").Namespace(v1.NamespaceDefault).Obj(),
st.MakePodGroup().Name("pg2").UID("pg2").Namespace(v1.NamespaceDefault).Obj(),
st.MakePodGroup().Name("pg1").UID("pg1").Namespace(v1.NamespaceDefault).Priority(highPriority).Obj(),
st.MakePodGroup().Name("pg2").UID("pg2").Namespace(v1.NamespaceDefault).Priority(highPriority).Obj(),
},
features: feature.Features{EnableAsyncPreemption: true, EnableWorkloadAwarePreemption: true},
expectPreemption: true,
@ -2533,7 +2533,7 @@ func TestPreEnqueue(t *testing.T) {
podToTriggerPreemption: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).PodGroupName("pg1").Priority(highPriority).Obj(),
podToCheck: st.MakePod().Name("p_other").UID("p_other").Namespace(v1.NamespaceDefault).PodGroupName("pg_missing").Priority(highPriority).Obj(),
pgs: []*v1alpha2.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").Namespace(v1.NamespaceDefault).Obj(),
st.MakePodGroup().Name("pg1").UID("pg1").Namespace(v1.NamespaceDefault).Priority(highPriority).Obj(),
},
features: feature.Features{EnableAsyncPreemption: true, EnableWorkloadAwarePreemption: true},
expectPreemption: false,

View file

@ -496,15 +496,7 @@ func (p *podGroupExecutorPreemptor) Obj() runtime.Object {
}
func (p *podGroupExecutorPreemptor) Priority() int32 {
// TODO(Argh4k): Replace it with v1alpha2 pod group priority
// once it's available.
maxPrio := int32(0)
for _, pod := range p.pods {
if prio := corev1helpers.PodPriority(pod); prio > maxPrio {
maxPrio = prio
}
}
return maxPrio
return util.PodGroupPriority(p.pg)
}
func (p *podGroupExecutorPreemptor) Pods() map[string]*v1.Pod {

View file

@ -26,6 +26,7 @@ import (
policy "k8s.io/api/policy/v1"
schedulingapi "k8s.io/api/scheduling/v1alpha2"
policylisters "k8s.io/client-go/listers/policy/v1"
schedulinglisters "k8s.io/client-go/listers/scheduling/v1alpha2"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/names"
"k8s.io/klog/v2"
@ -36,8 +37,9 @@ import (
// PodGroupEvaluator is a preemption evaluator that knows how to run
// preemption where a preemptor is a pod group and the domain is the whole cluster.
type PodGroupEvaluator struct {
Handle fwk.Handle
pdbLister policylisters.PodDisruptionBudgetLister
Handle fwk.Handle
pdbLister policylisters.PodDisruptionBudgetLister
podGroupLister schedulinglisters.PodGroupLister
Executor *Executor
}
@ -45,9 +47,10 @@ type PodGroupEvaluator struct {
// NewPodGroupEvaluator creates a new PodGroupEvaluator.
func NewPodGroupEvaluator(fh fwk.Handle, executor *Executor) *PodGroupEvaluator {
return &PodGroupEvaluator{
Handle: fh,
pdbLister: fh.SharedInformerFactory().Policy().V1().PodDisruptionBudgets().Lister(),
Executor: executor,
Handle: fh,
pdbLister: fh.SharedInformerFactory().Policy().V1().PodDisruptionBudgets().Lister(),
podGroupLister: fh.SharedInformerFactory().Scheduling().V1alpha2().PodGroups().Lister(),
Executor: executor,
}
}
@ -68,7 +71,7 @@ func (ev *PodGroupEvaluator) Preempt(ctx context.Context, pg *schedulingapi.PodG
if err != nil {
return fwk.AsStatus(fmt.Errorf("failed to list node infos: %w", err))
}
domain := newDomainForWorkloadPreemption(allNodes, "cluster-domain")
domain := newDomainForWorkloadPreemption(allNodes, ev.podGroupLister, "cluster-domain")
preemptor := newPodGroupPreemptor(pg, pods)
pdbs, err := getPodDisruptionBudgets(ev.pdbLister)
if err != nil {

View file

@ -18,6 +18,7 @@ package preemption
import (
"context"
"fmt"
"testing"
"time"
@ -27,12 +28,35 @@ import (
schedulingapi "k8s.io/api/scheduling/v1alpha2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
schedulinglisters "k8s.io/client-go/listers/scheduling/v1alpha2"
"k8s.io/klog/v2/ktesting"
fwk "k8s.io/kube-scheduler/framework"
"k8s.io/kubernetes/pkg/scheduler/framework"
st "k8s.io/kubernetes/pkg/scheduler/testing"
)
type mockPodGroupLister struct {
schedulinglisters.PodGroupLister
podGroups map[string]*schedulingapi.PodGroup
}
func (m *mockPodGroupLister) PodGroups(namespace string) schedulinglisters.PodGroupNamespaceLister {
return &mockPodGroupNamespaceLister{podGroups: m.podGroups, namespace: namespace}
}
type mockPodGroupNamespaceLister struct {
schedulinglisters.PodGroupNamespaceLister
podGroups map[string]*schedulingapi.PodGroup
namespace string
}
func (m *mockPodGroupNamespaceLister) Get(name string) (*schedulingapi.PodGroup, error) {
if pg, ok := m.podGroups[name]; ok {
return pg, nil
}
return nil, fmt.Errorf("pod group %s not found", name)
}
func TestPodGroupEvaluator_SelectVictimsOnDomain(t *testing.T) {
// blockingRule mocks complex scheduling constraints for tests.
// It states: "Node 'nodeName' can host 'capacity' preempting pods,
@ -47,6 +71,7 @@ func TestPodGroupEvaluator_SelectVictimsOnDomain(t *testing.T) {
name string
nodeNames []string
initPods []*v1.Pod
initPodGroups []*schedulingapi.PodGroup
preemptor *podGroupPreemptor
pdbs []*policy.PodDisruptionBudget
blockingRules []blockingRule
@ -61,8 +86,12 @@ func TestPodGroupEvaluator_SelectVictimsOnDomain(t *testing.T) {
st.MakePod().Name("p2").UID("v2").Node("node2").Priority(lowPriority).PodGroupName("pg1").Obj(),
st.MakePod().Name("p3").UID("v3").Node("node3").Priority(lowPriority).PodGroupName("pg2").Obj(),
},
initPodGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").DisruptionMode(schedulingapi.DisruptionModePod).Obj(),
st.MakePodGroup().Name("pg2").UID("pg2").DisruptionMode(schedulingapi.DisruptionModePod).Obj(),
},
preemptor: newPodGroupPreemptor(
&schedulingapi.PodGroup{ObjectMeta: metav1.ObjectMeta{Name: "preemptor-pg"}},
st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(highPriority).Obj()},
),
blockingRules: []blockingRule{
@ -81,8 +110,11 @@ func TestPodGroupEvaluator_SelectVictimsOnDomain(t *testing.T) {
st.MakePod().Name("p2").UID("v2").Node("node2").Priority(lowPriority).PodGroupName("pg1").StartTime(metav1.Unix(0, 0)).Obj(),
st.MakePod().Name("p3").UID("v3").Node("node3").Priority(midPriority).Obj(),
},
initPodGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").DisruptionMode(schedulingapi.DisruptionModePod).Obj(),
},
preemptor: newPodGroupPreemptor(
&schedulingapi.PodGroup{ObjectMeta: metav1.ObjectMeta{Name: "preemptor-pg"}},
st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(highPriority).Obj()},
),
blockingRules: []blockingRule{
@ -100,8 +132,11 @@ func TestPodGroupEvaluator_SelectVictimsOnDomain(t *testing.T) {
st.MakePod().Name("p1").UID("v1").Node("node1").Priority(lowPriority).PodGroupName("pg1").StartTime(metav1.Unix(1, 0)).Obj(),
st.MakePod().Name("p2").UID("v2").Node("node2").Priority(lowPriority).PodGroupName("pg1").StartTime(metav1.Unix(0, 0)).Obj(),
},
initPodGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").DisruptionMode(schedulingapi.DisruptionModePod).Obj(),
},
preemptor: newPodGroupPreemptor(
&schedulingapi.PodGroup{ObjectMeta: metav1.ObjectMeta{Name: "preemptor-pg"}},
st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(highPriority).Obj()},
),
blockingRules: []blockingRule{
@ -121,8 +156,13 @@ func TestPodGroupEvaluator_SelectVictimsOnDomain(t *testing.T) {
st.MakePod().Name("p4").UID("v4").Node("node4").Priority(midPriority).Obj(),
st.MakePod().Name("p5").UID("v5").Node("node5").Priority(highPriority).PodGroupName("pg3").StartTime(metav1.Unix(0, 0)).Obj(),
},
initPodGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").DisruptionMode(schedulingapi.DisruptionModePod).Obj(),
st.MakePodGroup().Name("pg2").UID("pg2").DisruptionMode(schedulingapi.DisruptionModePod).Obj(),
st.MakePodGroup().Name("pg3").UID("pg3").DisruptionMode(schedulingapi.DisruptionModePod).Obj(),
},
preemptor: newPodGroupPreemptor(
&schedulingapi.PodGroup{ObjectMeta: metav1.ObjectMeta{Name: "preemptor-pg"}},
st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj(),
[]*v1.Pod{
st.MakePod().Name("p-1").UID("p-1").Priority(highPriority).Obj(),
st.MakePod().Name("p-2").UID("p-2").Priority(highPriority).Obj(),
@ -153,7 +193,7 @@ func TestPodGroupEvaluator_SelectVictimsOnDomain(t *testing.T) {
},
},
preemptor: newPodGroupPreemptor(
&schedulingapi.PodGroup{ObjectMeta: metav1.ObjectMeta{Name: "preemptor-pg"}},
st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(highPriority).Obj()},
),
blockingRules: []blockingRule{
@ -172,7 +212,7 @@ func TestPodGroupEvaluator_SelectVictimsOnDomain(t *testing.T) {
st.MakePod().Name("p3").UID("v3").Node("node3").Priority(lowPriority).PodGroupName("pg2").Obj(),
},
preemptor: newPodGroupPreemptor(
&schedulingapi.PodGroup{ObjectMeta: metav1.ObjectMeta{Name: "preemptor-pg"}},
st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj(),
[]*v1.Pod{
st.MakePod().Name("p-1").UID("p-1").Priority(highPriority).PreemptionPolicy(v1.PreemptNever).Obj(),
st.MakePod().Name("p-2").UID("p-2").Priority(highPriority).Obj(),
@ -187,6 +227,358 @@ func TestPodGroupEvaluator_SelectVictimsOnDomain(t *testing.T) {
expectedPods: []string{},
expectedStatus: fwk.NewStatus(fwk.Unschedulable),
},
{
name: "Preempt single lower priority pod",
nodeNames: []string{"node1"},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("v1").Node("node1").Priority(lowPriority).Obj(),
},
preemptor: newPodGroupPreemptor(
st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(highPriority).Obj()},
),
blockingRules: []blockingRule{
{nodeName: "node1", blockingVictims: sets.New("p1"), capacity: 1},
},
expectedPods: []string{"p1"},
expectedStatus: fwk.NewStatus(fwk.Success),
},
{
name: "Priority: Prefer lower priority victim",
nodeNames: []string{"node1"},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("v1").Node("node1").Priority(lowPriority).Obj(),
st.MakePod().Name("p2").UID("v2").Node("node1").Priority(midPriority).Obj(),
st.MakePod().Name("p3").UID("v3").Node("node1").Priority(highPriority).Obj(),
},
preemptor: newPodGroupPreemptor(
st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(highPriority).Obj()},
),
blockingRules: []blockingRule{
{nodeName: "node1", blockingVictims: sets.New("p1"), capacity: 1},
{nodeName: "node1", blockingVictims: sets.New("p2"), capacity: 1},
{nodeName: "node1", blockingVictims: sets.New("p3"), capacity: 1},
},
expectedPods: []string{"p1"},
expectedStatus: fwk.NewStatus(fwk.Success),
},
{
name: "Efficiency: Preempt minimum number of victims",
nodeNames: []string{"node1"},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("v1").Node("node1").Priority(lowPriority).Obj(),
st.MakePod().Name("p2").UID("v2").Node("node1").Priority(lowPriority).Obj(),
st.MakePod().Name("p3").UID("v3").Node("node1").Priority(lowPriority).Obj(),
st.MakePod().Name("p4").UID("v4").Node("node1").Priority(lowPriority).Obj(),
},
preemptor: newPodGroupPreemptor(
st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(highPriority).Obj()},
),
blockingRules: []blockingRule{
{nodeName: "node1", blockingVictims: sets.New("p1", "p2"), capacity: 1},
{nodeName: "node1", blockingVictims: sets.New("p1"), capacity: 1},
},
expectedPods: []string{"p1"},
expectedStatus: fwk.NewStatus(fwk.Success),
},
{
name: "PDB: Prefer non-violating victim",
nodeNames: []string{"node1"},
initPods: []*v1.Pod{
st.MakePod().Name("victim-pdb").UID("v1").Node("node1").Label("app", "foo").Priority(lowPriority).Obj(),
st.MakePod().Name("victim-no-pdb").UID("v2").Node("node1").Priority(lowPriority).Obj(),
},
pdbs: []*policy.PodDisruptionBudget{
{
Spec: policy.PodDisruptionBudgetSpec{Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}}},
Status: policy.PodDisruptionBudgetStatus{DisruptionsAllowed: 0},
},
},
preemptor: newPodGroupPreemptor(
st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(highPriority).Obj()},
),
blockingRules: []blockingRule{
{nodeName: "node1", blockingVictims: sets.New("victim-pdb"), capacity: 1},
{nodeName: "node1", blockingVictims: sets.New("victim-no-pdb"), capacity: 1},
},
expectedPods: []string{"victim-no-pdb"},
expectedStatus: fwk.NewStatus(fwk.Success),
},
{
name: "PodGroup with PreemptNever does not perform preemption",
nodeNames: []string{"node1", "node2", "node3"},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("v1").Node("node1").Priority(lowPriority).Obj(),
st.MakePod().Name("p2").UID("v2").Node("node2").Priority(lowPriority).PodGroupName("pg1").Obj(),
st.MakePod().Name("p3").UID("v3").Node("node3").Priority(lowPriority).PodGroupName("pg2").Obj(),
},
initPodGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").DisruptionMode(schedulingapi.DisruptionModePod).Priority(lowPriority).Obj(),
st.MakePodGroup().Name("pg2").UID("pg2").DisruptionMode(schedulingapi.DisruptionModePod).Priority(lowPriority).Obj(),
},
preemptor: newPodGroupPreemptor(
st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj(),
[]*v1.Pod{
st.MakePod().Name("p-1").UID("p-1").Priority(highPriority).PreemptionPolicy(v1.PreemptNever).Obj(),
st.MakePod().Name("p-2").UID("p-2").Priority(highPriority).Obj(),
},
),
blockingRules: []blockingRule{
{nodeName: "node1", capacity: 1, blockingVictims: sets.New("p1")},
{nodeName: "node2", capacity: 1, blockingVictims: sets.New("p2")},
{nodeName: "node3", capacity: 1, blockingVictims: sets.New("p3")},
},
expectedPods: []string{},
expectedStatus: fwk.NewStatus(fwk.Unschedulable),
},
{
name: "PDB: Prefer lower priority pod for preemption, when preemption without pdb violation is not possible",
nodeNames: []string{"node1"},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("v1").Node("node1").Label("app", "foo").Priority(lowPriority).Obj(),
st.MakePod().Name("p2").UID("v2").Node("node1").Label("app", "foo").Priority(midPriority).Obj(),
},
pdbs: []*policy.PodDisruptionBudget{
{
Spec: policy.PodDisruptionBudgetSpec{Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}}},
Status: policy.PodDisruptionBudgetStatus{DisruptionsAllowed: 0},
},
},
preemptor: newPodGroupPreemptor(
st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(highPriority).Obj()},
),
blockingRules: []blockingRule{
{nodeName: "node1", blockingVictims: sets.New("p1"), capacity: 1},
{nodeName: "node1", blockingVictims: sets.New("p2"), capacity: 1},
},
expectedPods: []string{"p1"},
expectedStatus: fwk.NewStatus(fwk.Success),
},
{
name: "PodGroup: Preempt group as a whole",
nodeNames: []string{"node1", "node2"},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("v1").Node("node1").Priority(lowPriority).PodGroupName("pg1").Obj(),
st.MakePod().Name("p2").UID("v2").Node("node2").Priority(lowPriority).PodGroupName("pg1").Obj(),
},
initPodGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").DisruptionMode(schedulingapi.DisruptionModePodGroup).Priority(lowPriority).Obj(),
},
preemptor: newPodGroupPreemptor(
st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(highPriority).Obj()},
),
blockingRules: []blockingRule{
{nodeName: "node1", capacity: 1, blockingVictims: sets.New("p1")},
},
expectedPods: []string{"p1", "p2"},
expectedStatus: fwk.NewStatus(fwk.Success),
},
{
name: "PodGroup: Prefer single pod over podGroup for preemption candidate",
nodeNames: []string{"node1"},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").Priority(lowPriority).Obj(),
st.MakePod().Name("g1-1").UID("g1").Node("node1").PodGroupName("pg1").Priority(lowPriority).Obj(),
st.MakePod().Name("g1-2").UID("g2").Node("node1").PodGroupName("pg1").Priority(lowPriority).Obj(),
},
initPodGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").DisruptionMode(schedulingapi.DisruptionModePodGroup).Priority(lowPriority).Obj(),
},
preemptor: newPodGroupPreemptor(
st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(highPriority).Obj()},
),
blockingRules: []blockingRule{
{nodeName: "node1", blockingVictims: sets.New("g1-1", "g1-2"), capacity: 1},
{nodeName: "node1", blockingVictims: sets.New("p1"), capacity: 1},
},
expectedPods: []string{"p1"},
expectedStatus: fwk.NewStatus(fwk.Success),
},
{
name: "PodGroup: Preempt group as a whole on single node",
nodeNames: []string{"node1"},
initPods: []*v1.Pod{
st.MakePod().Name("g1-1").UID("g1").Node("node1").PodGroupName("pg1").Priority(lowPriority).Obj(),
st.MakePod().Name("g1-2").UID("g2").Node("node1").PodGroupName("pg1").Priority(lowPriority).Obj(),
st.MakePod().Name("p1").UID("p1").Node("node1").Priority(midPriority).Obj(),
},
initPodGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").DisruptionMode(schedulingapi.DisruptionModePodGroup).Priority(lowPriority).Obj(),
},
preemptor: newPodGroupPreemptor(
st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(highPriority).Obj()},
),
blockingRules: []blockingRule{
{nodeName: "node1", capacity: 1, blockingVictims: sets.New("g1-1")}, // Only g1-1 is blocking
},
expectedPods: []string{"g1-1", "g1-2"}, // Both must be preempted
expectedStatus: fwk.NewStatus(fwk.Success),
},
{
name: "PDB: Unit violation if any member violates",
nodeNames: []string{"node1"},
initPods: []*v1.Pod{
st.MakePod().Name("g1-1").UID("g1").Node("node1").Label("app", "foo").PodGroupName("pg1").Priority(lowPriority).Obj(),
st.MakePod().Name("g1-2").UID("g2").Node("node1").PodGroupName("pg1").Priority(lowPriority).Obj(),
st.MakePod().Name("p1").UID("p1").Node("node1").Priority(lowPriority).Obj(),
},
initPodGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").DisruptionMode(schedulingapi.DisruptionModePodGroup).Priority(lowPriority).Obj(),
},
pdbs: []*policy.PodDisruptionBudget{
{
Spec: policy.PodDisruptionBudgetSpec{Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}}},
Status: policy.PodDisruptionBudgetStatus{DisruptionsAllowed: 0},
},
},
preemptor: newPodGroupPreemptor(
st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(highPriority).Obj()},
),
blockingRules: []blockingRule{
{nodeName: "node1", capacity: 1, blockingVictims: sets.New("g1-1")}, // Only g1-1 is blocking
{nodeName: "node1", capacity: 1, blockingVictims: sets.New("p1")}, // p1 is also blocking
},
expectedPods: []string{"p1"}, // p1 is preferred because pg1 unit-violates PDB (via g1-1)
expectedStatus: fwk.NewStatus(fwk.Success),
},
{
name: "PodGroup: Prefer preempting single pod over group of same priority",
nodeNames: []string{"node1"},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").Priority(lowPriority).Obj(),
st.MakePod().Name("g1-1").UID("g1").Node("node1").PodGroupName("pg1").Priority(lowPriority).Obj(),
st.MakePod().Name("g1-2").UID("g2").Node("node1").PodGroupName("pg1").Priority(lowPriority).Obj(),
},
initPodGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").DisruptionMode(schedulingapi.DisruptionModePodGroup).Priority(lowPriority).Obj(),
},
preemptor: newPodGroupPreemptor(
st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(highPriority).Obj()},
),
blockingRules: []blockingRule{
{nodeName: "node1", capacity: 1, blockingVictims: sets.New("g1-1", "g1-2")},
{nodeName: "node1", capacity: 1, blockingVictims: sets.New("p1")},
},
expectedPods: []string{"p1"}, // p1 is preempted because the PodGroup is "more important" at the same priority level
expectedStatus: fwk.NewStatus(fwk.Success),
},
{
name: "Failure: Cannot preempt the victim with higher priority",
nodeNames: []string{"node1"},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("v1").Node("node1").Priority(highPriority).Obj(),
},
preemptor: newPodGroupPreemptor(
st.MakePodGroup().Name("preemptor-pg").Priority(midPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(midPriority).Obj()},
),
blockingRules: []blockingRule{
{nodeName: "node1", blockingVictims: sets.New("p1")},
},
expectedPods: []string{},
expectedStatus: fwk.NewStatus(fwk.UnschedulableAndUnresolvable),
},
{
name: "Failure: Cannot preempt if node is empty",
nodeNames: []string{"node1"},
initPods: []*v1.Pod{},
preemptor: newPodGroupPreemptor(
st.MakePodGroup().Name("preemptor-pg").Priority(midPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(midPriority).Obj()},
),
blockingRules: []blockingRule{},
expectedPods: []string{},
expectedStatus: fwk.NewStatus(fwk.UnschedulableAndUnresolvable),
},
{
name: "Priority divergence: candidate victim PodGroup has lower priority than the Pods from that group",
nodeNames: []string{"node1", "node2"},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("v1").Node("node1").Priority(highPriority).PodGroupName("pg1").Obj(),
st.MakePod().Name("p2").UID("v2").Node("node2").Priority(highPriority).PodGroupName("pg1").Obj(),
},
initPodGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").DisruptionMode(schedulingapi.DisruptionModePodGroup).Priority(midPriority).Obj(),
},
preemptor: newPodGroupPreemptor(
st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(highPriority).Obj()},
),
blockingRules: []blockingRule{
{nodeName: "node1", capacity: 1, blockingVictims: sets.New("p1")},
},
expectedPods: []string{"p1", "p2"},
expectedStatus: fwk.NewStatus(fwk.Success),
},
{
name: "Priority divergence: candidate victim PodGroup has higher priority than the Pods from that group",
nodeNames: []string{"node1", "node2"},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("v1").Node("node1").Priority(lowPriority).PodGroupName("pg1").Obj(),
st.MakePod().Name("p2").UID("v2").Node("node2").Priority(lowPriority).PodGroupName("pg1").Obj(),
},
initPodGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").DisruptionMode(schedulingapi.DisruptionModePodGroup).Priority(midPriority).Obj(),
},
preemptor: newPodGroupPreemptor(
st.MakePodGroup().Name("preemptor-pg").Priority(midPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(midPriority).Obj()},
),
blockingRules: []blockingRule{
{nodeName: "node1", capacity: 1, blockingVictims: sets.New("p1")},
},
expectedPods: []string{},
expectedStatus: fwk.NewStatus(fwk.UnschedulableAndUnresolvable),
},
{
name: "Priority divergence: preemptor PodGroup has higher priority than the Pod from preemptor PodGroup",
nodeNames: []string{"node1", "node2"},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("v1").Node("node1").Priority(midPriority).PodGroupName("pg1").Obj(),
st.MakePod().Name("p2").UID("v2").Node("node2").Priority(midPriority).PodGroupName("pg1").Obj(),
},
initPodGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").DisruptionMode(schedulingapi.DisruptionModePodGroup).Priority(midPriority).Obj(),
},
preemptor: newPodGroupPreemptor(
st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(lowPriority).Obj()},
),
blockingRules: []blockingRule{
{nodeName: "node1", capacity: 1, blockingVictims: sets.New("p1")},
},
expectedPods: []string{"p1", "p2"},
expectedStatus: fwk.NewStatus(fwk.Success),
},
{
name: "Priority divergence: preemptor PodGroup has lower priority than the Pod from preemptor PodGroup",
nodeNames: []string{"node1", "node2"},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("v1").Node("node1").Priority(midPriority).PodGroupName("pg1").Obj(),
st.MakePod().Name("p2").UID("v2").Node("node2").Priority(midPriority).PodGroupName("pg1").Obj(),
},
initPodGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").DisruptionMode(schedulingapi.DisruptionModePodGroup).Priority(midPriority).Obj(),
},
preemptor: newPodGroupPreemptor(
st.MakePodGroup().Name("preemptor-pg").Priority(lowPriority).Obj(),
[]*v1.Pod{st.MakePod().Name("p").UID("p").Priority(highPriority).Obj()},
),
blockingRules: []blockingRule{
{nodeName: "node1", capacity: 1, blockingVictims: sets.New("p1")},
},
expectedPods: []string{},
expectedStatus: fwk.NewStatus(fwk.UnschedulableAndUnresolvable),
},
}
for _, tt := range tests {
@ -213,7 +605,12 @@ func TestPodGroupEvaluator_SelectVictimsOnDomain(t *testing.T) {
domainNodes = append(domainNodes, nodeInfos[name])
}
domain := newDomainForWorkloadPreemption(domainNodes, "test-domain")
podGroups := make(map[string]*schedulingapi.PodGroup)
for _, pg := range tt.initPodGroups {
podGroups[pg.Name] = pg
}
pgLister := &mockPodGroupLister{podGroups: podGroups}
domain := newDomainForWorkloadPreemption(domainNodes, pgLister, "test-domain")
// Create a mock podGroupSchedulingFunc.
// This simulates whether the preempting PodGroup can schedule given the current
@ -268,10 +665,12 @@ func TestPodGroupEvaluator_SelectVictimsOnDomain(t *testing.T) {
if wantCode != fwk.Success {
return
}
if victims == nil {
t.Fatalf("expected non-nil victims on success")
}
gotPods := victims.Pods
gotNames := sets.Set[string]{}
for _, p := range gotPods {
for _, p := range victims.Pods {
gotNames.Insert(p.Name)
}
wantNames := sets.New(tt.expectedPods...)

View file

@ -17,11 +17,15 @@ limitations under the License.
package preemption
import (
"maps"
"slices"
"sync/atomic"
v1 "k8s.io/api/core/v1"
schedulingapi "k8s.io/api/scheduling/v1alpha2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
schedulinglisters "k8s.io/client-go/listers/scheduling/v1alpha2"
corev1helpers "k8s.io/component-helpers/scheduling/corev1"
extenderv1 "k8s.io/kube-scheduler/extender/v1"
fwk "k8s.io/kube-scheduler/framework"
@ -37,21 +41,14 @@ type podGroupPreemptor struct {
}
func newPodGroupPreemptor(pg *schedulingapi.PodGroup, pods []*v1.Pod) *podGroupPreemptor {
prio := int32(0)
preemptionPolicy := v1.PreemptLowerPriority
// TODO(Argh4k): Replace it with pg.Spec.Priority once it's implemented:
// https://github.com/kubernetes/kubernetes/pull/136589
for _, pod := range pods {
if p := corev1helpers.PodPriority(pod); p > prio {
prio = p
}
if p := pod.Spec.PreemptionPolicy; p != nil && *p == v1.PreemptNever {
preemptionPolicy = *p
}
}
return &podGroupPreemptor{
priority: prio,
priority: util.PodGroupPriority(pg),
pods: pods,
podGroup: pg,
preemptionPolicy: preemptionPolicy,
@ -86,19 +83,53 @@ type domain struct {
allPossibleVictims []*victim
}
func newDomainForWorkloadPreemption(nodes []fwk.NodeInfo, name string) *domain {
// TODO(Argh4k): PodGroups with a DisruptionMode == DisruptionModePodGroup
// should be treated as a single Victim.
// https://github.com/kubernetes/kubernetes/pull/136589
// TODO(Argh4k): For pod groups victims use pg.Spec.Priority once it's implemented:
// https://github.com/kubernetes/kubernetes/pull/136589
allPossibleVictims := make([]*victim, 0, len(nodes))
// isPodGroupPreemptiblePod checks if a pod is a part of a pod group that should
// be treated as a single unit for preemption purposes.
// If the pod is a part of such a pod group, it returns the pod group and true.
// In all other cases, it returns nil and false.
func isPodGroupPreemptiblePod(p *v1.Pod, pgLister schedulinglisters.PodGroupLister) (*schedulingapi.PodGroup, bool) {
if p.Spec.SchedulingGroup == nil {
return nil, false
}
pgName := p.Spec.SchedulingGroup.PodGroupName
pg, err := pgLister.PodGroups(p.Namespace).Get(*pgName)
if err != nil {
return nil, false
}
if mode := pg.Spec.DisruptionMode; mode == nil || *mode != schedulingapi.DisruptionModePodGroup {
return nil, false
}
return pg, true
}
// newDomainForWorkloadPreemption creates a new domain for workload preemption.
// The domain is the whole cluster and it contains victims that are computed based
// on the pods and their scheduling groups.
// Pods that are part of a pod group with the PodGroup disruption mode are grouped
// together into a single victim. Otherwise, they are treated as individual victims.
func newDomainForWorkloadPreemption(nodes []fwk.NodeInfo, pgLister schedulinglisters.PodGroupLister, name string) *domain {
victimMap := map[types.UID]*victim{}
for _, node := range nodes {
for _, p := range node.GetPods() {
allPossibleVictims = append(allPossibleVictims, newVictim([]fwk.PodInfo{p}, corev1helpers.PodPriority(p.GetPod()), []fwk.NodeInfo{node}))
// TODO: Calling the lister here is not ideal given we do this
// for every pod in the cluster. Instead, we should be getting
// this information from the snapshot.
pg, ok := isPodGroupPreemptiblePod(p.GetPod(), pgLister)
if !ok {
victimMap[p.GetPod().UID] = newVictim([]fwk.PodInfo{p}, corev1helpers.PodPriority(p.GetPod()), []fwk.NodeInfo{node})
continue
}
victim, ok := victimMap[pg.UID]
if ok {
victim.pods = append(victim.pods, p)
victim.affectedNodes[node.Node().Name] = node
continue
}
victimMap[pg.UID] = newVictim([]fwk.PodInfo{p}, util.PodGroupPriority(pg), []fwk.NodeInfo{node})
}
}
allPossibleVictims := slices.Collect(maps.Values(victimMap))
return &domain{
nodes: nodes,
allPossibleVictims: allPossibleVictims,

View file

@ -0,0 +1,263 @@
/*
Copyright 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 preemption
import (
"sort"
"testing"
"github.com/google/go-cmp/cmp"
v1 "k8s.io/api/core/v1"
schedulingapi "k8s.io/api/scheduling/v1alpha2"
"k8s.io/apimachinery/pkg/util/sets"
fwk "k8s.io/kube-scheduler/framework"
"k8s.io/kubernetes/pkg/scheduler/framework"
st "k8s.io/kubernetes/pkg/scheduler/testing"
)
func TestIsPodGroupPreemptiblePod(t *testing.T) {
tests := []struct {
name string
pod *v1.Pod
podGroups map[string]*schedulingapi.PodGroup
wantPodGroup *schedulingapi.PodGroup
wantOk bool
}{
{
name: "pod without scheduling group",
pod: st.MakePod().Name("p1").Namespace("default").Obj(),
wantPodGroup: nil,
wantOk: false,
},
{
name: "pod group not found",
pod: st.MakePod().Name("p1").Namespace("default").PodGroupName("pg1").Obj(),
podGroups: map[string]*schedulingapi.PodGroup{},
wantPodGroup: nil,
wantOk: false,
},
{
name: "pod group with nil disruption mode",
pod: st.MakePod().Name("p1").Namespace("default").PodGroupName("pg1").Obj(),
podGroups: map[string]*schedulingapi.PodGroup{
"pg1": st.MakePodGroup().Name("pg1").Namespace("default").Obj(),
},
wantPodGroup: nil,
wantOk: false,
},
{
name: "pod group with DisruptionModePod",
pod: st.MakePod().Name("p1").Namespace("default").PodGroupName("pg1").Obj(),
podGroups: map[string]*schedulingapi.PodGroup{
"pg1": st.MakePodGroup().Name("pg1").Namespace("default").DisruptionMode(schedulingapi.DisruptionModePod).Obj(),
},
wantPodGroup: nil,
wantOk: false,
},
{
name: "pod group with DisruptionModePodGroup",
pod: st.MakePod().Name("p1").Namespace("default").PodGroupName("pg1").Obj(),
podGroups: map[string]*schedulingapi.PodGroup{
"pg1": st.MakePodGroup().Name("pg1").Namespace("default").DisruptionMode(schedulingapi.DisruptionModePodGroup).Obj(),
},
wantPodGroup: st.MakePodGroup().Name("pg1").Namespace("default").DisruptionMode(schedulingapi.DisruptionModePodGroup).Obj(),
wantOk: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pgLister := &mockPodGroupLister{podGroups: tt.podGroups}
podGroup, ok := isPodGroupPreemptiblePod(tt.pod, pgLister)
if ok != tt.wantOk {
t.Errorf("isPodGroupPreemptiblePod() gotOk = %v, want %v", ok, tt.wantOk)
}
if diff := cmp.Diff(tt.wantPodGroup, podGroup); diff != "" {
t.Errorf("isPodGroupPreemptiblePod() gotPodGroup mismatch (-want +got):\n%s", diff)
}
})
}
}
type expectedVictim struct {
pods sets.Set[string]
affectedNodes sets.Set[string]
priority int32
}
func TestNewDomainForWorkloadPreemption(t *testing.T) {
tests := []struct {
name string
nodes []*v1.Node
pods []*v1.Pod
podGroups map[string]*schedulingapi.PodGroup
domainName string
wantVictims []expectedVictim
}{
{
name: "no pods",
nodes: []*v1.Node{
st.MakeNode().Name("node1").Obj(),
},
pods: nil,
podGroups: nil,
domainName: "test-domain",
wantVictims: nil,
},
{
name: "pods without pod groups",
nodes: []*v1.Node{
st.MakeNode().Name("node1").Obj(),
st.MakeNode().Name("node2").Obj(),
},
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").Priority(10).Obj(),
st.MakePod().Name("p2").UID("p2").Node("node2").Priority(20).Obj(),
},
podGroups: nil,
domainName: "test-domain",
wantVictims: []expectedVictim{
{pods: sets.New("p1"), affectedNodes: sets.New("node1"), priority: 10},
{pods: sets.New("p2"), affectedNodes: sets.New("node2"), priority: 20},
},
},
{
name: "pods with pod group (DisruptionModePodGroup)",
nodes: []*v1.Node{
st.MakeNode().Name("node1").Obj(),
st.MakeNode().Name("node2").Obj(),
},
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").PodGroupName("pg1").Priority(10).Obj(),
st.MakePod().Name("p2").UID("p2").Node("node2").PodGroupName("pg1").Priority(10).Obj(),
},
podGroups: map[string]*schedulingapi.PodGroup{
"pg1": st.MakePodGroup().Name("pg1").UID("pg1").DisruptionMode(schedulingapi.DisruptionModePodGroup).Priority(50).Obj(),
},
domainName: "test-domain",
wantVictims: []expectedVictim{
{pods: sets.New("p1", "p2"), affectedNodes: sets.New("node1", "node2"), priority: 50},
},
},
{
name: "pods with pod group (DisruptionModePod)",
nodes: []*v1.Node{
st.MakeNode().Name("node1").Obj(),
st.MakeNode().Name("node2").Obj(),
},
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").PodGroupName("pg1").Priority(10).Obj(),
st.MakePod().Name("p2").UID("p2").Node("node2").PodGroupName("pg1").Priority(20).Obj(),
},
podGroups: map[string]*schedulingapi.PodGroup{
"pg1": st.MakePodGroup().Name("pg1").UID("pg1").DisruptionMode(schedulingapi.DisruptionModePod).Priority(50).Obj(),
},
domainName: "test-domain",
wantVictims: []expectedVictim{
{pods: sets.New("p1"), affectedNodes: sets.New("node1"), priority: 10},
{pods: sets.New("p2"), affectedNodes: sets.New("node2"), priority: 20},
},
},
{
name: "mix of pod groups and individual pods",
nodes: []*v1.Node{
st.MakeNode().Name("node1").Obj(),
st.MakeNode().Name("node2").Obj(),
},
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").PodGroupName("pg1").Priority(10).Obj(),
st.MakePod().Name("p2").UID("p2").Node("node2").PodGroupName("pg1").Priority(10).Obj(),
st.MakePod().Name("p3").UID("p3").Node("node1").PodGroupName("pg2").Priority(20).Obj(),
st.MakePod().Name("p4").UID("p4").Node("node2").Priority(30).Obj(),
},
podGroups: map[string]*schedulingapi.PodGroup{
"pg1": st.MakePodGroup().Name("pg1").UID("pg1").DisruptionMode(schedulingapi.DisruptionModePodGroup).Priority(50).Obj(),
"pg2": st.MakePodGroup().Name("pg2").UID("pg2").DisruptionMode(schedulingapi.DisruptionModePod).Priority(60).Obj(),
},
domainName: "test-domain",
wantVictims: []expectedVictim{
{pods: sets.New("p1", "p2"), affectedNodes: sets.New("node1", "node2"), priority: 50},
{pods: sets.New("p3"), affectedNodes: sets.New("node1"), priority: 20},
{pods: sets.New("p4"), affectedNodes: sets.New("node2"), priority: 30},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
nodeInfos := make(map[string]fwk.NodeInfo)
for _, node := range tt.nodes {
ni := framework.NewNodeInfo()
ni.SetNode(node)
nodeInfos[node.Name] = ni
}
for _, p := range tt.pods {
if ni, ok := nodeInfos[p.Spec.NodeName]; ok {
pi, _ := framework.NewPodInfo(p)
ni.AddPodInfo(pi)
}
}
var domainNodes []fwk.NodeInfo
for _, node := range tt.nodes {
if ni, ok := nodeInfos[node.Name]; ok {
domainNodes = append(domainNodes, ni)
}
}
pgLister := &mockPodGroupLister{podGroups: tt.podGroups}
domain := newDomainForWorkloadPreemption(domainNodes, pgLister, tt.domainName)
if domain.GetName() != tt.domainName {
t.Errorf("expected domain name %q, got %q", tt.domainName, domain.GetName())
}
victims := domain.GetAllPossibleVictims()
var gotVictims []expectedVictim
for _, v := range victims {
ev := expectedVictim{
pods: sets.New[string](),
affectedNodes: sets.New[string](),
priority: v.Priority(),
}
for _, p := range v.Pods() {
ev.pods.Insert(p.GetPod().Name)
}
for n := range v.AffectedNodes() {
ev.affectedNodes.Insert(n)
}
gotVictims = append(gotVictims, ev)
}
sortVictims := func(vs []expectedVictim) {
sort.Slice(vs, func(i, j int) bool {
if vs[i].pods.Len() == 0 || vs[j].pods.Len() == 0 {
return vs[i].pods.Len() < vs[j].pods.Len()
}
return sets.List(vs[i].pods)[0] < sets.List(vs[j].pods)[0]
})
}
sortVictims(gotVictims)
sortVictims(tt.wantVictims)
if diff := cmp.Diff(tt.wantVictims, gotVictims, cmp.AllowUnexported(expectedVictim{})); diff != "" {
t.Errorf("victims mismatch (-want +got):\n%s", diff)
}
})
}
}

View file

@ -1654,6 +1654,18 @@ func (wrapper *PodGroupWrapper) ResourceClaimStatuses(statuses ...schedulingapi.
return wrapper
}
// DisruptionMode sets the disruption mode of the inner PodGroup.
func (wrapper *PodGroupWrapper) DisruptionMode(mode schedulingapi.DisruptionMode) *PodGroupWrapper {
wrapper.PodGroup.Spec.DisruptionMode = &mode
return wrapper
}
// Priority sets the priority of the inner PodGroup.
func (wrapper *PodGroupWrapper) Priority(priority int32) *PodGroupWrapper {
wrapper.PodGroup.Spec.Priority = &priority
return wrapper
}
// WorkloadWrapper wraps a Workload inside.
type WorkloadWrapper struct{ schedulingapi.Workload }

View file

@ -257,3 +257,15 @@ func GetHostPorts(pod *v1.Pod) []v1.ContainerPort {
}
return ports
}
// PodGroupPriority returns priority of a given pod group.
func PodGroupPriority(pg *schedulingv1alpha2.PodGroup) int32 {
if pg.Spec.Priority != nil {
return *pg.Spec.Priority
}
// When priority of a pod group is nil, it means it was created at a time
// that there was no global default priority class and the priority class
// name of the pod group was empty (or when the WorkloadAwarePreemption
// feature gate was disabled). So, we resolve to the static default priority.
return 0
}

View file

@ -27,11 +27,13 @@ import (
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apiserver/pkg/admission"
genericadmissioninitializers "k8s.io/apiserver/pkg/admission/initializer"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
schedulingv1listers "k8s.io/client-go/listers/scheduling/v1"
"k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/scheduling"
"k8s.io/kubernetes/pkg/features"
)
const (
@ -90,11 +92,11 @@ func (p *Plugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactor
var (
podResource = core.Resource("pods")
podGroupResource = scheduling.Resource("podgroups")
priorityClassResource = scheduling.Resource("priorityclasses")
)
// Admit checks Pods and admits or rejects them. It also resolves the priority of pods based on their PriorityClass.
// Note that pod validation mechanism prevents update of a pod priority.
// Admit checks Pods and PodGroups and admits or rejects them. It also resolves the priority of pods and pod groups based on their PriorityClass.
func (p *Plugin) Admit(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
operation := a.GetOperation()
// Ignore all calls to subresources
@ -107,7 +109,11 @@ func (p *Plugin) Admit(ctx context.Context, a admission.Attributes, o admission.
return p.admitPod(a)
}
return nil
case podGroupResource:
if operation == admission.Create {
return p.admitPodGroup(a)
}
return nil
default:
return nil
}
@ -185,6 +191,31 @@ func (p *Plugin) admitPod(a admission.Attributes) error {
return nil
}
// admitPodGroup makes sure a new pod group does not set spec.Priority field. It also makes sure that
// the PriorityClassName exists if it is provided and resolves the pod group priority from the PriorityClassName.
func (p *Plugin) admitPodGroup(attributes admission.Attributes) error {
if !utilfeature.DefaultFeatureGate.Enabled(features.WorkloadAwarePreemption) {
return nil
}
pg, ok := attributes.GetObject().(*scheduling.PodGroup)
if !ok {
return errors.NewBadRequest("resource was marked with kind PodGroup but was unable to be converted")
}
priorityClassName, priority, _, err := p.establishPriority(attributes, &pg.Spec.PriorityClassName)
if err != nil {
return err
}
// Reject if the pod group already contained a priority that differs from the one computed from the priority class.
if pg.Spec.Priority != nil && *pg.Spec.Priority != priority {
return admission.NewForbidden(attributes, fmt.Errorf("priority set in the pod group (%d) must match the priority computed (%d) based on the priority class set in the spec", *pg.Spec.Priority, priority))
}
pg.Spec.Priority = &priority
pg.Spec.PriorityClassName = priorityClassName
return nil
}
// validatePriorityClass ensures that the value field is not larger than the highest user definable priority. If the GlobalDefault is set, it ensures that there is no other PriorityClass whose GlobalDefault is set.
func (p *Plugin) validatePriorityClass(a admission.Attributes) error {
operation := a.GetOperation()

View file

@ -20,18 +20,24 @@ import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"k8s.io/klog/v2"
"k8s.io/klog/v2/ktesting"
schedulingv1 "k8s.io/api/scheduling/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
admissiontesting "k8s.io/apiserver/pkg/admission/testing"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/informers"
featuregatetesting "k8s.io/component-base/featuregate/testing"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/scheduling"
v1 "k8s.io/kubernetes/pkg/apis/scheduling/v1"
"k8s.io/kubernetes/pkg/controller"
"k8s.io/kubernetes/pkg/features"
)
func addPriorityClasses(ctrl *Plugin, priorityClasses []*scheduling.PriorityClass) error {
@ -760,3 +766,160 @@ func TestPodAdmission(t *testing.T) {
}
}
}
func TestAdmitPodGroup(t *testing.T) {
podGroup := func(priorityClassName string) *scheduling.PodGroup {
return &scheduling.PodGroup{
ObjectMeta: metav1.ObjectMeta{
Name: "test-podgroup",
Namespace: metav1.NamespaceDefault,
},
Spec: scheduling.PodGroupSpec{
SchedulingPolicy: scheduling.PodGroupSchedulingPolicy{
Basic: &scheduling.BasicSchedulingPolicy{},
},
PriorityClassName: priorityClassName,
},
}
}
podGroupWithPriority := func(priorityClassName string, priority int32) *scheduling.PodGroup {
pg := podGroup(priorityClassName)
pg.Spec.Priority = new(priority)
return pg
}
attributes := func(podGroup *scheduling.PodGroup, operation admission.Operation) admission.Attributes {
var oldPodGroup runtime.Object
var options runtime.Object = &metav1.CreateOptions{}
if operation == admission.Update {
oldPodGroup = podGroup.DeepCopy()
options = &metav1.UpdateOptions{}
}
return admission.NewAttributesRecord(
podGroup,
oldPodGroup,
scheduling.Kind("PodGroup").WithVersion("v1alpha2"),
podGroup.ObjectMeta.Namespace,
"",
scheduling.Resource("podgroups").WithVersion("v1alpha2"),
"",
operation,
options,
false,
nil,
)
}
testCases := []struct {
name string
priorityClasses []*scheduling.PriorityClass
preparePodGroup *scheduling.PodGroup
operation admission.Operation
expectedPriorityClass string
expectedPriority int32
enableWorkloadAwarePreemption bool
expectError bool
}{
{
name: "pod group with empty priorityClassName, accepted and set to global default",
priorityClasses: []*scheduling.PriorityClass{defaultClass1, nondefaultClass1},
preparePodGroup: podGroup("" /* empty priorityClassName */),
operation: admission.Create,
expectedPriorityClass: "default1",
expectedPriority: defaultClass1.Value,
enableWorkloadAwarePreemption: true,
},
{
name: "pod group with explicit priorityClassName, accepted",
priorityClasses: []*scheduling.PriorityClass{defaultClass1, nondefaultClass1},
preparePodGroup: podGroup("nondefault1"),
operation: admission.Create,
expectedPriorityClass: "nondefault1",
expectedPriority: nondefaultClass1.Value,
enableWorkloadAwarePreemption: true,
},
{
name: "pod group with non-existent priorityClassName, rejected",
priorityClasses: []*scheduling.PriorityClass{defaultClass1, nondefaultClass1},
preparePodGroup: podGroup("non-existent"),
operation: admission.Create,
enableWorkloadAwarePreemption: true,
expectError: true,
},
{
name: "pod group with any priorityClassName but feature gate disabled, skips validation",
priorityClasses: []*scheduling.PriorityClass{defaultClass1, nondefaultClass1},
preparePodGroup: podGroup("non-existent"),
operation: admission.Create,
},
{
name: "pod group with no priorityClassName and no global default, accepted and priority should be zero",
priorityClasses: []*scheduling.PriorityClass{nondefaultClass1},
preparePodGroup: podGroup("" /* empty priorityClassName */),
operation: admission.Create,
expectedPriorityClass: "",
expectedPriority: 0,
enableWorkloadAwarePreemption: true,
},
{
name: "pod group create with pre-set Priority matching computed value, accepted",
priorityClasses: []*scheduling.PriorityClass{defaultClass1, nondefaultClass1},
preparePodGroup: podGroupWithPriority("nondefault1", nondefaultClass1.Value),
operation: admission.Create,
expectedPriorityClass: "nondefault1",
expectedPriority: nondefaultClass1.Value,
enableWorkloadAwarePreemption: true,
},
{
name: "pod group create with pre-set Priority not matching computed value, rejected",
priorityClasses: []*scheduling.PriorityClass{defaultClass1, nondefaultClass1},
preparePodGroup: podGroupWithPriority("nondefault1", int32(9999)),
operation: admission.Create,
enableWorkloadAwarePreemption: true,
expectError: true,
},
{
name: "update operation is a no-op, admission does not mutate pod group on update",
priorityClasses: []*scheduling.PriorityClass{defaultClass1, nondefaultClass1},
preparePodGroup: podGroup("non-existent"),
operation: admission.Update,
enableWorkloadAwarePreemption: true,
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
featuregatetesting.SetFeatureGatesDuringTest(t, feature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
features.GenericWorkload: true,
features.GangScheduling: true,
features.WorkloadAwarePreemption: tt.enableWorkloadAwarePreemption,
})
admissionPlugin := NewPlugin()
if err := addPriorityClasses(admissionPlugin, tt.priorityClasses); err != nil {
t.Fatalf("unable to configure priority classes: %v", err)
}
_, ctx := ktesting.NewTestContext(t)
podGroupCopy := tt.preparePodGroup.DeepCopy()
err := admissionPlugin.Admit(ctx, attributes(tt.preparePodGroup, tt.operation), nil)
if (err != nil) != tt.expectError {
t.Errorf("PodGroup Admit(), error = %v, want = %v", err, tt.expectError)
}
if !tt.expectError && tt.operation == admission.Create && tt.enableWorkloadAwarePreemption && tt.preparePodGroup.Spec.PodGroupTemplateRef == nil {
if tt.preparePodGroup.Spec.PriorityClassName != tt.expectedPriorityClass {
t.Errorf("PodGroup Admit(), priorityClassName = %v, want = %v", tt.preparePodGroup.Spec.PriorityClassName, tt.expectedPriorityClass)
}
if *tt.preparePodGroup.Spec.Priority != tt.expectedPriority {
t.Errorf("PodGroup Admit(), Priority = %v, want = %v", *tt.preparePodGroup.Spec.Priority, tt.expectedPriority)
}
}
if tt.operation != admission.Create {
if diff := cmp.Diff(tt.preparePodGroup, podGroupCopy); len(diff) > 0 {
t.Errorf("PodGroup Admit() should not modify the PodGroup (-want +got):\n%s", diff)
}
}
})
}
}

View file

@ -397,6 +397,23 @@ func (m *PodGroupSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) {
_ = i
var l int
_ = l
if m.Priority != nil {
i = encodeVarintGenerated(dAtA, i, uint64(*m.Priority))
i--
dAtA[i] = 0x38
}
i -= len(m.PriorityClassName)
copy(dAtA[i:], m.PriorityClassName)
i = encodeVarintGenerated(dAtA, i, uint64(len(m.PriorityClassName)))
i--
dAtA[i] = 0x32
if m.DisruptionMode != nil {
i -= len(*m.DisruptionMode)
copy(dAtA[i:], *m.DisruptionMode)
i = encodeVarintGenerated(dAtA, i, uint64(len(*m.DisruptionMode)))
i--
dAtA[i] = 0x2a
}
if len(m.ResourceClaims) > 0 {
for iNdEx := len(m.ResourceClaims) - 1; iNdEx >= 0; iNdEx-- {
{
@ -519,6 +536,23 @@ func (m *PodGroupTemplate) MarshalToSizedBuffer(dAtA []byte) (int, error) {
_ = i
var l int
_ = l
if m.Priority != nil {
i = encodeVarintGenerated(dAtA, i, uint64(*m.Priority))
i--
dAtA[i] = 0x38
}
i -= len(m.PriorityClassName)
copy(dAtA[i:], m.PriorityClassName)
i = encodeVarintGenerated(dAtA, i, uint64(len(m.PriorityClassName)))
i--
dAtA[i] = 0x32
if m.DisruptionMode != nil {
i -= len(*m.DisruptionMode)
copy(dAtA[i:], *m.DisruptionMode)
i = encodeVarintGenerated(dAtA, i, uint64(len(*m.DisruptionMode)))
i--
dAtA[i] = 0x2a
}
if len(m.ResourceClaims) > 0 {
for iNdEx := len(m.ResourceClaims) - 1; iNdEx >= 0; iNdEx-- {
{
@ -986,6 +1020,15 @@ func (m *PodGroupSpec) Size() (n int) {
n += 1 + l + sovGenerated(uint64(l))
}
}
if m.DisruptionMode != nil {
l = len(*m.DisruptionMode)
n += 1 + l + sovGenerated(uint64(l))
}
l = len(m.PriorityClassName)
n += 1 + l + sovGenerated(uint64(l))
if m.Priority != nil {
n += 1 + sovGenerated(uint64(*m.Priority))
}
return n
}
@ -1030,6 +1073,15 @@ func (m *PodGroupTemplate) Size() (n int) {
n += 1 + l + sovGenerated(uint64(l))
}
}
if m.DisruptionMode != nil {
l = len(*m.DisruptionMode)
n += 1 + l + sovGenerated(uint64(l))
}
l = len(m.PriorityClassName)
n += 1 + l + sovGenerated(uint64(l))
if m.Priority != nil {
n += 1 + sovGenerated(uint64(*m.Priority))
}
return n
}
@ -1250,6 +1302,9 @@ func (this *PodGroupSpec) String() string {
`SchedulingPolicy:` + strings.Replace(strings.Replace(this.SchedulingPolicy.String(), "PodGroupSchedulingPolicy", "PodGroupSchedulingPolicy", 1), `&`, ``, 1) + `,`,
`SchedulingConstraints:` + strings.Replace(this.SchedulingConstraints.String(), "PodGroupSchedulingConstraints", "PodGroupSchedulingConstraints", 1) + `,`,
`ResourceClaims:` + repeatedStringForResourceClaims + `,`,
`DisruptionMode:` + valueToStringGenerated(this.DisruptionMode) + `,`,
`PriorityClassName:` + fmt.Sprintf("%v", this.PriorityClassName) + `,`,
`Priority:` + valueToStringGenerated(this.Priority) + `,`,
`}`,
}, "")
return s
@ -1289,6 +1344,9 @@ func (this *PodGroupTemplate) String() string {
`SchedulingPolicy:` + strings.Replace(strings.Replace(this.SchedulingPolicy.String(), "PodGroupSchedulingPolicy", "PodGroupSchedulingPolicy", 1), `&`, ``, 1) + `,`,
`SchedulingConstraints:` + strings.Replace(this.SchedulingConstraints.String(), "PodGroupSchedulingConstraints", "PodGroupSchedulingConstraints", 1) + `,`,
`ResourceClaims:` + repeatedStringForResourceClaims + `,`,
`DisruptionMode:` + valueToStringGenerated(this.DisruptionMode) + `,`,
`PriorityClassName:` + fmt.Sprintf("%v", this.PriorityClassName) + `,`,
`Priority:` + valueToStringGenerated(this.Priority) + `,`,
`}`,
}, "")
return s
@ -2409,6 +2467,91 @@ func (m *PodGroupSpec) Unmarshal(dAtA []byte) error {
return err
}
iNdEx = postIndex
case 5:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field DisruptionMode", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthGenerated
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
s := DisruptionMode(dAtA[iNdEx:postIndex])
m.DisruptionMode = &s
iNdEx = postIndex
case 6:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field PriorityClassName", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthGenerated
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.PriorityClassName = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 7:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Priority", wireType)
}
var v int32
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
v |= int32(b&0x7F) << shift
if b < 0x80 {
break
}
}
m.Priority = &v
default:
iNdEx = preIndex
skippy, err := skipGenerated(dAtA[iNdEx:])
@ -2712,6 +2855,91 @@ func (m *PodGroupTemplate) Unmarshal(dAtA []byte) error {
return err
}
iNdEx = postIndex
case 5:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field DisruptionMode", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthGenerated
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
s := DisruptionMode(dAtA[iNdEx:postIndex])
m.DisruptionMode = &s
iNdEx = postIndex
case 6:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field PriorityClassName", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthGenerated
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.PriorityClassName = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 7:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Priority", wireType)
}
var v int32
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
v |= int32(b&0x7F) << shift
if b < 0x80 {
break
}
}
m.Priority = &v
default:
iNdEx = preIndex
skippy, err := skipGenerated(dAtA[iNdEx:])

View file

@ -194,7 +194,7 @@ message PodGroupSpec {
//
// +optional
// +k8s:optional
// +k8s:alpha(since:"1.36")=+k8s:immutable
// +k8s:immutable
optional PodGroupTemplateReference podGroupTemplateRef = 1;
// SchedulingPolicy defines the scheduling policy for this instance of the PodGroup.
@ -202,7 +202,7 @@ message PodGroupSpec {
// This field is immutable.
//
// +required
// +k8s:alpha(since:"1.36")=+k8s:immutable
// +k8s:immutable
optional PodGroupSchedulingPolicy schedulingPolicy = 2;
// SchedulingConstraints defines optional scheduling constraints (e.g. topology) for this PodGroup.
@ -237,9 +237,59 @@ message PodGroupSpec {
// +k8s:listType=map
// +k8s:listMapKey=name
// +k8s:maxItems=4
// +k8s:alpha(since:"1.36")=+k8s:immutable
// +k8s:immutable
// +featureGate=DRAWorkloadResourceClaims
repeated PodGroupResourceClaim resourceClaims = 4;
// DisruptionMode defines the mode in which a given PodGroup can be disrupted.
// Controllers are expected to fill this field by copying it from a PodGroupTemplate.
// One of Pod, PodGroup. Defaults to Pod if unset.
// This field is immutable.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
//
// +featureGate=WorkloadAwarePreemption
// +optional
// +k8s:ifDisabled("WorkloadAwarePreemption")=+k8s:forbidden
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:optional
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:immutable
// +default="Pod"
optional string disruptionMode = 5;
// PriorityClassName defines the priority that should be considered when scheduling this pod group.
// Controllers are expected to fill this field by copying it from a PodGroupTemplate.
// Otherwise, it is validated and resolved similarly to the PriorityClassName on PodGroupTemplate
// (i.e. if no priority class is specified, admission control can set this to the global default
// priority class if it exists. Otherwise, the pod group's priority will be zero).
// This field is immutable.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
//
// +featureGate=WorkloadAwarePreemption
// +optional
// +k8s:ifDisabled("WorkloadAwarePreemption")=+k8s:forbidden
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:optional
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:format=k8s-long-name
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:immutable
optional string priorityClassName = 6;
// Priority is the value of priority of this pod group. Various system components
// use this field to find the priority of the pod group. When Priority Admission
// Controller is enabled, it prevents users from setting this field. The admission
// controller populates this field from PriorityClassName.
// The higher the value, the higher the priority.
// This field is immutable.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
//
// +featureGate=WorkloadAwarePreemption
// +optional
// +k8s:ifDisabled("WorkloadAwarePreemption")=+k8s:forbidden
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:optional
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:immutable
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:maximum=1000000000 # HighestUserDefinablePriority
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:minimum=-2147483648
optional int32 priority = 7;
}
// PodGroupStatus represents information about the status of a pod group.
@ -326,9 +376,50 @@ message PodGroupTemplate {
// +k8s:listType=map
// +k8s:listMapKey=name
// +k8s:maxItems=4
// +k8s:alpha(since:"1.36")=+k8s:immutable
// +k8s:immutable
// +featureGate=DRAWorkloadResourceClaims
repeated PodGroupResourceClaim resourceClaims = 4;
// DisruptionMode defines the mode in which a given PodGroup can be disrupted.
// One of Pod, PodGroup.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
//
// +featureGate=WorkloadAwarePreemption
// +optional
// +k8s:ifDisabled("WorkloadAwarePreemption")=+k8s:forbidden
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:optional
optional string disruptionMode = 5;
// PriorityClassName indicates the priority that should be considered when scheduling
// a pod group created from this template. If no priority class is specified, admission
// control can set this to the global default priority class if it exists. Otherwise,
// pod groups created from this template will have the priority set to zero.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
//
// +featureGate=WorkloadAwarePreemption
// +optional
// +k8s:ifDisabled("WorkloadAwarePreemption")=+k8s:forbidden
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:optional
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:format=k8s-long-name
optional string priorityClassName = 6;
// Priority is the value of priority of pod groups created from this template. Various
// system components use this field to find the priority of the pod group. When
// Priority Admission Controller is enabled, it prevents users from setting this field.
// The admission controller populates this field from PriorityClassName.
// The higher the value, the higher the priority.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
//
// +featureGate=WorkloadAwarePreemption
// +optional
// +k8s:ifDisabled("WorkloadAwarePreemption")=+k8s:forbidden
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:optional
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:maximum=1000000000 # HighestUserDefinablePriority
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:minimum=-2147483648
optional int32 priority = 7;
}
// PodGroupTemplateReference references a PodGroup template defined in some object (e.g. Workload).
@ -440,7 +531,7 @@ message WorkloadSpec {
//
// +optional
// +k8s:optional
// +k8s:alpha(since:"1.36")=+k8s:immutable
// +k8s:immutable
optional TypedLocalObjectReference controllerRef = 1;
// PodGroupTemplates is the list of templates that make up the Workload.
@ -453,7 +544,7 @@ message WorkloadSpec {
// +k8s:listType=map
// +k8s:listMapKey=name
// +k8s:maxItems=8
// +k8s:alpha(since:"1.36")=+k8s:immutable
// +k8s:immutable
repeated PodGroupTemplate podGroupTemplates = 2;
}

View file

@ -67,7 +67,7 @@ type WorkloadSpec struct {
//
// +optional
// +k8s:optional
// +k8s:alpha(since:"1.36")=+k8s:immutable
// +k8s:immutable
ControllerRef *TypedLocalObjectReference `json:"controllerRef,omitempty" protobuf:"bytes,1,opt,name=controllerRef"`
// PodGroupTemplates is the list of templates that make up the Workload.
@ -80,7 +80,7 @@ type WorkloadSpec struct {
// +k8s:listType=map
// +k8s:listMapKey=name
// +k8s:maxItems=8
// +k8s:alpha(since:"1.36")=+k8s:immutable
// +k8s:immutable
PodGroupTemplates []PodGroupTemplate `json:"podGroupTemplates" protobuf:"bytes,2,rep,name=podGroupTemplates"`
}
@ -159,9 +159,50 @@ type PodGroupTemplate struct {
// +k8s:listType=map
// +k8s:listMapKey=name
// +k8s:maxItems=4
// +k8s:alpha(since:"1.36")=+k8s:immutable
// +k8s:immutable
// +featureGate=DRAWorkloadResourceClaims
ResourceClaims []PodGroupResourceClaim `json:"resourceClaims,omitempty" patchStrategy:"merge,retainKeys" patchMergeKey:"name" protobuf:"bytes,4,rep,name=resourceClaims"`
// DisruptionMode defines the mode in which a given PodGroup can be disrupted.
// One of Pod, PodGroup.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
//
// +featureGate=WorkloadAwarePreemption
// +optional
// +k8s:ifDisabled("WorkloadAwarePreemption")=+k8s:forbidden
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:optional
DisruptionMode *DisruptionMode `json:"disruptionMode,omitempty" protobuf:"bytes,5,opt,name=disruptionMode,casttype=DisruptionMode"`
// PriorityClassName indicates the priority that should be considered when scheduling
// a pod group created from this template. If no priority class is specified, admission
// control can set this to the global default priority class if it exists. Otherwise,
// pod groups created from this template will have the priority set to zero.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
//
// +featureGate=WorkloadAwarePreemption
// +optional
// +k8s:ifDisabled("WorkloadAwarePreemption")=+k8s:forbidden
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:optional
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:format=k8s-long-name
PriorityClassName string `json:"priorityClassName,omitempty" protobuf:"bytes,6,opt,name=priorityClassName"`
// Priority is the value of priority of pod groups created from this template. Various
// system components use this field to find the priority of the pod group. When
// Priority Admission Controller is enabled, it prevents users from setting this field.
// The admission controller populates this field from PriorityClassName.
// The higher the value, the higher the priority.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
//
// +featureGate=WorkloadAwarePreemption
// +optional
// +k8s:ifDisabled("WorkloadAwarePreemption")=+k8s:forbidden
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:optional
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:maximum=1000000000 # HighestUserDefinablePriority
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:minimum=-2147483648
Priority *int32 `json:"priority,omitempty" protobuf:"varint,7,opt,name=priority"`
}
// PodGroupSchedulingPolicy defines the scheduling configuration for a PodGroup.
@ -259,6 +300,20 @@ type PodGroupResourceClaim struct {
ResourceClaimTemplateName *string `json:"resourceClaimTemplateName,omitempty" protobuf:"bytes,3,opt,name=resourceClaimTemplateName"`
}
// DisruptionMode describes the mode in which a PodGroup can be disrupted (e.g. preempted).
// +enum
// +k8s:enum
type DisruptionMode string
const (
// DisruptionModePod means that individual pods can be disrupted or preempted independently.
// It doesn't depend on exact set of pods currently running in this PodGroup.
DisruptionModePod DisruptionMode = "Pod"
// DisruptionModePodGroup means that the whole PodGroup needs to be disrupted
// or preempted together.
DisruptionModePodGroup DisruptionMode = "PodGroup"
)
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:supportsSubresource="/status"
@ -307,7 +362,7 @@ type PodGroupSpec struct {
//
// +optional
// +k8s:optional
// +k8s:alpha(since:"1.36")=+k8s:immutable
// +k8s:immutable
PodGroupTemplateRef *PodGroupTemplateReference `json:"podGroupTemplateRef" protobuf:"bytes,1,opt,name=podGroupTemplateRef"`
// SchedulingPolicy defines the scheduling policy for this instance of the PodGroup.
@ -315,7 +370,7 @@ type PodGroupSpec struct {
// This field is immutable.
//
// +required
// +k8s:alpha(since:"1.36")=+k8s:immutable
// +k8s:immutable
SchedulingPolicy PodGroupSchedulingPolicy `json:"schedulingPolicy" protobuf:"bytes,2,opt,name=schedulingPolicy"`
// SchedulingConstraints defines optional scheduling constraints (e.g. topology) for this PodGroup.
@ -350,9 +405,59 @@ type PodGroupSpec struct {
// +k8s:listType=map
// +k8s:listMapKey=name
// +k8s:maxItems=4
// +k8s:alpha(since:"1.36")=+k8s:immutable
// +k8s:immutable
// +featureGate=DRAWorkloadResourceClaims
ResourceClaims []PodGroupResourceClaim `json:"resourceClaims,omitempty" patchStrategy:"merge,retainKeys" patchMergeKey:"name" protobuf:"bytes,4,rep,name=resourceClaims"`
// DisruptionMode defines the mode in which a given PodGroup can be disrupted.
// Controllers are expected to fill this field by copying it from a PodGroupTemplate.
// One of Pod, PodGroup. Defaults to Pod if unset.
// This field is immutable.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
//
// +featureGate=WorkloadAwarePreemption
// +optional
// +k8s:ifDisabled("WorkloadAwarePreemption")=+k8s:forbidden
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:optional
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:immutable
// +default="Pod"
DisruptionMode *DisruptionMode `json:"disruptionMode,omitempty" protobuf:"bytes,5,opt,name=disruptionMode,casttype=DisruptionMode"`
// PriorityClassName defines the priority that should be considered when scheduling this pod group.
// Controllers are expected to fill this field by copying it from a PodGroupTemplate.
// Otherwise, it is validated and resolved similarly to the PriorityClassName on PodGroupTemplate
// (i.e. if no priority class is specified, admission control can set this to the global default
// priority class if it exists. Otherwise, the pod group's priority will be zero).
// This field is immutable.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
//
// +featureGate=WorkloadAwarePreemption
// +optional
// +k8s:ifDisabled("WorkloadAwarePreemption")=+k8s:forbidden
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:optional
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:format=k8s-long-name
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:immutable
PriorityClassName string `json:"priorityClassName,omitempty" protobuf:"bytes,6,opt,name=priorityClassName"`
// Priority is the value of priority of this pod group. Various system components
// use this field to find the priority of the pod group. When Priority Admission
// Controller is enabled, it prevents users from setting this field. The admission
// controller populates this field from PriorityClassName.
// The higher the value, the higher the priority.
// This field is immutable.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
//
// +featureGate=WorkloadAwarePreemption
// +optional
// +k8s:ifDisabled("WorkloadAwarePreemption")=+k8s:forbidden
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:optional
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:immutable
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:maximum=1000000000 # HighestUserDefinablePriority
// +k8s:ifEnabled("WorkloadAwarePreemption")=+k8s:minimum=-2147483648
Priority *int32 `json:"priority,omitempty" protobuf:"varint,7,opt,name=priority"`
}
// PodGroupStatus represents information about the status of a pod group.

View file

@ -111,6 +111,9 @@ var map_PodGroupSpec = map[string]string{
"schedulingPolicy": "SchedulingPolicy defines the scheduling policy for this instance of the PodGroup. Controllers are expected to fill this field by copying it from a PodGroupTemplate. This field is immutable.",
"schedulingConstraints": "SchedulingConstraints defines optional scheduling constraints (e.g. topology) for this PodGroup. Controllers are expected to fill this field by copying it from a PodGroupTemplate. This field is immutable. This field is only available when the TopologyAwareWorkloadScheduling feature gate is enabled.",
"resourceClaims": "ResourceClaims defines which ResourceClaims may be shared among Pods in the group. Pods consume the devices allocated to a PodGroup's claim by defining a claim in its own Spec.ResourceClaims that matches the PodGroup's claim exactly. The claim must have the same name and refer to the same ResourceClaim or ResourceClaimTemplate.\n\nThis is an alpha-level field and requires that the DRAWorkloadResourceClaims feature gate is enabled.\n\nThis field is immutable.",
"disruptionMode": "DisruptionMode defines the mode in which a given PodGroup can be disrupted. Controllers are expected to fill this field by copying it from a PodGroupTemplate. One of Pod, PodGroup. Defaults to Pod if unset. This field is immutable. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
"priorityClassName": "PriorityClassName defines the priority that should be considered when scheduling this pod group. Controllers are expected to fill this field by copying it from a PodGroupTemplate. Otherwise, it is validated and resolved similarly to the PriorityClassName on PodGroupTemplate (i.e. if no priority class is specified, admission control can set this to the global default priority class if it exists. Otherwise, the pod group's priority will be zero). This field is immutable. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
"priority": "Priority is the value of priority of this pod group. Various system components use this field to find the priority of the pod group. When Priority Admission Controller is enabled, it prevents users from setting this field. The admission controller populates this field from PriorityClassName. The higher the value, the higher the priority. This field is immutable. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
}
func (PodGroupSpec) SwaggerDoc() map[string]string {
@ -133,6 +136,9 @@ var map_PodGroupTemplate = map[string]string{
"schedulingPolicy": "SchedulingPolicy defines the scheduling policy for this PodGroupTemplate.",
"schedulingConstraints": "SchedulingConstraints defines optional scheduling constraints (e.g. topology) for this PodGroupTemplate. This field is only available when the TopologyAwareWorkloadScheduling feature gate is enabled.",
"resourceClaims": "ResourceClaims defines which ResourceClaims may be shared among Pods in the group. Pods consume the devices allocated to a PodGroup's claim by defining a claim in its own Spec.ResourceClaims that matches the PodGroup's claim exactly. The claim must have the same name and refer to the same ResourceClaim or ResourceClaimTemplate.\n\nThis is an alpha-level field and requires that the DRAWorkloadResourceClaims feature gate is enabled.\n\nThis field is immutable.",
"disruptionMode": "DisruptionMode defines the mode in which a given PodGroup can be disrupted. One of Pod, PodGroup. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
"priorityClassName": "PriorityClassName indicates the priority that should be considered when scheduling a pod group created from this template. If no priority class is specified, admission control can set this to the global default priority class if it exists. Otherwise, pod groups created from this template will have the priority set to zero. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
"priority": "Priority is the value of priority of pod groups created from this template. Various system components use this field to find the priority of the pod group. When Priority Admission Controller is enabled, it prevents users from setting this field. The admission controller populates this field from PriorityClassName. The higher the value, the higher the priority. This field is available only when the WorkloadAwarePreemption feature gate is enabled.",
}
func (PodGroupTemplate) SwaggerDoc() map[string]string {

View file

@ -234,6 +234,16 @@ func (in *PodGroupSpec) DeepCopyInto(out *PodGroupSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.DisruptionMode != nil {
in, out := &in.DisruptionMode, &out.DisruptionMode
*out = new(DisruptionMode)
**out = **in
}
if in.Priority != nil {
in, out := &in.Priority, &out.Priority
*out = new(int32)
**out = **in
}
return
}
@ -293,6 +303,16 @@ func (in *PodGroupTemplate) DeepCopyInto(out *PodGroupTemplate) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.DisruptionMode != nil {
in, out := &in.DisruptionMode, &out.DisruptionMode
*out = new(DisruptionMode)
**out = **in
}
if in.Priority != nil {
in, out := &in.Priority, &out.Priority
*out = new(int32)
**out = **in
}
return
}

View file

@ -69,7 +69,10 @@
"resourceClaimName": "resourceClaimNameValue",
"resourceClaimTemplateName": "resourceClaimTemplateNameValue"
}
]
],
"disruptionMode": "disruptionModeValue",
"priorityClassName": "priorityClassNameValue",
"priority": 7
},
"status": {
"conditions": [

View file

@ -33,10 +33,13 @@ metadata:
selfLink: selfLinkValue
uid: uidValue
spec:
disruptionMode: disruptionModeValue
podGroupTemplateRef:
workload:
podGroupTemplateName: podGroupTemplateNameValue
workloadName: workloadNameValue
priority: 7
priorityClassName: priorityClassNameValue
resourceClaims:
- name: nameValue
resourceClaimName: resourceClaimNameValue

View file

@ -71,7 +71,10 @@
"resourceClaimName": "resourceClaimNameValue",
"resourceClaimTemplateName": "resourceClaimTemplateNameValue"
}
]
],
"disruptionMode": "disruptionModeValue",
"priorityClassName": "priorityClassNameValue",
"priority": 7
}
]
}

View file

@ -38,7 +38,10 @@ spec:
kind: kindValue
name: nameValue
podGroupTemplates:
- name: nameValue
- disruptionMode: disruptionModeValue
name: nameValue
priority: 7
priorityClassName: priorityClassNameValue
resourceClaims:
- name: nameValue
resourceClaimName: resourceClaimNameValue

View file

@ -14801,9 +14801,19 @@ var schemaYAML = typed.YAMLObject(`types:
- name: io.k8s.api.scheduling.v1alpha2.PodGroupSpec
map:
fields:
- name: disruptionMode
type:
scalar: string
default: Pod
- name: podGroupTemplateRef
type:
namedType: io.k8s.api.scheduling.v1alpha2.PodGroupTemplateReference
- name: priority
type:
scalar: numeric
- name: priorityClassName
type:
scalar: string
- name: resourceClaims
type:
list:
@ -14841,10 +14851,19 @@ var schemaYAML = typed.YAMLObject(`types:
- name: io.k8s.api.scheduling.v1alpha2.PodGroupTemplate
map:
fields:
- name: disruptionMode
type:
scalar: string
- name: name
type:
scalar: string
default: ""
- name: priority
type:
scalar: numeric
- name: priorityClassName
type:
scalar: string
- name: resourceClaims
type:
list:

View file

@ -18,6 +18,10 @@ limitations under the License.
package v1alpha2
import (
schedulingv1alpha2 "k8s.io/api/scheduling/v1alpha2"
)
// PodGroupSpecApplyConfiguration represents a declarative configuration of the PodGroupSpec type for use
// with apply.
//
@ -46,6 +50,31 @@ type PodGroupSpecApplyConfiguration struct {
//
// This field is immutable.
ResourceClaims []PodGroupResourceClaimApplyConfiguration `json:"resourceClaims,omitempty"`
// DisruptionMode defines the mode in which a given PodGroup can be disrupted.
// Controllers are expected to fill this field by copying it from a PodGroupTemplate.
// One of Pod, PodGroup. Defaults to Pod if unset.
// This field is immutable.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
DisruptionMode *schedulingv1alpha2.DisruptionMode `json:"disruptionMode,omitempty"`
// PriorityClassName defines the priority that should be considered when scheduling this pod group.
// Controllers are expected to fill this field by copying it from a PodGroupTemplate.
// Otherwise, it is validated and resolved similarly to the PriorityClassName on PodGroupTemplate
// (i.e. if no priority class is specified, admission control can set this to the global default
// priority class if it exists. Otherwise, the pod group's priority will be zero).
// This field is immutable.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
PriorityClassName *string `json:"priorityClassName,omitempty"`
// Priority is the value of priority of this pod group. Various system components
// use this field to find the priority of the pod group. When Priority Admission
// Controller is enabled, it prevents users from setting this field. The admission
// controller populates this field from PriorityClassName.
// The higher the value, the higher the priority.
// This field is immutable.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
Priority *int32 `json:"priority,omitempty"`
}
// PodGroupSpecApplyConfiguration constructs a declarative configuration of the PodGroupSpec type for use with
@ -90,3 +119,27 @@ func (b *PodGroupSpecApplyConfiguration) WithResourceClaims(values ...*PodGroupR
}
return b
}
// WithDisruptionMode sets the DisruptionMode field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the DisruptionMode field is set to the value of the last call.
func (b *PodGroupSpecApplyConfiguration) WithDisruptionMode(value schedulingv1alpha2.DisruptionMode) *PodGroupSpecApplyConfiguration {
b.DisruptionMode = &value
return b
}
// WithPriorityClassName sets the PriorityClassName field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the PriorityClassName field is set to the value of the last call.
func (b *PodGroupSpecApplyConfiguration) WithPriorityClassName(value string) *PodGroupSpecApplyConfiguration {
b.PriorityClassName = &value
return b
}
// WithPriority sets the Priority field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Priority field is set to the value of the last call.
func (b *PodGroupSpecApplyConfiguration) WithPriority(value int32) *PodGroupSpecApplyConfiguration {
b.Priority = &value
return b
}

View file

@ -18,6 +18,10 @@ limitations under the License.
package v1alpha2
import (
schedulingv1alpha2 "k8s.io/api/scheduling/v1alpha2"
)
// PodGroupTemplateApplyConfiguration represents a declarative configuration of the PodGroupTemplate type for use
// with apply.
//
@ -42,6 +46,26 @@ type PodGroupTemplateApplyConfiguration struct {
//
// This field is immutable.
ResourceClaims []PodGroupResourceClaimApplyConfiguration `json:"resourceClaims,omitempty"`
// DisruptionMode defines the mode in which a given PodGroup can be disrupted.
// One of Pod, PodGroup.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
DisruptionMode *schedulingv1alpha2.DisruptionMode `json:"disruptionMode,omitempty"`
// PriorityClassName indicates the priority that should be considered when scheduling
// a pod group created from this template. If no priority class is specified, admission
// control can set this to the global default priority class if it exists. Otherwise,
// pod groups created from this template will have the priority set to zero.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
PriorityClassName *string `json:"priorityClassName,omitempty"`
// Priority is the value of priority of pod groups created from this template. Various
// system components use this field to find the priority of the pod group. When
// Priority Admission Controller is enabled, it prevents users from setting this field.
// The admission controller populates this field from PriorityClassName.
// The higher the value, the higher the priority.
// This field is available only when the WorkloadAwarePreemption feature gate
// is enabled.
Priority *int32 `json:"priority,omitempty"`
}
// PodGroupTemplateApplyConfiguration constructs a declarative configuration of the PodGroupTemplate type for use with
@ -86,3 +110,27 @@ func (b *PodGroupTemplateApplyConfiguration) WithResourceClaims(values ...*PodGr
}
return b
}
// WithDisruptionMode sets the DisruptionMode field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the DisruptionMode field is set to the value of the last call.
func (b *PodGroupTemplateApplyConfiguration) WithDisruptionMode(value schedulingv1alpha2.DisruptionMode) *PodGroupTemplateApplyConfiguration {
b.DisruptionMode = &value
return b
}
// WithPriorityClassName sets the PriorityClassName field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the PriorityClassName field is set to the value of the last call.
func (b *PodGroupTemplateApplyConfiguration) WithPriorityClassName(value string) *PodGroupTemplateApplyConfiguration {
b.PriorityClassName = &value
return b
}
// WithPriority sets the Priority field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Priority field is set to the value of the last call.
func (b *PodGroupTemplateApplyConfiguration) WithPriority(value int32) *PodGroupTemplateApplyConfiguration {
b.Priority = &value
return b
}

View file

@ -68,9 +68,11 @@ func TestPodGroupScheduling(t *testing.T) {
PodGroupTemplate(st.MakePodGroupTemplate().Name("t").MinCount(3).Obj()).
Obj()
gangPodGroup := st.MakePodGroup().Name("pg1").TemplateRef("t1", "workload").MinCount(3).Obj()
gangPodGroup := st.MakePodGroup().Name("pg1").TemplateRef("t1", "workload").
Priority(100).MinCount(3).Obj()
otherGangPodGroup := st.MakePodGroup().Name("pg2").TemplateRef("t", "other-workload").MinCount(3).Obj()
otherGangPodGroup := st.MakePodGroup().Name("pg2").TemplateRef("t", "other-workload").
Priority(100).MinCount(3).Obj()
basicPodGroup := st.MakePodGroup().Name("pg1").TemplateRef("t2", "workload").BasicPolicy().Obj()
@ -108,8 +110,10 @@ func TestPodGroupScheduling(t *testing.T) {
midP2 := st.MakePod().Name("mid-p2").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").
PodGroupName("mid-pg").Priority(50).Obj()
midPodGroup := st.MakePodGroup().Name("mid-pg").TemplateRef("t-mid", "workload").MinCount(2).Obj()
midPodGroupWithConstraint := st.MakePodGroup().Name("mid-pg").TemplateRef("t-mid", "workload").MinCount(2).TopologyKey("topology.kubernetes.io/zone").Obj()
midPodGroup := st.MakePodGroup().Name("mid-pg").TemplateRef("t-mid", "workload").
Priority(50).MinCount(2).Obj()
midPodGroupWithConstraint := st.MakePodGroup().Name("mid-pg").TemplateRef("t-mid", "workload").
Priority(50).MinCount(2).TopologyKey("topology.kubernetes.io/zone").Obj()
otherP1 := st.MakePod().Name("other-p1").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").
PodGroupName("pg2").Priority(100).Obj()
@ -792,8 +796,10 @@ func TestWorkloadAwarePreemptionInvocation(t *testing.T) {
})
node := st.MakeNode().Name("node").Capacity(map[v1.ResourceName]string{v1.ResourceCPU: "4"}).Obj()
workload := st.MakeWorkload().Name("workload").PodGroupTemplate(st.MakePodGroupTemplate().Name("t1").MinCount(3).Obj()).Obj()
pg := st.MakePodGroup().Namespace("default").Name("pg1").TemplateRef("t1", "workload").MinCount(3).Obj()
pg := st.MakePodGroup().Namespace("default").Name("pg1").TemplateRef("t1", "workload").
DisruptionMode(schedulingapi.DisruptionModePodGroup).Priority(100).MinCount(3).Obj()
// Low priority pods taking up all resources
lowPods := []*v1.Pod{
@ -901,8 +907,10 @@ func TestPostFilterInvocationCount(t *testing.T) {
})
node := st.MakeNode().Name("node").Capacity(map[v1.ResourceName]string{v1.ResourceCPU: "4"}).Obj()
workload := st.MakeWorkload().Name("workload").PodGroupTemplate(st.MakePodGroupTemplate().Name("t1").MinCount(3).Obj()).Obj()
pg := st.MakePodGroup().Namespace("default").Name("pg1").TemplateRef("t1", "workload").MinCount(3).Obj()
pg := st.MakePodGroup().Namespace("default").Name("pg1").TemplateRef("t1", "workload").
DisruptionMode(schedulingapi.DisruptionModePodGroup).Priority(100).MinCount(3).Obj()
// Low priority pods taking up all resources
lowPods := []*v1.Pod{

View file

@ -64,7 +64,7 @@ func TestPodGroupPreemption(t *testing.T) {
st.MakeNode().Name("node1").Capacity(map[v1.ResourceName]string{v1.ResourceCPU: "3", v1.ResourceMemory: "4Gi", v1.ResourcePods: "32"}).Obj(),
},
podGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").Namespace("default").MinCount(3).Obj(),
st.MakePodGroup().Name("pg1").Namespace("default").Priority(100).MinCount(3).Obj(),
},
initialPods: []*v1.Pod{
st.MakePod().Name("low-1").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").ZeroTerminationGracePeriod().Priority(10).Obj(),
@ -86,7 +86,7 @@ func TestPodGroupPreemption(t *testing.T) {
st.MakeNode().Name("node1").Capacity(map[v1.ResourceName]string{v1.ResourceCPU: "3", v1.ResourceMemory: "4Gi", v1.ResourcePods: "32"}).Obj(),
},
podGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").Namespace("default").BasicPolicy().Obj(),
st.MakePodGroup().Name("pg1").Namespace("default").Priority(100).BasicPolicy().Obj(),
},
initialPods: []*v1.Pod{
st.MakePod().Name("low-1").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").ZeroTerminationGracePeriod().Priority(10).Obj(),
@ -109,7 +109,7 @@ func TestPodGroupPreemption(t *testing.T) {
st.MakeNode().Name("node2").Capacity(map[v1.ResourceName]string{v1.ResourceCPU: "2", v1.ResourceMemory: "4Gi", v1.ResourcePods: "32"}).Obj(),
},
podGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").Namespace("default").MinCount(3).Obj(),
st.MakePodGroup().Name("pg1").Namespace("default").Priority(100).MinCount(3).Obj(),
},
initialPods: []*v1.Pod{
// low-1 takes all CPU on node1
@ -135,7 +135,7 @@ func TestPodGroupPreemption(t *testing.T) {
st.MakeNode().Name("node2").Capacity(map[v1.ResourceName]string{v1.ResourceCPU: "2", v1.ResourceMemory: "4Gi", v1.ResourcePods: "32"}).Obj(),
},
podGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").Namespace("default").BasicPolicy().Obj(),
st.MakePodGroup().Name("pg1").Namespace("default").Priority(100).BasicPolicy().Obj(),
},
initialPods: []*v1.Pod{
// low-1 takes half CPU on node1
@ -160,7 +160,7 @@ func TestPodGroupPreemption(t *testing.T) {
st.MakeNode().Name("node1").Capacity(map[v1.ResourceName]string{v1.ResourceCPU: "3", v1.ResourceMemory: "4Gi", v1.ResourcePods: "32"}).Obj(),
},
podGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").Namespace("default").MinCount(2).Obj(),
st.MakePodGroup().Name("pg1").Namespace("default").Priority(100).MinCount(2).Obj(),
},
initialPods: []*v1.Pod{
st.MakePod().Name("low-1").Label("app", "foo").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").ZeroTerminationGracePeriod().Priority(10).Obj(),
@ -189,7 +189,7 @@ func TestPodGroupPreemption(t *testing.T) {
st.MakeNode().Name("node2").Capacity(map[v1.ResourceName]string{v1.ResourceCPU: "2", v1.ResourceMemory: "4Gi", v1.ResourcePods: "32"}).Obj(),
},
podGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").Namespace("default").MinCount(4).Obj(),
st.MakePodGroup().Name("pg1").Namespace("default").Priority(100).MinCount(4).Obj(),
},
initialPods: []*v1.Pod{
st.MakePod().Name("low-1").Node("node1").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").ZeroTerminationGracePeriod().Priority(10).Obj(),
@ -213,7 +213,7 @@ func TestPodGroupPreemption(t *testing.T) {
st.MakeNode().Name("node1").Capacity(map[v1.ResourceName]string{v1.ResourceCPU: "3", v1.ResourceMemory: "4Gi", v1.ResourcePods: "32"}).Obj(),
},
podGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").Namespace("default").MinCount(3).Obj(),
st.MakePodGroup().Name("pg1").Namespace("default").Priority(100).MinCount(3).Obj(),
},
initialPods: []*v1.Pod{
st.MakePod().Name("mid-1").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").ZeroTerminationGracePeriod().Priority(500).Obj(),
@ -236,7 +236,7 @@ func TestPodGroupPreemption(t *testing.T) {
st.MakeNode().Name("node1").Capacity(map[v1.ResourceName]string{v1.ResourceCPU: "3", v1.ResourceMemory: "4Gi", v1.ResourcePods: "32"}).Obj(),
},
podGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").Namespace("default").MinCount(2).Obj(),
st.MakePodGroup().Name("pg1").Namespace("default").Priority(100).MinCount(2).Obj(),
},
initialPods: []*v1.Pod{
st.MakePod().Name("mid-1").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").ZeroTerminationGracePeriod().Priority(50).Obj(),
@ -251,6 +251,111 @@ func TestPodGroupPreemption(t *testing.T) {
expectedPreempted: []string{"low-1", "low-2"},
expectedPodsPreemptedByWAP: 2,
},
{
name: "Preempt the whole PodGroup even if preempting a single Pod would suffice",
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(map[v1.ResourceName]string{v1.ResourceCPU: "3", v1.ResourceMemory: "4Gi", v1.ResourcePods: "32"}).Obj(),
},
podGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").Namespace("default").Priority(100).MinCount(1).Obj(),
st.MakePodGroup().Name("pg2").Namespace("default").DisruptionMode(schedulingapi.DisruptionModePodGroup).Priority(10).MinCount(3).Obj(),
},
initialPods: []*v1.Pod{
st.MakePod().Name("low-1").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").PodGroupName("pg2").ZeroTerminationGracePeriod().Priority(10).Obj(),
st.MakePod().Name("low-2").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").PodGroupName("pg2").ZeroTerminationGracePeriod().Priority(10).Obj(),
st.MakePod().Name("low-3").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").PodGroupName("pg2").ZeroTerminationGracePeriod().Priority(10).Obj(),
},
preemptorPods: []*v1.Pod{
st.MakePod().Name("high-1").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").PodGroupName("pg1").ZeroTerminationGracePeriod().Priority(100).Obj(),
},
expectedScheduled: []string{"high-1"},
expectedPreempted: []string{"low-1", "low-2", "low-3"},
expectedPodsPreemptedByWAP: 3,
},
{
name: "Preempt the whole basic PodGroup with a PodGroup disruption mode",
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(map[v1.ResourceName]string{v1.ResourceCPU: "3", v1.ResourceMemory: "4Gi", v1.ResourcePods: "32"}).Obj(),
},
podGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").Namespace("default").Priority(100).MinCount(1).Obj(),
st.MakePodGroup().Name("pg2").Namespace("default").DisruptionMode(schedulingapi.DisruptionModePodGroup).Priority(10).BasicPolicy().Obj(),
},
initialPods: []*v1.Pod{
st.MakePod().Name("low-1").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").PodGroupName("pg2").ZeroTerminationGracePeriod().Priority(10).Obj(),
st.MakePod().Name("low-2").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").PodGroupName("pg2").ZeroTerminationGracePeriod().Priority(10).Obj(),
st.MakePod().Name("low-3").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").PodGroupName("pg2").ZeroTerminationGracePeriod().Priority(10).Obj(),
},
preemptorPods: []*v1.Pod{
st.MakePod().Name("high-1").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").PodGroupName("pg1").ZeroTerminationGracePeriod().Priority(100).Obj(),
},
expectedScheduled: []string{"high-1"},
expectedPreempted: []string{"low-1", "low-2", "low-3"},
expectedPodsPreemptedByWAP: 3,
},
{
name: "Priority divergence in PodGroups - preemptor PodGroup has higher priority than the victim candidate PodGroup",
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(map[v1.ResourceName]string{v1.ResourceCPU: "3", v1.ResourceMemory: "4Gi", v1.ResourcePods: "32"}).Obj(),
},
podGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").Namespace("default").Priority(100).MinCount(1).Obj(),
st.MakePodGroup().Name("pg2").Namespace("default").DisruptionMode(schedulingapi.DisruptionModePodGroup).Priority(10).MinCount(3).Obj(),
},
initialPods: []*v1.Pod{
st.MakePod().Name("high-1").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").PodGroupName("pg2").ZeroTerminationGracePeriod().Priority(100).Obj(),
st.MakePod().Name("high-2").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").PodGroupName("pg2").ZeroTerminationGracePeriod().Priority(100).Obj(),
st.MakePod().Name("high-3").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").PodGroupName("pg2").ZeroTerminationGracePeriod().Priority(100).Obj(),
},
preemptorPods: []*v1.Pod{
st.MakePod().Name("low-1").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").PodGroupName("pg1").ZeroTerminationGracePeriod().Priority(10).Obj(),
},
expectedScheduled: []string{"low-1"},
expectedPreempted: []string{"high-1", "high-2", "high-3"},
expectedPodsPreemptedByWAP: 3,
},
{
name: "Priority divergence in PodGroups - preemptor PodGroup has too low priority",
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(map[v1.ResourceName]string{v1.ResourceCPU: "3", v1.ResourceMemory: "4Gi", v1.ResourcePods: "32"}).Obj(),
},
podGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").Namespace("default").Priority(10).MinCount(1).Obj(),
st.MakePodGroup().Name("pg2").Namespace("default").DisruptionMode(schedulingapi.DisruptionModePodGroup).Priority(100).MinCount(3).Obj(),
},
initialPods: []*v1.Pod{
st.MakePod().Name("low-1").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").PodGroupName("pg2").ZeroTerminationGracePeriod().Priority(10).Obj(),
st.MakePod().Name("low-2").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").PodGroupName("pg2").ZeroTerminationGracePeriod().Priority(10).Obj(),
st.MakePod().Name("low-3").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").PodGroupName("pg2").ZeroTerminationGracePeriod().Priority(10).Obj(),
},
preemptorPods: []*v1.Pod{
st.MakePod().Name("high-1").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").PodGroupName("pg1").ZeroTerminationGracePeriod().Priority(100).Obj(),
},
expectedScheduled: []string{"low-1", "low-2", "low-3"},
expectedPreempted: []string{},
expectedUnschedulable: []string{"high-1"},
expectedPodsPreemptedByWAP: 0,
},
{
name: "Preemptor Pod without PodGroupName does not respect the PodGroup disruption mode",
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(map[v1.ResourceName]string{v1.ResourceCPU: "3", v1.ResourceMemory: "4Gi", v1.ResourcePods: "32"}).Obj(),
},
podGroups: []*schedulingapi.PodGroup{
st.MakePodGroup().Name("pg1").Namespace("default").Priority(10).MinCount(1).Obj(),
},
initialPods: []*v1.Pod{
st.MakePod().Name("low-1").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").PodGroupName("pg1").ZeroTerminationGracePeriod().Priority(30).Obj(),
st.MakePod().Name("low-2").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").PodGroupName("pg1").ZeroTerminationGracePeriod().Priority(20).Obj(),
st.MakePod().Name("low-3").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").PodGroupName("pg1").ZeroTerminationGracePeriod().Priority(10).Obj(),
},
preemptorPods: []*v1.Pod{
st.MakePod().Name("high-1").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Container("image").ZeroTerminationGracePeriod().Priority(100).Obj(),
},
expectedScheduled: []string{"high-1", "low-1", "low-2"},
expectedPreempted: []string{"low-3"},
expectedPodsPreemptedByWAP: 0,
},
}
for _, tt := range tests {