sigs.k8s.io/kueue@v0.6.2/test/integration/webhook/clusterqueue_test.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6      http://www.apache.org/licenses/LICENSE-2.0
     7  Unless required by applicable law or agreed to in writing, software
     8  distributed under the License is distributed on an "AS IS" BASIS,
     9  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    10  See the License for the specific language governing permissions and
    11  limitations under the License.
    12  */
    13  
    14  package webhook
    15  
    16  import (
    17  	"fmt"
    18  
    19  	"github.com/google/go-cmp/cmp/cmpopts"
    20  	"github.com/onsi/ginkgo/v2"
    21  	"github.com/onsi/gomega"
    22  	corev1 "k8s.io/api/core/v1"
    23  	"k8s.io/apimachinery/pkg/api/errors"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	"k8s.io/utils/ptr"
    26  	"sigs.k8s.io/controller-runtime/pkg/client"
    27  
    28  	kueue "sigs.k8s.io/kueue/apis/kueue/v1beta1"
    29  	"sigs.k8s.io/kueue/pkg/util/testing"
    30  	"sigs.k8s.io/kueue/test/util"
    31  )
    32  
    33  const (
    34  	resourcesMaxItems = 16
    35  	flavorsMaxItems   = 16
    36  )
    37  
    38  const (
    39  	isValid = iota
    40  	isForbidden
    41  	isInvalid
    42  )
    43  
    44  var _ = ginkgo.Describe("ClusterQueue Webhook", func() {
    45  	var ns *corev1.Namespace
    46  	defaultFlavorFungibility := &kueue.FlavorFungibility{
    47  		WhenCanBorrow:  kueue.Borrow,
    48  		WhenCanPreempt: kueue.TryNextFlavor,
    49  	}
    50  
    51  	ginkgo.BeforeEach(func() {
    52  		ns = &corev1.Namespace{
    53  			ObjectMeta: metav1.ObjectMeta{
    54  				GenerateName: "core-",
    55  			},
    56  		}
    57  		gomega.Expect(k8sClient.Create(ctx, ns)).To(gomega.Succeed())
    58  	})
    59  
    60  	ginkgo.AfterEach(func() {
    61  		gomega.Expect(util.DeleteNamespace(ctx, k8sClient, ns)).To(gomega.Succeed())
    62  	})
    63  
    64  	ginkgo.When("Creating a ClusterQueue", func() {
    65  
    66  		ginkgo.DescribeTable("Defaulting on creation", func(cq, wantCQ kueue.ClusterQueue) {
    67  			gomega.Expect(k8sClient.Create(ctx, &cq)).Should(gomega.Succeed())
    68  			defer func() {
    69  				util.ExpectClusterQueueToBeDeleted(ctx, k8sClient, &cq, true)
    70  			}()
    71  			gomega.Expect(cq).To(gomega.BeComparableTo(wantCQ,
    72  				cmpopts.IgnoreTypes(kueue.ClusterQueueStatus{}),
    73  				cmpopts.IgnoreFields(metav1.ObjectMeta{}, "UID", "ResourceVersion", "Generation", "CreationTimestamp", "ManagedFields")))
    74  		},
    75  			ginkgo.Entry("All defaults",
    76  				kueue.ClusterQueue{
    77  					ObjectMeta: metav1.ObjectMeta{
    78  						Name: "foo",
    79  					},
    80  				},
    81  				kueue.ClusterQueue{
    82  					ObjectMeta: metav1.ObjectMeta{
    83  						Name:       "foo",
    84  						Finalizers: []string{kueue.ResourceInUseFinalizerName},
    85  					},
    86  					Spec: kueue.ClusterQueueSpec{
    87  						QueueingStrategy:  kueue.BestEffortFIFO,
    88  						StopPolicy:        ptr.To(kueue.None),
    89  						FlavorFungibility: defaultFlavorFungibility,
    90  						Preemption: &kueue.ClusterQueuePreemption{
    91  							WithinClusterQueue:  kueue.PreemptionPolicyNever,
    92  							ReclaimWithinCohort: kueue.PreemptionPolicyNever,
    93  							BorrowWithinCohort: &kueue.BorrowWithinCohort{
    94  								Policy: kueue.BorrowWithinCohortPolicyNever,
    95  							},
    96  						},
    97  					},
    98  				},
    99  			),
   100  			ginkgo.Entry("Preemption overridden",
   101  				kueue.ClusterQueue{
   102  					ObjectMeta: metav1.ObjectMeta{
   103  						Name: "foo",
   104  					},
   105  					Spec: kueue.ClusterQueueSpec{
   106  						FlavorFungibility: defaultFlavorFungibility,
   107  						Preemption: &kueue.ClusterQueuePreemption{
   108  							WithinClusterQueue:  kueue.PreemptionPolicyLowerPriority,
   109  							ReclaimWithinCohort: kueue.PreemptionPolicyAny,
   110  							BorrowWithinCohort: &kueue.BorrowWithinCohort{
   111  								Policy:               kueue.BorrowWithinCohortPolicyLowerPriority,
   112  								MaxPriorityThreshold: ptr.To[int32](100),
   113  							},
   114  						},
   115  					},
   116  				},
   117  				kueue.ClusterQueue{
   118  					ObjectMeta: metav1.ObjectMeta{
   119  						Name:       "foo",
   120  						Finalizers: []string{kueue.ResourceInUseFinalizerName},
   121  					},
   122  					Spec: kueue.ClusterQueueSpec{
   123  						QueueingStrategy:  kueue.BestEffortFIFO,
   124  						StopPolicy:        ptr.To(kueue.None),
   125  						FlavorFungibility: defaultFlavorFungibility,
   126  						Preemption: &kueue.ClusterQueuePreemption{
   127  							WithinClusterQueue:  kueue.PreemptionPolicyLowerPriority,
   128  							ReclaimWithinCohort: kueue.PreemptionPolicyAny,
   129  							BorrowWithinCohort: &kueue.BorrowWithinCohort{
   130  								Policy:               kueue.BorrowWithinCohortPolicyLowerPriority,
   131  								MaxPriorityThreshold: ptr.To[int32](100),
   132  							},
   133  						},
   134  					},
   135  				},
   136  			),
   137  		)
   138  
   139  		ginkgo.It("Should have qualified flavor names when updating", func() {
   140  			ginkgo.By("Creating a new clusterQueue")
   141  			cq := testing.MakeClusterQueue("cluster-queue").
   142  				ResourceGroup(*testing.MakeFlavorQuotas("x86").Resource(corev1.ResourceMemory).Obj()).
   143  				Obj()
   144  			gomega.Expect(k8sClient.Create(ctx, cq)).Should(gomega.Succeed())
   145  
   146  			defer func() {
   147  				util.ExpectClusterQueueToBeDeleted(ctx, k8sClient, cq, true)
   148  			}()
   149  
   150  			gomega.Eventually(func() error {
   151  				var updateCQ kueue.ClusterQueue
   152  				gomega.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(cq), &updateCQ)).Should(gomega.Succeed())
   153  				updateCQ.Spec.ResourceGroups[0].Flavors[0].Name = "@x86"
   154  				return k8sClient.Update(ctx, &updateCQ)
   155  			}, util.Timeout, util.Interval).Should(testing.BeForbiddenError())
   156  		})
   157  
   158  		ginkgo.DescribeTable("Validate ClusterQueue on creation", func(cq *kueue.ClusterQueue, errorType int) {
   159  			err := k8sClient.Create(ctx, cq)
   160  			if err == nil {
   161  				defer func() {
   162  					util.ExpectClusterQueueToBeDeleted(ctx, k8sClient, cq, true)
   163  				}()
   164  			}
   165  			switch errorType {
   166  			case isForbidden:
   167  				gomega.Expect(err).Should(gomega.HaveOccurred())
   168  				gomega.Expect(errors.IsForbidden(err)).Should(gomega.BeTrue(), "error: %v", err)
   169  			case isInvalid:
   170  				gomega.Expect(err).Should(gomega.HaveOccurred())
   171  				gomega.Expect(errors.IsInvalid(err)).Should(gomega.BeTrue(), "error: %v", err)
   172  			default:
   173  				gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
   174  				// Validating that defaults are set.
   175  				gomega.Expect(cq.Spec.QueueingStrategy).ToNot(gomega.BeEmpty())
   176  				if cq.Spec.Preemption != nil {
   177  					preemption := cq.Spec.Preemption
   178  					gomega.Expect(preemption.ReclaimWithinCohort).ToNot(gomega.BeEmpty())
   179  					gomega.Expect(preemption.WithinClusterQueue).ToNot(gomega.BeEmpty())
   180  				}
   181  			}
   182  		},
   183  			ginkgo.Entry("Should have non-negative borrowing limit",
   184  				testing.MakeClusterQueue("cluster-queue").
   185  					ResourceGroup(*testing.MakeFlavorQuotas("x86").Resource(corev1.ResourceCPU, "2", "-1").Obj()).
   186  					Cohort("cohort").
   187  					Obj(),
   188  				isForbidden),
   189  			ginkgo.Entry("Should have non-negative quota value",
   190  				testing.MakeClusterQueue("cluster-queue").
   191  					ResourceGroup(*testing.MakeFlavorQuotas("x86").Resource(corev1.ResourceCPU, "-1").Obj()).
   192  					Obj(),
   193  				isForbidden),
   194  			ginkgo.Entry("Should have at least one flavor",
   195  				testing.MakeClusterQueue("cluster-queue").ResourceGroup().Obj(),
   196  				isInvalid),
   197  			ginkgo.Entry("Should have at least one resource",
   198  				testing.MakeClusterQueue("cluster-queue").
   199  					ResourceGroup(*testing.MakeFlavorQuotas("foo").Obj()).
   200  					Obj(),
   201  				isInvalid),
   202  			ginkgo.Entry("Should have qualified flavor name",
   203  				testing.MakeClusterQueue("cluster-queue").
   204  					ResourceGroup(*testing.MakeFlavorQuotas("invalid_name").Resource(corev1.ResourceCPU, "5").Obj()).
   205  					Obj(),
   206  				isForbidden),
   207  			ginkgo.Entry("Should have qualified resource name",
   208  				testing.MakeClusterQueue("cluster-queue").
   209  					ResourceGroup(*testing.MakeFlavorQuotas("x86").Resource("@cpu", "5").Obj()).
   210  					Obj(),
   211  				isForbidden),
   212  			ginkgo.Entry("Should have valid resources quantity",
   213  				func() *kueue.ClusterQueue {
   214  					flvQuotas := testing.MakeFlavorQuotas("flavor")
   215  					for i := 0; i < resourcesMaxItems+1; i++ {
   216  						flvQuotas = flvQuotas.Resource(corev1.ResourceName(fmt.Sprintf("r%d", i)))
   217  					}
   218  					return testing.MakeClusterQueue("cluster-queue").ResourceGroup(*flvQuotas.Obj()).Obj()
   219  				}(),
   220  				isInvalid),
   221  			ginkgo.Entry("Should have valid flavors quantity",
   222  				func() *kueue.ClusterQueue {
   223  					flavors := make([]kueue.FlavorQuotas, flavorsMaxItems+1)
   224  					for i := range flavors {
   225  						flavors[i] = *testing.MakeFlavorQuotas(fmt.Sprintf("f%d", i)).
   226  							Resource(corev1.ResourceCPU).
   227  							Obj()
   228  					}
   229  					return testing.MakeClusterQueue("cluster-queue").ResourceGroup(flavors...).Obj()
   230  				}(),
   231  				isInvalid),
   232  			ginkgo.Entry("Should forbid clusterQueue creation with unqualified labelSelector",
   233  				testing.MakeClusterQueue("cluster-queue").NamespaceSelector(&metav1.LabelSelector{
   234  					MatchLabels: map[string]string{"nospecialchars^=@": "bar"},
   235  				}).Obj(),
   236  				isForbidden),
   237  			ginkgo.Entry("Should forbid to create clusterQueue with unknown clusterQueueingStrategy",
   238  				testing.MakeClusterQueue("cluster-queue").QueueingStrategy(kueue.QueueingStrategy("unknown")).Obj(),
   239  				isInvalid),
   240  			ginkgo.Entry("Should allow to create clusterQueue with empty clusterQueueingStrategy",
   241  				testing.MakeClusterQueue("cluster-queue").QueueingStrategy(kueue.QueueingStrategy("")).Obj(),
   242  				isValid),
   243  			ginkgo.Entry("Should allow to create clusterQueue with empty preemption",
   244  				testing.MakeClusterQueue("cluster-queue").Preemption(kueue.ClusterQueuePreemption{}).Obj(),
   245  				isValid),
   246  			ginkgo.Entry("Should allow to create clusterQueue with preemption policies",
   247  				testing.MakeClusterQueue("cluster-queue").Preemption(kueue.ClusterQueuePreemption{
   248  					ReclaimWithinCohort: kueue.PreemptionPolicyAny,
   249  					WithinClusterQueue:  kueue.PreemptionPolicyLowerPriority,
   250  				}).FlavorFungibility(*defaultFlavorFungibility).Obj(),
   251  				isValid),
   252  			ginkgo.Entry("Should forbid to create clusterQueue with unknown preemption.withinCohort",
   253  				testing.MakeClusterQueue("cluster-queue").Preemption(kueue.ClusterQueuePreemption{ReclaimWithinCohort: "unknown"}).Obj(),
   254  				isInvalid),
   255  			ginkgo.Entry("Should forbid to create clusterQueue with unknown preemption.withinClusterQueue",
   256  				testing.MakeClusterQueue("cluster-queue").Preemption(kueue.ClusterQueuePreemption{WithinClusterQueue: "unknown"}).Obj(),
   257  				isInvalid),
   258  		)
   259  	})
   260  })