sigs.k8s.io/kueue@v0.6.2/pkg/controller/jobs/pod/pod_webhook_test.go (about)

     1  /*
     2  Copyright 2023 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 pod
    18  
    19  import (
    20  	"testing"
    21  
    22  	"github.com/google/go-cmp/cmp"
    23  	"github.com/google/go-cmp/cmp/cmpopts"
    24  	rayjobapi "github.com/ray-project/kuberay/ray-operator/apis/ray/v1alpha1"
    25  	batchv1 "k8s.io/api/batch/v1"
    26  	corev1 "k8s.io/api/core/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/runtime/schema"
    29  	"k8s.io/apimachinery/pkg/util/validation/field"
    30  	"sigs.k8s.io/controller-runtime/pkg/client"
    31  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    32  
    33  	configapi "sigs.k8s.io/kueue/apis/config/v1beta1"
    34  	_ "sigs.k8s.io/kueue/pkg/controller/jobs/kubeflow/jobs"
    35  	_ "sigs.k8s.io/kueue/pkg/controller/jobs/mpijob"
    36  	utiltesting "sigs.k8s.io/kueue/pkg/util/testing"
    37  	testingpod "sigs.k8s.io/kueue/pkg/util/testingjobs/pod"
    38  )
    39  
    40  func TestDefault(t *testing.T) {
    41  	defaultNamespace := &corev1.Namespace{
    42  		ObjectMeta: metav1.ObjectMeta{
    43  			Name: "test-ns",
    44  			Labels: map[string]string{
    45  				"kubernetes.io/metadata.name": "test-ns",
    46  			},
    47  		},
    48  	}
    49  
    50  	defaultNamespaceSelector := &metav1.LabelSelector{
    51  		MatchExpressions: []metav1.LabelSelectorRequirement{
    52  			{
    53  				Key:      "kubernetes.io/metadata.name",
    54  				Operator: metav1.LabelSelectorOpNotIn,
    55  				Values:   []string{"kube-system"},
    56  			},
    57  		},
    58  	}
    59  
    60  	testCases := map[string]struct {
    61  		initObjects                []client.Object
    62  		pod                        *corev1.Pod
    63  		manageJobsWithoutQueueName bool
    64  		namespaceSelector          *metav1.LabelSelector
    65  		podSelector                *metav1.LabelSelector
    66  		want                       *corev1.Pod
    67  	}{
    68  		"pod with queue nil ns selector": {
    69  			initObjects: []client.Object{defaultNamespace},
    70  			pod: testingpod.MakePod("test-pod", defaultNamespace.Name).
    71  				Queue("test-queue").
    72  				Obj(),
    73  			want: testingpod.MakePod("test-pod", defaultNamespace.Name).
    74  				Queue("test-queue").
    75  				Obj(),
    76  		},
    77  		"pod with queue matching ns selector": {
    78  			initObjects: []client.Object{defaultNamespace},
    79  			pod: testingpod.MakePod("test-pod", defaultNamespace.Name).
    80  				Queue("test-queue").
    81  				Obj(),
    82  			namespaceSelector: defaultNamespaceSelector,
    83  			podSelector:       &metav1.LabelSelector{},
    84  			want: testingpod.MakePod("test-pod", defaultNamespace.Name).
    85  				Queue("test-queue").
    86  				Label("kueue.x-k8s.io/managed", "true").
    87  				KueueSchedulingGate().
    88  				KueueFinalizer().
    89  				Obj(),
    90  		},
    91  		"pod without queue matching ns selector manage jobs without queue name": {
    92  			initObjects: []client.Object{defaultNamespace},
    93  			pod: testingpod.MakePod("test-pod", defaultNamespace.Name).
    94  				Obj(),
    95  			manageJobsWithoutQueueName: true,
    96  			namespaceSelector:          defaultNamespaceSelector,
    97  			podSelector:                &metav1.LabelSelector{},
    98  			want: testingpod.MakePod("test-pod", defaultNamespace.Name).
    99  				Label("kueue.x-k8s.io/managed", "true").
   100  				KueueSchedulingGate().
   101  				KueueFinalizer().
   102  				Obj(),
   103  		},
   104  		"pod with owner managed by kueue (Job)": {
   105  			initObjects:       []client.Object{defaultNamespace},
   106  			podSelector:       &metav1.LabelSelector{},
   107  			namespaceSelector: defaultNamespaceSelector,
   108  			pod: testingpod.MakePod("test-pod", defaultNamespace.Name).
   109  				Queue("test-queue").
   110  				OwnerReference("parent-job", batchv1.SchemeGroupVersion.WithKind("Job")).
   111  				Obj(),
   112  			want: testingpod.MakePod("test-pod", defaultNamespace.Name).
   113  				Queue("test-queue").
   114  				OwnerReference("parent-job", batchv1.SchemeGroupVersion.WithKind("Job")).
   115  				Obj(),
   116  		},
   117  		"pod with owner managed by kueue (RayCluster)": {
   118  			initObjects:       []client.Object{defaultNamespace},
   119  			podSelector:       &metav1.LabelSelector{},
   120  			namespaceSelector: defaultNamespaceSelector,
   121  			pod: testingpod.MakePod("test-pod", defaultNamespace.Name).
   122  				Queue("test-queue").
   123  				OwnerReference("parent-ray-cluster", rayjobapi.GroupVersion.WithKind("RayCluster")).
   124  				Obj(),
   125  			want: testingpod.MakePod("test-pod", defaultNamespace.Name).
   126  				Queue("test-queue").
   127  				OwnerReference("parent-ray-cluster", rayjobapi.GroupVersion.WithKind("RayCluster")).
   128  				Obj(),
   129  		},
   130  		"pod with owner managed by kueue (MPIJob)": {
   131  			initObjects:       []client.Object{defaultNamespace},
   132  			podSelector:       &metav1.LabelSelector{},
   133  			namespaceSelector: defaultNamespaceSelector,
   134  			pod: testingpod.MakePod("test-pod", defaultNamespace.Name).
   135  				Queue("test-queue").
   136  				OwnerReference(
   137  					"parent-mpi-job",
   138  					schema.GroupVersionKind{Group: "kubeflow.org", Version: "v2beta1", Kind: "MPIJob"},
   139  				).
   140  				Obj(),
   141  			want: testingpod.MakePod("test-pod", defaultNamespace.Name).
   142  				Queue("test-queue").
   143  				OwnerReference(
   144  					"parent-mpi-job",
   145  					schema.GroupVersionKind{Group: "kubeflow.org", Version: "v2beta1", Kind: "MPIJob"},
   146  				).
   147  				Obj(),
   148  		},
   149  		"pod with owner managed by kueue (PyTorchJob)": {
   150  			initObjects:       []client.Object{defaultNamespace},
   151  			podSelector:       &metav1.LabelSelector{},
   152  			namespaceSelector: defaultNamespaceSelector,
   153  			pod: testingpod.MakePod("test-pod", defaultNamespace.Name).
   154  				Queue("test-queue").
   155  				OwnerReference(
   156  					"parent-pytorch-job",
   157  					schema.GroupVersionKind{Group: "kubeflow.org", Version: "v1", Kind: "PyTorchJob"},
   158  				).
   159  				Obj(),
   160  			want: testingpod.MakePod("test-pod", defaultNamespace.Name).
   161  				Queue("test-queue").
   162  				OwnerReference(
   163  					"parent-pytorch-job",
   164  					schema.GroupVersionKind{Group: "kubeflow.org", Version: "v1", Kind: "PyTorchJob"},
   165  				).
   166  				Obj(),
   167  		},
   168  		"pod with owner managed by kueue (TFJob)": {
   169  			initObjects:       []client.Object{defaultNamespace},
   170  			podSelector:       &metav1.LabelSelector{},
   171  			namespaceSelector: defaultNamespaceSelector,
   172  			pod: testingpod.MakePod("test-pod", defaultNamespace.Name).
   173  				Queue("test-queue").
   174  				OwnerReference(
   175  					"parent-tf-job",
   176  					schema.GroupVersionKind{Group: "kubeflow.org", Version: "v1", Kind: "TFJob"},
   177  				).
   178  				Obj(),
   179  			want: testingpod.MakePod("test-pod", defaultNamespace.Name).
   180  				Queue("test-queue").
   181  				OwnerReference(
   182  					"parent-tf-job",
   183  					schema.GroupVersionKind{Group: "kubeflow.org", Version: "v1", Kind: "TFJob"},
   184  				).
   185  				Obj(),
   186  		},
   187  		"pod with owner managed by kueue (XGBoostJob)": {
   188  			initObjects:       []client.Object{defaultNamespace},
   189  			podSelector:       &metav1.LabelSelector{},
   190  			namespaceSelector: defaultNamespaceSelector,
   191  			pod: testingpod.MakePod("test-pod", defaultNamespace.Name).
   192  				Queue("test-queue").
   193  				OwnerReference(
   194  					"parent-xgboost-job",
   195  					schema.GroupVersionKind{Group: "kubeflow.org", Version: "v1", Kind: "XGBoostJob"},
   196  				).
   197  				Obj(),
   198  			want: testingpod.MakePod("test-pod", defaultNamespace.Name).
   199  				Queue("test-queue").
   200  				OwnerReference(
   201  					"parent-xgboost-job",
   202  					schema.GroupVersionKind{Group: "kubeflow.org", Version: "v1", Kind: "XGBoostJob"},
   203  				).
   204  				Obj(),
   205  		},
   206  		"pod with owner managed by kueue (PaddleJob)": {
   207  			initObjects:       []client.Object{defaultNamespace},
   208  			podSelector:       &metav1.LabelSelector{},
   209  			namespaceSelector: defaultNamespaceSelector,
   210  			pod: testingpod.MakePod("test-pod", defaultNamespace.Name).
   211  				Queue("test-queue").
   212  				OwnerReference(
   213  					"parent-paddle-job",
   214  					schema.GroupVersionKind{Group: "kubeflow.org", Version: "v1", Kind: "PaddleJob"},
   215  				).
   216  				Obj(),
   217  			want: testingpod.MakePod("test-pod", defaultNamespace.Name).
   218  				Queue("test-queue").
   219  				OwnerReference(
   220  					"parent-paddle-job",
   221  					schema.GroupVersionKind{Group: "kubeflow.org", Version: "v1", Kind: "PaddleJob"},
   222  				).
   223  				Obj(),
   224  		},
   225  		"pod with a group name label, but without group total count label": {
   226  			initObjects:       []client.Object{defaultNamespace},
   227  			podSelector:       &metav1.LabelSelector{},
   228  			namespaceSelector: defaultNamespaceSelector,
   229  			pod: testingpod.MakePod("test-pod", defaultNamespace.Name).
   230  				Queue("test-queue").
   231  				Group("test-group").
   232  				Obj(),
   233  			want: testingpod.MakePod("test-pod", defaultNamespace.Name).
   234  				Queue("test-queue").
   235  				Group("test-group").
   236  				RoleHash("a9f06f3a").
   237  				Label("kueue.x-k8s.io/managed", "true").
   238  				KueueSchedulingGate().
   239  				KueueFinalizer().
   240  				Obj(),
   241  		},
   242  		"pod with a group name label": {
   243  			initObjects:       []client.Object{defaultNamespace},
   244  			podSelector:       &metav1.LabelSelector{},
   245  			namespaceSelector: defaultNamespaceSelector,
   246  			pod: testingpod.MakePod("test-pod", defaultNamespace.Name).
   247  				Queue("test-queue").
   248  				Group("test-group").
   249  				Obj(),
   250  			want: testingpod.MakePod("test-pod", defaultNamespace.Name).
   251  				Queue("test-queue").
   252  				Group("test-group").
   253  				RoleHash("a9f06f3a").
   254  				Label("kueue.x-k8s.io/managed", "true").
   255  				KueueSchedulingGate().
   256  				KueueFinalizer().
   257  				Obj(),
   258  		},
   259  	}
   260  
   261  	for name, tc := range testCases {
   262  		t.Run(name, func(t *testing.T) {
   263  			builder := utiltesting.NewClientBuilder()
   264  			builder = builder.WithObjects(tc.initObjects...)
   265  			cli := builder.Build()
   266  
   267  			w := &PodWebhook{
   268  				client:                     cli,
   269  				manageJobsWithoutQueueName: tc.manageJobsWithoutQueueName,
   270  				namespaceSelector:          tc.namespaceSelector,
   271  				podSelector:                tc.podSelector,
   272  			}
   273  
   274  			ctx, _ := utiltesting.ContextWithLog(t)
   275  
   276  			if err := w.Default(ctx, tc.pod); err != nil {
   277  				t.Errorf("failed to set defaults for v1/pod: %s", err)
   278  			}
   279  			if diff := cmp.Diff(tc.want, tc.pod); len(diff) != 0 {
   280  				t.Errorf("Default() mismatch (-want,+got):\n%s", diff)
   281  			}
   282  		})
   283  	}
   284  }
   285  
   286  func TestGetRoleHash(t *testing.T) {
   287  	testCases := map[string]struct {
   288  		pods []*Pod
   289  		// If true, hash for all the pods in test should be equal
   290  		wantEqualHash bool
   291  		wantErr       error
   292  	}{
   293  		"kueue.x-k8s.io/* labels shouldn't affect the role": {
   294  			pods: []*Pod{
   295  				{pod: *testingpod.MakePod("pod1", "test-ns").
   296  					Label("kueue.x-k8s.io/managed", "true").
   297  					Obj()},
   298  				{pod: *testingpod.MakePod("pod2", "test-ns").
   299  					Obj()},
   300  			},
   301  			wantEqualHash: true,
   302  		},
   303  		"volume name shouldn't affect the role": {
   304  			pods: []*Pod{
   305  				{pod: *testingpod.MakePod("pod1", "test-ns").
   306  					Volume(corev1.Volume{
   307  						Name: "volume1",
   308  					}).
   309  					Obj()},
   310  				{pod: *testingpod.MakePod("pod1", "test-ns").
   311  					Volume(corev1.Volume{
   312  						Name: "volume2",
   313  					}).
   314  					Obj()},
   315  			},
   316  			wantEqualHash: true,
   317  		},
   318  		// NOTE: volumes used to be included in the role hash.
   319  		// https://github.com/kubernetes-sigs/kueue/issues/1697
   320  		"volumes with different claims shouldn't affect the role": {
   321  			pods: []*Pod{
   322  				{pod: *testingpod.MakePod("pod1", "test-ns").
   323  					Volume(corev1.Volume{
   324  						Name: "volume",
   325  						VolumeSource: corev1.VolumeSource{
   326  							PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
   327  								ClaimName: "claim1",
   328  							},
   329  						},
   330  					}).
   331  					Obj()},
   332  				{pod: *testingpod.MakePod("pod1", "test-ns").
   333  					Volume(corev1.Volume{
   334  						Name: "volume",
   335  						VolumeSource: corev1.VolumeSource{
   336  							PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
   337  								ClaimName: "claim2",
   338  							},
   339  						},
   340  					}).
   341  					Obj()},
   342  			},
   343  			wantEqualHash: true,
   344  		},
   345  	}
   346  
   347  	for name, tc := range testCases {
   348  		t.Run(name, func(t *testing.T) {
   349  
   350  			var previousHash string
   351  			for i := range tc.pods {
   352  				hash, err := getRoleHash(tc.pods[i].pod)
   353  
   354  				if diff := cmp.Diff(tc.wantErr, err); diff != "" {
   355  					t.Errorf("Unexpected error (-want,+got):\n%s", diff)
   356  				}
   357  
   358  				if previousHash != "" {
   359  					if tc.wantEqualHash {
   360  						if previousHash != hash {
   361  							t.Errorf("Hash of pod shapes shouldn't be different %s!=%s", previousHash, hash)
   362  						}
   363  					} else {
   364  						if previousHash == hash {
   365  							t.Errorf("Hash of pod shapes shouldn't be equal %s==%s", previousHash, hash)
   366  						}
   367  					}
   368  				}
   369  
   370  				previousHash = hash
   371  			}
   372  		})
   373  	}
   374  }
   375  
   376  func TestValidateCreate(t *testing.T) {
   377  	testCases := map[string]struct {
   378  		pod       *corev1.Pod
   379  		wantErr   error
   380  		wantWarns admission.Warnings
   381  	}{
   382  		"pod owner is managed by kueue": {
   383  			pod: testingpod.MakePod("test-pod", "test-ns").
   384  				Label("kueue.x-k8s.io/managed", "true").
   385  				OwnerReference("parent-job", batchv1.SchemeGroupVersion.WithKind("Job")).
   386  				Obj(),
   387  			wantWarns: admission.Warnings{
   388  				"pod owner is managed by kueue, label 'kueue.x-k8s.io/managed=true' might lead to unexpected behaviour",
   389  			},
   390  		},
   391  		"pod with group name and no group total count": {
   392  			pod: testingpod.MakePod("test-pod", "test-ns").
   393  				Label("kueue.x-k8s.io/managed", "true").
   394  				Group("test-group").
   395  				Obj(),
   396  			wantErr: field.ErrorList{
   397  				&field.Error{
   398  					Type:  field.ErrorTypeRequired,
   399  					Field: "metadata.annotations[kueue.x-k8s.io/pod-group-total-count]",
   400  				},
   401  			}.ToAggregate(),
   402  		},
   403  		"pod with group total count and no group name": {
   404  			pod: testingpod.MakePod("test-pod", "test-ns").
   405  				Label("kueue.x-k8s.io/managed", "true").
   406  				GroupTotalCount("3").
   407  				Obj(),
   408  			wantErr: field.ErrorList{
   409  				&field.Error{
   410  					Type:  field.ErrorTypeRequired,
   411  					Field: "metadata.labels[kueue.x-k8s.io/pod-group-name]",
   412  				},
   413  			}.ToAggregate(),
   414  		},
   415  		"pod with 0 group total count": {
   416  			pod: testingpod.MakePod("test-pod", "test-ns").
   417  				Label("kueue.x-k8s.io/managed", "true").
   418  				Group("test-group").
   419  				GroupTotalCount("0").
   420  				Obj(),
   421  			wantErr: field.ErrorList{
   422  				&field.Error{
   423  					Type:  field.ErrorTypeInvalid,
   424  					Field: "metadata.annotations[kueue.x-k8s.io/pod-group-total-count]",
   425  				},
   426  			}.ToAggregate(),
   427  		},
   428  		"pod with empty group total count": {
   429  			pod: testingpod.MakePod("test-pod", "test-ns").
   430  				Label("kueue.x-k8s.io/managed", "true").
   431  				Group("test-group").
   432  				GroupTotalCount("").
   433  				Obj(),
   434  			wantErr: field.ErrorList{
   435  				&field.Error{
   436  					Type:  field.ErrorTypeInvalid,
   437  					Field: "metadata.annotations[kueue.x-k8s.io/pod-group-total-count]",
   438  				},
   439  			}.ToAggregate(),
   440  		},
   441  		"pod with incorrect group name": {
   442  			pod: testingpod.MakePod("test-pod", "test-ns").
   443  				Label("kueue.x-k8s.io/managed", "true").
   444  				Group("notAdns1123Subdomain*").
   445  				GroupTotalCount("2").
   446  				Obj(),
   447  			wantErr: field.ErrorList{
   448  				&field.Error{
   449  					Type:  field.ErrorTypeInvalid,
   450  					Field: "metadata.labels[kueue.x-k8s.io/pod-group-name]",
   451  				},
   452  			}.ToAggregate(),
   453  		},
   454  	}
   455  
   456  	for name, tc := range testCases {
   457  		t.Run(name, func(t *testing.T) {
   458  			builder := utiltesting.NewClientBuilder()
   459  			cli := builder.Build()
   460  
   461  			w := &PodWebhook{
   462  				client: cli,
   463  			}
   464  
   465  			ctx, _ := utiltesting.ContextWithLog(t)
   466  
   467  			warns, err := w.ValidateCreate(ctx, tc.pod)
   468  			if diff := cmp.Diff(tc.wantErr, err, cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail")); diff != "" {
   469  				t.Errorf("Unexpected error (-want,+got):\n%s", diff)
   470  			}
   471  			if diff := cmp.Diff(warns, tc.wantWarns); diff != "" {
   472  				t.Errorf("Expected different list of warnings (-want,+got):\n%s", diff)
   473  			}
   474  		})
   475  	}
   476  }
   477  
   478  func TestValidateUpdate(t *testing.T) {
   479  	testCases := map[string]struct {
   480  		oldPod    *corev1.Pod
   481  		newPod    *corev1.Pod
   482  		wantErr   error
   483  		wantWarns admission.Warnings
   484  	}{
   485  		"pods owner is managed by kueue, managed label is set for both pods": {
   486  			oldPod: testingpod.MakePod("test-pod", "test-ns").
   487  				Label("kueue.x-k8s.io/managed", "true").
   488  				OwnerReference("parent-job", batchv1.SchemeGroupVersion.WithKind("Job")).
   489  				Obj(),
   490  			newPod: testingpod.MakePod("test-pod", "test-ns").
   491  				Label("kueue.x-k8s.io/managed", "true").
   492  				OwnerReference("parent-job", batchv1.SchemeGroupVersion.WithKind("Job")).
   493  				Obj(),
   494  			wantWarns: admission.Warnings{
   495  				"pod owner is managed by kueue, label 'kueue.x-k8s.io/managed=true' might lead to unexpected behaviour",
   496  			},
   497  		},
   498  		"pod owner is managed by kueue, managed label is set for new pod": {
   499  			oldPod: testingpod.MakePod("test-pod", "test-ns").
   500  				OwnerReference("parent-job", batchv1.SchemeGroupVersion.WithKind("Job")).
   501  				Obj(),
   502  			newPod: testingpod.MakePod("test-pod", "test-ns").
   503  				Label("kueue.x-k8s.io/managed", "true").
   504  				OwnerReference("parent-job", batchv1.SchemeGroupVersion.WithKind("Job")).
   505  				Obj(),
   506  			wantWarns: admission.Warnings{
   507  				"pod owner is managed by kueue, label 'kueue.x-k8s.io/managed=true' might lead to unexpected behaviour",
   508  			},
   509  		},
   510  		"pod with group name and no group total count": {
   511  			oldPod: testingpod.MakePod("test-pod", "test-ns").Group("test-group").Obj(),
   512  			newPod: testingpod.MakePod("test-pod", "test-ns").
   513  				Label("kueue.x-k8s.io/managed", "true").
   514  				Group("test-group").
   515  				Obj(),
   516  			wantErr: field.ErrorList{
   517  				&field.Error{
   518  					Type:  field.ErrorTypeRequired,
   519  					Field: "metadata.annotations[kueue.x-k8s.io/pod-group-total-count]",
   520  				},
   521  			}.ToAggregate(),
   522  		},
   523  		"pod with 0 group total count": {
   524  			oldPod: testingpod.MakePod("test-pod", "test-ns").Group("test-group").Obj(),
   525  			newPod: testingpod.MakePod("test-pod", "test-ns").
   526  				Label("kueue.x-k8s.io/managed", "true").
   527  				Group("test-group").
   528  				GroupTotalCount("0").
   529  				Obj(),
   530  			wantErr: field.ErrorList{
   531  				&field.Error{
   532  					Type:  field.ErrorTypeInvalid,
   533  					Field: "metadata.annotations[kueue.x-k8s.io/pod-group-total-count]",
   534  				},
   535  			}.ToAggregate(),
   536  		},
   537  		"pod with empty group total count": {
   538  			oldPod: testingpod.MakePod("test-pod", "test-ns").Group("test-group").Obj(),
   539  			newPod: testingpod.MakePod("test-pod", "test-ns").
   540  				Label("kueue.x-k8s.io/managed", "true").
   541  				Group("test-group").
   542  				GroupTotalCount("").
   543  				Obj(),
   544  			wantErr: field.ErrorList{
   545  				&field.Error{
   546  					Type:  field.ErrorTypeInvalid,
   547  					Field: "metadata.annotations[kueue.x-k8s.io/pod-group-total-count]",
   548  				},
   549  			}.ToAggregate(),
   550  		},
   551  		"pod group name is changed": {
   552  			oldPod: testingpod.MakePod("test-pod", "test-ns").
   553  				Group("test-group").
   554  				GroupTotalCount("2").
   555  				Obj(),
   556  			newPod: testingpod.MakePod("test-pod", "test-ns").
   557  				Group("test-group-new").
   558  				GroupTotalCount("2").
   559  				Obj(),
   560  			wantErr: field.ErrorList{
   561  				&field.Error{
   562  					Type:  field.ErrorTypeInvalid,
   563  					Field: "metadata.labels[kueue.x-k8s.io/pod-group-name]",
   564  				},
   565  			}.ToAggregate(),
   566  		},
   567  		"retriable in group annotation is removed": {
   568  			oldPod: testingpod.MakePod("test-pod", "test-ns").
   569  				Group("test-group").
   570  				GroupTotalCount("2").
   571  				Annotation("kueue.x-k8s.io/retriable-in-group", "false").
   572  				Obj(),
   573  			newPod: testingpod.MakePod("test-pod", "test-ns").
   574  				Group("test-group").
   575  				GroupTotalCount("2").
   576  				Obj(),
   577  			wantErr: field.ErrorList{
   578  				&field.Error{
   579  					Type:  field.ErrorTypeForbidden,
   580  					Field: "metadata.annotations[kueue.x-k8s.io/retriable-in-group]",
   581  				},
   582  			}.ToAggregate(),
   583  		},
   584  		"retriable in group annotation is changed from false to true": {
   585  			oldPod: testingpod.MakePod("test-pod", "test-ns").
   586  				Group("test-group").
   587  				GroupTotalCount("2").
   588  				Annotation("kueue.x-k8s.io/retriable-in-group", "false").
   589  				Obj(),
   590  			newPod: testingpod.MakePod("test-pod", "test-ns").
   591  				Group("test-group").
   592  				GroupTotalCount("2").
   593  				Annotation("kueue.x-k8s.io/retriable-in-group", "true").
   594  				Obj(),
   595  			wantErr: field.ErrorList{
   596  				&field.Error{
   597  					Type:  field.ErrorTypeForbidden,
   598  					Field: "metadata.annotations[kueue.x-k8s.io/retriable-in-group]",
   599  				},
   600  			}.ToAggregate(),
   601  		},
   602  	}
   603  
   604  	for name, tc := range testCases {
   605  		t.Run(name, func(t *testing.T) {
   606  			builder := utiltesting.NewClientBuilder()
   607  			cli := builder.Build()
   608  
   609  			w := &PodWebhook{
   610  				client: cli,
   611  			}
   612  
   613  			ctx, _ := utiltesting.ContextWithLog(t)
   614  
   615  			warns, err := w.ValidateUpdate(ctx, tc.oldPod, tc.newPod)
   616  			if diff := cmp.Diff(tc.wantErr, err, cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail")); diff != "" {
   617  				t.Errorf("Unexpected error (-want,+got):\n%s", diff)
   618  			}
   619  			if diff := cmp.Diff(warns, tc.wantWarns); diff != "" {
   620  				t.Errorf("Expected different list of warnings (-want,+got):\n%s", diff)
   621  			}
   622  		})
   623  	}
   624  }
   625  
   626  func TestGetPodOptions(t *testing.T) {
   627  	cases := map[string]struct {
   628  		integrationOpts map[string]any
   629  		wantOpts        configapi.PodIntegrationOptions
   630  		wantError       error
   631  	}{
   632  		"proper podIntegrationOptions exists": {
   633  			integrationOpts: map[string]any{
   634  				corev1.SchemeGroupVersion.WithKind("Pod").String(): &configapi.PodIntegrationOptions{
   635  					PodSelector: &metav1.LabelSelector{
   636  						MatchLabels: map[string]string{"podKey": "podValue"},
   637  					},
   638  					NamespaceSelector: &metav1.LabelSelector{
   639  						MatchLabels: map[string]string{"nsKey": "nsValue"},
   640  					},
   641  				},
   642  				batchv1.SchemeGroupVersion.WithKind("Job").String(): nil,
   643  			},
   644  			wantOpts: configapi.PodIntegrationOptions{
   645  				PodSelector: &metav1.LabelSelector{
   646  					MatchLabels: map[string]string{"podKey": "podValue"},
   647  				},
   648  				NamespaceSelector: &metav1.LabelSelector{
   649  					MatchLabels: map[string]string{"nsKey": "nsValue"},
   650  				},
   651  			},
   652  		},
   653  		"integrationOptions doesn't have podIntegrationOptions": {
   654  			integrationOpts: map[string]any{
   655  				batchv1.SchemeGroupVersion.WithKind("Job").String(): nil,
   656  			},
   657  			wantError: errPodOptsNotFound,
   658  		},
   659  		"podIntegrationOptions isn't of type PodIntegrationOptions": {
   660  			integrationOpts: map[string]any{
   661  				corev1.SchemeGroupVersion.WithKind("Pod").String(): &configapi.WaitForPodsReady{},
   662  			},
   663  			wantError: errPodOptsTypeAssertion,
   664  		},
   665  	}
   666  	for name, tc := range cases {
   667  		t.Run(name, func(t *testing.T) {
   668  			gotOpts, gotError := getPodOptions(tc.integrationOpts)
   669  			if diff := cmp.Diff(tc.wantError, gotError, cmpopts.EquateErrors()); len(diff) != 0 {
   670  				t.Errorf("Unexpected error from getPodOptions (-want,+got):\n%s", diff)
   671  			}
   672  			if diff := cmp.Diff(tc.wantOpts, gotOpts, cmpopts.EquateEmpty()); len(diff) != 0 {
   673  				t.Errorf("Unexpected podIntegrationOptions from gotPodOptions (-want,+got):\n%s", diff)
   674  			}
   675  		})
   676  	}
   677  }