Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1998,10 +1998,11 @@ func (r *HostedControlPlaneReconciler) reconcileValidIDPConfigurationCondition(c
Message: fmt.Sprintf("failed to initialize identity providers: %v", err),
}
}
// Update the condition on the HCP if it has changed
// Patch the condition on the HCP if it has changed
originalHCP := hcp.DeepCopy()
if meta.SetStatusCondition(&hcp.Status.Conditions, new) {
if err := r.Status().Update(ctx, hcp); err != nil {
return fmt.Errorf("failed to update valid IDP configuration condition: %w", err)
if err := r.Status().Patch(ctx, hcp, client.MergeFromWithOptions(originalHCP, client.MergeFromWithOptimisticLock{})); err != nil {
return fmt.Errorf("failed to patch valid IDP configuration condition: %w", err)
}
}
return nil
Expand Down Expand Up @@ -2518,14 +2519,15 @@ func (r *HostedControlPlaneReconciler) removeCloudResources(ctx context.Context,
if resourcesDestroyedCond != nil && resourcesDestroyedCond.Message != "" {
message = fmt.Sprintf("%s (last status: %s)", message, resourcesDestroyedCond.Message)
}
originalHCP := hcp.DeepCopy()
meta.SetStatusCondition(&hcp.Status.Conditions, metav1.Condition{
Type: string(hyperv1.CloudResourcesDestroyed),
Status: metav1.ConditionFalse,
Reason: string(hyperv1.CloudResourcesDeletionTimedOutReason),
Message: message,
})
if err := r.Status().Update(ctx, hcp); err != nil {
return false, fmt.Errorf("failed to update cloud resources destroyed condition: %w", err)
if err := r.Status().Patch(ctx, hcp, client.MergeFromWithOptions(originalHCP, client.MergeFromWithOptimisticLock{})); err != nil {
return false, fmt.Errorf("failed to patch cloud resources destroyed condition: %w", err)
}
return true, nil
}
Expand All @@ -2550,15 +2552,16 @@ func (r *HostedControlPlaneReconciler) removeCloudResources(ctx context.Context,
return false, nil
}
if cvoScaledDownCond == nil || cvoScaledDownCond.Status != metav1.ConditionTrue {
originalHCP := hcp.DeepCopy()
cvoScaledDownCond = &metav1.Condition{
Type: string(hyperv1.CVOScaledDown),
Status: metav1.ConditionTrue,
Reason: "CVOScaledDown",
LastTransitionTime: metav1.Now(),
}
meta.SetStatusCondition(&hcp.Status.Conditions, *cvoScaledDownCond)
if err := r.Status().Update(ctx, hcp); err != nil {
return false, fmt.Errorf("failed to set CVO scaled down condition: %w", err)
if err := r.Status().Patch(ctx, hcp, client.MergeFromWithOptions(originalHCP, client.MergeFromWithOptimisticLock{})); err != nil {
return false, fmt.Errorf("failed to patch CVO scaled down condition: %w", err)
}
}
return false, nil
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package hcpstatus

import (
"bytes"
"context"
"encoding/json"
"fmt"
"reflect"

hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
"github.com/openshift/hypershift/control-plane-operator/hostedclusterconfigoperator/operator"
"github.com/openshift/hypershift/support/releaseinfo"

configv1 "github.com/openshift/api/config/v1"

"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
Expand Down Expand Up @@ -74,17 +76,76 @@ func (h *hcpStatusReconciler) Reconcile(ctx context.Context, req reconcile.Reque
return reconcile.Result{}, err
}

if !reflect.DeepEqual(hcp.Status, originalHCP.Status) {
log.Info("Updating HCP status with new configuration and version status")
if err := h.mgtClusterClient.Status().Update(ctx, hcp); err != nil {
return reconcile.Result{}, fmt.Errorf("failed to update hcp: %w", err)
}
log.Info("Successfully updated HCP status")
if equality.Semantic.DeepEqual(hcp.Status, originalHCP.Status) {
return reconcile.Result{}, nil
}

// Use JSON Patch (RFC 6902) instead of JSON Merge Patch (RFC 7386).
// Merge patch interprets null as "delete field" (RFC 7386 §7), which corrupts
// +required +nullable fields in configv1.ClusterVersionStatus that have no

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these fields were not +nullable, how would this change the issue here? (They should never have been nullable)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

relates to #8562 (comment)

Ideally they would align with new controlPlaneVersion fields and make them optional/omitempty, would that be problematic to unmarshal stored "null" values?

// omitempty and serialize nil as JSON null:
// - AvailableUpdates []configv1.Release `json:"availableUpdates"` — nil slice → null
// - CompletionTime *metav1.Time `json:"completionTime"` (in UpdateHistory) — nil pointer → null
// JSON Patch "replace"/"add" ops carry null as a literal value, preserving it correctly.
patch, err := buildStatusPatch(originalHCP, hcp)
if err != nil {
return reconcile.Result{}, fmt.Errorf("failed to build status patch: %w", err)
}

log.Info("Patching HCP status with new configuration and version status")
if err := h.mgtClusterClient.Status().Patch(ctx, hcp,
crclient.RawPatch(types.JSONPatchType, patch)); err != nil {
return reconcile.Result{}, fmt.Errorf("failed to patch hcp status: %w", err)
}
log.Info("Successfully patched HCP status")

return reconcile.Result{}, nil
}

type jsonPatchOp struct {
Op string `json:"op"`
Path string `json:"path"`
Value interface{} `json:"value,omitempty"`
}

func buildStatusPatch(original, modified *hyperv1.HostedControlPlane) ([]byte, error) {
origJSON, err := json.Marshal(original.Status)
if err != nil {
return nil, err
}
modJSON, err := json.Marshal(modified.Status)
if err != nil {
return nil, err
}

var origMap, modMap map[string]json.RawMessage
if err := json.Unmarshal(origJSON, &origMap); err != nil {
return nil, err
}
if err := json.Unmarshal(modJSON, &modMap); err != nil {
return nil, err
}

// Optimistic lock: fail if the object was modified since our read.
ops := []jsonPatchOp{{Op: "test", Path: "/metadata/resourceVersion", Value: original.ResourceVersion}}

for key, modVal := range modMap {
origVal, exists := origMap[key]
if !exists {
ops = append(ops, jsonPatchOp{Op: "add", Path: "/status/" + key, Value: modVal})
} else if !bytes.Equal(origVal, modVal) {
ops = append(ops, jsonPatchOp{Op: "replace", Path: "/status/" + key, Value: modVal})
}
}
for key := range origMap {
if _, exists := modMap[key]; !exists {
ops = append(ops, jsonPatchOp{Op: "remove", Path: "/status/" + key})
}
}

return json.Marshal(ops)
}

// findClusterOperatorStatusCondition is identical to meta.FindStatusCondition except that it works on config1.ClusterOperatorStatusCondition instead of
// metav1.StatusCondition
func findClusterOperatorStatusCondition(conditions []configv1.ClusterOperatorStatusCondition, conditionType configv1.ClusterStatusConditionType) *configv1.ClusterOperatorStatusCondition {
Expand Down
Loading