@@ -27,6 +27,7 @@ import (
2727 "github.com/kai-scheduler/KAI-scheduler/test/e2e/modules/resources/rd"
2828 "github.com/kai-scheduler/KAI-scheduler/test/e2e/modules/resources/rd/queue"
2929 "github.com/kai-scheduler/KAI-scheduler/test/e2e/modules/utils"
30+ "github.com/kai-scheduler/KAI-scheduler/test/e2e/modules/wait"
3031)
3132
3233const (
@@ -192,7 +193,7 @@ func DescribeWorkloadSpecs() bool {
192193 pod := rd .CreatePodObject (testQ , corev1.ResourceRequirements {})
193194 pod .Spec .WorkloadRef = & corev1.WorkloadReference {Name : wlName , PodGroup : "g" }
194195 pod .Annotations [commonconstants .WorkloadIgnoreAnnotationKey ] = "true"
195- _ , err := rd .CreatePod (ctx , testCtx .KubeClientset , pod )
196+ pod , err := rd .CreatePod (ctx , testCtx .KubeClientset , pod )
196197 Expect (err ).NotTo (HaveOccurred ())
197198
198199 // The Workload-derived PodGroup must NOT appear.
@@ -203,6 +204,26 @@ func DescribeWorkloadSpecs() bool {
203204 return kerrors .IsNotFound (err )
204205 }, 3 * time .Second , pgPollTick ).Should (BeTrue (),
205206 "Workload-derived PodGroup must not be created when opt-out is set" )
207+
208+ // Opt-out must release the pod into the default top-owner path:
209+ // the pod is annotated with a non-Workload PodGroup name and the
210+ // pod schedules. Without this assertion, opt-out could silently
211+ // strand the pod.
212+ var pgName string
213+ Eventually (func () string {
214+ cur := & corev1.Pod {}
215+ if err := testCtx .ControllerClient .Get (ctx , types.NamespacedName {
216+ Namespace : pod .Namespace , Name : pod .Name ,
217+ }, cur ); err != nil {
218+ return ""
219+ }
220+ pgName = cur .Annotations [commonconstants .PodGroupAnnotationForPod ]
221+ return pgName
222+ }, pgWaitTimeout , pgPollTick ).ShouldNot (BeEmpty (),
223+ "opt-out pod must receive a default PodGroup annotation" )
224+ Expect (pgName ).NotTo (Equal (wlName + "-g" ),
225+ "opt-out pod must not be annotated with the Workload-derived PodGroup name" )
226+ wait .ForPodScheduled (ctx , testCtx .ControllerClient , pod )
206227 })
207228
208229 It ("layers a Workload override on top of a real top-owner controller (Job)" , func (ctx context.Context ) {
@@ -289,6 +310,64 @@ func DescribeWorkloadSpecs() bool {
289310 "PodGroup ownerReferences must point at the Job (top owner), not the Workload" )
290311 })
291312
313+ It ("gates gang scheduling on the Workload's MinCount" , func (ctx context.Context ) {
314+ // Smoke test that the Workload-derived PodGroup actually drives
315+ // scheduling end-to-end: a single pod referencing a Workload with
316+ // gang.MinCount=2 must stay unscheduled until the second pod
317+ // arrives, then both schedule together. Pure-translation tests
318+ // can't catch a regression where the spec is correct but the
319+ // scheduler ignores the resulting PodGroup.
320+ wlName := "gang-quorum-" + rand .String (6 )
321+ ns := queue .GetConnectedNamespaceToQueue (testQ )
322+ wl := & schedulingv1alpha1.Workload {
323+ ObjectMeta : metav1.ObjectMeta {Namespace : ns , Name : wlName },
324+ Spec : schedulingv1alpha1.WorkloadSpec {
325+ PodGroups : []schedulingv1alpha1.PodGroup {{
326+ Name : "workers" ,
327+ Policy : schedulingv1alpha1.PodGroupPolicy {
328+ Gang : & schedulingv1alpha1.GangSchedulingPolicy {MinCount : 2 },
329+ },
330+ }},
331+ },
332+ }
333+ Expect (testCtx .ControllerClient .Create (ctx , wl )).To (Succeed ())
334+ DeferCleanup (func (ctx context.Context ) {
335+ _ = testCtx .ControllerClient .Delete (ctx , wl )
336+ })
337+
338+ newGangPod := func () * corev1.Pod {
339+ p := rd .CreatePodObject (testQ , corev1.ResourceRequirements {})
340+ p .Spec .WorkloadRef = & corev1.WorkloadReference {
341+ Name : wlName , PodGroup : "workers" , PodGroupReplicaKey : "0" ,
342+ }
343+ return p
344+ }
345+
346+ pod1 , err := rd .CreatePod (ctx , testCtx .KubeClientset , newGangPod ())
347+ Expect (err ).NotTo (HaveOccurred ())
348+
349+ waitForPodGroup (ctx , testCtx , ns , fmt .Sprintf ("%s-workers-0" , wlName ))
350+
351+ // Below quorum: pod1 must remain unbound to a node. NodeName is
352+ // the authoritative bind signal — checking it avoids depending on
353+ // the exact Unschedulable-condition reason the scheduler chose.
354+ Consistently (func () string {
355+ cur := & corev1.Pod {}
356+ if err := testCtx .ControllerClient .Get (ctx , types.NamespacedName {
357+ Namespace : pod1 .Namespace , Name : pod1 .Name ,
358+ }, cur ); err != nil {
359+ return ""
360+ }
361+ return cur .Spec .NodeName
362+ }, 5 * time .Second , pgPollTick ).Should (BeEmpty (),
363+ "pod must stay unbound while the gang is below MinCount=2" )
364+
365+ pod2 , err := rd .CreatePod (ctx , testCtx .KubeClientset , newGangPod ())
366+ Expect (err ).NotTo (HaveOccurred ())
367+
368+ wait .ForPodsScheduled (ctx , testCtx .ControllerClient , ns , []* corev1.Pod {pod1 , pod2 })
369+ })
370+
292371 })
293372}
294373
0 commit comments