99 hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
1010 "github.com/openshift/hypershift/control-plane-operator/hostedclusterconfigoperator/controllers/resources/manifests"
1111 "github.com/openshift/hypershift/support/globalconfig"
12+ "github.com/openshift/hypershift/support/k8sutil"
1213 "github.com/openshift/hypershift/support/releaseinfo"
1314 "github.com/openshift/hypershift/support/upsert"
1415
@@ -58,6 +59,11 @@ const (
5859 TokenSecretPayloadKey = "payload"
5960 TokenSecretReleaseKey = "release"
6061 TokenSecretReleaseVersionKey = "release-version"
62+
63+ // upgradeRequeueInterval is how often the controller rechecks while an
64+ // upgrade is in progress, closing the gap when a force-deleted pod's
65+ // deletion event is missed.
66+ upgradeRequeueInterval = 30 * time .Second
6167)
6268
6369type Reconciler struct {
@@ -146,7 +152,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
146152 return ctrl.Result {}, fmt .Errorf ("token secret %s/%s is missing %q key" , tokenSecret .Namespace , tokenSecret .Name , TokenSecretReleaseVersionKey )
147153 }
148154
149- return ctrl.Result {}, r .reconcileInPlaceUpgrade (ctx , nodePoolUpgradeAPI , tokenSecret , mcoImage , releaseVersion )
155+ if err := r .reconcileInPlaceUpgrade (ctx , nodePoolUpgradeAPI , tokenSecret , mcoImage , releaseVersion ); err != nil {
156+ return ctrl.Result {}, err
157+ }
158+ // Requeue periodically while an upgrade is in progress. The controller only
159+ // watches Nodes and MachineSets, so if an upgrade pod is force-deleted the
160+ // deletion event is missed and the replacement pod is never created. A
161+ // periodic recheck closes that gap.
162+ return ctrl.Result {RequeueAfter : upgradeRequeueInterval }, nil
150163}
151164
152165type nodePoolUpgradeAPI struct {
@@ -276,7 +289,7 @@ func (r *Reconciler) reconcileInPlaceUpgrade(ctx context.Context, nodePoolUpgrad
276289
277290 err = r .reconcileUpgradePods (ctx , r .guestClusterClient , nodes , nodePoolUpgradeAPI .spec .poolRef .GetName (), mcoImage , nodePoolUpgradeAPI .proxy )
278291 if err != nil {
279- return fmt .Errorf ("failed to delete idle upgrade pods: %w" , err )
292+ return fmt .Errorf ("failed to reconcile upgrade pods: %w" , err )
280293 }
281294 return nil
282295}
@@ -311,24 +324,14 @@ func (r *Reconciler) reconcileUpgradePods(ctx context.Context, hostedClusterClie
311324 pod := inPlaceUpgradePod (namespace .Name , node .Name )
312325
313326 if node .Annotations [CurrentMachineConfigAnnotationKey ] == node .Annotations [DesiredMachineConfigAnnotationKey ] &&
314- node .Annotations [DesiredDrainerAnnotationKey ] == node .Annotations [LastAppliedDrainerAnnotationKey ] {
327+ node .Annotations [DesiredDrainerAnnotationKey ] == node .Annotations [LastAppliedDrainerAnnotationKey ] &&
328+ node .Annotations [MachineConfigDaemonStateAnnotationKey ] == MachineConfigDaemonStateDone {
315329 // the node is updated and does not require a MCD running
316- if err := hostedClusterClient .Get (ctx , client .ObjectKeyFromObject (pod ), pod ); err != nil {
317- if apierrors .IsNotFound (err ) {
318- continue
319- }
320- return fmt .Errorf ("error getting upgrade MCD pod: %w" , err )
321- }
322- if pod .DeletionTimestamp != nil {
323- continue
324- }
325- if err := hostedClusterClient .Delete (ctx , pod ); err != nil {
326- if apierrors .IsNotFound (err ) {
327- continue
328- }
329- return fmt .Errorf ("error deleting upgrade MCD pod: %w" , err )
330+ if existed , err := k8sutil .DeleteIfNeeded (ctx , hostedClusterClient , pod ); err != nil {
331+ return err
332+ } else if existed {
333+ log .Info ("Deleted idle upgrade pod" )
330334 }
331- log .Info ("Deleted idle upgrade pod" )
332335 } else {
333336 if err := hostedClusterClient .Get (ctx , types.NamespacedName {Namespace : pod .Namespace , Name : pod .Name }, pod ); err != nil {
334337 if ! apierrors .IsNotFound (err ) {
@@ -348,6 +351,19 @@ func (r *Reconciler) reconcileUpgradePods(ctx context.Context, hostedClusterClie
348351 } else {
349352 log .Info ("create upgrade pod" , "result" , result )
350353 }
354+ // A pod with RestartPolicy=OnFailure only reaches a terminal phase after kubelet has exhausted its restart attempts (e.g. eviction or node loss), so deleting it here does not interrupt an active retry.
355+ } else if pod .Status .Phase == corev1 .PodSucceeded || pod .Status .Phase == corev1 .PodFailed {
356+ if pod .DeletionTimestamp != nil {
357+ continue
358+ }
359+ log .Info ("Detected terminated upgrade pod on node that still needs upgrade, deleting for retry" ,
360+ "node" , node .Name , "podPhase" , pod .Status .Phase )
361+ if err := hostedClusterClient .Delete (ctx , pod ); err != nil {
362+ if apierrors .IsNotFound (err ) {
363+ continue
364+ }
365+ return fmt .Errorf ("error deleting terminated upgrade MCD pod for node %s: %w" , node .Name , err )
366+ }
351367 }
352368 }
353369 }
@@ -483,20 +499,8 @@ func deleteUpgradeManifests(ctx context.Context, hostedClusterClient client.Clie
483499 namespace := inPlaceUpgradeNamespace (poolName )
484500 for _ , node := range nodes {
485501 pod := inPlaceUpgradePod (namespace .Name , node .Name )
486- if err := hostedClusterClient .Get (ctx , client .ObjectKeyFromObject (pod ), pod ); err != nil {
487- if apierrors .IsNotFound (err ) {
488- continue
489- }
490- return fmt .Errorf ("error getting upgrade MCD pod: %w" , err )
491- }
492- if pod .DeletionTimestamp != nil {
493- continue
494- }
495- if err := hostedClusterClient .Delete (ctx , pod ); err != nil {
496- if apierrors .IsNotFound (err ) {
497- continue
498- }
499- return fmt .Errorf ("error deleting upgrade MCD pod: %w" , err )
502+ if _ , err := k8sutil .DeleteIfNeeded (ctx , hostedClusterClient , pod ); err != nil {
503+ return err
500504 }
501505 }
502506 return nil
0 commit comments