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