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

     1  /*
     2  Copyright 2021 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  	"testing"
    21  
    22  	"github.com/google/go-cmp/cmp"
    23  	"github.com/google/go-cmp/cmp/cmpopts"
    24  	corev1 "k8s.io/api/core/v1"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/util/validation/field"
    27  	"k8s.io/utils/ptr"
    28  
    29  	kueue "sigs.k8s.io/kueue/apis/kueue/v1beta1"
    30  	"sigs.k8s.io/kueue/pkg/features"
    31  	testingutil "sigs.k8s.io/kueue/pkg/util/testing"
    32  )
    33  
    34  func TestValidateClusterQueue(t *testing.T) {
    35  	specPath := field.NewPath("spec")
    36  	resourceGroupsPath := specPath.Child("resourceGroups")
    37  	preemptionPath := specPath.Child("preemption")
    38  
    39  	testcases := []struct {
    40  		name               string
    41  		clusterQueue       *kueue.ClusterQueue
    42  		wantErr            field.ErrorList
    43  		enableLendingLimit bool
    44  	}{
    45  		{
    46  			name: "built-in resources with qualified names",
    47  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").
    48  				ResourceGroup(*testingutil.MakeFlavorQuotas("default").Resource("cpu").Obj()).
    49  				Obj(),
    50  		},
    51  		{
    52  			name: "invalid resource name",
    53  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").
    54  				ResourceGroup(*testingutil.MakeFlavorQuotas("default").Resource("@cpu").Obj()).
    55  				Obj(),
    56  			wantErr: field.ErrorList{
    57  				field.Invalid(resourceGroupsPath.Index(0).Child("coveredResources").Index(0), "@cpu", ""),
    58  			},
    59  		},
    60  		{
    61  			name:         "in cohort",
    62  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").Cohort("prod").Obj(),
    63  		},
    64  		{
    65  			name:         "invalid cohort",
    66  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").Cohort("@prod").Obj(),
    67  			wantErr: field.ErrorList{
    68  				field.Invalid(specPath.Child("cohort"), "@prod", ""),
    69  			},
    70  		},
    71  		{
    72  			name: "extended resources with qualified names",
    73  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").
    74  				ResourceGroup(*testingutil.MakeFlavorQuotas("default").Resource("example.com/gpu").Obj()).
    75  				Obj(),
    76  		},
    77  		{
    78  			name: "flavor with qualified names",
    79  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").
    80  				ResourceGroup(*testingutil.MakeFlavorQuotas("x86").Obj()).
    81  				Obj(),
    82  		},
    83  		{
    84  			name: "flavor with unqualified names",
    85  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").
    86  				ResourceGroup(*testingutil.MakeFlavorQuotas("invalid_name").Obj()).
    87  				Obj(),
    88  			wantErr: field.ErrorList{
    89  				field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("name"), "invalid_name", ""),
    90  			},
    91  		},
    92  		{
    93  			name: "flavor quota with negative value",
    94  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").
    95  				ResourceGroup(
    96  					*testingutil.MakeFlavorQuotas("x86").Resource("cpu", "-1").Obj()).
    97  				Obj(),
    98  			wantErr: field.ErrorList{
    99  				field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources").Index(0).Child("nominalQuota"), "-1", ""),
   100  			},
   101  		},
   102  		{
   103  			name: "flavor quota with zero value",
   104  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").
   105  				ResourceGroup(
   106  					*testingutil.MakeFlavorQuotas("x86").Resource("cpu", "0").Obj()).
   107  				Obj(),
   108  		},
   109  		{
   110  			name: "flavor quota with borrowingLimit 0",
   111  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").
   112  				ResourceGroup(
   113  					*testingutil.MakeFlavorQuotas("x86").Resource("cpu", "1", "0").Obj()).
   114  				Cohort("cohort").
   115  				Obj(),
   116  		},
   117  		{
   118  			name: "flavor quota with negative borrowingLimit",
   119  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").
   120  				ResourceGroup(
   121  					*testingutil.MakeFlavorQuotas("x86").Resource("cpu", "1", "-1").Obj()).
   122  				Cohort("cohort").
   123  				Obj(),
   124  			wantErr: field.ErrorList{
   125  				field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources").Index(0).Child("borrowingLimit"), "-1", ""),
   126  			},
   127  		},
   128  		{
   129  			name: "flavor quota with borrowingLimit and empty cohort",
   130  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").
   131  				ResourceGroup(
   132  					*testingutil.MakeFlavorQuotas("x86").Resource("cpu", "1", "1").Obj()).
   133  				Obj(),
   134  			wantErr: field.ErrorList{
   135  				field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources").Index(0).Child("borrowingLimit"), "1", limitIsEmptyErrorMsg),
   136  			},
   137  		},
   138  		{
   139  			name: "flavor quota with lendingLimit 0",
   140  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").
   141  				ResourceGroup(
   142  					*testingutil.MakeFlavorQuotas("x86").Resource("cpu", "1", "", "0").Obj()).
   143  				Cohort("cohort").
   144  				Obj(),
   145  			enableLendingLimit: true,
   146  		},
   147  		{
   148  			name: "flavor quota with negative lendingLimit",
   149  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").
   150  				ResourceGroup(
   151  					*testingutil.MakeFlavorQuotas("x86").Resource("cpu", "1", "", "-1").Obj()).
   152  				Cohort("cohort").
   153  				Obj(),
   154  			wantErr: field.ErrorList{
   155  				field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources").Index(0).Child("lendingLimit"), "-1", ""),
   156  			},
   157  			enableLendingLimit: true,
   158  		},
   159  		{
   160  			name: "flavor quota with lendingLimit and empty cohort",
   161  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").
   162  				ResourceGroup(
   163  					*testingutil.MakeFlavorQuotas("x86").Resource("cpu", "1", "", "1").Obj()).
   164  				Obj(),
   165  			wantErr: field.ErrorList{
   166  				field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources").Index(0).Child("lendingLimit"), "1", limitIsEmptyErrorMsg),
   167  			},
   168  			enableLendingLimit: true,
   169  		},
   170  		{
   171  			name: "flavor quota with lendingLimit greater than nominalQuota",
   172  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").
   173  				ResourceGroup(
   174  					*testingutil.MakeFlavorQuotas("x86").Resource("cpu", "1", "", "2").Obj()).
   175  				Cohort("cohort").
   176  				Obj(),
   177  			wantErr: field.ErrorList{
   178  				field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources").Index(0).Child("lendingLimit"), "2", lendingLimitErrorMsg),
   179  			},
   180  			enableLendingLimit: true,
   181  		},
   182  		{
   183  			name: "empty queueing strategy is supported",
   184  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").
   185  				QueueingStrategy("").
   186  				Obj(),
   187  		},
   188  		{
   189  			name: "namespaceSelector with invalid labels",
   190  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").NamespaceSelector(&metav1.LabelSelector{
   191  				MatchLabels: map[string]string{"nospecialchars^=@": "bar"},
   192  			}).Obj(),
   193  			wantErr: field.ErrorList{
   194  				field.Invalid(specPath.Child("namespaceSelector", "matchLabels"), "nospecialchars^=@", ""),
   195  			},
   196  		},
   197  		{
   198  			name: "namespaceSelector with invalid expressions",
   199  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").NamespaceSelector(&metav1.LabelSelector{
   200  				MatchExpressions: []metav1.LabelSelectorRequirement{
   201  					{
   202  						Key:      "key",
   203  						Operator: "In",
   204  					},
   205  				},
   206  			}).Obj(),
   207  			wantErr: field.ErrorList{
   208  				field.Required(specPath.Child("namespaceSelector", "matchExpressions").Index(0).Child("values"), ""),
   209  			},
   210  		},
   211  		{
   212  			name: "multiple resource groups",
   213  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").
   214  				ResourceGroup(
   215  					*testingutil.MakeFlavorQuotas("alpha").
   216  						Resource("cpu", "0").
   217  						Resource("memory", "0").
   218  						Obj(),
   219  					*testingutil.MakeFlavorQuotas("beta").
   220  						Resource("cpu", "0").
   221  						Resource("memory", "0").
   222  						Obj(),
   223  				).
   224  				ResourceGroup(
   225  					*testingutil.MakeFlavorQuotas("gamma").
   226  						Resource("example.com/gpu", "0").
   227  						Obj(),
   228  					*testingutil.MakeFlavorQuotas("omega").
   229  						Resource("example.com/gpu", "0").
   230  						Obj(),
   231  				).
   232  				Obj(),
   233  		},
   234  		{
   235  			name: "resources in a flavor in different order",
   236  			clusterQueue: &kueue.ClusterQueue{
   237  				ObjectMeta: metav1.ObjectMeta{
   238  					Name: "cluster-queue",
   239  				},
   240  				Spec: kueue.ClusterQueueSpec{
   241  					ResourceGroups: []kueue.ResourceGroup{
   242  						{
   243  							CoveredResources: []corev1.ResourceName{"cpu", "memory"},
   244  							Flavors: []kueue.FlavorQuotas{
   245  								*testingutil.MakeFlavorQuotas("alpha").
   246  									Resource("cpu", "0").
   247  									Resource("memory", "0").
   248  									Obj(),
   249  								*testingutil.MakeFlavorQuotas("beta").
   250  									Resource("memory", "0").
   251  									Resource("cpu", "0").
   252  									Obj(),
   253  							},
   254  						},
   255  					},
   256  				},
   257  			},
   258  			wantErr: field.ErrorList{
   259  				field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(1).Child("resources").Index(0).Child("name"), nil, ""),
   260  				field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(1).Child("resources").Index(1).Child("name"), nil, ""),
   261  			},
   262  		},
   263  		{
   264  			name: "missing resources in a flavor",
   265  			clusterQueue: &kueue.ClusterQueue{
   266  				ObjectMeta: metav1.ObjectMeta{
   267  					Name: "cluster-queue",
   268  				},
   269  				Spec: kueue.ClusterQueueSpec{
   270  					ResourceGroups: []kueue.ResourceGroup{
   271  						{
   272  							CoveredResources: []corev1.ResourceName{"cpu", "memory"},
   273  							Flavors: []kueue.FlavorQuotas{
   274  								*testingutil.MakeFlavorQuotas("alpha").
   275  									Resource("cpu", "0").
   276  									Obj(),
   277  							},
   278  						},
   279  					},
   280  				},
   281  			},
   282  			wantErr: field.ErrorList{
   283  				field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources"), nil, ""),
   284  			},
   285  		},
   286  		{
   287  			name: "missing resources in a flavor",
   288  			clusterQueue: &kueue.ClusterQueue{
   289  				ObjectMeta: metav1.ObjectMeta{
   290  					Name: "cluster-queue",
   291  				},
   292  				Spec: kueue.ClusterQueueSpec{
   293  					ResourceGroups: []kueue.ResourceGroup{
   294  						{
   295  							CoveredResources: []corev1.ResourceName{"cpu"},
   296  							Flavors: []kueue.FlavorQuotas{
   297  								*testingutil.MakeFlavorQuotas("alpha").
   298  									Resource("cpu", "0").
   299  									Resource("memory", "0").
   300  									Obj(),
   301  							},
   302  						},
   303  					},
   304  				},
   305  			},
   306  			wantErr: field.ErrorList{
   307  				field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources"), nil, ""),
   308  			},
   309  		},
   310  		{
   311  			name: "missing resources in a flavor and mismatch",
   312  			clusterQueue: &kueue.ClusterQueue{
   313  				ObjectMeta: metav1.ObjectMeta{
   314  					Name: "cluster-queue",
   315  				},
   316  				Spec: kueue.ClusterQueueSpec{
   317  					ResourceGroups: []kueue.ResourceGroup{
   318  						{
   319  							CoveredResources: []corev1.ResourceName{"blah"},
   320  							Flavors: []kueue.FlavorQuotas{
   321  								*testingutil.MakeFlavorQuotas("alpha").
   322  									Resource("cpu", "0").
   323  									Resource("memory", "0").
   324  									Obj(),
   325  							},
   326  						},
   327  					},
   328  				},
   329  			},
   330  			wantErr: field.ErrorList{
   331  				field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources"), nil, ""),
   332  				field.Invalid(resourceGroupsPath.Index(0).Child("flavors").Index(0).Child("resources").Index(0).Child("name"), nil, ""),
   333  			},
   334  		},
   335  		{
   336  			name: "resource in more than one resource group",
   337  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").
   338  				ResourceGroup(
   339  					*testingutil.MakeFlavorQuotas("alpha").
   340  						Resource("cpu", "0").
   341  						Resource("memory", "0").
   342  						Obj(),
   343  				).
   344  				ResourceGroup(
   345  					*testingutil.MakeFlavorQuotas("beta").
   346  						Resource("memory", "0").
   347  						Obj(),
   348  				).
   349  				Obj(),
   350  			wantErr: field.ErrorList{
   351  				field.Duplicate(resourceGroupsPath.Index(1).Child("coveredResources").Index(0), nil),
   352  			},
   353  		},
   354  		{
   355  			name: "flavor in more than one resource group",
   356  			clusterQueue: testingutil.MakeClusterQueue("cluster-queue").
   357  				ResourceGroup(
   358  					*testingutil.MakeFlavorQuotas("alpha").Resource("cpu").Obj(),
   359  					*testingutil.MakeFlavorQuotas("beta").Resource("cpu").Obj(),
   360  				).
   361  				ResourceGroup(
   362  					*testingutil.MakeFlavorQuotas("beta").Resource("memory").Obj(),
   363  				).
   364  				Obj(),
   365  			wantErr: field.ErrorList{
   366  				field.Duplicate(resourceGroupsPath.Index(1).Child("flavors").Index(0).Child("name"), nil),
   367  			},
   368  		},
   369  		{
   370  			name: "invalid preemption due to reclaimWithinCohort=Never, while borrowWithinCohort!=nil",
   371  			clusterQueue: &kueue.ClusterQueue{
   372  				ObjectMeta: metav1.ObjectMeta{
   373  					Name: "cluster-queue",
   374  				},
   375  				Spec: kueue.ClusterQueueSpec{
   376  					Preemption: &kueue.ClusterQueuePreemption{
   377  						ReclaimWithinCohort: kueue.PreemptionPolicyNever,
   378  						BorrowWithinCohort: &kueue.BorrowWithinCohort{
   379  							Policy: kueue.BorrowWithinCohortPolicyLowerPriority,
   380  						},
   381  					},
   382  				},
   383  			},
   384  			wantErr: field.ErrorList{
   385  				field.Invalid(preemptionPath, nil, ""),
   386  			},
   387  		},
   388  		{
   389  			name: "valid preemption with borrowWithinCohort",
   390  			clusterQueue: &kueue.ClusterQueue{
   391  				ObjectMeta: metav1.ObjectMeta{
   392  					Name: "cluster-queue",
   393  				},
   394  				Spec: kueue.ClusterQueueSpec{
   395  					Preemption: &kueue.ClusterQueuePreemption{
   396  						ReclaimWithinCohort: kueue.PreemptionPolicyLowerPriority,
   397  						BorrowWithinCohort: &kueue.BorrowWithinCohort{
   398  							Policy:               kueue.BorrowWithinCohortPolicyLowerPriority,
   399  							MaxPriorityThreshold: ptr.To[int32](10),
   400  						},
   401  					},
   402  				},
   403  			},
   404  		},
   405  		{
   406  			name: "existing cluster queue created with older Kueue version that has a nil borrowWithinCohort field",
   407  			clusterQueue: &kueue.ClusterQueue{
   408  				ObjectMeta: metav1.ObjectMeta{
   409  					Name: "cluster-queue",
   410  				},
   411  				Spec: kueue.ClusterQueueSpec{
   412  					Preemption: &kueue.ClusterQueuePreemption{
   413  						ReclaimWithinCohort: kueue.PreemptionPolicyNever,
   414  					},
   415  				},
   416  			},
   417  		},
   418  	}
   419  
   420  	for _, tc := range testcases {
   421  		t.Run(tc.name, func(t *testing.T) {
   422  			defer features.SetFeatureGateDuringTest(t, features.LendingLimit, tc.enableLendingLimit)()
   423  			gotErr := ValidateClusterQueue(tc.clusterQueue)
   424  			if diff := cmp.Diff(tc.wantErr, gotErr, cmpopts.IgnoreFields(field.Error{}, "Detail", "BadValue")); diff != "" {
   425  				t.Errorf("ValidateResources() mismatch (-want +got):\n%s", diff)
   426  			}
   427  		})
   428  	}
   429  }
   430  
   431  func TestValidateClusterQueueUpdate(t *testing.T) {
   432  	testcases := []struct {
   433  		name            string
   434  		newClusterQueue *kueue.ClusterQueue
   435  		oldClusterQueue *kueue.ClusterQueue
   436  		wantErr         field.ErrorList
   437  	}{
   438  		{
   439  			name:            "queueingStrategy cannot be updated",
   440  			newClusterQueue: testingutil.MakeClusterQueue("cluster-queue").QueueingStrategy("BestEffortFIFO").Obj(),
   441  			oldClusterQueue: testingutil.MakeClusterQueue("cluster-queue").QueueingStrategy("StrictFIFO").Obj(),
   442  			wantErr: field.ErrorList{
   443  				field.Invalid(field.NewPath("spec", "queueingStrategy"), nil, ""),
   444  			},
   445  		},
   446  		{
   447  			name:            "same queueingStrategy",
   448  			newClusterQueue: testingutil.MakeClusterQueue("cluster-queue").QueueingStrategy("BestEffortFIFO").Obj(),
   449  			oldClusterQueue: testingutil.MakeClusterQueue("cluster-queue").QueueingStrategy("BestEffortFIFO").Obj(),
   450  			wantErr:         nil,
   451  		},
   452  	}
   453  
   454  	for _, tc := range testcases {
   455  		t.Run(tc.name, func(t *testing.T) {
   456  			gotErr := ValidateClusterQueueUpdate(tc.newClusterQueue, tc.oldClusterQueue)
   457  			if diff := cmp.Diff(tc.wantErr, gotErr, cmpopts.IgnoreFields(field.Error{}, "Detail", "BadValue")); diff != "" {
   458  				t.Errorf("ValidateResources() mismatch (-want +got):\n%s", diff)
   459  			}
   460  		})
   461  	}
   462  }