sigs.k8s.io/kueue@v0.6.2/test/integration/controller/core/workload_controller_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 core 18 19 import ( 20 "fmt" 21 22 "github.com/google/go-cmp/cmp/cmpopts" 23 "github.com/onsi/ginkgo/v2" 24 "github.com/onsi/gomega" 25 corev1 "k8s.io/api/core/v1" 26 apimeta "k8s.io/apimachinery/pkg/api/meta" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "sigs.k8s.io/controller-runtime/pkg/client" 29 30 kueue "sigs.k8s.io/kueue/apis/kueue/v1beta1" 31 "sigs.k8s.io/kueue/pkg/constants" 32 "sigs.k8s.io/kueue/pkg/util/slices" 33 "sigs.k8s.io/kueue/pkg/util/testing" 34 "sigs.k8s.io/kueue/pkg/workload" 35 "sigs.k8s.io/kueue/test/integration/framework" 36 "sigs.k8s.io/kueue/test/util" 37 ) 38 39 // +kubebuilder:docs-gen:collapse=Imports 40 41 var _ = ginkgo.Describe("Workload controller", ginkgo.Ordered, ginkgo.ContinueOnFailure, func() { 42 var ( 43 ns *corev1.Namespace 44 updatedQueueWorkload kueue.Workload 45 finalQueueWorkload kueue.Workload 46 localQueue *kueue.LocalQueue 47 wl *kueue.Workload 48 message string 49 clusterQueue *kueue.ClusterQueue 50 workloadPriorityClass *kueue.WorkloadPriorityClass 51 updatedWorkloadPriorityClass *kueue.WorkloadPriorityClass 52 ) 53 54 ginkgo.BeforeAll(func() { 55 fwk = &framework.Framework{CRDPath: crdPath, WebhookPath: webhookPath} 56 cfg = fwk.Init() 57 ctx, k8sClient = fwk.RunManager(cfg, managerSetup) 58 }) 59 ginkgo.AfterAll(func() { 60 fwk.Teardown() 61 }) 62 63 ginkgo.BeforeEach(func() { 64 ns = &corev1.Namespace{ 65 ObjectMeta: metav1.ObjectMeta{ 66 GenerateName: "core-workload-", 67 }, 68 } 69 gomega.Expect(k8sClient.Create(ctx, ns)).To(gomega.Succeed()) 70 }) 71 72 ginkgo.AfterEach(func() { 73 clusterQueue = nil 74 localQueue = nil 75 updatedQueueWorkload = kueue.Workload{} 76 }) 77 78 ginkgo.When("the queue is not defined in the workload", func() { 79 ginkgo.AfterEach(func() { 80 gomega.Expect(util.DeleteNamespace(ctx, k8sClient, ns)).To(gomega.Succeed()) 81 }) 82 ginkgo.It("Should update status when workloads are created", func() { 83 wl = testing.MakeWorkload("one", ns.Name).Request(corev1.ResourceCPU, "1").Obj() 84 message = fmt.Sprintf("LocalQueue %s doesn't exist", "") 85 gomega.Expect(k8sClient.Create(ctx, wl)).To(gomega.Succeed()) 86 gomega.Eventually(func() int { 87 gomega.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(wl), &updatedQueueWorkload)).To(gomega.Succeed()) 88 return len(updatedQueueWorkload.Status.Conditions) 89 }, util.Timeout, util.Interval).Should(gomega.BeComparableTo(1)) 90 gomega.Expect(updatedQueueWorkload.Status.Conditions[0].Message).To(gomega.BeComparableTo(message)) 91 }) 92 }) 93 94 ginkgo.When("the queue doesn't exist", func() { 95 ginkgo.AfterEach(func() { 96 gomega.Expect(util.DeleteNamespace(ctx, k8sClient, ns)).To(gomega.Succeed()) 97 }) 98 ginkgo.It("Should update status when workloads are created", func() { 99 wl = testing.MakeWorkload("two", ns.Name).Queue("non-created-queue").Request(corev1.ResourceCPU, "1").Obj() 100 message = fmt.Sprintf("LocalQueue %s doesn't exist", "non-created-queue") 101 gomega.Expect(k8sClient.Create(ctx, wl)).To(gomega.Succeed()) 102 gomega.Eventually(func() int { 103 gomega.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(wl), &updatedQueueWorkload)).To(gomega.Succeed()) 104 return len(updatedQueueWorkload.Status.Conditions) 105 }, util.Timeout, util.Interval).Should(gomega.BeComparableTo(1)) 106 gomega.Expect(updatedQueueWorkload.Status.Conditions[0].Message).To(gomega.BeComparableTo(message)) 107 }) 108 }) 109 110 ginkgo.When("the clusterqueue doesn't exist", func() { 111 ginkgo.BeforeEach(func() { 112 localQueue = testing.MakeLocalQueue("queue", ns.Name).ClusterQueue("fooclusterqueue").Obj() 113 gomega.Expect(k8sClient.Create(ctx, localQueue)).To(gomega.Succeed()) 114 }) 115 ginkgo.AfterEach(func() { 116 gomega.Expect(util.DeleteNamespace(ctx, k8sClient, ns)).To(gomega.Succeed()) 117 }) 118 ginkgo.It("Should update status when workloads are created", func() { 119 wl = testing.MakeWorkload("three", ns.Name).Queue(localQueue.Name).Request(corev1.ResourceCPU, "1").Obj() 120 message = fmt.Sprintf("ClusterQueue %s doesn't exist", "fooclusterqueue") 121 gomega.Expect(k8sClient.Create(ctx, wl)).To(gomega.Succeed()) 122 gomega.Eventually(func() []metav1.Condition { 123 gomega.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(wl), &updatedQueueWorkload)).To(gomega.Succeed()) 124 return updatedQueueWorkload.Status.Conditions 125 }, util.Timeout, util.Interval).ShouldNot(gomega.BeNil()) 126 gomega.Expect(updatedQueueWorkload.Status.Conditions[0].Message).To(gomega.BeComparableTo(message)) 127 }) 128 }) 129 130 ginkgo.When("the workload is admitted", func() { 131 var flavor *kueue.ResourceFlavor 132 133 ginkgo.BeforeEach(func() { 134 flavor = testing.MakeResourceFlavor(flavorOnDemand).Obj() 135 gomega.Expect(k8sClient.Create(ctx, flavor)).Should(gomega.Succeed()) 136 clusterQueue = testing.MakeClusterQueue("cluster-queue"). 137 ResourceGroup(*testing.MakeFlavorQuotas(flavorOnDemand). 138 Resource(resourceGPU, "5", "5").Obj()). 139 Cohort("cohort"). 140 Obj() 141 gomega.Expect(k8sClient.Create(ctx, clusterQueue)).To(gomega.Succeed()) 142 localQueue = testing.MakeLocalQueue("queue", ns.Name).ClusterQueue(clusterQueue.Name).Obj() 143 gomega.Expect(k8sClient.Create(ctx, localQueue)).To(gomega.Succeed()) 144 }) 145 ginkgo.AfterEach(func() { 146 gomega.Expect(util.DeleteNamespace(ctx, k8sClient, ns)).To(gomega.Succeed()) 147 util.ExpectClusterQueueToBeDeleted(ctx, k8sClient, clusterQueue, true) 148 util.ExpectResourceFlavorToBeDeleted(ctx, k8sClient, flavor, true) 149 }) 150 }) 151 152 ginkgo.When("the queue has admission checks", func() { 153 var ( 154 flavor *kueue.ResourceFlavor 155 check1 *kueue.AdmissionCheck 156 check2 *kueue.AdmissionCheck 157 ) 158 159 ginkgo.BeforeEach(func() { 160 flavor = testing.MakeResourceFlavor(flavorOnDemand).Obj() 161 gomega.Expect(k8sClient.Create(ctx, flavor)).Should(gomega.Succeed()) 162 163 check1 = testing.MakeAdmissionCheck("check1").ControllerName("ctrl").Obj() 164 gomega.Expect(k8sClient.Create(ctx, check1)).Should(gomega.Succeed()) 165 util.SetAdmissionCheckActive(ctx, k8sClient, check1, metav1.ConditionTrue) 166 167 check2 = testing.MakeAdmissionCheck("check2").ControllerName("ctrl").Obj() 168 gomega.Expect(k8sClient.Create(ctx, check2)).Should(gomega.Succeed()) 169 util.SetAdmissionCheckActive(ctx, k8sClient, check2, metav1.ConditionTrue) 170 171 clusterQueue = testing.MakeClusterQueue("cluster-queue"). 172 ResourceGroup(*testing.MakeFlavorQuotas(flavorOnDemand). 173 Resource(resourceGPU, "5", "5").Obj()). 174 Cohort("cohort"). 175 AdmissionChecks("check1", "check2"). 176 Obj() 177 gomega.Expect(k8sClient.Create(ctx, clusterQueue)).To(gomega.Succeed()) 178 localQueue = testing.MakeLocalQueue("queue", ns.Name).ClusterQueue(clusterQueue.Name).Obj() 179 gomega.Expect(k8sClient.Create(ctx, localQueue)).To(gomega.Succeed()) 180 }) 181 ginkgo.AfterEach(func() { 182 gomega.Expect(util.DeleteNamespace(ctx, k8sClient, ns)).To(gomega.Succeed()) 183 util.ExpectClusterQueueToBeDeleted(ctx, k8sClient, clusterQueue, true) 184 util.ExpectAdmissionCheckToBeDeleted(ctx, k8sClient, check2, true) 185 util.ExpectAdmissionCheckToBeDeleted(ctx, k8sClient, check1, true) 186 util.ExpectResourceFlavorToBeDeleted(ctx, k8sClient, flavor, true) 187 }) 188 189 ginkgo.It("the workload should get the AdditionalChecks added", func() { 190 wl := testing.MakeWorkload("wl", ns.Name).Queue("queue").Obj() 191 wlKey := client.ObjectKeyFromObject(wl) 192 createdWl := kueue.Workload{} 193 ginkgo.By("creating the workload, the check conditions should be added", func() { 194 gomega.Expect(k8sClient.Create(ctx, wl)).To(gomega.Succeed()) 195 196 gomega.Eventually(func() []string { 197 gomega.Expect(k8sClient.Get(ctx, wlKey, &createdWl)).To(gomega.Succeed()) 198 return slices.Map(createdWl.Status.AdmissionChecks, func(c *kueue.AdmissionCheckState) string { return c.Name }) 199 }, util.Timeout, util.Interval).Should(gomega.ConsistOf("check1", "check2")) 200 }) 201 202 ginkgo.By("setting the check conditions", func() { 203 gomega.Eventually(func() error { 204 gomega.Expect(k8sClient.Get(ctx, wlKey, &createdWl)).To(gomega.Succeed()) 205 workload.SetAdmissionCheckState(&createdWl.Status.AdmissionChecks, kueue.AdmissionCheckState{ 206 Name: "check1", 207 State: kueue.CheckStateReady, 208 Message: "check successfully passed", 209 }) 210 workload.SetAdmissionCheckState(&createdWl.Status.AdmissionChecks, kueue.AdmissionCheckState{ 211 Name: "check2", 212 State: kueue.CheckStateRetry, 213 Message: "check rejected", 214 }) 215 return k8sClient.Status().Update(ctx, &createdWl) 216 }, util.Timeout, util.Interval).Should(gomega.Succeed()) 217 }) 218 219 // save check2 condition 220 oldCheck2Cond := workload.FindAdmissionCheck(createdWl.Status.AdmissionChecks, "check2") 221 gomega.Expect(oldCheck2Cond).NotTo(gomega.BeNil()) 222 223 ginkgo.By("updating the queue checks, the changes should propagate to the workload", func() { 224 createdQueue := kueue.ClusterQueue{} 225 queueKey := client.ObjectKeyFromObject(clusterQueue) 226 gomega.Eventually(func() error { 227 gomega.Expect(k8sClient.Get(ctx, queueKey, &createdQueue)).To(gomega.Succeed()) 228 createdQueue.Spec.AdmissionChecks = []string{"check2", "check3"} 229 return k8sClient.Update(ctx, &createdQueue) 230 }, util.Timeout, util.Interval).Should(gomega.Succeed()) 231 232 createdWl := kueue.Workload{} 233 gomega.Eventually(func() []string { 234 gomega.Expect(k8sClient.Get(ctx, wlKey, &createdWl)).To(gomega.Succeed()) 235 return slices.Map(createdWl.Status.AdmissionChecks, func(c *kueue.AdmissionCheckState) string { return c.Name }) 236 }, util.Timeout, util.Interval).Should(gomega.ConsistOf("check2", "check3")) 237 238 check2Cond := workload.FindAdmissionCheck(createdWl.Status.AdmissionChecks, "check2") 239 gomega.Expect(check2Cond).To(gomega.Equal(oldCheck2Cond)) 240 }) 241 }) 242 ginkgo.It("should finish an unadmitted workload with failure when a check is rejected", func() { 243 wl := testing.MakeWorkload("wl", ns.Name).Queue("queue").Obj() 244 wlKey := client.ObjectKeyFromObject(wl) 245 createdWl := kueue.Workload{} 246 ginkgo.By("creating the workload, the check conditions should be added", func() { 247 gomega.Expect(k8sClient.Create(ctx, wl)).To(gomega.Succeed()) 248 249 gomega.Eventually(func() []string { 250 gomega.Expect(k8sClient.Get(ctx, wlKey, &createdWl)).To(gomega.Succeed()) 251 return slices.Map(createdWl.Status.AdmissionChecks, func(c *kueue.AdmissionCheckState) string { return c.Name }) 252 }, util.Timeout, util.Interval).Should(gomega.ConsistOf("check1", "check2")) 253 }) 254 255 ginkgo.By("setting the check conditions", func() { 256 gomega.Eventually(func() error { 257 gomega.Expect(k8sClient.Get(ctx, wlKey, &createdWl)).To(gomega.Succeed()) 258 workload.SetAdmissionCheckState(&createdWl.Status.AdmissionChecks, kueue.AdmissionCheckState{ 259 Name: "check1", 260 State: kueue.CheckStateRejected, 261 Message: "check rejected", 262 }) 263 return k8sClient.Status().Update(ctx, &createdWl) 264 }, util.Timeout, util.Interval).Should(gomega.Succeed()) 265 }) 266 267 ginkgo.By("checking the finish condition", func() { 268 gomega.Eventually(func() *metav1.Condition { 269 gomega.Expect(k8sClient.Get(ctx, wlKey, &createdWl)).To(gomega.Succeed()) 270 return apimeta.FindStatusCondition(createdWl.Status.Conditions, kueue.WorkloadFinished) 271 }, util.Timeout, util.Interval).Should(gomega.BeComparableTo(&metav1.Condition{ 272 Type: kueue.WorkloadFinished, 273 Status: metav1.ConditionTrue, 274 Reason: "AdmissionChecksRejected", 275 Message: "Admission checks [check1] are rejected", 276 }, cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime"))) 277 }) 278 }) 279 280 ginkgo.It("should evict then finish with failure an admitted workload when a check is rejected", func() { 281 wl := testing.MakeWorkload("wl", ns.Name).Queue("queue").Obj() 282 wlKey := client.ObjectKeyFromObject(wl) 283 createdWl := kueue.Workload{} 284 ginkgo.By("creating the workload, the check conditions should be added", func() { 285 gomega.Expect(k8sClient.Create(ctx, wl)).To(gomega.Succeed()) 286 287 gomega.Eventually(func() []string { 288 gomega.Expect(k8sClient.Get(ctx, wlKey, &createdWl)).To(gomega.Succeed()) 289 return slices.Map(createdWl.Status.AdmissionChecks, func(c *kueue.AdmissionCheckState) string { return c.Name }) 290 }, util.Timeout, util.Interval).Should(gomega.ConsistOf("check1", "check2")) 291 }) 292 293 ginkgo.By("setting quota reservation and the checks ready, should admit the workload", func() { 294 gomega.Eventually(func(g gomega.Gomega) { 295 g.Expect(k8sClient.Get(ctx, wlKey, &createdWl)).To(gomega.Succeed()) 296 g.Expect(util.SetQuotaReservation(ctx, k8sClient, &createdWl, testing.MakeAdmission(clusterQueue.Name).Obj())).To(gomega.Succeed()) 297 }, util.Timeout, util.Interval).Should(gomega.Succeed()) 298 299 gomega.Eventually(func() error { 300 gomega.Expect(k8sClient.Get(ctx, wlKey, &createdWl)).To(gomega.Succeed()) 301 workload.SetAdmissionCheckState(&createdWl.Status.AdmissionChecks, kueue.AdmissionCheckState{ 302 Name: "check1", 303 State: kueue.CheckStateReady, 304 Message: "check ready", 305 }) 306 workload.SetAdmissionCheckState(&createdWl.Status.AdmissionChecks, kueue.AdmissionCheckState{ 307 Name: "check2", 308 State: kueue.CheckStateReady, 309 Message: "check ready", 310 }) 311 return k8sClient.Status().Update(ctx, &createdWl) 312 }, util.Timeout, util.Interval).Should(gomega.Succeed()) 313 314 gomega.Eventually(func(g gomega.Gomega) { 315 g.Expect(k8sClient.Get(ctx, wlKey, &createdWl)).To(gomega.Succeed()) 316 g.Expect(createdWl.Status.Conditions).To(gomega.ContainElement(gomega.BeComparableTo(metav1.Condition{ 317 Type: kueue.WorkloadAdmitted, 318 Status: metav1.ConditionTrue, 319 Reason: "Admitted", 320 Message: "The workload is admitted", 321 }, cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime")))) 322 }, util.Timeout, util.Interval).Should(gomega.Succeed()) 323 }) 324 325 ginkgo.By("setting a rejected check conditions the workload should be evicted and admitted condition kept", func() { 326 gomega.Eventually(func() error { 327 gomega.Expect(k8sClient.Get(ctx, wlKey, &createdWl)).To(gomega.Succeed()) 328 workload.SetAdmissionCheckState(&createdWl.Status.AdmissionChecks, kueue.AdmissionCheckState{ 329 Name: "check1", 330 State: kueue.CheckStateRejected, 331 Message: "check rejected", 332 }) 333 return k8sClient.Status().Update(ctx, &createdWl) 334 }, util.Timeout, util.Interval).Should(gomega.Succeed()) 335 336 gomega.Eventually(func(g gomega.Gomega) { 337 g.Expect(k8sClient.Get(ctx, wlKey, &createdWl)).To(gomega.Succeed()) 338 g.Expect(createdWl.Status.Conditions).To(gomega.ContainElements( 339 gomega.BeComparableTo(metav1.Condition{ 340 Type: kueue.WorkloadEvicted, 341 Status: metav1.ConditionTrue, 342 Reason: "AdmissionCheck", 343 Message: "At least one admission check is false", 344 }, cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime")), 345 gomega.BeComparableTo(metav1.Condition{ 346 Type: kueue.WorkloadAdmitted, 347 Status: metav1.ConditionTrue, 348 Reason: "Admitted", 349 Message: "The workload is admitted", 350 }, cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime")), 351 )) 352 }, util.Timeout, util.Interval).Should(gomega.Succeed()) 353 }) 354 355 ginkgo.By("finishing the eviction the finish condition should be set and admitted condition false", func() { 356 util.FinishEvictionForWorkloads(ctx, k8sClient, &createdWl) 357 gomega.Eventually(func(g gomega.Gomega) { 358 g.Expect(k8sClient.Get(ctx, wlKey, &createdWl)).To(gomega.Succeed()) 359 g.Expect(createdWl.Status.Conditions).To(gomega.ContainElements( 360 gomega.BeComparableTo(metav1.Condition{ 361 Type: kueue.WorkloadFinished, 362 Status: metav1.ConditionTrue, 363 Reason: "AdmissionChecksRejected", 364 Message: "Admission checks [check1] are rejected", 365 }, cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime")), 366 gomega.BeComparableTo(metav1.Condition{ 367 Type: kueue.WorkloadAdmitted, 368 Status: metav1.ConditionFalse, 369 Reason: "NoReservationNoChecks", 370 Message: "The workload has no reservation and not all checks ready", 371 }, cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime")), 372 )) 373 }, util.Timeout, util.Interval).Should(gomega.Succeed()) 374 }) 375 }) 376 }) 377 378 ginkgo.When("changing the priority value of PriorityClass doesn't affect the priority of the workload", func() { 379 ginkgo.BeforeEach(func() { 380 workloadPriorityClass = testing.MakeWorkloadPriorityClass("workload-priority-class").PriorityValue(200).Obj() 381 gomega.Expect(k8sClient.Create(ctx, workloadPriorityClass)).To(gomega.Succeed()) 382 383 }) 384 ginkgo.AfterEach(func() { 385 gomega.Expect(util.DeleteNamespace(ctx, k8sClient, ns)).To(gomega.Succeed()) 386 gomega.Expect(k8sClient.Delete(ctx, workloadPriorityClass)).To(gomega.Succeed()) 387 }) 388 ginkgo.It("case of WorkloadPriorityClass", func() { 389 ginkgo.By("creating workload") 390 wl = testing.MakeWorkload("wl", ns.Name).Queue("lq").Request(corev1.ResourceCPU, "1"). 391 PriorityClass("workload-priority-class").PriorityClassSource(constants.WorkloadPriorityClassSource).Priority(200).Obj() 392 gomega.Expect(k8sClient.Create(ctx, wl)).To(gomega.Succeed()) 393 gomega.Eventually(func() []metav1.Condition { 394 gomega.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(wl), &updatedQueueWorkload)).To(gomega.Succeed()) 395 return updatedQueueWorkload.Status.Conditions 396 }, util.Timeout, util.Interval).ShouldNot(gomega.BeNil()) 397 initialPriority := int32(200) 398 gomega.Expect(updatedQueueWorkload.Spec.Priority).To(gomega.Equal(&initialPriority)) 399 400 ginkgo.By("updating workloadPriorityClass") 401 updatedPriority := int32(150) 402 updatedWorkloadPriorityClass = workloadPriorityClass.DeepCopy() 403 workloadPriorityClass.Value = updatedPriority 404 gomega.Expect(k8sClient.Update(ctx, workloadPriorityClass)).To(gomega.Succeed()) 405 gomega.Eventually(func() int32 { 406 gomega.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(workloadPriorityClass), updatedWorkloadPriorityClass)).To(gomega.Succeed()) 407 return updatedWorkloadPriorityClass.Value 408 }, util.Timeout, util.Interval).Should(gomega.Equal(updatedPriority)) 409 gomega.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(&updatedQueueWorkload), &finalQueueWorkload)).To(gomega.Succeed()) 410 gomega.Expect(finalQueueWorkload.Spec.Priority).To(gomega.Equal(&initialPriority)) 411 }) 412 }) 413 })