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

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package webhooks
    18  
    19  import (
    20  	"context"
    21  	"testing"
    22  	"time"
    23  
    24  	"github.com/google/go-cmp/cmp"
    25  	"github.com/google/go-cmp/cmp/cmpopts"
    26  	corev1 "k8s.io/api/core/v1"
    27  	"k8s.io/apimachinery/pkg/api/resource"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/util/validation/field"
    30  	"k8s.io/utils/ptr"
    31  
    32  	kueue "sigs.k8s.io/kueue/apis/kueue/v1beta1"
    33  	"sigs.k8s.io/kueue/pkg/constants"
    34  	testingutil "sigs.k8s.io/kueue/pkg/util/testing"
    35  )
    36  
    37  const (
    38  	testWorkloadName      = "test-workload"
    39  	testWorkloadNamespace = "test-ns"
    40  )
    41  
    42  func TestWorkloadWebhookDefault(t *testing.T) {
    43  	cases := map[string]struct {
    44  		wl     kueue.Workload
    45  		wantWl kueue.Workload
    46  	}{
    47  		"add default podSet name": {
    48  			wl: kueue.Workload{
    49  				Spec: kueue.WorkloadSpec{
    50  					PodSets: []kueue.PodSet{
    51  						{},
    52  					},
    53  				},
    54  			},
    55  			wantWl: kueue.Workload{
    56  				Spec: kueue.WorkloadSpec{
    57  					PodSets: []kueue.PodSet{
    58  						{Name: "main"},
    59  					},
    60  				},
    61  			},
    62  		},
    63  		"don't set podSetName if multiple": {
    64  			wl: kueue.Workload{
    65  				Spec: kueue.WorkloadSpec{
    66  					PodSets: []kueue.PodSet{
    67  						{},
    68  						{},
    69  					},
    70  				},
    71  			},
    72  			wantWl: kueue.Workload{
    73  				Spec: kueue.WorkloadSpec{
    74  					PodSets: []kueue.PodSet{
    75  						{},
    76  						{},
    77  					},
    78  				},
    79  			},
    80  		},
    81  	}
    82  	for name, tc := range cases {
    83  		t.Run(name, func(t *testing.T) {
    84  			wh := &WorkloadWebhook{}
    85  			wlCopy := tc.wl.DeepCopy()
    86  			if err := wh.Default(context.Background(), wlCopy); err != nil {
    87  				t.Fatalf("Could not apply defaults: %v", err)
    88  			}
    89  			if diff := cmp.Diff(tc.wantWl, *wlCopy,
    90  				cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime")); diff != "" {
    91  				t.Errorf("Obtained wrong defaults (-want,+got):\n%s", diff)
    92  			}
    93  		})
    94  	}
    95  }
    96  
    97  func TestValidateWorkload(t *testing.T) {
    98  	specPath := field.NewPath("spec")
    99  	podSetsPath := specPath.Child("podSets")
   100  	statusPath := field.NewPath("status")
   101  	firstAdmissionChecksPath := statusPath.Child("admissionChecks").Index(0)
   102  	podSetUpdatePath := firstAdmissionChecksPath.Child("podSetUpdates")
   103  	firstPodSetSpecPath := podSetsPath.Index(0).Child("template", "spec")
   104  	testCases := map[string]struct {
   105  		workload *kueue.Workload
   106  		wantErr  field.ErrorList
   107  	}{
   108  		"valid": {
   109  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).PodSets(
   110  				kueue.PodSet{
   111  					Name:  "driver",
   112  					Count: 1,
   113  				},
   114  				kueue.PodSet{
   115  					Name:  "workers",
   116  					Count: 100,
   117  				},
   118  			).Obj(),
   119  		},
   120  		"should have a valid podSet name": {
   121  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).PodSets(
   122  				kueue.PodSet{
   123  					Name:  "@driver",
   124  					Count: 1,
   125  				},
   126  			).Obj(),
   127  			wantErr: field.ErrorList{field.Invalid(podSetsPath.Index(0).Child("name"), nil, "")},
   128  		},
   129  		"should have valid priorityClassName": {
   130  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   131  				PriorityClass("invalid_class").
   132  				Priority(0).
   133  				Obj(),
   134  			wantErr: field.ErrorList{
   135  				field.Invalid(specPath.Child("priorityClassName"), nil, ""),
   136  			},
   137  		},
   138  		"should pass validation when priorityClassName is empty": {
   139  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Obj(),
   140  			wantErr:  nil,
   141  		},
   142  		"should have priority once priorityClassName is set": {
   143  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   144  				PriorityClass("priority").
   145  				Obj(),
   146  			wantErr: field.ErrorList{
   147  				field.Invalid(specPath.Child("priority"), nil, ""),
   148  			},
   149  		},
   150  		"should have a valid queueName": {
   151  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   152  				Queue("@invalid").
   153  				Obj(),
   154  			wantErr: field.ErrorList{
   155  				field.Invalid(specPath.Child("queueName"), nil, ""),
   156  			},
   157  		},
   158  		"should have a valid clusterQueue name": {
   159  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   160  				ReserveQuota(testingutil.MakeAdmission("@invalid").Obj()).
   161  				Obj(),
   162  			wantErr: field.ErrorList{
   163  				field.Invalid(statusPath.Child("admission", "clusterQueue"), nil, ""),
   164  			},
   165  		},
   166  		"should have a valid podSet name in status assigment": {
   167  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   168  				ReserveQuota(testingutil.MakeAdmission("cluster-queue", "@invalid").Obj()).
   169  				Obj(),
   170  			wantErr: field.ErrorList{
   171  				field.NotFound(statusPath.Child("admission", "podSetAssignments").Index(0).Child("name"), nil),
   172  			},
   173  		},
   174  		"should have same podSets in admission": {
   175  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   176  				PodSets(
   177  					kueue.PodSet{
   178  						Name:  "main2",
   179  						Count: 1,
   180  					},
   181  					kueue.PodSet{
   182  						Name:  "main1",
   183  						Count: 1,
   184  					},
   185  				).
   186  				ReserveQuota(testingutil.MakeAdmission("cluster-queue", "main1", "main2", "main3").Obj()).
   187  				Obj(),
   188  			wantErr: field.ErrorList{
   189  				field.Invalid(statusPath.Child("admission", "podSetAssignments"), nil, ""),
   190  				field.NotFound(statusPath.Child("admission", "podSetAssignments").Index(2).Child("name"), nil),
   191  			},
   192  		},
   193  		"assignment usage should be divisible by count": {
   194  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   195  				PodSets(*testingutil.MakePodSet("main", 3).
   196  					Request(corev1.ResourceCPU, "1").
   197  					Obj()).
   198  				ReserveQuota(testingutil.MakeAdmission("cluster-queue").
   199  					Assignment(corev1.ResourceCPU, "flv", "1").
   200  					AssignmentPodCount(3).
   201  					Obj()).
   202  				Obj(),
   203  			wantErr: field.ErrorList{
   204  				field.Invalid(statusPath.Child("admission", "podSetAssignments").Index(0).Child("resourceUsage").Key(string(corev1.ResourceCPU)), nil, ""),
   205  			},
   206  		},
   207  		"should not request num-pods resource": {
   208  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   209  				PodSets(kueue.PodSet{
   210  					Name:  "bad",
   211  					Count: 1,
   212  					Template: corev1.PodTemplateSpec{
   213  						Spec: corev1.PodSpec{
   214  							InitContainers: []corev1.Container{
   215  								{
   216  									Resources: corev1.ResourceRequirements{
   217  										Requests: corev1.ResourceList{
   218  											corev1.ResourcePods: resource.MustParse("1"),
   219  										},
   220  									},
   221  								},
   222  							},
   223  							Containers: []corev1.Container{
   224  								{
   225  									Resources: corev1.ResourceRequirements{
   226  										Requests: corev1.ResourceList{
   227  											corev1.ResourcePods: resource.MustParse("1"),
   228  										},
   229  									},
   230  								},
   231  							},
   232  						},
   233  					},
   234  				}).
   235  				Obj(),
   236  			wantErr: field.ErrorList{
   237  				field.Invalid(firstPodSetSpecPath.Child("initContainers").Index(0).Child("resources", "requests").Key(string(corev1.ResourcePods)), nil, ""),
   238  				field.Invalid(firstPodSetSpecPath.Child("containers").Index(0).Child("resources", "requests").Key(string(corev1.ResourcePods)), nil, ""),
   239  			},
   240  		},
   241  		"empty podSetUpdates": {
   242  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).AdmissionChecks(kueue.AdmissionCheckState{}).Obj(),
   243  			wantErr:  nil,
   244  		},
   245  		"should podSetUpdates have the same number of podSets": {
   246  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).PodSets(
   247  				*testingutil.MakePodSet("first", 1).Obj(),
   248  				*testingutil.MakePodSet("second", 1).Obj(),
   249  			).AdmissionChecks(
   250  				kueue.AdmissionCheckState{PodSetUpdates: []kueue.PodSetUpdate{{Name: "first"}}},
   251  			).Obj(),
   252  			wantErr: field.ErrorList{
   253  				field.Invalid(podSetUpdatePath, nil, "must have the same number of podSetUpdates as the podSets"),
   254  			},
   255  		},
   256  		"mismatched names in podSetUpdates with names in podSets": {
   257  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).PodSets(
   258  				*testingutil.MakePodSet("first", 1).Obj(),
   259  				*testingutil.MakePodSet("second", 1).Obj(),
   260  			).AdmissionChecks(
   261  				kueue.AdmissionCheckState{PodSetUpdates: []kueue.PodSetUpdate{{Name: "first"}, {Name: "third"}}},
   262  			).Obj(),
   263  			wantErr: field.ErrorList{
   264  				field.NotSupported(firstAdmissionChecksPath.Child("podSetUpdates").Index(1).Child("name"), nil, []string{}),
   265  			},
   266  		},
   267  		"matched names in podSetUpdates with names in podSets": {
   268  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).PodSets(
   269  				*testingutil.MakePodSet("first", 1).Obj(),
   270  				*testingutil.MakePodSet("second", 1).Obj(),
   271  			).AdmissionChecks(
   272  				kueue.AdmissionCheckState{
   273  					PodSetUpdates: []kueue.PodSetUpdate{
   274  						{
   275  							Name:        "first",
   276  							Labels:      map[string]string{"l1": "first"},
   277  							Annotations: map[string]string{"foo": "bar"},
   278  							Tolerations: []corev1.Toleration{
   279  								{
   280  									Key:               "t1",
   281  									Operator:          corev1.TolerationOpEqual,
   282  									Value:             "t1v",
   283  									Effect:            corev1.TaintEffectNoExecute,
   284  									TolerationSeconds: ptr.To[int64](5),
   285  								},
   286  							},
   287  							NodeSelector: map[string]string{"type": "first"},
   288  						},
   289  						{
   290  							Name:        "second",
   291  							Labels:      map[string]string{"l2": "second"},
   292  							Annotations: map[string]string{"foo": "baz"},
   293  							Tolerations: []corev1.Toleration{
   294  								{
   295  									Key:               "t2",
   296  									Operator:          corev1.TolerationOpEqual,
   297  									Value:             "t2v",
   298  									Effect:            corev1.TaintEffectNoExecute,
   299  									TolerationSeconds: ptr.To[int64](10),
   300  								},
   301  							},
   302  							NodeSelector: map[string]string{"type": "second"},
   303  						},
   304  					},
   305  				},
   306  			).Obj(),
   307  		},
   308  		"invalid label name of podSetUpdate": {
   309  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   310  				AdmissionChecks(
   311  					kueue.AdmissionCheckState{PodSetUpdates: []kueue.PodSetUpdate{{Name: "main", Labels: map[string]string{"@abc": "foo"}}}},
   312  				).
   313  				Obj(),
   314  			wantErr: field.ErrorList{
   315  				field.Invalid(podSetUpdatePath.Index(0).Child("labels"), "@abc", ""),
   316  			},
   317  		},
   318  		"invalid node selector name of podSetUpdate": {
   319  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   320  				AdmissionChecks(
   321  					kueue.AdmissionCheckState{PodSetUpdates: []kueue.PodSetUpdate{{Name: "main", NodeSelector: map[string]string{"@abc": "foo"}}}},
   322  				).
   323  				Obj(),
   324  			wantErr: field.ErrorList{
   325  				field.Invalid(podSetUpdatePath.Index(0).Child("nodeSelector"), "@abc", ""),
   326  			},
   327  		},
   328  		"invalid label value of podSetUpdate": {
   329  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   330  				AdmissionChecks(
   331  					kueue.AdmissionCheckState{PodSetUpdates: []kueue.PodSetUpdate{{Name: "main", Labels: map[string]string{"foo": "@abc"}}}},
   332  				).
   333  				Obj(),
   334  			wantErr: field.ErrorList{
   335  				field.Invalid(podSetUpdatePath.Index(0).Child("labels"), "@abc", ""),
   336  			},
   337  		},
   338  		"invalid reclaimablePods": {
   339  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   340  				PodSets(
   341  					*testingutil.MakePodSet("ps1", 3).Obj(),
   342  				).
   343  				ReclaimablePods(
   344  					kueue.ReclaimablePod{Name: "ps1", Count: 4},
   345  					kueue.ReclaimablePod{Name: "ps2", Count: 1},
   346  				).
   347  				Obj(),
   348  			wantErr: field.ErrorList{
   349  				field.Invalid(statusPath.Child("reclaimablePods").Key("ps1").Child("count"), nil, ""),
   350  				field.NotSupported(statusPath.Child("reclaimablePods").Key("ps2").Child("name"), nil, []string{}),
   351  			},
   352  		},
   353  		"invalid podSet minCount (negative)": {
   354  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   355  				PodSets(
   356  					*testingutil.MakePodSet("ps1", 3).SetMinimumCount(-1).Obj(),
   357  				).
   358  				Obj(),
   359  			wantErr: field.ErrorList{
   360  				field.Forbidden(podSetsPath.Index(0).Child("minCount"), ""),
   361  			},
   362  		},
   363  		"invalid podSet minCount (too big)": {
   364  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   365  				PodSets(
   366  					*testingutil.MakePodSet("ps1", 3).SetMinimumCount(4).Obj(),
   367  				).
   368  				Obj(),
   369  			wantErr: field.ErrorList{
   370  				field.Forbidden(podSetsPath.Index(0).Child("minCount"), ""),
   371  			},
   372  		},
   373  		"too many variable count podSets": {
   374  			workload: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   375  				PodSets(
   376  					*testingutil.MakePodSet("ps1", 3).SetMinimumCount(2).Obj(),
   377  					*testingutil.MakePodSet("ps2", 3).SetMinimumCount(1).Obj(),
   378  				).
   379  				Obj(),
   380  			wantErr: field.ErrorList{
   381  				field.Invalid(podSetsPath, nil, ""),
   382  			},
   383  		},
   384  	}
   385  	for name, tc := range testCases {
   386  		t.Run(name, func(t *testing.T) {
   387  			gotErr := ValidateWorkload(tc.workload)
   388  			if diff := cmp.Diff(tc.wantErr, gotErr, cmpopts.IgnoreFields(field.Error{}, "Detail", "BadValue")); diff != "" {
   389  				t.Errorf("ValidateWorkload() mismatch (-want +got):\n%s", diff)
   390  			}
   391  		})
   392  	}
   393  }
   394  
   395  func TestValidateWorkloadUpdate(t *testing.T) {
   396  	testCases := map[string]struct {
   397  		before, after *kueue.Workload
   398  		wantErr       field.ErrorList
   399  	}{
   400  		"podSets should not be updated when has quota reservation: count": {
   401  			before: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).ReserveQuota(testingutil.MakeAdmission("cq").Obj()).Obj(),
   402  			after: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).PodSets(
   403  				*testingutil.MakePodSet("main", 2).Obj(),
   404  			).Obj(),
   405  			wantErr: field.ErrorList{
   406  				field.Invalid(field.NewPath("spec").Child("podSets"), nil, ""),
   407  			},
   408  		},
   409  		"podSets should not be updated: podSpec": {
   410  			before: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).ReserveQuota(testingutil.MakeAdmission("cq").Obj()).Obj(),
   411  			after: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).PodSets(
   412  				kueue.PodSet{
   413  					Name:  "main",
   414  					Count: 1,
   415  					Template: corev1.PodTemplateSpec{
   416  						Spec: corev1.PodSpec{
   417  							Containers: []corev1.Container{
   418  								{
   419  									Name: "c-after",
   420  									Resources: corev1.ResourceRequirements{
   421  										Requests: make(corev1.ResourceList),
   422  									},
   423  								},
   424  							},
   425  						},
   426  					},
   427  				},
   428  			).Obj(),
   429  			wantErr: field.ErrorList{
   430  				field.Invalid(field.NewPath("spec").Child("podSets"), nil, ""),
   431  			},
   432  		},
   433  		"queueName can be updated when not admitted": {
   434  			before:  testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Queue("q1").Obj(),
   435  			after:   testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Queue("q2").Obj(),
   436  			wantErr: nil,
   437  		},
   438  		"queueName can be updated when admitting": {
   439  			before: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Obj(),
   440  			after: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Queue("q").
   441  				ReserveQuota(testingutil.MakeAdmission("cq").Obj()).Obj(),
   442  		},
   443  		"queueName should not be updated once admitted": {
   444  			before: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Queue("q1").
   445  				ReserveQuota(testingutil.MakeAdmission("cq").Obj()).Obj(),
   446  			after: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Queue("q2").
   447  				ReserveQuota(testingutil.MakeAdmission("cq").Obj()).Obj(),
   448  			wantErr: field.ErrorList{
   449  				field.Invalid(field.NewPath("spec").Child("queueName"), nil, ""),
   450  			},
   451  		},
   452  		"queueName can be updated when admission is reset": {
   453  			before: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Queue("q1").
   454  				ReserveQuota(testingutil.MakeAdmission("cq").Obj()).Obj(),
   455  			after: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Queue("q2").Obj(),
   456  		},
   457  		"admission can be set": {
   458  			before: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Obj(),
   459  			after: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).ReserveQuota(
   460  				testingutil.MakeAdmission("cluster-queue").Assignment("on-demand", "5", "1").Obj(),
   461  			).Obj(),
   462  			wantErr: nil,
   463  		},
   464  		"admission can be unset": {
   465  			before: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).ReserveQuota(
   466  				testingutil.MakeAdmission("cluster-queue").Assignment("on-demand", "5", "1").Obj(),
   467  			).Obj(),
   468  			after:   testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Obj(),
   469  			wantErr: nil,
   470  		},
   471  		"admission should not be updated once set": {
   472  			before: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).ReserveQuota(
   473  				testingutil.MakeAdmission("cluster-queue").Obj(),
   474  			).Obj(),
   475  			after: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).ReserveQuota(
   476  				testingutil.MakeAdmission("cluster-queue").Assignment("on-demand", "5", "1").Obj(),
   477  			).Obj(),
   478  			wantErr: field.ErrorList{
   479  				field.Invalid(field.NewPath("status", "admission"), nil, ""),
   480  			},
   481  		},
   482  
   483  		"reclaimable pod count can change up": {
   484  			before: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   485  				PodSets(
   486  					*testingutil.MakePodSet("ps1", 3).Obj(),
   487  					*testingutil.MakePodSet("ps2", 3).Obj(),
   488  				).
   489  				ReserveQuota(
   490  					testingutil.MakeAdmission("cluster-queue").
   491  						PodSets(kueue.PodSetAssignment{Name: "ps1"}, kueue.PodSetAssignment{Name: "ps2"}).
   492  						Obj(),
   493  				).
   494  				ReclaimablePods(
   495  					kueue.ReclaimablePod{Name: "ps1", Count: 1},
   496  				).
   497  				Obj(),
   498  			after: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   499  				PodSets(
   500  					*testingutil.MakePodSet("ps1", 3).Obj(),
   501  					*testingutil.MakePodSet("ps2", 3).Obj(),
   502  				).
   503  				ReserveQuota(
   504  					testingutil.MakeAdmission("cluster-queue").
   505  						PodSets(kueue.PodSetAssignment{Name: "ps1"}, kueue.PodSetAssignment{Name: "ps2"}).
   506  						Obj(),
   507  				).
   508  				ReclaimablePods(
   509  					kueue.ReclaimablePod{Name: "ps1", Count: 2},
   510  					kueue.ReclaimablePod{Name: "ps2", Count: 1},
   511  				).
   512  				Obj(),
   513  			wantErr: nil,
   514  		},
   515  		"reclaimable pod count cannot change down": {
   516  			before: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   517  				PodSets(
   518  					*testingutil.MakePodSet("ps1", 3).Obj(),
   519  					*testingutil.MakePodSet("ps2", 3).Obj(),
   520  				).
   521  				ReserveQuota(
   522  					testingutil.MakeAdmission("cluster-queue").
   523  						PodSets(kueue.PodSetAssignment{Name: "ps1"}, kueue.PodSetAssignment{Name: "ps2"}).
   524  						Obj(),
   525  				).
   526  				ReclaimablePods(
   527  					kueue.ReclaimablePod{Name: "ps1", Count: 2},
   528  					kueue.ReclaimablePod{Name: "ps2", Count: 1},
   529  				).
   530  				Obj(),
   531  			after: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   532  				PodSets(
   533  					*testingutil.MakePodSet("ps1", 3).Obj(),
   534  					*testingutil.MakePodSet("ps2", 3).Obj(),
   535  				).
   536  				ReserveQuota(
   537  					testingutil.MakeAdmission("cluster-queue").
   538  						PodSets(kueue.PodSetAssignment{Name: "ps1"}, kueue.PodSetAssignment{Name: "ps2"}).
   539  						Obj(),
   540  				).
   541  				ReclaimablePods(
   542  					kueue.ReclaimablePod{Name: "ps1", Count: 1},
   543  				).
   544  				Obj(),
   545  			wantErr: field.ErrorList{
   546  				field.Invalid(field.NewPath("status", "reclaimablePods").Key("ps1").Child("count"), nil, ""),
   547  				field.Required(field.NewPath("status", "reclaimablePods").Key("ps2"), ""),
   548  			},
   549  		},
   550  		"reclaimable pod count can go to 0 if the job is suspended": {
   551  			before: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   552  				PodSets(
   553  					*testingutil.MakePodSet("ps1", 3).Obj(),
   554  					*testingutil.MakePodSet("ps2", 3).Obj(),
   555  				).
   556  				ReserveQuota(
   557  					testingutil.MakeAdmission("cluster-queue").
   558  						PodSets(kueue.PodSetAssignment{Name: "ps1"}, kueue.PodSetAssignment{Name: "ps2"}).
   559  						Obj(),
   560  				).
   561  				ReclaimablePods(
   562  					kueue.ReclaimablePod{Name: "ps1", Count: 2},
   563  					kueue.ReclaimablePod{Name: "ps2", Count: 1},
   564  				).
   565  				Obj(),
   566  			after: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).
   567  				PodSets(
   568  					*testingutil.MakePodSet("ps1", 3).Obj(),
   569  					*testingutil.MakePodSet("ps2", 3).Obj(),
   570  				).
   571  				AdmissionChecks(kueue.AdmissionCheckState{
   572  					PodSetUpdates: []kueue.PodSetUpdate{{Name: "ps1"}, {Name: "ps2"}},
   573  					State:         kueue.CheckStateReady,
   574  				}).
   575  				ReclaimablePods(
   576  					kueue.ReclaimablePod{Name: "ps1", Count: 0},
   577  					kueue.ReclaimablePod{Name: "ps2", Count: 1},
   578  				).
   579  				Obj(),
   580  			wantErr: nil,
   581  		},
   582  		"priorityClassSource should not be updated": {
   583  			before: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Queue("q").
   584  				PriorityClass("test-class").PriorityClassSource(constants.PodPriorityClassSource).
   585  				Priority(10).ReserveQuota(testingutil.MakeAdmission("cq").Obj()).Obj(),
   586  			after: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Queue("q").
   587  				PriorityClass("test-class").PriorityClassSource(constants.WorkloadPriorityClassSource).
   588  				Priority(10).Obj(),
   589  			wantErr: field.ErrorList{
   590  				field.Invalid(field.NewPath("spec").Child("priorityClassSource"), nil, ""),
   591  			},
   592  		},
   593  		"priorityClassName should not be updated": {
   594  			before: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Queue("q").
   595  				PriorityClass("test-class-1").PriorityClassSource(constants.PodPriorityClassSource).
   596  				Priority(10).ReserveQuota(testingutil.MakeAdmission("cq").Obj()).Obj(),
   597  			after: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Queue("q").
   598  				PriorityClass("test-class-2").PriorityClassSource(constants.PodPriorityClassSource).
   599  				Priority(10).Obj(),
   600  			wantErr: field.ErrorList{
   601  				field.Invalid(field.NewPath("spec").Child("priorityClassName"), nil, ""),
   602  			},
   603  		},
   604  		"podSetUpdates should be immutable when state is ready": {
   605  			before: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).PodSets(
   606  				*testingutil.MakePodSet("first", 1).Obj(),
   607  				*testingutil.MakePodSet("second", 1).Obj(),
   608  			).AdmissionChecks(kueue.AdmissionCheckState{
   609  				PodSetUpdates: []kueue.PodSetUpdate{{Name: "first", Labels: map[string]string{"foo": "bar"}}, {Name: "second"}},
   610  				State:         kueue.CheckStateReady,
   611  			}).Obj(),
   612  			after: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).PodSets(
   613  				*testingutil.MakePodSet("first", 1).Obj(),
   614  				*testingutil.MakePodSet("second", 1).Obj(),
   615  			).AdmissionChecks(kueue.AdmissionCheckState{
   616  				PodSetUpdates: []kueue.PodSetUpdate{{Name: "first", Labels: map[string]string{"foo": "baz"}}, {Name: "second"}},
   617  				State:         kueue.CheckStateReady,
   618  			}).Obj(),
   619  			wantErr: field.ErrorList{
   620  				field.Invalid(field.NewPath("status").Child("admissionChecks").Index(0).Child("podSetUpdates"), nil, ""),
   621  			},
   622  		},
   623  		"should change other fields of admissionchecks when podSetUpdates is immutable": {
   624  			before: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).PodSets(
   625  				*testingutil.MakePodSet("first", 1).Obj(),
   626  				*testingutil.MakePodSet("second", 1).Obj(),
   627  			).AdmissionChecks(kueue.AdmissionCheckState{
   628  				Name:          "ac1",
   629  				Message:       "old",
   630  				PodSetUpdates: []kueue.PodSetUpdate{{Name: "first", Labels: map[string]string{"foo": "bar"}}, {Name: "second"}},
   631  				State:         kueue.CheckStateReady,
   632  			}).Obj(),
   633  			after: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).PodSets(
   634  				*testingutil.MakePodSet("first", 1).Obj(),
   635  				*testingutil.MakePodSet("second", 1).Obj(),
   636  			).AdmissionChecks(kueue.AdmissionCheckState{
   637  				Name:               "ac1",
   638  				Message:            "new",
   639  				LastTransitionTime: metav1.NewTime(time.Now()),
   640  				PodSetUpdates:      []kueue.PodSetUpdate{{Name: "first", Labels: map[string]string{"foo": "bar"}}, {Name: "second"}},
   641  				State:              kueue.CheckStateReady,
   642  			}).Obj(),
   643  		},
   644  		"updating priorityClassName before setting reserve quota for workload": {
   645  			before: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Queue("q").
   646  				PriorityClass("test-class-1").PriorityClassSource(constants.PodPriorityClassSource).
   647  				Priority(10).Obj(),
   648  			after: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Queue("q").
   649  				PriorityClass("test-class-2").PriorityClassSource(constants.PodPriorityClassSource).
   650  				Priority(10).Obj(),
   651  			wantErr: nil,
   652  		},
   653  		"updating priorityClassSource before setting reserve quota for workload": {
   654  			before: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Queue("q").
   655  				PriorityClass("test-class").PriorityClassSource(constants.PodPriorityClassSource).
   656  				Priority(10).Obj(),
   657  			after: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Queue("q").
   658  				PriorityClass("test-class").PriorityClassSource(constants.WorkloadPriorityClassSource).
   659  				Priority(10).Obj(),
   660  			wantErr: nil,
   661  		},
   662  		"updating podSets  before setting reserve quota for workload": {
   663  			before: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).Obj(),
   664  			after: testingutil.MakeWorkload(testWorkloadName, testWorkloadNamespace).PodSets(
   665  				kueue.PodSet{
   666  					Name:  "main",
   667  					Count: 1,
   668  					Template: corev1.PodTemplateSpec{
   669  						Spec: corev1.PodSpec{
   670  							Containers: []corev1.Container{
   671  								{
   672  									Name: "c-after",
   673  									Resources: corev1.ResourceRequirements{
   674  										Requests: make(corev1.ResourceList),
   675  									},
   676  								},
   677  							},
   678  						},
   679  					},
   680  				},
   681  			).Obj(),
   682  			wantErr: nil,
   683  		},
   684  	}
   685  	for name, tc := range testCases {
   686  		t.Run(name, func(t *testing.T) {
   687  			errList := ValidateWorkloadUpdate(tc.after, tc.before)
   688  			if diff := cmp.Diff(tc.wantErr, errList, cmpopts.IgnoreFields(field.Error{}, "Detail", "BadValue")); diff != "" {
   689  				t.Errorf("ValidateWorkloadUpdate() mismatch (-want +got):\n%s", diff)
   690  			}
   691  		})
   692  	}
   693  }