@@ -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.
@@ -1434,7 +1531,21 @@ func (r *InstanceReconciler) reconcileInstanceReadyCondition(
14341531 logger := log .FromContext (ctx )
14351532
14361533 quotaGrantedCondition := apimeta .FindStatusCondition (instance .Status .Conditions , computev1alpha .InstanceQuotaGranted )
1437- if quotaGrantedCondition != nil && quotaGrantedCondition .Status == metav1 .ConditionFalse {
1534+ quotaDenied := quotaGrantedCondition != nil && quotaGrantedCondition .Status == metav1 .ConditionFalse
1535+
1536+ // When scheduling gates are present, all blocking causes — including quota —
1537+ // are evaluated together so the priority function picks the most actionable
1538+ // one to surface on Ready. Quota side effects (Programmed=False, Running=False)
1539+ // are preserved unconditionally when quota is denied, regardless of which
1540+ // reason wins Ready.
1541+ //
1542+ // When no gates are present and quota is denied, fall through to the early
1543+ // return below which sets Ready, Programmed, and Running atomically.
1544+ hasSchedulingGates := instance .Spec .Controller != nil && len (instance .Spec .Controller .SchedulingGates ) > 0
1545+
1546+ if quotaDenied && ! hasSchedulingGates {
1547+ // No gates: quota is the only active blocking cause. Set all three
1548+ // conditions atomically and return — same behavior as before.
14381549 msg := quotaGrantedCondition .Message
14391550 changed = apimeta .SetStatusCondition (& instance .Status .Conditions , metav1.Condition {
14401551 Type : computev1alpha .InstanceProgrammed ,
@@ -1473,58 +1584,8 @@ func (r *InstanceReconciler) reconcileInstanceReadyCondition(
14731584 readyCondition = readyCondition .DeepCopy ()
14741585 }
14751586
1476- if instance .Spec .Controller != nil && len (instance .Spec .Controller .SchedulingGates ) > 0 {
1477- var schedulingGateNames []string
1478- for _ , gate := range instance .Spec .Controller .SchedulingGates {
1479- schedulingGateNames = append (schedulingGateNames , gate .Name )
1480- }
1481-
1482- // Evaluate ALL blocking sub-conditions before choosing which to surface.
1483- // A short-circuit on the first match would let a low-priority transient
1484- // cause (e.g. network provisioning) mask a high-priority actionable error
1485- // (e.g. a missing source ConfigMap).
1486- type candidate struct {
1487- status metav1.ConditionStatus
1488- reason string
1489- message string
1490- priority int
1491- }
1492-
1493- // Start with the generic fallback so there is always a winner.
1494- best := candidate {
1495- status : metav1 .ConditionFalse ,
1496- reason : computev1alpha .InstanceReadyReasonSchedulingGatesPresent ,
1497- message : fmt .Sprintf ("Scheduling gates present: %s" , strings .Join (schedulingGateNames , ", " )),
1498- priority : 0 ,
1499- }
1500-
1501- consider := func (status metav1.ConditionStatus , reason , message string ) {
1502- p := instanceBlockingReasonPriority (reason )
1503- if p > best .priority {
1504- best = candidate {status : status , reason : reason , message : message , priority : p }
1505- }
1506- }
1507-
1508- // Check the ReferencedDataReady sub-condition set earlier in this reconcile.
1509- if refDataCond := apimeta .FindStatusCondition (instance .Status .Conditions , computev1alpha .ReferencedDataReady ); refDataCond != nil && refDataCond .Status != metav1 .ConditionTrue {
1510- consider (refDataCond .Status , refDataCond .Reason , refDataCond .Message )
1511- }
1512-
1513- // Network creation failure is a hard error; call the checker unconditionally
1514- // so priority logic can compare it against other blocking causes.
1515- networkCreationFailure , networkCreationFailureMessage , err := networkFailureChecker (ctx , clusterClient , instance )
1516- if err != nil {
1517- return false , fmt .Errorf ("failed checking for network creation failure: %w" , err )
1518- }
1519- if networkCreationFailure {
1520- consider (metav1 .ConditionFalse , reasonNetworkFailedToCreate , networkCreationFailureMessage )
1521- }
1522-
1523- readyCondition .Status = best .status
1524- readyCondition .Reason = best .reason
1525- readyCondition .Message = best .message
1526-
1527- return apimeta .SetStatusCondition (& instance .Status .Conditions , * readyCondition ), nil
1587+ if hasSchedulingGates {
1588+ return r .reconcileGatedReadyCondition (ctx , clusterClient , instance , quotaDenied , quotaGrantedCondition , readyCondition , networkFailureChecker )
15281589 }
15291590
15301591 pendingReason := "Pending"
0 commit comments