sigs.k8s.io/kueue@v0.6.2/test/util/util.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 util 18 19 import ( 20 "context" 21 "fmt" 22 23 "github.com/google/go-cmp/cmp/cmpopts" 24 "github.com/onsi/ginkgo/v2" 25 "github.com/onsi/gomega" 26 batchv1 "k8s.io/api/batch/v1" 27 corev1 "k8s.io/api/core/v1" 28 nodev1 "k8s.io/api/node/v1" 29 "k8s.io/apimachinery/pkg/api/errors" 30 apierrors "k8s.io/apimachinery/pkg/api/errors" 31 apimeta "k8s.io/apimachinery/pkg/api/meta" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/types" 34 "k8s.io/component-base/metrics/testutil" 35 "k8s.io/utils/ptr" 36 "sigs.k8s.io/controller-runtime/pkg/client" 37 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 38 39 kueue "sigs.k8s.io/kueue/apis/kueue/v1beta1" 40 "sigs.k8s.io/kueue/pkg/controller/jobs/pod" 41 "sigs.k8s.io/kueue/pkg/metrics" 42 "sigs.k8s.io/kueue/pkg/util/testing" 43 "sigs.k8s.io/kueue/pkg/workload" 44 ) 45 46 func DeleteAdmissionCheck(ctx context.Context, c client.Client, ac *kueue.AdmissionCheck) error { 47 if ac != nil { 48 if err := c.Delete(ctx, ac); err != nil && !apierrors.IsNotFound(err) { 49 return err 50 } 51 } 52 return nil 53 } 54 55 func DeleteProvisioningRequestConfig(ctx context.Context, c client.Client, ac *kueue.ProvisioningRequestConfig) error { 56 if ac != nil { 57 if err := c.Delete(ctx, ac); err != nil && !apierrors.IsNotFound(err) { 58 return err 59 } 60 } 61 return nil 62 } 63 64 func DeleteWorkload(ctx context.Context, c client.Client, wl *kueue.Workload) error { 65 if wl != nil { 66 if err := c.Delete(ctx, wl); err != nil && !apierrors.IsNotFound(err) { 67 return err 68 } 69 } 70 return nil 71 } 72 73 func DeleteClusterQueue(ctx context.Context, c client.Client, cq *kueue.ClusterQueue) error { 74 if cq == nil { 75 return nil 76 } 77 if err := c.Delete(ctx, cq); err != nil && !apierrors.IsNotFound(err) { 78 return err 79 } 80 return nil 81 } 82 83 func DeleteResourceFlavor(ctx context.Context, c client.Client, rf *kueue.ResourceFlavor) error { 84 if rf == nil { 85 return nil 86 } 87 if err := c.Delete(ctx, rf); err != nil && !apierrors.IsNotFound(err) { 88 return err 89 } 90 return nil 91 } 92 93 func DeleteLocalQueue(ctx context.Context, c client.Client, q *kueue.LocalQueue) error { 94 if q == nil { 95 return nil 96 } 97 if err := c.Delete(ctx, q); err != nil && !apierrors.IsNotFound(err) { 98 return err 99 } 100 return nil 101 } 102 103 // DeleteNamespace deletes all objects the tests typically create in the namespace. 104 func DeleteNamespace(ctx context.Context, c client.Client, ns *corev1.Namespace) error { 105 if ns == nil { 106 return nil 107 } 108 if err := DeleteAllJobsInNamespace(ctx, c, ns); err != nil { 109 return err 110 } 111 if err := c.DeleteAllOf(ctx, &kueue.LocalQueue{}, client.InNamespace(ns.Name)); err != nil && !apierrors.IsNotFound(err) { 112 return err 113 } 114 if err := DeleteAllPodsInNamespace(ctx, c, ns); err != nil { 115 return err 116 } 117 if err := DeleteWorkloadsInNamespace(ctx, c, ns); err != nil { 118 return err 119 } 120 err := c.DeleteAllOf(ctx, &corev1.LimitRange{}, client.InNamespace(ns.Name), client.PropagationPolicy(metav1.DeletePropagationBackground)) 121 if err != nil && !apierrors.IsNotFound(err) { 122 return err 123 } 124 if err := c.Delete(ctx, ns); err != nil && !apierrors.IsNotFound(err) { 125 return err 126 } 127 return nil 128 } 129 130 func DeleteAllJobsInNamespace(ctx context.Context, c client.Client, ns *corev1.Namespace) error { 131 err := c.DeleteAllOf(ctx, &batchv1.Job{}, client.InNamespace(ns.Name), client.PropagationPolicy(metav1.DeletePropagationBackground)) 132 if err != nil && !apierrors.IsNotFound(err) { 133 return err 134 } 135 return nil 136 } 137 138 func DeleteAllPodsInNamespace(ctx context.Context, c client.Client, ns *corev1.Namespace) error { 139 err := c.DeleteAllOf(ctx, &corev1.Pod{}, client.InNamespace(ns.Name)) 140 if err != nil && !apierrors.IsNotFound(err) { 141 return fmt.Errorf("deleting all Pods in namespace %q: %w", ns.Name, err) 142 } 143 144 gomega.Eventually(func() error { 145 lst := corev1.PodList{} 146 err := c.List(ctx, &lst, client.InNamespace(ns.Name)) 147 if err != nil && !errors.IsNotFound(err) { 148 return fmt.Errorf("listing Pods with a finalizer in namespace %q: %w", ns.Name, err) 149 } 150 151 for _, p := range lst.Items { 152 if controllerutil.RemoveFinalizer(&p, pod.PodFinalizer) { 153 err = c.Update(ctx, &p) 154 if err != nil && !apierrors.IsNotFound(err) { 155 return fmt.Errorf("removing finalizer: %w", err) 156 } 157 } 158 } 159 160 return nil 161 }, LongTimeout, Interval).Should(gomega.Succeed()) 162 163 return nil 164 } 165 166 func DeleteWorkloadsInNamespace(ctx context.Context, c client.Client, ns *corev1.Namespace) error { 167 if err := c.DeleteAllOf(ctx, &kueue.Workload{}, client.InNamespace(ns.Name)); err != nil && !apierrors.IsNotFound(err) { 168 return err 169 } 170 171 gomega.Eventually(func() error { 172 lst := kueue.WorkloadList{} 173 err := c.List(ctx, &lst, client.InNamespace(ns.Name)) 174 if err != nil && !errors.IsNotFound(err) { 175 return fmt.Errorf("listing Workloads with a finalizer in namespace %q: %w", ns.Name, err) 176 } 177 178 for _, wl := range lst.Items { 179 if controllerutil.RemoveFinalizer(&wl, kueue.ResourceInUseFinalizerName) { 180 err = c.Update(ctx, &wl) 181 if err != nil && !apierrors.IsNotFound(err) { 182 return fmt.Errorf("removing finalizer: %w", err) 183 } 184 } 185 } 186 187 return nil 188 }, LongTimeout, Interval).Should(gomega.Succeed()) 189 190 return nil 191 } 192 193 func DeleteRuntimeClass(ctx context.Context, c client.Client, runtimeClass *nodev1.RuntimeClass) error { 194 if runtimeClass == nil { 195 return nil 196 } 197 if err := c.Delete(ctx, runtimeClass); err != nil && !apierrors.IsNotFound(err) { 198 return err 199 } 200 return nil 201 } 202 203 func UnholdQueue(ctx context.Context, k8sClient client.Client, cq *kueue.ClusterQueue) { 204 gomega.EventuallyWithOffset(1, func(g gomega.Gomega) { 205 var cqCopy kueue.ClusterQueue 206 g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(cq), &cqCopy)).To(gomega.Succeed()) 207 if ptr.Deref(cqCopy.Spec.StopPolicy, kueue.None) == kueue.None { 208 return 209 } 210 cqCopy.Spec.StopPolicy = ptr.To(kueue.None) 211 g.Expect(k8sClient.Update(ctx, &cqCopy)).To(gomega.Succeed()) 212 }, Timeout, Interval).Should(gomega.Succeed()) 213 } 214 215 func FinishWorkloads(ctx context.Context, k8sClient client.Client, workloads ...*kueue.Workload) { 216 for _, w := range workloads { 217 gomega.EventuallyWithOffset(1, func() error { 218 var newWL kueue.Workload 219 gomega.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(w), &newWL)).To(gomega.Succeed()) 220 newWL.Status.Conditions = append(w.Status.Conditions, metav1.Condition{ 221 Type: kueue.WorkloadFinished, 222 Status: metav1.ConditionTrue, 223 LastTransitionTime: metav1.Now(), 224 Reason: "ByTest", 225 Message: "Finished by test", 226 }) 227 return k8sClient.Status().Update(ctx, &newWL) 228 }, Timeout, Interval).Should(gomega.Succeed()) 229 } 230 } 231 232 func ExpectWorkloadsToHaveQuotaReservation(ctx context.Context, k8sClient client.Client, cqName string, wls ...*kueue.Workload) { 233 gomega.EventuallyWithOffset(1, func() int { 234 admitted := 0 235 var updatedWorkload kueue.Workload 236 for _, wl := range wls { 237 gomega.ExpectWithOffset(1, k8sClient.Get(ctx, client.ObjectKeyFromObject(wl), &updatedWorkload)).To(gomega.Succeed()) 238 if workload.HasQuotaReservation(&updatedWorkload) && string(updatedWorkload.Status.Admission.ClusterQueue) == cqName { 239 admitted++ 240 } 241 } 242 return admitted 243 }, Timeout, Interval).Should(gomega.Equal(len(wls)), "Not enough workloads were admitted") 244 } 245 246 func FilterAdmittedWorkloads(ctx context.Context, k8sClient client.Client, wls ...*kueue.Workload) []*kueue.Workload { 247 return filterWorkloads(ctx, k8sClient, workload.HasQuotaReservation, wls...) 248 } 249 250 func FilterEvictedWorkloads(ctx context.Context, k8sClient client.Client, wls ...*kueue.Workload) []*kueue.Workload { 251 return filterWorkloads(ctx, k8sClient, func(wl *kueue.Workload) bool { 252 return apimeta.IsStatusConditionTrue(wl.Status.Conditions, kueue.WorkloadEvicted) 253 }, wls...) 254 } 255 256 func filterWorkloads(ctx context.Context, k8sClient client.Client, filter func(*kueue.Workload) bool, wls ...*kueue.Workload) []*kueue.Workload { 257 ret := make([]*kueue.Workload, 0, len(wls)) 258 var updatedWorkload kueue.Workload 259 for _, wl := range wls { 260 err := k8sClient.Get(ctx, client.ObjectKeyFromObject(wl), &updatedWorkload) 261 if err == nil && filter(&updatedWorkload) { 262 ret = append(ret, wl) 263 } 264 } 265 return ret 266 } 267 268 func ExpectWorkloadsToBePending(ctx context.Context, k8sClient client.Client, wls ...*kueue.Workload) { 269 gomega.EventuallyWithOffset(1, func() int { 270 pending := 0 271 var updatedWorkload kueue.Workload 272 for _, wl := range wls { 273 gomega.ExpectWithOffset(1, k8sClient.Get(ctx, client.ObjectKeyFromObject(wl), &updatedWorkload)).To(gomega.Succeed()) 274 cond := apimeta.FindStatusCondition(updatedWorkload.Status.Conditions, kueue.WorkloadQuotaReserved) 275 if cond == nil { 276 continue 277 } 278 if cond.Status == metav1.ConditionFalse && cond.Reason == "Pending" { 279 pending++ 280 } 281 } 282 return pending 283 }, Timeout, Interval).Should(gomega.Equal(len(wls)), "Not enough workloads are pending") 284 } 285 286 func ExpectWorkloadsToBeAdmitted(ctx context.Context, k8sClient client.Client, wls ...*kueue.Workload) { 287 expectWorkloadsToBeAdmittedCountWithOffset(ctx, 2, k8sClient, len(wls), wls...) 288 } 289 290 func ExpectWorkloadsToBeAdmittedCount(ctx context.Context, k8sClient client.Client, count int, wls ...*kueue.Workload) { 291 expectWorkloadsToBeAdmittedCountWithOffset(ctx, 2, k8sClient, count, wls...) 292 } 293 294 func expectWorkloadsToBeAdmittedCountWithOffset(ctx context.Context, offset int, k8sClient client.Client, count int, wls ...*kueue.Workload) { 295 gomega.EventuallyWithOffset(offset, func() int { 296 admitted := 0 297 var updatedWorkload kueue.Workload 298 for _, wl := range wls { 299 gomega.ExpectWithOffset(offset, k8sClient.Get(ctx, client.ObjectKeyFromObject(wl), &updatedWorkload)).To(gomega.Succeed()) 300 if apimeta.IsStatusConditionTrue(updatedWorkload.Status.Conditions, kueue.WorkloadAdmitted) { 301 admitted++ 302 } 303 } 304 return admitted 305 }, Timeout, Interval).Should(gomega.Equal(count), "Not enough workloads are admitted") 306 } 307 308 func ExpectWorkloadToFinish(ctx context.Context, k8sClient client.Client, wlKey client.ObjectKey) { 309 gomega.EventuallyWithOffset(1, func(g gomega.Gomega) { 310 var wl kueue.Workload 311 g.Expect(k8sClient.Get(ctx, wlKey, &wl)).To(gomega.Succeed()) 312 g.Expect(apimeta.IsStatusConditionTrue(wl.Status.Conditions, kueue.WorkloadFinished)). 313 To(gomega.BeTrueBecause("it's finished")) 314 }, LongTimeout, Interval).Should(gomega.Succeed()) 315 } 316 317 func ExpectWorkloadToHaveRequeueCount(ctx context.Context, k8sClient client.Client, wlKey client.ObjectKey, requeueCount *int32) { 318 gomega.EventuallyWithOffset(1, func(g gomega.Gomega) { 319 var wl kueue.Workload 320 g.Expect(k8sClient.Get(ctx, wlKey, &wl)).Should(gomega.Succeed()) 321 g.Expect(ptr.Deref(wl.Status.RequeueState, kueue.RequeueState{})).Should(gomega.BeComparableTo(kueue.RequeueState{ 322 Count: requeueCount, 323 }, cmpopts.IgnoreFields(kueue.RequeueState{}, "RequeueAt"))) 324 if requeueCount != nil { 325 g.Expect(wl.Status.RequeueState.RequeueAt).ShouldNot(gomega.BeNil()) 326 } 327 }, Timeout, Interval).Should(gomega.Succeed()) 328 } 329 330 func ExpectWorkloadsToBePreempted(ctx context.Context, k8sClient client.Client, wls ...*kueue.Workload) { 331 gomega.EventuallyWithOffset(1, func() int { 332 preempted := 0 333 var updatedWorkload kueue.Workload 334 for _, wl := range wls { 335 gomega.ExpectWithOffset(1, k8sClient.Get(ctx, client.ObjectKeyFromObject(wl), &updatedWorkload)).To(gomega.Succeed()) 336 cond := apimeta.FindStatusCondition(updatedWorkload.Status.Conditions, kueue.WorkloadEvicted) 337 if cond == nil { 338 continue 339 } 340 if cond.Status == metav1.ConditionTrue { 341 preempted++ 342 } 343 } 344 return preempted 345 }, Timeout, Interval).Should(gomega.Equal(len(wls)), "Not enough workloads are preempted") 346 } 347 348 func ExpectWorkloadsToBeWaiting(ctx context.Context, k8sClient client.Client, wls ...*kueue.Workload) { 349 gomega.EventuallyWithOffset(1, func() int { 350 pending := 0 351 var updatedWorkload kueue.Workload 352 for _, wl := range wls { 353 gomega.ExpectWithOffset(1, k8sClient.Get(ctx, client.ObjectKeyFromObject(wl), &updatedWorkload)).To(gomega.Succeed()) 354 cond := apimeta.FindStatusCondition(updatedWorkload.Status.Conditions, kueue.WorkloadQuotaReserved) 355 if cond == nil { 356 continue 357 } 358 if cond.Status == metav1.ConditionFalse && cond.Reason == "Waiting" { 359 pending++ 360 } 361 } 362 return pending 363 }, Timeout, Interval).Should(gomega.Equal(len(wls)), "Not enough workloads are waiting") 364 } 365 366 func ExpectWorkloadsToBeFrozen(ctx context.Context, k8sClient client.Client, cq string, wls ...*kueue.Workload) { 367 gomega.EventuallyWithOffset(1, func() int { 368 frozen := 0 369 var updatedWorkload kueue.Workload 370 for _, wl := range wls { 371 gomega.ExpectWithOffset(1, k8sClient.Get(ctx, client.ObjectKeyFromObject(wl), &updatedWorkload)).To(gomega.Succeed()) 372 cond := apimeta.FindStatusCondition(updatedWorkload.Status.Conditions, kueue.WorkloadQuotaReserved) 373 if cond == nil { 374 continue 375 } 376 msg := fmt.Sprintf("ClusterQueue %s is inactive", cq) 377 if cond.Status == metav1.ConditionFalse && cond.Reason == "Inadmissible" && cond.Message == msg { 378 frozen++ 379 } 380 } 381 return frozen 382 }, Timeout, Interval).Should(gomega.Equal(len(wls)), "Not enough workloads are frozen") 383 } 384 385 func ExpectWorkloadToBeAdmittedAs(ctx context.Context, k8sClient client.Client, wl *kueue.Workload, admission *kueue.Admission) { 386 var updatedWorkload kueue.Workload 387 gomega.EventuallyWithOffset(1, func() *kueue.Admission { 388 gomega.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(wl), &updatedWorkload)).To(gomega.Succeed()) 389 return updatedWorkload.Status.Admission 390 }, Timeout, Interval).Should(gomega.BeComparableTo(admission)) 391 } 392 393 var attemptStatuses = []metrics.AdmissionResult{metrics.AdmissionResultInadmissible, metrics.AdmissionResultSuccess} 394 395 func ExpectAdmissionAttemptsMetric(pending, admitted int) { 396 vals := []int{pending, admitted} 397 398 for i, status := range attemptStatuses { 399 metric := metrics.AdmissionAttemptsTotal.WithLabelValues(string(status)) 400 gomega.EventuallyWithOffset(1, func() int { 401 v, err := testutil.GetCounterMetricValue(metric) 402 gomega.Expect(err).ToNot(gomega.HaveOccurred()) 403 return int(v) 404 }, Timeout, Interval).Should(gomega.Equal(vals[i]), "pending_workloads with status=%s", status) 405 } 406 } 407 408 var pendingStatuses = []string{metrics.PendingStatusActive, metrics.PendingStatusInadmissible} 409 410 func ExpectPendingWorkloadsMetric(cq *kueue.ClusterQueue, active, inadmissible int) { 411 vals := []int{active, inadmissible} 412 for i, status := range pendingStatuses { 413 metric := metrics.PendingWorkloads.WithLabelValues(cq.Name, status) 414 gomega.EventuallyWithOffset(1, func() int { 415 v, err := testutil.GetGaugeMetricValue(metric) 416 gomega.Expect(err).ToNot(gomega.HaveOccurred()) 417 return int(v) 418 }, Timeout, Interval).Should(gomega.Equal(vals[i]), "pending_workloads with status=%s", status) 419 } 420 } 421 422 func ExpectReservingActiveWorkloadsMetric(cq *kueue.ClusterQueue, v int) { 423 metric := metrics.ReservingActiveWorkloads.WithLabelValues(cq.Name) 424 gomega.EventuallyWithOffset(1, func() int { 425 v, err := testutil.GetGaugeMetricValue(metric) 426 gomega.Expect(err).ToNot(gomega.HaveOccurred()) 427 return int(v) 428 }, Timeout, Interval).Should(gomega.Equal(v)) 429 } 430 431 func ExpectAdmittedWorkloadsTotalMetric(cq *kueue.ClusterQueue, v int) { 432 metric := metrics.AdmittedWorkloadsTotal.WithLabelValues(cq.Name) 433 gomega.EventuallyWithOffset(1, func() int { 434 v, err := testutil.GetCounterMetricValue(metric) 435 gomega.Expect(err).ToNot(gomega.HaveOccurred()) 436 return int(v) 437 }, Timeout, Interval).Should(gomega.Equal(v)) 438 } 439 440 func ExpectClusterQueueStatusMetric(cq *kueue.ClusterQueue, status metrics.ClusterQueueStatus) { 441 for i, s := range metrics.CQStatuses { 442 var wantV float64 443 if metrics.CQStatuses[i] == status { 444 wantV = 1 445 } 446 metric := metrics.ClusterQueueByStatus.WithLabelValues(cq.Name, string(s)) 447 gomega.EventuallyWithOffset(1, func() float64 { 448 v, err := testutil.GetGaugeMetricValue(metric) 449 gomega.Expect(err).ToNot(gomega.HaveOccurred()) 450 return v 451 }, Timeout, Interval).Should(gomega.Equal(wantV), "cluster_queue_status with status=%s", s) 452 } 453 } 454 455 func ExpectAdmissionCheckToBeDeleted(ctx context.Context, k8sClient client.Client, ac *kueue.AdmissionCheck, deleteAC bool) { 456 if ac == nil { 457 return 458 } 459 if deleteAC { 460 gomega.Expect(client.IgnoreNotFound(DeleteAdmissionCheck(ctx, k8sClient, ac))).To(gomega.Succeed()) 461 } 462 gomega.EventuallyWithOffset(1, func() error { 463 var newAC kueue.AdmissionCheck 464 return k8sClient.Get(ctx, client.ObjectKeyFromObject(ac), &newAC) 465 }, Timeout, Interval).Should(testing.BeNotFoundError()) 466 } 467 468 func ExpectProvisioningRequestConfigToBeDeleted(ctx context.Context, k8sClient client.Client, prc *kueue.ProvisioningRequestConfig, deleteAC bool) { 469 if prc == nil { 470 return 471 } 472 if deleteAC { 473 gomega.ExpectWithOffset(1, client.IgnoreNotFound(DeleteProvisioningRequestConfig(ctx, k8sClient, prc))).To(gomega.Succeed()) 474 } 475 gomega.EventuallyWithOffset(1, func() error { 476 var newAC kueue.AdmissionCheck 477 return k8sClient.Get(ctx, client.ObjectKeyFromObject(prc), &newAC) 478 }, Timeout, Interval).Should(testing.BeNotFoundError()) 479 } 480 481 func ExpectClusterQueueToBeDeleted(ctx context.Context, k8sClient client.Client, cq *kueue.ClusterQueue, deleteCq bool) { 482 if deleteCq { 483 gomega.Expect(DeleteClusterQueue(ctx, k8sClient, cq)).ToNot(gomega.HaveOccurred()) 484 } 485 gomega.EventuallyWithOffset(1, func() error { 486 var newCQ kueue.ClusterQueue 487 return k8sClient.Get(ctx, client.ObjectKeyFromObject(cq), &newCQ) 488 }, Timeout, Interval).Should(testing.BeNotFoundError()) 489 } 490 491 func ExpectResourceFlavorToBeDeleted(ctx context.Context, k8sClient client.Client, rf *kueue.ResourceFlavor, deleteRf bool) { 492 if rf == nil { 493 return 494 } 495 if deleteRf { 496 gomega.Expect(DeleteResourceFlavor(ctx, k8sClient, rf)).To(gomega.Succeed()) 497 } 498 gomega.EventuallyWithOffset(1, func() error { 499 var newRF kueue.ResourceFlavor 500 return k8sClient.Get(ctx, client.ObjectKeyFromObject(rf), &newRF) 501 }, Timeout, Interval).Should(testing.BeNotFoundError()) 502 } 503 504 func ExpectCQResourceNominalQuota(cq *kueue.ClusterQueue, flavor, resource string, v float64) { 505 metric := metrics.ClusterQueueResourceNominalQuota.WithLabelValues(cq.Spec.Cohort, cq.Name, flavor, resource) 506 gomega.EventuallyWithOffset(1, func() float64 { 507 v, err := testutil.GetGaugeMetricValue(metric) 508 gomega.Expect(err).ToNot(gomega.HaveOccurred()) 509 return v 510 }, Timeout, Interval).Should(gomega.Equal(v)) 511 } 512 513 func ExpectCQResourceBorrowingQuota(cq *kueue.ClusterQueue, flavor, resource string, v float64) { 514 metric := metrics.ClusterQueueResourceBorrowingLimit.WithLabelValues(cq.Spec.Cohort, cq.Name, flavor, resource) 515 gomega.EventuallyWithOffset(1, func() float64 { 516 v, err := testutil.GetGaugeMetricValue(metric) 517 gomega.Expect(err).ToNot(gomega.HaveOccurred()) 518 return v 519 }, Timeout, Interval).Should(gomega.Equal(v)) 520 } 521 522 func ExpectCQResourceLendingQuota(cq *kueue.ClusterQueue, flavor, resource string, v float64) { 523 metric := metrics.ClusterQueueResourceLendingLimit.WithLabelValues(cq.Spec.Cohort, cq.Name, flavor, resource) 524 gomega.EventuallyWithOffset(1, func() float64 { 525 v, err := testutil.GetGaugeMetricValue(metric) 526 gomega.Expect(err).ToNot(gomega.HaveOccurred()) 527 return v 528 }, Timeout, Interval).Should(gomega.Equal(v)) 529 } 530 531 func ExpectCQResourceReservations(cq *kueue.ClusterQueue, flavor, resource string, v float64) { 532 metric := metrics.ClusterQueueResourceReservations.WithLabelValues(cq.Spec.Cohort, cq.Name, flavor, resource) 533 gomega.EventuallyWithOffset(1, func() float64 { 534 v, err := testutil.GetGaugeMetricValue(metric) 535 gomega.Expect(err).ToNot(gomega.HaveOccurred()) 536 return v 537 }, Timeout, Interval).Should(gomega.Equal(v)) 538 } 539 540 func SetQuotaReservation(ctx context.Context, k8sClient client.Client, wl *kueue.Workload, admission *kueue.Admission) error { 541 wl = wl.DeepCopy() 542 if admission == nil { 543 workload.UnsetQuotaReservationWithCondition(wl, "EvictedByTest", "Evicted By Test") 544 } else { 545 workload.SetQuotaReservation(wl, admission) 546 } 547 return workload.ApplyAdmissionStatus(ctx, k8sClient, wl, false) 548 } 549 550 // SyncAdmittedConditionForWorkloads sets the Admission condition of the provided workloads based on 551 // the state of quota reservation and admission checks. It should be use in tests that are not running 552 // the workload controller. 553 func SyncAdmittedConditionForWorkloads(ctx context.Context, k8sClient client.Client, wls ...*kueue.Workload) { 554 var updatedWorkload kueue.Workload 555 for _, wl := range wls { 556 gomega.ExpectWithOffset(1, k8sClient.Get(ctx, client.ObjectKeyFromObject(wl), &updatedWorkload)).To(gomega.Succeed()) 557 if workload.SyncAdmittedCondition(&updatedWorkload) { 558 gomega.ExpectWithOffset(1, workload.ApplyAdmissionStatus(ctx, k8sClient, &updatedWorkload, false)).To(gomega.Succeed()) 559 } 560 } 561 } 562 563 func FinishEvictionForWorkloads(ctx context.Context, k8sClient client.Client, wls ...*kueue.Workload) { 564 gomega.EventuallyWithOffset(1, func() int { 565 evicting := 0 566 var updatedWorkload kueue.Workload 567 for _, wl := range wls { 568 gomega.ExpectWithOffset(1, k8sClient.Get(ctx, client.ObjectKeyFromObject(wl), &updatedWorkload)).To(gomega.Succeed()) 569 if cond := apimeta.FindStatusCondition(updatedWorkload.Status.Conditions, kueue.WorkloadEvicted); cond != nil && cond.Status == metav1.ConditionTrue { 570 evicting++ 571 } 572 } 573 return evicting 574 }, Timeout, Interval).Should(gomega.Equal(len(wls)), "Not enough workloads were marked for eviction") 575 // unset the quota reservation 576 for i := range wls { 577 key := client.ObjectKeyFromObject(wls[i]) 578 gomega.EventuallyWithOffset(1, func() error { 579 var updatedWorkload kueue.Workload 580 if err := k8sClient.Get(ctx, key, &updatedWorkload); err != nil { 581 return err 582 } 583 584 if apimeta.IsStatusConditionTrue(updatedWorkload.Status.Conditions, kueue.WorkloadQuotaReserved) { 585 workload.UnsetQuotaReservationWithCondition(&updatedWorkload, "Pending", "By test") 586 return workload.ApplyAdmissionStatus(ctx, k8sClient, &updatedWorkload, true) 587 } 588 return nil 589 }, Timeout, Interval).Should(gomega.Succeed(), fmt.Sprintf("Unable to unset quota reservation for %q", key)) 590 } 591 } 592 593 func SetAdmissionCheckActive(ctx context.Context, k8sClient client.Client, admissionCheck *kueue.AdmissionCheck, status metav1.ConditionStatus) { 594 gomega.EventuallyWithOffset(1, func() error { 595 var updatedAc kueue.AdmissionCheck 596 err := k8sClient.Get(ctx, client.ObjectKeyFromObject(admissionCheck), &updatedAc) 597 if err != nil { 598 return err 599 } 600 apimeta.SetStatusCondition(&updatedAc.Status.Conditions, metav1.Condition{ 601 Type: kueue.AdmissionCheckActive, 602 Status: status, 603 Reason: "ByTest", 604 Message: "by test", 605 }) 606 return k8sClient.Status().Update(ctx, &updatedAc) 607 }, Timeout, Interval).Should(gomega.Succeed()) 608 } 609 610 func SetWorkloadsAdmissionCheck(ctx context.Context, k8sClient client.Client, wl *kueue.Workload, check string, state kueue.CheckState, expectExisting bool) { 611 var updatedWorkload kueue.Workload 612 gomega.EventuallyWithOffset(1, func(g gomega.Gomega) { 613 g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(wl), &updatedWorkload)).To(gomega.Succeed()) 614 if expectExisting { 615 currentCheck := workload.FindAdmissionCheck(updatedWorkload.Status.AdmissionChecks, check) 616 g.Expect(currentCheck).NotTo(gomega.BeNil(), "the check %s was not found in %s", check, workload.Key(wl)) 617 currentCheck.State = state 618 } else { 619 workload.SetAdmissionCheckState(&updatedWorkload.Status.AdmissionChecks, kueue.AdmissionCheckState{ 620 Name: check, 621 State: state, 622 }) 623 } 624 g.Expect(k8sClient.Status().Update(ctx, &updatedWorkload)).To(gomega.Succeed()) 625 }, Timeout, Interval).Should(gomega.Succeed()) 626 } 627 628 func AwaitAndVerifyWorkloadQueueName(ctx context.Context, client client.Client, createdWorkload *kueue.Workload, wlLookupKey types.NamespacedName, jobQueueName string) { 629 gomega.EventuallyWithOffset(1, func() bool { 630 if err := client.Get(ctx, wlLookupKey, createdWorkload); err != nil { 631 return false 632 } 633 return createdWorkload.Spec.QueueName == jobQueueName 634 }, Timeout, Interval).Should(gomega.BeTrue()) 635 } 636 637 func AwaitAndVerifyCreatedWorkload(ctx context.Context, client client.Client, wlLookupKey types.NamespacedName, createdJob metav1.Object) *kueue.Workload { 638 createdWorkload := &kueue.Workload{} 639 gomega.EventuallyWithOffset(1, func() error { 640 return client.Get(ctx, wlLookupKey, createdWorkload) 641 }, Timeout, Interval).Should(gomega.Succeed()) 642 gomega.ExpectWithOffset(1, metav1.IsControlledBy(createdWorkload, createdJob)).To(gomega.BeTrue(), "The Workload should be owned by the Job") 643 return createdWorkload 644 } 645 646 func VerifyWorkloadPriority(createdWorkload *kueue.Workload, priorityClassName string, priorityValue int32) { 647 ginkgo.By("checking the workload is created with priority and priorityName") 648 gomega.ExpectWithOffset(1, createdWorkload.Spec.PriorityClassName).Should(gomega.Equal(priorityClassName)) 649 gomega.ExpectWithOffset(1, *createdWorkload.Spec.Priority).Should(gomega.Equal(int32(priorityValue))) 650 } 651 652 func SetPodsPhase(ctx context.Context, k8sClient client.Client, phase corev1.PodPhase, pods ...*corev1.Pod) { 653 for _, p := range pods { 654 updatedPod := corev1.Pod{} 655 gomega.ExpectWithOffset(1, k8sClient.Get(ctx, client.ObjectKeyFromObject(p), &updatedPod)).To(gomega.Succeed()) 656 updatedPod.Status.Phase = phase 657 gomega.ExpectWithOffset(1, k8sClient.Status().Update(ctx, &updatedPod)).To(gomega.Succeed()) 658 } 659 } 660 661 func ExpectPodUnsuspendedWithNodeSelectors(ctx context.Context, k8sClient client.Client, key types.NamespacedName, ns map[string]string) { 662 createdPod := &corev1.Pod{} 663 gomega.EventuallyWithOffset(1, func(g gomega.Gomega) { 664 g.Expect(k8sClient.Get(ctx, key, createdPod)).To(gomega.Succeed()) 665 g.Expect(createdPod.Spec.SchedulingGates).NotTo(gomega.ContainElement(corev1.PodSchedulingGate{Name: pod.SchedulingGateName})) 666 g.Expect(createdPod.Spec.NodeSelector).To(gomega.BeComparableTo(ns)) 667 }, Timeout, Interval).Should(gomega.Succeed()) 668 } 669 670 func ExpectPodsFinalized(ctx context.Context, k8sClient client.Client, keys ...types.NamespacedName) { 671 for _, key := range keys { 672 createdPod := &corev1.Pod{} 673 gomega.EventuallyWithOffset(1, func(g gomega.Gomega) []string { 674 g.Expect(k8sClient.Get(ctx, key, createdPod)).To(gomega.Succeed()) 675 return createdPod.Finalizers 676 }, Timeout, Interval).Should(gomega.BeEmpty(), "Expected pod to be finalized") 677 } 678 }