From d9edcc46201a347081981bd591153142cb0e2d0d Mon Sep 17 00:00:00 2001 From: Cortney Reed Date: Thu, 30 Jul 2026 15:44:18 -0400 Subject: [PATCH] fix(hostedcluster): handle Unknown status in ClusterVersionFailing inversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ClusterVersionFailing → ClusterVersionSucceeding condition inversion switch only handled True and False. When ClusterVersionFailing had Status: Unknown (common during control plane disruptions), the switch fell through and set ClusterVersionSucceeding.Status to "" (zero value), which the API server permanently rejects on Status().Update. This blocked the entire reconcile loop, preventing HC-to-HCP annotation sync and causing a scheduling deadlock where request-serving pods remained Pending indefinitely. Extract invertConditionStatus() helper shared by both reconcile and reconcileLegacy paths, with a default case mapping Unknown and any unexpected status to ConditionUnknown. Fixes: OCPBUGS-100301 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../hostedcluster/hostedcluster_controller.go | 20 +++++---- .../hostedcluster_controller_test.go | 41 +++++++++++++++++++ .../hostedcluster/reconcile_legacy.go | 9 +--- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go index 4490f888d1a0..e73fe3722afa 100644 --- a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go +++ b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go @@ -752,14 +752,7 @@ func (r *HostedClusterReconciler) reconcile(ctx context.Context, req ctrl.Reques // So consumers e.g. UI can categorize as good (True) / bad (False). if conditionType == hyperv1.ClusterVersionSucceeding { hcCVOCondition.Type = string(hyperv1.ClusterVersionSucceeding) - var status metav1.ConditionStatus - switch hcpCVOConditions[conditionType].Status { - case metav1.ConditionTrue: - status = metav1.ConditionFalse - case metav1.ConditionFalse: - status = metav1.ConditionTrue - } - hcCVOCondition.Status = status + hcCVOCondition.Status = invertConditionStatus(hcpCVOConditions[conditionType].Status) } } @@ -3318,6 +3311,17 @@ func reconcileCAPIManagerClusterRoleBinding(binding *rbacv1.ClusterRoleBinding, return nil } +func invertConditionStatus(s metav1.ConditionStatus) metav1.ConditionStatus { + switch s { + case metav1.ConditionTrue: + return metav1.ConditionFalse + case metav1.ConditionFalse: + return metav1.ConditionTrue + default: + return metav1.ConditionUnknown + } +} + // computeClusterVersionStatus determines the ClusterVersionStatus of the // given HostedCluster and returns it. func computeClusterVersionStatus(clock clock.WithTickerAndDelayedExecution, hcluster *hyperv1.HostedCluster, hcp *hyperv1.HostedControlPlane) *hyperv1.ClusterVersionStatus { diff --git a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go index 76c5b9538049..f4ce7ee8f84d 100644 --- a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go +++ b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go @@ -3995,6 +3995,47 @@ func TestIsProgressing(t *testing.T) { } } +func TestInvertConditionStatus(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + input metav1.ConditionStatus + expectedStatus metav1.ConditionStatus + }{ + { + name: "When status is True it should invert to False", + input: metav1.ConditionTrue, + expectedStatus: metav1.ConditionFalse, + }, + { + name: "When status is False it should invert to True", + input: metav1.ConditionFalse, + expectedStatus: metav1.ConditionTrue, + }, + { + name: "When status is Unknown it should produce Unknown", + input: metav1.ConditionUnknown, + expectedStatus: metav1.ConditionUnknown, + }, + { + name: "When status is empty string it should produce Unknown", + input: metav1.ConditionStatus(""), + expectedStatus: metav1.ConditionUnknown, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + result := invertConditionStatus(tc.input) + g.Expect(result).To(Equal(tc.expectedStatus)) + g.Expect(result).ToNot(BeEmpty(), "Status must not be empty string — API server rejects it") + }) + } +} + func TestComputeAWSDefaultSGDeletedCondition(t *testing.T) { t.Parallel() diff --git a/hypershift-operator/controllers/hostedcluster/reconcile_legacy.go b/hypershift-operator/controllers/hostedcluster/reconcile_legacy.go index 4b84a70b8dea..9e36c7d392a9 100644 --- a/hypershift-operator/controllers/hostedcluster/reconcile_legacy.go +++ b/hypershift-operator/controllers/hostedcluster/reconcile_legacy.go @@ -434,14 +434,7 @@ func (r *HostedClusterReconciler) reconcileLegacy(ctx context.Context, req ctrl. // So consumers e.g. UI can categorize as good (True) / bad (False). if conditionType == hyperv1.ClusterVersionSucceeding { hcCVOCondition.Type = string(hyperv1.ClusterVersionSucceeding) - var status metav1.ConditionStatus - switch hcpCVOConditions[conditionType].Status { - case metav1.ConditionTrue: - status = metav1.ConditionFalse - case metav1.ConditionFalse: - status = metav1.ConditionTrue - } - hcCVOCondition.Status = status + hcCVOCondition.Status = invertConditionStatus(hcpCVOConditions[conditionType].Status) } }