kubernetes/test/declarative_validation/scheduling/workload/declarative_validation_test.go

806 lines
40 KiB
Go

/*
Copyright 2025 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 workload
import (
"fmt"
"strconv"
"strings"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
apitesting "k8s.io/kubernetes/pkg/api/testing"
"k8s.io/kubernetes/pkg/apis/scheduling"
"k8s.io/kubernetes/pkg/features"
registry "k8s.io/kubernetes/pkg/registry/scheduling/workload"
// Ensure all API groups are registered with the scheme
_ "k8s.io/kubernetes/pkg/apis/scheduling/install"
)
func TestDeclarativeValidate(t *testing.T) {
for _, apiVersion := range apiVersions {
t.Run(apiVersion, func(t *testing.T) {
testDeclarativeValidate(t, apiVersion)
})
}
}
func testDeclarativeValidate(t *testing.T, apiVersion string) {
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "scheduling.k8s.io",
APIVersion: apiVersion,
Resource: "workloads",
IsResourceRequest: true,
Verb: "create",
})
testCases := map[string]struct {
input scheduling.Workload
enableTopologyAwareScheduling bool
enableDRAWorkloadResourceClaims bool
enableWorkloadAwarePreemption bool
expectedErrs field.ErrorList
}{
"valid": {
input: mkValidWorkload(),
},
"empty podGroupTemplates": {
input: mkValidWorkload(clearPodGroupTemplates()),
expectedErrs: field.ErrorList{field.Required(field.NewPath("spec", "podGroupTemplates"), "must have at least one item")},
},
"too many podGroupTemplates": {
input: mkValidWorkload(setManyPodGroupTemplates(scheduling.WorkloadMaxPodGroupTemplates + 1)),
expectedErrs: field.ErrorList{field.TooMany(field.NewPath("spec", "podGroupTemplates"), scheduling.WorkloadMaxPodGroupTemplates+1, scheduling.WorkloadMaxPodGroupTemplates).WithOrigin("maxItems")},
},
"empty podGroupTemplate name": {
input: mkValidWorkload(setPodGroupName(0, "")),
expectedErrs: field.ErrorList{field.Required(field.NewPath("spec", "podGroupTemplates").Index(0).Child("name"), "")},
},
"invalid podGroupTemplate name": {
input: mkValidWorkload(setPodGroupName(0, "Invalid_Name")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("name"), nil, "").WithOrigin("format=k8s-short-name")},
},
"duplicate podGroupTemplate names": {
input: mkValidWorkload(addPodGroupTemplate("main")),
expectedErrs: field.ErrorList{field.Duplicate(field.NewPath("spec", "podGroupTemplates").Index(1), nil)},
},
// Declarative validation treats 0 as "missing" and returns Required error
// instead of checking minimum constraint and returning Invalid error.
"gang minCount zero": {
input: mkValidWorkload(setPodGroupMinCount(0, 0)),
expectedErrs: field.ErrorList{field.Required(field.NewPath("spec", "podGroupTemplates").Index(0).Child("schedulingPolicy", "gang", "minCount"), "")},
},
"gang minCount negative": {
input: mkValidWorkload(setPodGroupMinCount(0, -1)),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("schedulingPolicy", "gang", "minCount"), nil, "").WithOrigin("minimum")},
},
"valid with controllerRef": {
input: mkValidWorkload(setControllerRef("apps", "Deployment", "my-deployment")),
},
"controllerRef with empty APIGroup": {
input: mkValidWorkload(setControllerRef("", "Pod", "my-pod")),
},
"controllerRef invalid APIGroup": {
input: mkValidWorkload(setControllerRef("invalid_api_group", "Deployment", "my-deployment")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "controllerRef", "apiGroup"), nil, "").WithOrigin("format=k8s-long-name")},
},
"controllerRef too long APIGroup": {
input: mkValidWorkload(setControllerRef(strings.Repeat("g", 254), "Deployment", "my-deployment")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "controllerRef", "apiGroup"), nil, "").WithOrigin("format=k8s-long-name")},
},
"controllerRef missing kind": {
input: mkValidWorkload(setControllerRef("apps", "", "my-deployment")),
expectedErrs: field.ErrorList{field.Required(field.NewPath("spec", "controllerRef", "kind"), "")},
},
"controllerRef invalid kind with slash": {
input: mkValidWorkload(setControllerRef("apps", "Deploy/ment", "my-deployment")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "controllerRef", "kind"), nil, "").WithOrigin("format=k8s-path-segment-name")},
},
"controllerRef missing name": {
input: mkValidWorkload(setControllerRef("apps", "Deployment", "")),
expectedErrs: field.ErrorList{field.Required(field.NewPath("spec", "controllerRef", "name"), "")},
},
"controllerRef invalid name": {
input: mkValidWorkload(setControllerRef("apps", "Deployment", "/invalid-name")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "controllerRef", "name"), nil, "").WithOrigin("format=k8s-path-segment-name")},
},
"controllerRef invalid kind with percent": {
input: mkValidWorkload(setControllerRef("apps", "Deploy%ment", "my-deployment")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "controllerRef", "kind"), nil, "").WithOrigin("format=k8s-path-segment-name")},
},
"controllerRef invalid name with percent": {
input: mkValidWorkload(setControllerRef("apps", "Deployment", "my%deployment")),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "controllerRef", "name"), nil, "").WithOrigin("format=k8s-path-segment-name")},
},
"policy with neither basic nor gang": {
input: mkValidWorkload(clearPodGroupPolicy(0)),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("schedulingPolicy"), nil, "").WithOrigin("union")},
},
"policy with both basic and gang": {
input: mkValidWorkload(setBothPolicies(0)),
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("schedulingPolicy"), nil, "").WithOrigin("union")},
},
"valid with basic policy": {
input: mkValidWorkload(setBasicPolicy(0)),
},
"valid with schedulingConstraints": {
input: mkValidWorkload(addTopologyConstraint(0, "foo")),
enableTopologyAwareScheduling: true,
},
"valid with empty schedulingConstraints": {
input: mkValidWorkload(setSchedulingConstraints(0)),
enableTopologyAwareScheduling: true,
},
"with multiple topology constraints": {
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, "")),
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")),
enableTopologyAwareScheduling: true,
},
"valid with topology key with prefix with max length": {
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")),
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))),
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))),
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")),
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(setDisruptionModeSingle(0)),
enableWorkloadAwarePreemption: true,
},
"pod disruption mode, workload aware preemption disabled": {
input: mkValidWorkload(setDisruptionModeSingle(0)),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "podGroupTemplates").Index(0).Child("disruptionMode"), "")},
},
"pod group disruption mode, workload aware preemption enabled": {
input: mkValidWorkload(setDisruptionModeAll(0)),
enableWorkloadAwarePreemption: true,
},
"pod group disruption mode, workload aware preemption disabled": {
input: mkValidWorkload(setDisruptionModeAll(0)),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "podGroupTemplates").Index(0).Child("disruptionMode"), "")},
},
"disruption mode with neither single nor all, workload aware preemption enabled": {
input: mkValidWorkload(setDisruptionModeNeither(0)),
enableWorkloadAwarePreemption: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("disruptionMode"), nil, "").WithOrigin("union")},
},
"disruption mode with neither single nor all, workload aware preemption disabled": {
input: mkValidWorkload(setDisruptionModeNeither(0)),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "podGroupTemplates").Index(0).Child("disruptionMode"), "")},
},
"disruption mode with both single and all, workload aware preemption enabled": {
input: mkValidWorkload(setDisruptionModeBoth(0)),
enableWorkloadAwarePreemption: true,
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("disruptionMode"), nil, "").WithOrigin("union")},
},
"disruption mode with both single and all, workload aware preemption disabled": {
input: mkValidWorkload(setDisruptionModeBoth(0)),
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")})),
},
"ok resourceClaimTemplateName reference": {
input: mkValidWorkload(addResourceClaims(scheduling.PodGroupResourceClaim{Name: "claim", ResourceClaimTemplateName: new("resource-claim-template")})),
},
"ok multiple claims": {
input: mkValidWorkload(addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "claim-1", ResourceClaimName: new("resource-claim-1")},
scheduling.PodGroupResourceClaim{Name: "claim-2", ResourceClaimName: new("resource-claim-2")},
)),
},
"claim name with prefix": {
input: mkValidWorkload(addResourceClaims(scheduling.PodGroupResourceClaim{Name: "../my-claim", ResourceClaimName: new("resource-claim")})),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("resourceClaims").Index(0).Child("name"), nil, "").WithOrigin("format=k8s-short-name"),
},
},
"claim name with path": {
input: mkValidWorkload(addResourceClaims(scheduling.PodGroupResourceClaim{Name: "my/claim", ResourceClaimName: new("resource-claim")})),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("resourceClaims").Index(0).Child("name"), nil, "").WithOrigin("format=k8s-short-name"),
},
},
"duplicate claim entries": {
input: mkValidWorkload(addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-claim", ResourceClaimName: new("resource-claim-1")},
scheduling.PodGroupResourceClaim{Name: "my-claim", ResourceClaimName: new("resource-claim-2")},
)),
expectedErrs: field.ErrorList{
field.Duplicate(field.NewPath("spec", "podGroupTemplates").Index(0).Child("resourceClaims").Index(1), nil),
},
},
"resource claim source empty": {
input: mkValidWorkload(addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-claim"},
)),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("resourceClaims").Index(0), nil, "").WithOrigin("union"),
},
},
"resource claim reference and template": {
input: mkValidWorkload(addResourceClaims(
scheduling.PodGroupResourceClaim{
Name: "my-claim",
ResourceClaimName: new("resource-claim"),
ResourceClaimTemplateName: new("resource-claim-template"),
},
)),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("resourceClaims").Index(0), nil, "").WithOrigin("union"),
},
},
"invalid claim reference name": {
input: mkValidWorkload(addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-claim", ResourceClaimName: new(".foo_bar")},
)),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("resourceClaims").Index(0).Child("resourceClaimName"), nil, "").WithOrigin("format=k8s-long-name"),
},
},
"invalid claim template name": {
input: mkValidWorkload(addResourceClaims(
scheduling.PodGroupResourceClaim{Name: "my-claim", ResourceClaimTemplateName: new(".foo_bar")},
)),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "podGroupTemplates").Index(0).Child("resourceClaims").Index(0).Child("resourceClaimTemplateName"), nil, "").WithOrigin("format=k8s-long-name"),
},
},
"too many claims": {
input: mkValidWorkload(func(pg *scheduling.Workload) {
for i := range scheduling.MaxPodGroupResourceClaims + 1 {
pg.Spec.PodGroupTemplates[0].ResourceClaims = append(pg.Spec.PodGroupTemplates[0].ResourceClaims, scheduling.PodGroupResourceClaim{
Name: "my-claim-" + strconv.Itoa(i),
ResourceClaimName: new("resource-claim"),
})
}
}),
expectedErrs: field.ErrorList{
field.TooMany(field.NewPath("spec", "podGroupTemplates").Index(0).Child("resourceClaims"), scheduling.MaxPodGroupResourceClaims+1, scheduling.MaxPodGroupResourceClaims).WithOrigin("maxItems"),
},
},
"empty claim name": {
input: mkValidWorkload(addResourceClaims(scheduling.PodGroupResourceClaim{Name: "", ResourceClaimName: new("resource-claim")})),
expectedErrs: field.ErrorList{
field.Required(field.NewPath("spec", "podGroupTemplates").Index(0).Child("resourceClaims").Index(0).Child("name"), ""),
},
},
"schedulingConstraints set with TAS disabled": {
input: mkValidWorkload(setSchedulingConstraints(0)),
expectedErrs: field.ErrorList{field.Forbidden(field.NewPath("spec", "podGroupTemplates").Index(0).Child("schedulingConstraints"), "")},
},
}
for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
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, registry.Strategy, tc.expectedErrs)
})
}
}
func TestDeclarativeValidateUpdate(t *testing.T) {
for _, apiVersion := range apiVersions {
t.Run(apiVersion, func(t *testing.T) {
testDeclarativeValidateUpdate(t, apiVersion)
})
}
}
func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) {
testCases := map[string]struct {
oldObj scheduling.Workload
updateObj scheduling.Workload
enableTopologyAwareScheduling bool
enableDRAWorkloadResourceClaims bool
enableWorkloadAwarePreemption bool
expectedErrs field.ErrorList
}{
"valid update": {
oldObj: mkValidWorkload(setResourceVersion("1")),
updateObj: mkValidWorkload(setResourceVersion("1")),
},
"valid update with unchanged controllerRef": {
oldObj: mkValidWorkload(setResourceVersion("1"), setControllerRef("apps", "Deployment", "my-deployment")),
updateObj: mkValidWorkload(setResourceVersion("1"), setControllerRef("apps", "Deployment", "my-deployment")),
},
"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, "").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, "").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, "").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"), 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": {
oldObj: mkValidWorkload(setResourceVersion("1")),
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, "").WithOrigin("immutable"),
},
},
"add podGroupTemplate": {
oldObj: mkValidWorkload(setResourceVersion("1")),
updateObj: mkValidWorkload(setResourceVersion("1"), addPodGroupTemplate("worker1")),
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, "").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, "").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, "").WithOrigin("immutable")},
},
"valid update with unchanged scheduling constraints": {
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)),
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")),
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")),
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")),
updateObj: mkValidWorkload(setResourceVersion("1"), addTopologyConstraint(0, "foo")),
},
"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")},
},
"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")},
},
"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")},
},
"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"), setDisruptionModeSingle(0)),
updateObj: mkValidWorkload(setResourceVersion("1"), setDisruptionModeAll(0)),
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"), setDisruptionModeSingle(0)),
updateObj: mkValidWorkload(setResourceVersion("1"), setDisruptionModeAll(0)),
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: 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",
APIGroup: "scheduling.k8s.io",
APIVersion: apiVersion,
Resource: "workloads",
Name: "valid-workload",
IsResourceRequest: true,
Verb: "update",
})
apitesting.VerifyUpdateValidationEquivalence(t, ctx, &tc.updateObj, &tc.oldObj, registry.Strategy, tc.expectedErrs)
})
}
}
func mkValidWorkload(tweaks ...func(obj *scheduling.Workload)) scheduling.Workload {
obj := scheduling.Workload{
ObjectMeta: metav1.ObjectMeta{
Name: "valid-workload",
Namespace: "default",
},
Spec: scheduling.WorkloadSpec{
PodGroupTemplates: []scheduling.PodGroupTemplate{
{
Name: "main",
SchedulingPolicy: scheduling.PodGroupSchedulingPolicy{
Gang: &scheduling.GangSchedulingPolicy{
MinCount: 1,
},
},
},
},
},
}
for _, tweak := range tweaks {
tweak(&obj)
}
return obj
}
func setResourceVersion(v string) func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.ResourceVersion = v
}
}
func clearPodGroupTemplates() func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.Spec.PodGroupTemplates = nil
}
}
func setEmptyPodGroupTemplates() func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.Spec.PodGroupTemplates = []scheduling.PodGroupTemplate{}
}
}
func setManyPodGroupTemplates(n int) func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.Spec.PodGroupTemplates = make([]scheduling.PodGroupTemplate, n)
for i := range obj.Spec.PodGroupTemplates {
obj.Spec.PodGroupTemplates[i] = scheduling.PodGroupTemplate{
Name: fmt.Sprintf("group-%d", i),
SchedulingPolicy: scheduling.PodGroupSchedulingPolicy{
Gang: &scheduling.GangSchedulingPolicy{
MinCount: 1,
},
},
}
}
}
}
func addPodGroupTemplate(name string) func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.Spec.PodGroupTemplates = append(obj.Spec.PodGroupTemplates, scheduling.PodGroupTemplate{
Name: name,
SchedulingPolicy: scheduling.PodGroupSchedulingPolicy{
Gang: &scheduling.GangSchedulingPolicy{
MinCount: 1,
},
},
})
}
}
func setPodGroupName(pgIdx int, name string) func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.Spec.PodGroupTemplates[pgIdx].Name = name
}
}
func setPodGroupMinCount(pgIdx, min int) func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.Spec.PodGroupTemplates[pgIdx].SchedulingPolicy.Gang.MinCount = int32(min)
}
}
func clearPodGroupPolicy(pgIdx int) func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.Spec.PodGroupTemplates[pgIdx].SchedulingPolicy = scheduling.PodGroupSchedulingPolicy{}
}
}
func setBasicPolicy(pgIdx int) func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.Spec.PodGroupTemplates[pgIdx].SchedulingPolicy = scheduling.PodGroupSchedulingPolicy{
Basic: &scheduling.BasicSchedulingPolicy{},
}
}
}
func setBothPolicies(pgIdx int) func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.Spec.PodGroupTemplates[pgIdx].SchedulingPolicy = scheduling.PodGroupSchedulingPolicy{
Basic: &scheduling.BasicSchedulingPolicy{},
Gang: &scheduling.GangSchedulingPolicy{MinCount: 1},
}
}
}
func setControllerRef(apiGroup, kind, name string) func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.Spec.ControllerRef = &scheduling.TypedLocalObjectReference{
APIGroup: apiGroup,
Kind: kind,
Name: name,
}
}
}
func setSchedulingConstraints(pgIdx int) func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.Spec.PodGroupTemplates[pgIdx].SchedulingConstraints = &scheduling.PodGroupSchedulingConstraints{}
}
}
func addTopologyConstraint(pgIdx int, topologyKey string) func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
if obj.Spec.PodGroupTemplates[pgIdx].SchedulingConstraints == nil {
setSchedulingConstraints(pgIdx)(obj)
}
obj.Spec.PodGroupTemplates[pgIdx].SchedulingConstraints.Topology = append(obj.Spec.PodGroupTemplates[pgIdx].SchedulingConstraints.Topology,
scheduling.TopologyConstraint{Key: topologyKey})
}
}
func addResourceClaims(claims ...scheduling.PodGroupResourceClaim) func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.Spec.PodGroupTemplates[0].ResourceClaims = append(obj.Spec.PodGroupTemplates[0].ResourceClaims, claims...)
}
}
func setDisruptionModeSingle(pgIdx int) func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.Spec.PodGroupTemplates[pgIdx].DisruptionMode = &scheduling.DisruptionMode{
Single: &scheduling.SingleDisruptionMode{},
}
}
}
func setDisruptionModeAll(pgIdx int) func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.Spec.PodGroupTemplates[pgIdx].DisruptionMode = &scheduling.DisruptionMode{
All: &scheduling.AllDisruptionMode{},
}
}
}
func setDisruptionModeNeither(pgIdx int) func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.Spec.PodGroupTemplates[pgIdx].DisruptionMode = &scheduling.DisruptionMode{}
}
}
func setDisruptionModeBoth(pgIdx int) func(obj *scheduling.Workload) {
return func(obj *scheduling.Workload) {
obj.Spec.PodGroupTemplates[pgIdx].DisruptionMode = &scheduling.DisruptionMode{
Single: &scheduling.SingleDisruptionMode{},
All: &scheduling.AllDisruptionMode{},
}
}
}
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)
}
}