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