@@ -635,6 +635,103 @@ func (r *InstanceReconciler) setReferencedDataConditionWithTransition(
635635 return changed
636636}
637637
638+ // reconcileGatedReadyCondition handles the scheduling-gates branch of
639+ // reconcileInstanceReadyCondition. It evaluates ALL blocking sub-conditions
640+ // (quota, referenced-data, network failure) via the priority function and sets
641+ // Instance.Ready to the highest-priority cause.
642+ //
643+ // When quota is denied, Programmed=False and Running=False are also set
644+ // regardless of which reason wins Ready — quota gates programmatic activity
645+ // independently of the Ready reason displayed to the user.
646+ //
647+ // This is extracted from reconcileInstanceReadyCondition to keep that function's
648+ // cyclomatic complexity within the project lint limit.
649+ func (r * InstanceReconciler ) reconcileGatedReadyCondition (
650+ ctx context.Context ,
651+ clusterClient client.Client ,
652+ instance * computev1alpha.Instance ,
653+ quotaDenied bool ,
654+ quotaGrantedCondition * metav1.Condition ,
655+ readyCondition * metav1.Condition ,
656+ checker networkFailureChecker ,
657+ ) (changed bool , err error ) {
658+ schedulingGateNames := make ([]string , 0 , len (instance .Spec .Controller .SchedulingGates ))
659+ for _ , gate := range instance .Spec .Controller .SchedulingGates {
660+ schedulingGateNames = append (schedulingGateNames , gate .Name )
661+ }
662+
663+ type candidate struct {
664+ status metav1.ConditionStatus
665+ reason string
666+ message string
667+ priority int
668+ }
669+
670+ // Start with the generic fallback so there is always a winner.
671+ best := candidate {
672+ status : metav1 .ConditionFalse ,
673+ reason : computev1alpha .InstanceReadyReasonSchedulingGatesPresent ,
674+ message : fmt .Sprintf ("Scheduling gates present: %s" , strings .Join (schedulingGateNames , ", " )),
675+ priority : 0 ,
676+ }
677+
678+ consider := func (status metav1.ConditionStatus , reason , message string ) {
679+ p := instanceBlockingReasonPriority (reason )
680+ if p > best .priority {
681+ best = candidate {status : status , reason : reason , message : message , priority : p }
682+ }
683+ }
684+
685+ // Quota is a gate-level blocker: feed it through the priority function so it
686+ // competes fairly with other causes (priority 3). A co-occurring SourceNotFound
687+ // (priority 5) will correctly beat it.
688+ if quotaDenied {
689+ consider (metav1 .ConditionFalse , computev1alpha .InstanceProgrammedReasonPendingQuota , quotaGrantedCondition .Message )
690+ }
691+
692+ // Check the ReferencedDataReady sub-condition set earlier in this reconcile.
693+ if refDataCond := apimeta .FindStatusCondition (instance .Status .Conditions , computev1alpha .ReferencedDataReady ); refDataCond != nil && refDataCond .Status != metav1 .ConditionTrue {
694+ consider (refDataCond .Status , refDataCond .Reason , refDataCond .Message )
695+ }
696+
697+ // Network creation failure is a hard error; call the checker unconditionally
698+ // so priority logic can compare it against other blocking causes.
699+ networkCreationFailure , networkCreationFailureMessage , err := checker (ctx , clusterClient , instance )
700+ if err != nil {
701+ return false , fmt .Errorf ("failed checking for network creation failure: %w" , err )
702+ }
703+ if networkCreationFailure {
704+ consider (metav1 .ConditionFalse , reasonNetworkFailedToCreate , networkCreationFailureMessage )
705+ }
706+
707+ // When quota is denied, always stamp Programmed=False and Available=False
708+ // regardless of which reason wins Ready. These reflect quota state independently
709+ // of the Ready reason selection.
710+ if quotaDenied {
711+ msg := quotaGrantedCondition .Message
712+ changed = apimeta .SetStatusCondition (& instance .Status .Conditions , metav1.Condition {
713+ Type : computev1alpha .InstanceProgrammed ,
714+ Status : metav1 .ConditionFalse ,
715+ Reason : computev1alpha .InstanceProgrammedReasonPendingQuota ,
716+ Message : msg ,
717+ ObservedGeneration : instance .Generation ,
718+ })
719+ changed = apimeta .SetStatusCondition (& instance .Status .Conditions , metav1.Condition {
720+ Type : computev1alpha .InstanceAvailable ,
721+ Status : metav1 .ConditionFalse ,
722+ Reason : computev1alpha .InstanceProgrammedReasonPendingQuota ,
723+ Message : msg ,
724+ ObservedGeneration : instance .Generation ,
725+ }) || changed
726+ }
727+
728+ readyCondition .Status = best .status
729+ readyCondition .Reason = best .reason
730+ readyCondition .Message = best .message
731+
732+ return apimeta .SetStatusCondition (& instance .Status .Conditions , * readyCondition ) || changed , nil
733+ }
734+
638735// isTerminalReferencedDataReason reports whether the given ReferencedData reason
639736// is terminal — i.e., the companion will never arrive because the source object
640737// is permanently unavailable, not just slow to propagate.
@@ -1464,7 +1561,21 @@ func (r *InstanceReconciler) reconcileInstanceReadyCondition(
14641561 logger := log .FromContext (ctx )
14651562
14661563 quotaGrantedCondition := apimeta .FindStatusCondition (instance .Status .Conditions , computev1alpha .InstanceQuotaGranted )
1467- if quotaGrantedCondition != nil && quotaGrantedCondition .Status == metav1 .ConditionFalse {
1564+ quotaDenied := quotaGrantedCondition != nil && quotaGrantedCondition .Status == metav1 .ConditionFalse
1565+
1566+ // When scheduling gates are present, all blocking causes — including quota —
1567+ // are evaluated together so the priority function picks the most actionable
1568+ // one to surface on Ready. Quota side effects (Programmed=False, Running=False)
1569+ // are preserved unconditionally when quota is denied, regardless of which
1570+ // reason wins Ready.
1571+ //
1572+ // When no gates are present and quota is denied, fall through to the early
1573+ // return below which sets Ready, Programmed, and Running atomically.
1574+ hasSchedulingGates := instance .Spec .Controller != nil && len (instance .Spec .Controller .SchedulingGates ) > 0
1575+
1576+ if quotaDenied && ! hasSchedulingGates {
1577+ // No gates: quota is the only active blocking cause. Set all three
1578+ // conditions atomically and return — same behavior as before.
14681579 msg := quotaGrantedCondition .Message
14691580 changed = apimeta .SetStatusCondition (& instance .Status .Conditions , metav1.Condition {
14701581 Type : computev1alpha .InstanceProgrammed ,
@@ -1503,58 +1614,8 @@ func (r *InstanceReconciler) reconcileInstanceReadyCondition(
15031614 readyCondition = readyCondition .DeepCopy ()
15041615 }
15051616
1506- if instance .Spec .Controller != nil && len (instance .Spec .Controller .SchedulingGates ) > 0 {
1507- var schedulingGateNames []string
1508- for _ , gate := range instance .Spec .Controller .SchedulingGates {
1509- schedulingGateNames = append (schedulingGateNames , gate .Name )
1510- }
1511-
1512- // Evaluate ALL blocking sub-conditions before choosing which to surface.
1513- // A short-circuit on the first match would let a low-priority transient
1514- // cause (e.g. network provisioning) mask a high-priority actionable error
1515- // (e.g. a missing source ConfigMap).
1516- type candidate struct {
1517- status metav1.ConditionStatus
1518- reason string
1519- message string
1520- priority int
1521- }
1522-
1523- // Start with the generic fallback so there is always a winner.
1524- best := candidate {
1525- status : metav1 .ConditionFalse ,
1526- reason : computev1alpha .InstanceReadyReasonSchedulingGatesPresent ,
1527- message : fmt .Sprintf ("Scheduling gates present: %s" , strings .Join (schedulingGateNames , ", " )),
1528- priority : 0 ,
1529- }
1530-
1531- consider := func (status metav1.ConditionStatus , reason , message string ) {
1532- p := instanceBlockingReasonPriority (reason )
1533- if p > best .priority {
1534- best = candidate {status : status , reason : reason , message : message , priority : p }
1535- }
1536- }
1537-
1538- // Check the ReferencedDataReady sub-condition set earlier in this reconcile.
1539- if refDataCond := apimeta .FindStatusCondition (instance .Status .Conditions , computev1alpha .ReferencedDataReady ); refDataCond != nil && refDataCond .Status != metav1 .ConditionTrue {
1540- consider (refDataCond .Status , refDataCond .Reason , refDataCond .Message )
1541- }
1542-
1543- // Network creation failure is a hard error; call the checker unconditionally
1544- // so priority logic can compare it against other blocking causes.
1545- networkCreationFailure , networkCreationFailureMessage , err := networkFailureChecker (ctx , clusterClient , instance )
1546- if err != nil {
1547- return false , fmt .Errorf ("failed checking for network creation failure: %w" , err )
1548- }
1549- if networkCreationFailure {
1550- consider (metav1 .ConditionFalse , reasonNetworkFailedToCreate , networkCreationFailureMessage )
1551- }
1552-
1553- readyCondition .Status = best .status
1554- readyCondition .Reason = best .reason
1555- readyCondition .Message = best .message
1556-
1557- return apimeta .SetStatusCondition (& instance .Status .Conditions , * readyCondition ), nil
1617+ if hasSchedulingGates {
1618+ return r .reconcileGatedReadyCondition (ctx , clusterClient , instance , quotaDenied , quotaGrantedCondition , readyCondition , networkFailureChecker )
15581619 }
15591620
15601621 pendingReason := "Pending"
0 commit comments