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 })