package reconcile import ( "time" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "edge-infra.dev/pkg/k8s/meta/status" "edge-infra.dev/pkg/k8s/runtime/conditions" "edge-infra.dev/pkg/k8s/runtime/controller/reconcile/recerr" "edge-infra.dev/pkg/k8s/runtime/patch" ) // Result is a type for creating an abstraction for the controller-runtime // reconcile Result to simplify the Result values. type Result int // Import alias for convenience type Error recerr.Error const ( // ResultEmpty indicates a reconcile result which does not requeue. It is // also used when returning an error, since the error overshadows result. // If ResultEmpty is returned with an object that implements [Retrier], then // the RetryInterval will be used as the requeue time. ResultEmpty Result = iota // ResultRequeue indicates a reconcile result which should immediately // requeue. ResultRequeue // ResultSuccess indicates a reconcile success result. If this is returned with // an object that implements [Requeuer], it will produce in a runtime result // that requeues at a fixed interval. Otherwise, an empty runtime result will // be produced. // // It is usually returned at the end of a reconciler/sub-reconciler. ResultSuccess ) // Requeuer is a reconciler that always requeues on an interval. type Requeuer interface { RequeueAfter() time.Duration } // Retrier is a reconciler that uses a specific requeue time when an error is // occurred. type Retrier interface { RetryInterval() time.Duration } // ComputeReconcileResult analyzes the reconcile results (result + error), // updates the status conditions of the object with any corrections and returns // object patch configuration, runtime result and runtime error. The caller is // responsible for using the patch configuration while patching the object in // the API server. // // If the error is a special reconcile error (e.g., [StalledError], [WaitError]), // the error's configuration will be used where appropriate when configuring // the requeue. // // If the input object implements [Requeuer] or [Retrier], the runtime result // will set the RequeueAfter field accordingly based on the reconciliation error. func ComputeResult(obj conditions.Setter, res Result, recErr error) ([]patch.Option, ctrl.Result, error) { var pOpts []patch.Option // Compute the controller-runtime result. result := buildControllerResult(obj, res, recErr) // Remove reconciling condition on successful reconciliation. if recErr == nil && res == ResultSuccess { conditions.Delete(obj, status.ReconcilingCondition) } // Presence of reconciling means that the reconciliation didn't succeed. // Set the Reconciling reason to ProgressingWithRetry to indicate a failure // retry. if conditions.IsReconciling(obj) { reconciling := conditions.Get(obj, status.ReconcilingCondition) reconciling.Reason = status.ProgressingWithRetryReason conditions.Set(obj, reconciling) } // Analyze the reconcile error. switch t := recErr.(type) { case *recerr.Stalled: if res == ResultEmpty { conditions.MarkStalled(obj, t.Reason, "error: %v", t) // The current generation has been reconciled successfully and it // has resulted in a stalled state. Return no error to stop further // requeuing. pOpts = addPatchOptionWithStatusObservedGeneration(obj, pOpts) return pOpts, result, nil } // NOTE: Non-empty result with stalling error indicates that the // returned result is incorrect. case *recerr.Wait: // The reconcile resulted in waiting error, remove stalled condition if // present. conditions.Delete(obj, status.StalledCondition) // The reconciler needs to wait and retry. Return no error. return pOpts, result, nil case nil: // The reconcile didn't result in any error, we are not in stalled // state. If a requeue is requested, the current generation has not been // reconciled successfully. if res != ResultRequeue { pOpts = addPatchOptionWithStatusObservedGeneration(obj, pOpts) } conditions.Delete(obj, status.StalledCondition) default: // The reconcile resulted in some error, but we are not in stalled // state. conditions.Delete(obj, status.StalledCondition) } return pOpts, result, recErr } func buildControllerResult(obj runtime.Object, r Result, err error) ctrl.Result { switch t := err.(type) { case *recerr.Stalled: // Don't requeue stalled errors. return ctrl.Result{} case *recerr.Wait: // Honor Wait error configuration if present return ctrl.Result{RequeueAfter: t.Config.RequeueAfter} } // If object is a Retrier and we hit a standard error, honor their RetryInterval if retrier, ok := obj.(Retrier); err != nil && ok { return ctrl.Result{RequeueAfter: retrier.RetryInterval()} } switch r { case ResultRequeue: return ctrl.Result{Requeue: true} case ResultSuccess: if o, ok := obj.(Requeuer); ok { return ctrl.Result{RequeueAfter: o.RequeueAfter()} } return ctrl.Result{} default: return ctrl.Result{} } } // FailureRecovery finds out if a failure recovery occurred by checking the fail // conditions in the old object and the new object. func FailureRecovery(oldObj, newObj conditions.Getter, failConditions []string) bool { failuresBefore := 0 for _, failCondition := range failConditions { if conditions.Get(oldObj, failCondition) != nil { failuresBefore++ } if conditions.Get(newObj, failCondition) != nil { // Short-circuit, there is failure now, can't be a recovery. return false } } return failuresBefore > 0 } // addPatchOptionWithStatusObservedGeneration adds patch option // WithStatusObservedGeneration to the provided patch option slice only if there // is any condition present on the object, and returns it. This is necessary to // prevent setting status observed generation without any effectual observation. // An object must have some condition in the status if it has been observed. // TODO: Move this to patch package after it has proven its need. func addPatchOptionWithStatusObservedGeneration(obj conditions.Getter, opts []patch.Option) []patch.Option { if len(obj.GetConditions()) > 0 { opts = append(opts, patch.WithStatusObservedGeneration{}) } return opts }