sigs.k8s.io/kueue@v0.6.2/test/e2e/singlecluster/visibility_test.go (about)

     1  /*
     2  Copyright 2024 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 e2e
    18  
    19  import (
    20  	"github.com/google/go-cmp/cmp"
    21  	"github.com/google/go-cmp/cmp/cmpopts"
    22  	"github.com/onsi/ginkgo/v2"
    23  	"github.com/onsi/gomega"
    24  	batchv1 "k8s.io/api/batch/v1"
    25  	corev1 "k8s.io/api/core/v1"
    26  	rbacv1 "k8s.io/api/rbac/v1"
    27  	schedulingv1 "k8s.io/api/scheduling/v1"
    28  	"k8s.io/apimachinery/pkg/api/errors"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"sigs.k8s.io/controller-runtime/pkg/client"
    31  
    32  	kueue "sigs.k8s.io/kueue/apis/kueue/v1beta1"
    33  	visibility "sigs.k8s.io/kueue/apis/visibility/v1alpha1"
    34  	"sigs.k8s.io/kueue/pkg/util/testing"
    35  	testingjob "sigs.k8s.io/kueue/pkg/util/testingjobs/job"
    36  	"sigs.k8s.io/kueue/test/util"
    37  )
    38  
    39  var _ = ginkgo.Describe("Kueue visibility server", func() {
    40  	const defaultFlavor = "default-flavor"
    41  
    42  	// We do not check workload's Name, CreationTimestamp, and its OwnerReference's UID as they are generated at the server-side.
    43  	var pendingWorkloadsCmpOpts = []cmp.Option{
    44  		cmpopts.IgnoreFields(metav1.ObjectMeta{}, "Name"),
    45  		cmpopts.IgnoreFields(metav1.ObjectMeta{}, "CreationTimestamp"),
    46  		cmpopts.IgnoreFields(metav1.OwnerReference{}, "UID"),
    47  	}
    48  
    49  	var (
    50  		defaultRF         *kueue.ResourceFlavor
    51  		localQueueA       *kueue.LocalQueue
    52  		localQueueB       *kueue.LocalQueue
    53  		clusterQueue      *kueue.ClusterQueue
    54  		nsA               *corev1.Namespace
    55  		nsB               *corev1.Namespace
    56  		blockingJob       *batchv1.Job
    57  		sampleJob2        *batchv1.Job
    58  		highPriorityClass *schedulingv1.PriorityClass
    59  		midPriorityClass  *schedulingv1.PriorityClass
    60  		lowPriorityClass  *schedulingv1.PriorityClass
    61  	)
    62  
    63  	ginkgo.BeforeEach(func() {
    64  		nsA = &corev1.Namespace{
    65  			ObjectMeta: metav1.ObjectMeta{
    66  				GenerateName: "e2e-",
    67  			},
    68  		}
    69  		gomega.Expect(k8sClient.Create(ctx, nsA)).To(gomega.Succeed())
    70  		nsB = &corev1.Namespace{
    71  			ObjectMeta: metav1.ObjectMeta{
    72  				GenerateName: "e2e-",
    73  			},
    74  		}
    75  		gomega.Expect(k8sClient.Create(ctx, nsB)).To(gomega.Succeed())
    76  	})
    77  	ginkgo.AfterEach(func() {
    78  		gomega.Expect(util.DeleteNamespace(ctx, k8sClient, nsA)).To(gomega.Succeed())
    79  		gomega.Expect(util.DeleteNamespace(ctx, k8sClient, nsB)).To(gomega.Succeed())
    80  	})
    81  
    82  	ginkgo.When("There are pending workloads due to capacity maxed by the admitted job", func() {
    83  		ginkgo.BeforeEach(func() {
    84  			defaultRF = testing.MakeResourceFlavor(defaultFlavor).Obj()
    85  			gomega.Expect(k8sClient.Create(ctx, defaultRF)).Should(gomega.Succeed())
    86  
    87  			clusterQueue = testing.MakeClusterQueue("cluster-queue").
    88  				ResourceGroup(
    89  					*testing.MakeFlavorQuotas(defaultFlavor).
    90  						Resource(corev1.ResourceCPU, "1").
    91  						Obj(),
    92  				).
    93  				Obj()
    94  			gomega.Expect(k8sClient.Create(ctx, clusterQueue)).Should(gomega.Succeed())
    95  
    96  			localQueueA = testing.MakeLocalQueue("a", nsA.Name).ClusterQueue(clusterQueue.Name).Obj()
    97  			gomega.Expect(k8sClient.Create(ctx, localQueueA)).Should(gomega.Succeed())
    98  
    99  			localQueueB = testing.MakeLocalQueue("b", nsA.Name).ClusterQueue(clusterQueue.Name).Obj()
   100  			gomega.Expect(k8sClient.Create(ctx, localQueueB)).Should(gomega.Succeed())
   101  
   102  			highPriorityClass = testing.MakePriorityClass("high").PriorityValue(100).Obj()
   103  			gomega.Expect(k8sClient.Create(ctx, highPriorityClass))
   104  
   105  			midPriorityClass = testing.MakePriorityClass("mid").PriorityValue(75).Obj()
   106  			gomega.Expect(k8sClient.Create(ctx, midPriorityClass))
   107  
   108  			lowPriorityClass = testing.MakePriorityClass("low").PriorityValue(50).Obj()
   109  			gomega.Expect(k8sClient.Create(ctx, lowPriorityClass))
   110  
   111  			ginkgo.By("Schedule a job that when admitted workload blocks the queue", func() {
   112  				blockingJob = testingjob.MakeJob("test-job-1", nsA.Name).
   113  					Queue(localQueueA.Name).
   114  					Image("gcr.io/k8s-staging-perf-tests/sleep:v0.1.0", []string{"1m"}).
   115  					Request(corev1.ResourceCPU, "1").
   116  					TerminationGracePeriod(1).
   117  					BackoffLimit(0).
   118  					PriorityClass(highPriorityClass.Name).
   119  					Obj()
   120  				gomega.Expect(k8sClient.Create(ctx, blockingJob)).Should(gomega.Succeed())
   121  			})
   122  			ginkgo.By("Ensure the workload is admitted, by awaiting until the job is unsuspended", func() {
   123  				expectJobUnsuspended(client.ObjectKeyFromObject(blockingJob))
   124  			})
   125  		})
   126  		ginkgo.AfterEach(func() {
   127  			gomega.Expect(k8sClient.Delete(ctx, lowPriorityClass)).To(gomega.Succeed())
   128  			gomega.Expect(k8sClient.Delete(ctx, midPriorityClass)).To(gomega.Succeed())
   129  			gomega.Expect(k8sClient.Delete(ctx, highPriorityClass)).To(gomega.Succeed())
   130  			gomega.Expect(util.DeleteLocalQueue(ctx, k8sClient, localQueueB)).Should(gomega.Succeed())
   131  			gomega.Expect(util.DeleteLocalQueue(ctx, k8sClient, localQueueA)).Should(gomega.Succeed())
   132  			gomega.Expect(util.DeleteAllJobsInNamespace(ctx, k8sClient, nsA)).Should(gomega.Succeed())
   133  			util.ExpectClusterQueueToBeDeleted(ctx, k8sClient, clusterQueue, true)
   134  			util.ExpectResourceFlavorToBeDeleted(ctx, k8sClient, defaultRF, true)
   135  		})
   136  
   137  		ginkgo.It("Should allow fetching information about pending workloads in ClusterQueue", func() {
   138  			ginkgo.By("Verify there are zero pending workloads", func() {
   139  				info, err := visibilityClient.ClusterQueues().GetPendingWorkloadsSummary(ctx, clusterQueue.Name, metav1.GetOptions{})
   140  				gomega.Expect(err).NotTo(gomega.HaveOccurred())
   141  				gomega.Expect(len(info.Items)).Should(gomega.Equal(0))
   142  			})
   143  
   144  			ginkgo.By("Schedule a job which is pending due to lower priority", func() {
   145  				sampleJob2 = testingjob.MakeJob("test-job-2", nsA.Name).
   146  					Queue(localQueueA.Name).
   147  					Image("gcr.io/k8s-staging-perf-tests/sleep:v0.1.0", []string{"1ms"}).
   148  					Request(corev1.ResourceCPU, "1").
   149  					PriorityClass(lowPriorityClass.Name).
   150  					Obj()
   151  				gomega.Expect(k8sClient.Create(ctx, sampleJob2)).Should(gomega.Succeed())
   152  			})
   153  
   154  			ginkgo.By("Verify there is one pending workload", func() {
   155  				gomega.Eventually(func() int {
   156  					info, err := visibilityClient.ClusterQueues().GetPendingWorkloadsSummary(ctx, clusterQueue.Name, metav1.GetOptions{})
   157  					gomega.Expect(err).NotTo(gomega.HaveOccurred())
   158  					return len(info.Items)
   159  				}, util.Timeout, util.Interval).Should(gomega.Equal(1))
   160  			})
   161  
   162  			ginkgo.By("Await for pods to be running", func() {
   163  				gomega.Eventually(func() int {
   164  					createdJob := &batchv1.Job{}
   165  					gomega.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(blockingJob), createdJob)).Should(gomega.Succeed())
   166  					return int(*createdJob.Status.Ready)
   167  				}, util.Timeout, util.Interval).Should(gomega.Equal(1))
   168  			})
   169  
   170  			ginkgo.By("Terminate execution of the first workload to release the quota", func() {
   171  				gomega.Expect(util.DeleteAllPodsInNamespace(ctx, k8sClient, nsA)).Should(gomega.Succeed())
   172  			})
   173  
   174  			ginkgo.By("Verify there are zero pending workloads, after the second workload is admitted", func() {
   175  				gomega.Eventually(func() int {
   176  					info, err := visibilityClient.ClusterQueues().GetPendingWorkloadsSummary(ctx, clusterQueue.Name, metav1.GetOptions{})
   177  					gomega.Expect(err).NotTo(gomega.HaveOccurred())
   178  					return len(info.Items)
   179  				}, util.Timeout, util.Interval).Should(gomega.Equal(0))
   180  			})
   181  		})
   182  
   183  		ginkgo.It("Should allow fetching information about position of pending workloads in ClusterQueue", func() {
   184  			ginkgo.By("Schedule three different jobs with different priorities and two different LocalQueues", func() {
   185  				jobCases := []struct {
   186  					JobName          string
   187  					JobPrioClassName string
   188  					LocalQueueName   string
   189  				}{
   190  					{
   191  						JobName:          "lq-a-high-prio",
   192  						JobPrioClassName: highPriorityClass.Name,
   193  						LocalQueueName:   localQueueA.Name,
   194  					},
   195  					{
   196  						JobName:          "lq-b-mid-prio",
   197  						JobPrioClassName: midPriorityClass.Name,
   198  						LocalQueueName:   localQueueB.Name,
   199  					},
   200  					{
   201  						JobName:          "lq-b-low-prio",
   202  						JobPrioClassName: lowPriorityClass.Name,
   203  						LocalQueueName:   localQueueB.Name,
   204  					},
   205  				}
   206  				for _, jobCase := range jobCases {
   207  					job := testingjob.MakeJob(jobCase.JobName, nsA.Name).
   208  						Queue(jobCase.LocalQueueName).
   209  						Image("gcr.io/k8s-staging-perf-tests/sleep:v0.1.0", []string{"1ms"}).
   210  						Request(corev1.ResourceCPU, "1").
   211  						PriorityClass(jobCase.JobPrioClassName).
   212  						Obj()
   213  					gomega.Expect(k8sClient.Create(ctx, job)).Should(gomega.Succeed())
   214  				}
   215  			})
   216  
   217  			ginkgo.By("Verify their positions and priorities", func() {
   218  				wantPendingWorkloads := []visibility.PendingWorkload{
   219  					{
   220  						ObjectMeta: metav1.ObjectMeta{
   221  							Namespace:       nsA.Name,
   222  							OwnerReferences: defaultOwnerReferenceForJob("lq-a-high-prio"),
   223  						},
   224  						Priority:               highPriorityClass.Value,
   225  						PositionInLocalQueue:   0,
   226  						PositionInClusterQueue: 0,
   227  						LocalQueueName:         localQueueA.Name,
   228  					},
   229  					{
   230  						ObjectMeta: metav1.ObjectMeta{
   231  							Namespace:       nsA.Name,
   232  							OwnerReferences: defaultOwnerReferenceForJob("lq-b-mid-prio"),
   233  						},
   234  						Priority:               midPriorityClass.Value,
   235  						PositionInLocalQueue:   0,
   236  						PositionInClusterQueue: 1,
   237  						LocalQueueName:         localQueueB.Name,
   238  					},
   239  					{
   240  						ObjectMeta: metav1.ObjectMeta{
   241  							Namespace:       nsA.Name,
   242  							OwnerReferences: defaultOwnerReferenceForJob("lq-b-low-prio"),
   243  						},
   244  						Priority:               lowPriorityClass.Value,
   245  						PositionInLocalQueue:   1,
   246  						PositionInClusterQueue: 2,
   247  						LocalQueueName:         localQueueB.Name,
   248  					},
   249  				}
   250  				gomega.Eventually(func() []visibility.PendingWorkload {
   251  					info, err := visibilityClient.ClusterQueues().GetPendingWorkloadsSummary(ctx, clusterQueue.Name, metav1.GetOptions{})
   252  					gomega.Expect(err).NotTo(gomega.HaveOccurred())
   253  					return info.Items
   254  				}, util.Timeout, util.Interval).Should(gomega.BeComparableTo(wantPendingWorkloads, pendingWorkloadsCmpOpts...))
   255  			})
   256  		})
   257  
   258  		ginkgo.It("Should allow fetching information about pending workloads in LocalQueue", func() {
   259  			ginkgo.By("Verify there are zero pending workloads", func() {
   260  				info, err := visibilityClient.LocalQueues(nsA.Name).GetPendingWorkloadsSummary(ctx, localQueueA.Name, metav1.GetOptions{})
   261  				gomega.Expect(err).NotTo(gomega.HaveOccurred())
   262  				gomega.Expect(len(info.Items)).Should(gomega.Equal(0))
   263  			})
   264  
   265  			ginkgo.By("Schedule a job which is pending due to lower priority", func() {
   266  				sampleJob2 = testingjob.MakeJob("test-job-2", nsA.Name).
   267  					Queue(localQueueA.Name).
   268  					Image("gcr.io/k8s-staging-perf-tests/sleep:v0.1.0", []string{"1ms"}).
   269  					Request(corev1.ResourceCPU, "1").
   270  					PriorityClass(lowPriorityClass.Name).
   271  					Obj()
   272  				gomega.Expect(k8sClient.Create(ctx, sampleJob2)).Should(gomega.Succeed())
   273  			})
   274  
   275  			ginkgo.By("Verify there is one pending workload", func() {
   276  				gomega.Eventually(func() int {
   277  					info, err := visibilityClient.LocalQueues(nsA.Name).GetPendingWorkloadsSummary(ctx, localQueueA.Name, metav1.GetOptions{})
   278  					gomega.Expect(err).NotTo(gomega.HaveOccurred())
   279  					return len(info.Items)
   280  				}, util.Timeout, util.Interval).Should(gomega.Equal(1))
   281  			})
   282  
   283  			ginkgo.By("Await for pods to be running", func() {
   284  				gomega.Eventually(func() int {
   285  					createdJob := &batchv1.Job{}
   286  					gomega.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(blockingJob), createdJob)).Should(gomega.Succeed())
   287  					return int(*createdJob.Status.Ready)
   288  				}, util.Timeout, util.Interval).Should(gomega.Equal(1))
   289  			})
   290  
   291  			ginkgo.By("Terminate execution of the first workload to release the quota", func() {
   292  				gomega.Expect(util.DeleteAllPodsInNamespace(ctx, k8sClient, nsA)).Should(gomega.Succeed())
   293  			})
   294  
   295  			ginkgo.By("Verify there are zero pending workloads, after the second workload is admitted", func() {
   296  				gomega.Eventually(func() int {
   297  					info, err := visibilityClient.LocalQueues(nsA.Name).GetPendingWorkloadsSummary(ctx, localQueueA.Name, metav1.GetOptions{})
   298  					gomega.Expect(err).NotTo(gomega.HaveOccurred())
   299  					return len(info.Items)
   300  				}, util.Timeout, util.Interval).Should(gomega.Equal(0))
   301  			})
   302  		})
   303  
   304  		ginkgo.It("Should allow fetching information about position of pending workloads from different LocalQueues", func() {
   305  			ginkgo.By("Schedule three different jobs with different priorities and two different LocalQueues", func() {
   306  				jobCases := []struct {
   307  					JobName          string
   308  					JobPrioClassName string
   309  					LocalQueueName   string
   310  				}{
   311  					{
   312  						JobName:          "lq-a-high-prio",
   313  						JobPrioClassName: highPriorityClass.Name,
   314  						LocalQueueName:   localQueueA.Name,
   315  					},
   316  					{
   317  						JobName:          "lq-b-mid-prio",
   318  						JobPrioClassName: midPriorityClass.Name,
   319  						LocalQueueName:   localQueueB.Name,
   320  					},
   321  					{
   322  						JobName:          "lq-b-low-prio",
   323  						JobPrioClassName: lowPriorityClass.Name,
   324  						LocalQueueName:   localQueueB.Name,
   325  					},
   326  				}
   327  				for _, jobCase := range jobCases {
   328  					job := testingjob.MakeJob(jobCase.JobName, nsA.Name).
   329  						Queue(jobCase.LocalQueueName).
   330  						Image("gcr.io/k8s-staging-perf-tests/sleep:v0.1.0", []string{"1ms"}).
   331  						Request(corev1.ResourceCPU, "1").
   332  						PriorityClass(jobCase.JobPrioClassName).
   333  						Obj()
   334  					gomega.Expect(k8sClient.Create(ctx, job)).Should(gomega.Succeed())
   335  				}
   336  			})
   337  
   338  			ginkgo.By("Verify their positions and priorities in LocalQueueA", func() {
   339  				wantPendingWorkloads := []visibility.PendingWorkload{
   340  					{
   341  						ObjectMeta: metav1.ObjectMeta{
   342  							Namespace:       nsA.Name,
   343  							OwnerReferences: defaultOwnerReferenceForJob("lq-a-high-prio"),
   344  						},
   345  						Priority:               highPriorityClass.Value,
   346  						PositionInLocalQueue:   0,
   347  						PositionInClusterQueue: 0,
   348  						LocalQueueName:         localQueueA.Name,
   349  					},
   350  				}
   351  				gomega.Eventually(func() []visibility.PendingWorkload {
   352  					info, err := visibilityClient.LocalQueues(nsA.Name).GetPendingWorkloadsSummary(ctx, localQueueA.Name, metav1.GetOptions{})
   353  					gomega.Expect(err).NotTo(gomega.HaveOccurred())
   354  					return info.Items
   355  				}, util.Timeout, util.Interval).Should(gomega.BeComparableTo(wantPendingWorkloads, pendingWorkloadsCmpOpts...))
   356  			})
   357  
   358  			ginkgo.By("Verify their positions and priorities in LocalQueueB", func() {
   359  				wantPendingWorkloads := []visibility.PendingWorkload{
   360  					{
   361  						ObjectMeta: metav1.ObjectMeta{
   362  							Namespace:       nsA.Name,
   363  							OwnerReferences: defaultOwnerReferenceForJob("lq-b-mid-prio"),
   364  						},
   365  						Priority:               midPriorityClass.Value,
   366  						PositionInLocalQueue:   0,
   367  						PositionInClusterQueue: 1,
   368  						LocalQueueName:         localQueueB.Name,
   369  					},
   370  					{
   371  						ObjectMeta: metav1.ObjectMeta{
   372  							Namespace:       nsA.Name,
   373  							OwnerReferences: defaultOwnerReferenceForJob("lq-b-low-prio"),
   374  						},
   375  						Priority:               lowPriorityClass.Value,
   376  						PositionInLocalQueue:   1,
   377  						PositionInClusterQueue: 2,
   378  						LocalQueueName:         localQueueB.Name,
   379  					},
   380  				}
   381  				gomega.Eventually(func() []visibility.PendingWorkload {
   382  					info, err := visibilityClient.LocalQueues(nsA.Name).GetPendingWorkloadsSummary(ctx, localQueueB.Name, metav1.GetOptions{})
   383  					gomega.Expect(err).NotTo(gomega.HaveOccurred())
   384  					return info.Items
   385  				}, util.Timeout, util.Interval).Should(gomega.BeComparableTo(wantPendingWorkloads, pendingWorkloadsCmpOpts...))
   386  			})
   387  		})
   388  		ginkgo.It("Should allow fetching information about position of pending workloads from different LocalQueues from different Namespaces", func() {
   389  
   390  			ginkgo.By("Create a LocalQueue in a different Namespace", func() {
   391  				localQueueB = testing.MakeLocalQueue("b", nsB.Name).ClusterQueue(clusterQueue.Name).Obj()
   392  				gomega.Expect(k8sClient.Create(ctx, localQueueB)).Should(gomega.Succeed())
   393  			})
   394  
   395  			ginkgo.By("Schedule three different jobs with different priorities and different LocalQueues in different Namespaces", func() {
   396  				jobCases := []struct {
   397  					JobName          string
   398  					JobPrioClassName string
   399  					LocalQueueName   string
   400  					nsName           string
   401  				}{
   402  					{
   403  						JobName:          "lq-a-high-prio",
   404  						JobPrioClassName: highPriorityClass.Name,
   405  						LocalQueueName:   localQueueA.Name,
   406  						nsName:           nsA.Name,
   407  					},
   408  					{
   409  						JobName:          "lq-b-mid-prio",
   410  						JobPrioClassName: midPriorityClass.Name,
   411  						LocalQueueName:   localQueueB.Name,
   412  						nsName:           nsB.Name,
   413  					},
   414  					{
   415  						JobName:          "lq-b-low-prio",
   416  						JobPrioClassName: lowPriorityClass.Name,
   417  						LocalQueueName:   localQueueB.Name,
   418  						nsName:           nsB.Name,
   419  					},
   420  				}
   421  				for _, jobCase := range jobCases {
   422  					job := testingjob.MakeJob(jobCase.JobName, jobCase.nsName).
   423  						Queue(jobCase.LocalQueueName).
   424  						Image("gcr.io/k8s-staging-perf-tests/sleep:v0.1.0", []string{"1ms"}).
   425  						Request(corev1.ResourceCPU, "1").
   426  						PriorityClass(jobCase.JobPrioClassName).
   427  						Obj()
   428  					gomega.Expect(k8sClient.Create(ctx, job)).Should(gomega.Succeed())
   429  				}
   430  			})
   431  
   432  			ginkgo.By("Verify their positions and priorities in LocalQueueA", func() {
   433  				wantPendingWorkloads := []visibility.PendingWorkload{
   434  					{
   435  						ObjectMeta: metav1.ObjectMeta{
   436  							Namespace:       nsA.Name,
   437  							OwnerReferences: defaultOwnerReferenceForJob("lq-a-high-prio"),
   438  						},
   439  						Priority:               highPriorityClass.Value,
   440  						PositionInLocalQueue:   0,
   441  						PositionInClusterQueue: 0,
   442  						LocalQueueName:         localQueueA.Name,
   443  					},
   444  				}
   445  				gomega.Eventually(func() []visibility.PendingWorkload {
   446  					info, err := visibilityClient.LocalQueues(nsA.Name).GetPendingWorkloadsSummary(ctx, localQueueA.Name, metav1.GetOptions{})
   447  					gomega.Expect(err).NotTo(gomega.HaveOccurred())
   448  					return info.Items
   449  				}, util.Timeout, util.Interval).Should(gomega.BeComparableTo(wantPendingWorkloads, pendingWorkloadsCmpOpts...))
   450  			})
   451  
   452  			ginkgo.By("Verify their positions and priorities in LocalQueueB", func() {
   453  				wantPendingWorkloads := []visibility.PendingWorkload{
   454  					{
   455  						ObjectMeta: metav1.ObjectMeta{
   456  							Namespace:       nsB.Name,
   457  							OwnerReferences: defaultOwnerReferenceForJob("lq-b-mid-prio"),
   458  						},
   459  						Priority:               midPriorityClass.Value,
   460  						PositionInLocalQueue:   0,
   461  						PositionInClusterQueue: 1,
   462  						LocalQueueName:         localQueueB.Name,
   463  					},
   464  					{
   465  						ObjectMeta: metav1.ObjectMeta{
   466  							Namespace:       nsB.Name,
   467  							OwnerReferences: defaultOwnerReferenceForJob("lq-b-low-prio"),
   468  						},
   469  						Priority:               lowPriorityClass.Value,
   470  						PositionInLocalQueue:   1,
   471  						PositionInClusterQueue: 2,
   472  						LocalQueueName:         localQueueB.Name,
   473  					},
   474  				}
   475  				gomega.Eventually(func() []visibility.PendingWorkload {
   476  					info, err := visibilityClient.LocalQueues(nsA.Name).GetPendingWorkloadsSummary(ctx, localQueueB.Name, metav1.GetOptions{})
   477  					gomega.Expect(err).NotTo(gomega.HaveOccurred())
   478  					return info.Items
   479  				}, util.Timeout, util.Interval).Should(gomega.BeComparableTo(wantPendingWorkloads, pendingWorkloadsCmpOpts...))
   480  			})
   481  		})
   482  	})
   483  
   484  	ginkgo.When("A subject is bound to kueue-batch-admin-role", func() {
   485  		var clusterRoleBinding *rbacv1.ClusterRoleBinding
   486  
   487  		ginkgo.BeforeEach(func() {
   488  			clusterRoleBinding = &rbacv1.ClusterRoleBinding{
   489  				ObjectMeta: metav1.ObjectMeta{Name: "read-pending-workloads"},
   490  				RoleRef:    rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: "kueue-batch-admin-role"},
   491  				Subjects: []rbacv1.Subject{
   492  					{Name: "default", APIGroup: "", Namespace: "kueue-system", Kind: rbacv1.ServiceAccountKind},
   493  				},
   494  			}
   495  			gomega.Expect(k8sClient.Create(ctx, clusterRoleBinding)).Should(gomega.Succeed())
   496  		})
   497  
   498  		ginkgo.AfterEach(func() {
   499  			gomega.Expect(k8sClient.Delete(ctx, clusterRoleBinding)).To(gomega.Succeed())
   500  		})
   501  
   502  		ginkgo.It("Should return an appropriate error", func() {
   503  			ginkgo.By("Returning a ResourceNotFound error for a nonexistent ClusterQueue", func() {
   504  				_, err := impersonatedVisibilityClient.ClusterQueues().GetPendingWorkloadsSummary(ctx, "non-existent", metav1.GetOptions{})
   505  				statusErr, ok := err.(*errors.StatusError)
   506  				gomega.Expect(ok).To(gomega.Equal(true))
   507  				gomega.Expect(statusErr.ErrStatus.Reason).To(gomega.Equal(metav1.StatusReasonNotFound))
   508  			})
   509  			ginkgo.By("Returning a ResourceNotFound error for a nonexistent LocalQueue", func() {
   510  				_, err := impersonatedVisibilityClient.LocalQueues(nsA.Name).GetPendingWorkloadsSummary(ctx, "non-existent", metav1.GetOptions{})
   511  				statusErr, ok := err.(*errors.StatusError)
   512  				gomega.Expect(ok).To(gomega.Equal(true))
   513  				gomega.Expect(statusErr.ErrStatus.Reason).To(gomega.Equal(metav1.StatusReasonNotFound))
   514  			})
   515  		})
   516  	})
   517  
   518  	ginkgo.When("A subject is bound to kueue-batch-user-role, but not to kueue-batch-admin-role", func() {
   519  		var roleBinding *rbacv1.RoleBinding
   520  
   521  		ginkgo.BeforeEach(func() {
   522  			roleBinding = &rbacv1.RoleBinding{
   523  				ObjectMeta: metav1.ObjectMeta{Name: "read-pending-workloads", Namespace: nsA.Name},
   524  				RoleRef:    rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: "kueue-batch-user-role"},
   525  				Subjects: []rbacv1.Subject{
   526  					{Name: "default", APIGroup: "", Namespace: "kueue-system", Kind: rbacv1.ServiceAccountKind},
   527  				},
   528  			}
   529  			gomega.Expect(k8sClient.Create(ctx, roleBinding)).Should(gomega.Succeed())
   530  		})
   531  
   532  		ginkgo.AfterEach(func() {
   533  			gomega.Expect(k8sClient.Delete(ctx, roleBinding)).To(gomega.Succeed())
   534  		})
   535  
   536  		ginkgo.It("Should return an appropriate error", func() {
   537  			ginkgo.By("Returning a Forbidden error due to insufficient permissions for the ClusterQueue request", func() {
   538  				_, err := impersonatedVisibilityClient.ClusterQueues().GetPendingWorkloadsSummary(ctx, "non-existent", metav1.GetOptions{})
   539  				statusErr, ok := err.(*errors.StatusError)
   540  				gomega.Expect(ok).To(gomega.Equal(true))
   541  				gomega.Expect(statusErr.ErrStatus.Reason).To(gomega.Equal(metav1.StatusReasonForbidden))
   542  			})
   543  			ginkgo.By("Returning a ResourceNotFound error for a nonexistent LocalQueue", func() {
   544  				_, err := impersonatedVisibilityClient.LocalQueues(nsA.Name).GetPendingWorkloadsSummary(ctx, "non-existent", metav1.GetOptions{})
   545  				statusErr, ok := err.(*errors.StatusError)
   546  				gomega.Expect(ok).To(gomega.Equal(true))
   547  				gomega.Expect(statusErr.ErrStatus.Reason).To(gomega.Equal(metav1.StatusReasonNotFound))
   548  			})
   549  			ginkgo.By("Returning a Forbidden error due to insufficient permissions for the LocalQueue request in different namespace", func() {
   550  				_, err := impersonatedVisibilityClient.LocalQueues("default").GetPendingWorkloadsSummary(ctx, "non-existent", metav1.GetOptions{})
   551  				statusErr, ok := err.(*errors.StatusError)
   552  				gomega.Expect(ok).To(gomega.Equal(true))
   553  				gomega.Expect(statusErr.ErrStatus.Reason).To(gomega.Equal(metav1.StatusReasonForbidden))
   554  			})
   555  		})
   556  	})
   557  
   558  	ginkgo.When("A subject is not bound to kueue-batch-user-role, nor to kueue-batch-admin-role", func() {
   559  		ginkgo.It("Should return an appropriate error", func() {
   560  			ginkgo.By("Returning a Forbidden error due to insufficient permissions for the ClusterQueue request", func() {
   561  				_, err := impersonatedVisibilityClient.ClusterQueues().GetPendingWorkloadsSummary(ctx, "non-existent", metav1.GetOptions{})
   562  				statusErr, ok := err.(*errors.StatusError)
   563  				gomega.Expect(ok).To(gomega.Equal(true))
   564  				gomega.Expect(statusErr.ErrStatus.Reason).To(gomega.Equal(metav1.StatusReasonForbidden))
   565  			})
   566  			ginkgo.By("Returning a Forbidden error due to insufficient permissions for the LocalQueue request", func() {
   567  				_, err := impersonatedVisibilityClient.LocalQueues(nsA.Name).GetPendingWorkloadsSummary(ctx, "non-existent", metav1.GetOptions{})
   568  				statusErr, ok := err.(*errors.StatusError)
   569  				gomega.Expect(ok).To(gomega.Equal(true))
   570  				gomega.Expect(statusErr.ErrStatus.Reason).To(gomega.Equal(metav1.StatusReasonForbidden))
   571  			})
   572  		})
   573  	})
   574  })