...

Source file src/edge-infra.dev/pkg/k8s/runtime/controller/reconcile/reconcile.go

Documentation: edge-infra.dev/pkg/k8s/runtime/controller/reconcile

     1  package reconcile
     2  
     3  import (
     4  	"time"
     5  
     6  	"k8s.io/apimachinery/pkg/runtime"
     7  	ctrl "sigs.k8s.io/controller-runtime"
     8  
     9  	"edge-infra.dev/pkg/k8s/meta/status"
    10  	"edge-infra.dev/pkg/k8s/runtime/conditions"
    11  	"edge-infra.dev/pkg/k8s/runtime/controller/reconcile/recerr"
    12  	"edge-infra.dev/pkg/k8s/runtime/patch"
    13  )
    14  
    15  // Result is a type for creating an abstraction for the controller-runtime
    16  // reconcile Result to simplify the Result values.
    17  type Result int
    18  
    19  // Import alias for convenience
    20  type Error recerr.Error
    21  
    22  const (
    23  	// ResultEmpty indicates a reconcile result which does not requeue. It is
    24  	// also used when returning an error, since the error overshadows result.
    25  	// If ResultEmpty is returned with an object that implements [Retrier], then
    26  	// the RetryInterval will be used as the requeue time.
    27  	ResultEmpty Result = iota
    28  	// ResultRequeue indicates a reconcile result which should immediately
    29  	// requeue.
    30  	ResultRequeue
    31  	// ResultSuccess indicates a reconcile success result. If this is returned with
    32  	// an object that implements [Requeuer], it will produce in a runtime result
    33  	// that requeues at a fixed interval. Otherwise, an empty runtime result will
    34  	// be produced.
    35  	//
    36  	// It is usually returned at the end of a reconciler/sub-reconciler.
    37  	ResultSuccess
    38  )
    39  
    40  // Requeuer is a reconciler that always requeues on an interval.
    41  type Requeuer interface {
    42  	RequeueAfter() time.Duration
    43  }
    44  
    45  // Retrier is a reconciler that uses a specific requeue time when an error is
    46  // occurred.
    47  type Retrier interface {
    48  	RetryInterval() time.Duration
    49  }
    50  
    51  // ComputeReconcileResult analyzes the reconcile results (result + error),
    52  // updates the status conditions of the object with any corrections and returns
    53  // object patch configuration, runtime result and runtime error. The caller is
    54  // responsible for using the patch configuration while patching the object in
    55  // the API server.
    56  //
    57  // If the error is a special reconcile error (e.g., [StalledError], [WaitError]),
    58  // the error's configuration will be used where appropriate when configuring
    59  // the requeue.
    60  //
    61  // If the input object implements [Requeuer] or [Retrier], the runtime result
    62  // will set the RequeueAfter field accordingly based on the reconciliation error.
    63  func ComputeResult(obj conditions.Setter, res Result, recErr error) ([]patch.Option, ctrl.Result, error) {
    64  	var pOpts []patch.Option
    65  
    66  	// Compute the controller-runtime result.
    67  	result := buildControllerResult(obj, res, recErr)
    68  
    69  	// Remove reconciling condition on successful reconciliation.
    70  	if recErr == nil && res == ResultSuccess {
    71  		conditions.Delete(obj, status.ReconcilingCondition)
    72  	}
    73  
    74  	// Presence of reconciling means that the reconciliation didn't succeed.
    75  	// Set the Reconciling reason to ProgressingWithRetry to indicate a failure
    76  	// retry.
    77  	if conditions.IsReconciling(obj) {
    78  		reconciling := conditions.Get(obj, status.ReconcilingCondition)
    79  		reconciling.Reason = status.ProgressingWithRetryReason
    80  		conditions.Set(obj, reconciling)
    81  	}
    82  
    83  	// Analyze the reconcile error.
    84  	switch t := recErr.(type) {
    85  	case *recerr.Stalled:
    86  		if res == ResultEmpty {
    87  			conditions.MarkStalled(obj, t.Reason, "error: %v", t)
    88  			// The current generation has been reconciled successfully and it
    89  			// has resulted in a stalled state. Return no error to stop further
    90  			// requeuing.
    91  			pOpts = addPatchOptionWithStatusObservedGeneration(obj, pOpts)
    92  			return pOpts, result, nil
    93  		}
    94  		// NOTE: Non-empty result with stalling error indicates that the
    95  		// returned result is incorrect.
    96  	case *recerr.Wait:
    97  		// The reconcile resulted in waiting error, remove stalled condition if
    98  		// present.
    99  		conditions.Delete(obj, status.StalledCondition)
   100  		// The reconciler needs to wait and retry. Return no error.
   101  		return pOpts, result, nil
   102  	case nil:
   103  		// The reconcile didn't result in any error, we are not in stalled
   104  		// state. If a requeue is requested, the current generation has not been
   105  		// reconciled successfully.
   106  		if res != ResultRequeue {
   107  			pOpts = addPatchOptionWithStatusObservedGeneration(obj, pOpts)
   108  		}
   109  		conditions.Delete(obj, status.StalledCondition)
   110  	default:
   111  		// The reconcile resulted in some error, but we are not in stalled
   112  		// state.
   113  		conditions.Delete(obj, status.StalledCondition)
   114  	}
   115  
   116  	return pOpts, result, recErr
   117  }
   118  
   119  func buildControllerResult(obj runtime.Object, r Result, err error) ctrl.Result {
   120  	switch t := err.(type) {
   121  	case *recerr.Stalled:
   122  		// Don't requeue stalled errors.
   123  		return ctrl.Result{}
   124  	case *recerr.Wait:
   125  		// Honor Wait error configuration if present
   126  		return ctrl.Result{RequeueAfter: t.Config.RequeueAfter}
   127  	}
   128  
   129  	// If object is a Retrier and we hit a standard error, honor their RetryInterval
   130  	if retrier, ok := obj.(Retrier); err != nil && ok {
   131  		return ctrl.Result{RequeueAfter: retrier.RetryInterval()}
   132  	}
   133  
   134  	switch r {
   135  	case ResultRequeue:
   136  		return ctrl.Result{Requeue: true}
   137  	case ResultSuccess:
   138  		if o, ok := obj.(Requeuer); ok {
   139  			return ctrl.Result{RequeueAfter: o.RequeueAfter()}
   140  		}
   141  		return ctrl.Result{}
   142  	default:
   143  		return ctrl.Result{}
   144  	}
   145  }
   146  
   147  // FailureRecovery finds out if a failure recovery occurred by checking the fail
   148  // conditions in the old object and the new object.
   149  func FailureRecovery(oldObj, newObj conditions.Getter, failConditions []string) bool {
   150  	failuresBefore := 0
   151  	for _, failCondition := range failConditions {
   152  		if conditions.Get(oldObj, failCondition) != nil {
   153  			failuresBefore++
   154  		}
   155  		if conditions.Get(newObj, failCondition) != nil {
   156  			// Short-circuit, there is failure now, can't be a recovery.
   157  			return false
   158  		}
   159  	}
   160  	return failuresBefore > 0
   161  }
   162  
   163  // addPatchOptionWithStatusObservedGeneration adds patch option
   164  // WithStatusObservedGeneration to the provided patch option slice only if there
   165  // is any condition present on the object, and returns it. This is necessary to
   166  // prevent setting status observed generation without any effectual observation.
   167  // An object must have some condition in the status if it has been observed.
   168  // TODO: Move this to patch package after it has proven its need.
   169  func addPatchOptionWithStatusObservedGeneration(obj conditions.Getter, opts []patch.Option) []patch.Option {
   170  	if len(obj.GetConditions()) > 0 {
   171  		opts = append(opts, patch.WithStatusObservedGeneration{})
   172  	}
   173  	return opts
   174  }
   175  

View as plain text