@@ -623,6 +623,103 @@ func (r *InstanceReconciler) setReferencedDataConditionWithTransition(
623623 return changed
624624}
625625
626+ // reconcileGatedReadyCondition handles the scheduling-gates branch of
627+ // reconcileInstanceReadyCondition. It evaluates ALL blocking sub-conditions
628+ // (quota, referenced-data, network failure) via the priority function and sets
629+ // Instance.Ready to the highest-priority cause.
630+ //
631+ // When quota is denied, Programmed=False and Running=False are also set
632+ // regardless of which reason wins Ready — quota gates programmatic activity
633+ // independently of the Ready reason displayed to the user.
634+ //
635+ // This is extracted from reconcileInstanceReadyCondition to keep that function's
636+ // cyclomatic complexity within the project lint limit.
637+ func (r * InstanceReconciler ) reconcileGatedReadyCondition (
638+ ctx context.Context ,
639+ clusterClient client.Client ,
640+ instance * computev1alpha.Instance ,
641+ quotaDenied bool ,
642+ quotaGrantedCondition * metav1.Condition ,
643+ readyCondition * metav1.Condition ,
644+ checker networkFailureChecker ,
645+ ) (changed bool , err error ) {
646+ schedulingGateNames := make ([]string , 0 , len (instance .Spec .Controller .SchedulingGates ))
647+ for _ , gate := range instance .Spec .Controller .SchedulingGates {
648+ schedulingGateNames = append (schedulingGateNames , gate .Name )
649+ }
650+
651+ type candidate struct {
652+ status metav1.ConditionStatus
653+ reason string
654+ message string
655+ priority int
656+ }
657+
658+ // Start with the generic fallback so there is always a winner.
659+ best := candidate {
660+ status : metav1 .ConditionFalse ,
661+ reason : computev1alpha .InstanceReadyReasonSchedulingGatesPresent ,
662+ message : fmt .Sprintf ("Scheduling gates present: %s" , strings .Join (schedulingGateNames , ", " )),
663+ priority : 0 ,
664+ }
665+
666+ consider := func (status metav1.ConditionStatus , reason , message string ) {
667+ p := instanceBlockingReasonPriority (reason )
668+ if p > best .priority {
669+ best = candidate {status : status , reason : reason , message : message , priority : p }
670+ }
671+ }
672+
673+ // Quota is a gate-level blocker: feed it through the priority function so it
674+ // competes fairly with other causes (priority 3). A co-occurring SourceNotFound
675+ // (priority 5) will correctly beat it.
676+ if quotaDenied {
677+ consider (metav1 .ConditionFalse , computev1alpha .InstanceProgrammedReasonPendingQuota , quotaGrantedCondition .Message )
678+ }
679+
680+ // Check the ReferencedDataReady sub-condition set earlier in this reconcile.
681+ if refDataCond := apimeta .FindStatusCondition (instance .Status .Conditions , computev1alpha .ReferencedDataReady ); refDataCond != nil && refDataCond .Status != metav1 .ConditionTrue {
682+ consider (refDataCond .Status , refDataCond .Reason , refDataCond .Message )
683+ }
684+
685+ // Network creation failure is a hard error; call the checker unconditionally
686+ // so priority logic can compare it against other blocking causes.
687+ networkCreationFailure , networkCreationFailureMessage , err := checker (ctx , clusterClient , instance )
688+ if err != nil {
689+ return false , fmt .Errorf ("failed checking for network creation failure: %w" , err )
690+ }
691+ if networkCreationFailure {
692+ consider (metav1 .ConditionFalse , reasonNetworkFailedToCreate , networkCreationFailureMessage )
693+ }
694+
695+ // When quota is denied, always stamp Programmed=False and Available=False
696+ // regardless of which reason wins Ready. These reflect quota state independently
697+ // of the Ready reason selection.
698+ if quotaDenied {
699+ msg := quotaGrantedCondition .Message
700+ changed = apimeta .SetStatusCondition (& instance .Status .Conditions , metav1.Condition {
701+ Type : computev1alpha .InstanceProgrammed ,
702+ Status : metav1 .ConditionFalse ,
703+ Reason : computev1alpha .InstanceProgrammedReasonPendingQuota ,
704+ Message : msg ,
705+ ObservedGeneration : instance .Generation ,
706+ })
707+ changed = apimeta .SetStatusCondition (& instance .Status .Conditions , metav1.Condition {
708+ Type : computev1alpha .InstanceAvailable ,
709+ Status : metav1 .ConditionFalse ,
710+ Reason : computev1alpha .InstanceProgrammedReasonPendingQuota ,
711+ Message : msg ,
712+ ObservedGeneration : instance .Generation ,
713+ }) || changed
714+ }
715+
716+ readyCondition .Status = best .status
717+ readyCondition .Reason = best .reason
718+ readyCondition .Message = best .message
719+
720+ return apimeta .SetStatusCondition (& instance .Status .Conditions , * readyCondition ) || changed , nil
721+ }
722+
626723// isTerminalReferencedDataReason reports whether the given ReferencedData reason
627724// is terminal — i.e., the companion will never arrive because the source object
628725// is permanently unavailable, not just slow to propagate.
@@ -1433,7 +1530,21 @@ func (r *InstanceReconciler) reconcileInstanceReadyCondition(
14331530 logger := log .FromContext (ctx )
14341531
14351532 quotaGrantedCondition := apimeta .FindStatusCondition (instance .Status .Conditions , computev1alpha .InstanceQuotaGranted )
1436- if quotaGrantedCondition != nil && quotaGrantedCondition .Status == metav1 .ConditionFalse {
1533+ quotaDenied := quotaGrantedCondition != nil && quotaGrantedCondition .Status == metav1 .ConditionFalse
1534+
1535+ // When scheduling gates are present, all blocking causes — including quota —
1536+ // are evaluated together so the priority function picks the most actionable
1537+ // one to surface on Ready. Quota side effects (Programmed=False, Running=False)
1538+ // are preserved unconditionally when quota is denied, regardless of which
1539+ // reason wins Ready.
1540+ //
1541+ // When no gates are present and quota is denied, fall through to the early
1542+ // return below which sets Ready, Programmed, and Running atomically.
1543+ hasSchedulingGates := instance .Spec .Controller != nil && len (instance .Spec .Controller .SchedulingGates ) > 0
1544+
1545+ if quotaDenied && ! hasSchedulingGates {
1546+ // No gates: quota is the only active blocking cause. Set all three
1547+ // conditions atomically and return — same behavior as before.
14371548 msg := quotaGrantedCondition .Message
14381549 changed = apimeta .SetStatusCondition (& instance .Status .Conditions , metav1.Condition {
14391550 Type : computev1alpha .InstanceProgrammed ,
@@ -1472,58 +1583,8 @@ func (r *InstanceReconciler) reconcileInstanceReadyCondition(
14721583 readyCondition = readyCondition .DeepCopy ()
14731584 }
14741585
1475- if instance .Spec .Controller != nil && len (instance .Spec .Controller .SchedulingGates ) > 0 {
1476- var schedulingGateNames []string
1477- for _ , gate := range instance .Spec .Controller .SchedulingGates {
1478- schedulingGateNames = append (schedulingGateNames , gate .Name )
1479- }
1480-
1481- // Evaluate ALL blocking sub-conditions before choosing which to surface.
1482- // A short-circuit on the first match would let a low-priority transient
1483- // cause (e.g. network provisioning) mask a high-priority actionable error
1484- // (e.g. a missing source ConfigMap).
1485- type candidate struct {
1486- status metav1.ConditionStatus
1487- reason string
1488- message string
1489- priority int
1490- }
1491-
1492- // Start with the generic fallback so there is always a winner.
1493- best := candidate {
1494- status : metav1 .ConditionFalse ,
1495- reason : computev1alpha .InstanceReadyReasonSchedulingGatesPresent ,
1496- message : fmt .Sprintf ("Scheduling gates present: %s" , strings .Join (schedulingGateNames , ", " )),
1497- priority : 0 ,
1498- }
1499-
1500- consider := func (status metav1.ConditionStatus , reason , message string ) {
1501- p := instanceBlockingReasonPriority (reason )
1502- if p > best .priority {
1503- best = candidate {status : status , reason : reason , message : message , priority : p }
1504- }
1505- }
1506-
1507- // Check the ReferencedDataReady sub-condition set earlier in this reconcile.
1508- if refDataCond := apimeta .FindStatusCondition (instance .Status .Conditions , computev1alpha .ReferencedDataReady ); refDataCond != nil && refDataCond .Status != metav1 .ConditionTrue {
1509- consider (refDataCond .Status , refDataCond .Reason , refDataCond .Message )
1510- }
1511-
1512- // Network creation failure is a hard error; call the checker unconditionally
1513- // so priority logic can compare it against other blocking causes.
1514- networkCreationFailure , networkCreationFailureMessage , err := networkFailureChecker (ctx , clusterClient , instance )
1515- if err != nil {
1516- return false , fmt .Errorf ("failed checking for network creation failure: %w" , err )
1517- }
1518- if networkCreationFailure {
1519- consider (metav1 .ConditionFalse , reasonNetworkFailedToCreate , networkCreationFailureMessage )
1520- }
1521-
1522- readyCondition .Status = best .status
1523- readyCondition .Reason = best .reason
1524- readyCondition .Message = best .message
1525-
1526- return apimeta .SetStatusCondition (& instance .Status .Conditions , * readyCondition ), nil
1586+ if hasSchedulingGates {
1587+ return r .reconcileGatedReadyCondition (ctx , clusterClient , instance , quotaDenied , quotaGrantedCondition , readyCondition , networkFailureChecker )
15271588 }
15281589
15291590 pendingReason := "Pending"
0 commit comments