github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/controllerutil/controller_common.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package controllerutil
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"reflect"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/go-logr/logr"
    30  	corev1 "k8s.io/api/core/v1"
    31  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/apimachinery/pkg/conversion"
    34  	"k8s.io/apimachinery/pkg/runtime"
    35  	"k8s.io/client-go/tools/record"
    36  	ctrl "sigs.k8s.io/controller-runtime"
    37  	"sigs.k8s.io/controller-runtime/pkg/client"
    38  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    39  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    40  
    41  	"github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    42  	"github.com/1aal/kubeblocks/pkg/constant"
    43  )
    44  
    45  // ResultToP converts a Result object to a pointer.
    46  func ResultToP(res reconcile.Result, err error) (*reconcile.Result, error) {
    47  	return &res, err
    48  }
    49  
    50  // Reconciled returns an empty result with nil error to signal a successful reconcile
    51  // to the controller manager
    52  func Reconciled() (reconcile.Result, error) {
    53  	return reconcile.Result{}, nil
    54  }
    55  
    56  // CheckedRequeueWithError passes the error through to the controller
    57  // manager, it ignores unknown errors.
    58  func CheckedRequeueWithError(err error, logger logr.Logger, msg string, keysAndValues ...interface{}) (reconcile.Result, error) {
    59  	if apierrors.IsNotFound(err) {
    60  		return Reconciled()
    61  	}
    62  	return RequeueWithError(err, logger, msg, keysAndValues...)
    63  }
    64  
    65  // RequeueWithErrorAndRecordEvent requeues when an error occurs. if it is an unknown error, triggers an event
    66  func RequeueWithErrorAndRecordEvent(obj client.Object, recorder record.EventRecorder, err error, logger logr.Logger) (reconcile.Result, error) {
    67  	if apierrors.IsNotFound(err) && recorder != nil {
    68  		recorder.Eventf(obj, corev1.EventTypeWarning, constant.ReasonNotFoundCR, err.Error())
    69  	}
    70  	return RequeueWithError(err, logger, "")
    71  }
    72  
    73  // RequeueWithError requeues when an error occurs
    74  func RequeueWithError(err error, logger logr.Logger, msg string, keysAndValues ...interface{}) (reconcile.Result, error) {
    75  	if msg == "" {
    76  		logger.Info(err.Error())
    77  	} else {
    78  		// Info log the error message and then let the reconciler dump the stacktrace
    79  		logger.Info(msg, keysAndValues...)
    80  	}
    81  	return reconcile.Result{}, err
    82  }
    83  
    84  func RequeueAfter(duration time.Duration, logger logr.Logger, msg string, keysAndValues ...interface{}) (reconcile.Result, error) {
    85  	keysAndValues = append(keysAndValues, "duration")
    86  	keysAndValues = append(keysAndValues, duration)
    87  	if msg != "" {
    88  		msg = fmt.Sprintf("reason: %s; retry-after", msg)
    89  	} else {
    90  		msg = "retry-after"
    91  	}
    92  	logger.V(1).Info(msg, keysAndValues...)
    93  	return reconcile.Result{
    94  		Requeue:      true,
    95  		RequeueAfter: duration,
    96  	}, nil
    97  }
    98  
    99  func Requeue(logger logr.Logger, msg string, keysAndValues ...interface{}) (reconcile.Result, error) {
   100  	if msg == "" {
   101  		msg = "requeue"
   102  	}
   103  	logger.V(1).Info(msg, keysAndValues...)
   104  	return reconcile.Result{Requeue: true}, nil
   105  }
   106  
   107  // HandleCRDeletion handles CR deletion, adds finalizer if found a non-deleting object and removes finalizer during
   108  // deletion process. Passes optional 'deletionHandler' func for external dependency deletion. Returns Result pointer
   109  // if required to return out of outer 'Reconcile' reconciliation loop.
   110  func HandleCRDeletion(reqCtx RequestCtx,
   111  	r client.Writer,
   112  	cr client.Object,
   113  	finalizer string,
   114  	deletionHandler func() (*ctrl.Result, error)) (*ctrl.Result, error) {
   115  	// examine DeletionTimestamp to determine if object is under deletion
   116  	if cr.GetDeletionTimestamp().IsZero() {
   117  		// The object is not being deleted, so if it does not have our finalizer,
   118  		// then add the finalizer and update the object. This is equivalent to
   119  		// registering our finalizer.
   120  		if !controllerutil.ContainsFinalizer(cr, finalizer) {
   121  			controllerutil.AddFinalizer(cr, finalizer)
   122  			if err := r.Update(reqCtx.Ctx, cr); err != nil {
   123  				return ResultToP(CheckedRequeueWithError(err, reqCtx.Log, ""))
   124  			}
   125  		}
   126  	} else {
   127  		// The object is being deleted
   128  		if controllerutil.ContainsFinalizer(cr, finalizer) {
   129  			// We need to record the deletion event first.
   130  			// If the resource has dependencies, it will not be automatically deleted.
   131  			// It can also prevent users from manually deleting it without event records
   132  			if reqCtx.Recorder != nil {
   133  				cluster, ok := cr.(*v1alpha1.Cluster)
   134  				// throw warning event if terminationPolicy set to DoNotTerminate
   135  				if ok && cluster.Spec.TerminationPolicy == v1alpha1.DoNotTerminate {
   136  					reqCtx.Eventf(cr, corev1.EventTypeWarning, constant.ReasonDeleteFailed,
   137  						"Deleting %s: %s failed due to terminationPolicy set to DoNotTerminate",
   138  						strings.ToLower(cr.GetObjectKind().GroupVersionKind().Kind), cr.GetName())
   139  				} else {
   140  					reqCtx.Eventf(cr, corev1.EventTypeNormal, constant.ReasonDeletingCR, "Deleting %s: %s",
   141  						strings.ToLower(cr.GetObjectKind().GroupVersionKind().Kind), cr.GetName())
   142  				}
   143  			}
   144  
   145  			// our finalizer is present, so handle any external dependency
   146  			if deletionHandler != nil {
   147  				if res, err := deletionHandler(); err != nil {
   148  					// if failed to delete the external dependencies here, return with error
   149  					// so that it can be retried
   150  					if res == nil {
   151  						return ResultToP(CheckedRequeueWithError(err, reqCtx.Log, ""))
   152  					}
   153  					return res, err
   154  				} else if res != nil {
   155  					return res, nil
   156  				}
   157  			}
   158  			// remove our finalizer from the list and update it.
   159  			if controllerutil.RemoveFinalizer(cr, finalizer) {
   160  				if err := r.Update(reqCtx.Ctx, cr); err != nil {
   161  					return ResultToP(CheckedRequeueWithError(err, reqCtx.Log, ""))
   162  				}
   163  				// record resources deleted event
   164  				reqCtx.Eventf(cr, corev1.EventTypeNormal, constant.ReasonDeletedCR, "Deleted %s: %s",
   165  					strings.ToLower(cr.GetObjectKind().GroupVersionKind().Kind), cr.GetName())
   166  			}
   167  		}
   168  
   169  		// Stop reconciliation as the item is being deleted
   170  		res, err := Reconciled()
   171  		return &res, err
   172  	}
   173  	return nil, nil
   174  }
   175  
   176  // ValidateReferenceCR validates existing referencing CRs, if exists, requeue reconcile after 30 seconds
   177  func ValidateReferenceCR(reqCtx RequestCtx, cli client.Client, obj client.Object,
   178  	labelKey string, recordEvent func(), objLists ...client.ObjectList) (*ctrl.Result, error) {
   179  	for _, objList := range objLists {
   180  		// get referencing cr list
   181  		if err := cli.List(reqCtx.Ctx, objList,
   182  			client.MatchingLabels{labelKey: obj.GetName()}, client.Limit(1),
   183  		); err != nil {
   184  			return nil, err
   185  		}
   186  		if v, err := conversion.EnforcePtr(objList); err != nil {
   187  			return nil, err
   188  		} else {
   189  			// check list items
   190  			items := v.FieldByName("Items")
   191  			if !items.IsValid() || items.Kind() != reflect.Slice || items.Len() == 0 {
   192  				continue
   193  			}
   194  			if recordEvent != nil {
   195  				recordEvent()
   196  			}
   197  			return ResultToP(RequeueAfter(time.Second, reqCtx.Log, ""))
   198  		}
   199  	}
   200  	return nil, nil
   201  }
   202  
   203  // RecordCreatedEvent records an event when a CR created successfully
   204  func RecordCreatedEvent(r record.EventRecorder, cr client.Object) {
   205  	if r != nil && cr.GetGeneration() == 1 {
   206  		r.Eventf(cr, corev1.EventTypeNormal, constant.ReasonCreatedCR, "Created %s: %s", strings.ToLower(cr.GetObjectKind().GroupVersionKind().Kind), cr.GetName())
   207  	}
   208  }
   209  
   210  // WorkloadFilterPredicate provides filter predicate for workload objects, i.e., deployment/statefulset/pod/pvc.
   211  func WorkloadFilterPredicate(object client.Object) bool {
   212  	_, containCompNameLabelKey := object.GetLabels()[constant.KBAppComponentLabelKey]
   213  	return ManagedByKubeBlocksFilterPredicate(object) && containCompNameLabelKey
   214  }
   215  
   216  // ManagedByKubeBlocksFilterPredicate provides filter predicate for objects managed by kubeBlocks.
   217  func ManagedByKubeBlocksFilterPredicate(object client.Object) bool {
   218  	return object.GetLabels()[constant.AppManagedByLabelKey] == constant.AppName
   219  }
   220  
   221  // IgnoreIsAlreadyExists returns errors if 'err' is not type of AlreadyExists
   222  func IgnoreIsAlreadyExists(err error) error {
   223  	if !apierrors.IsAlreadyExists(err) {
   224  		return err
   225  	}
   226  	return nil
   227  }
   228  
   229  // BackgroundDeleteObject deletes the object in the background, usually used in the Reconcile method
   230  func BackgroundDeleteObject(cli client.Client, ctx context.Context, obj client.Object) error {
   231  	deletePropagation := metav1.DeletePropagationBackground
   232  	deleteOptions := &client.DeleteOptions{
   233  		PropagationPolicy: &deletePropagation,
   234  	}
   235  
   236  	if err := cli.Delete(ctx, obj, deleteOptions); err != nil {
   237  		return client.IgnoreNotFound(err)
   238  	}
   239  	return nil
   240  }
   241  
   242  // SetOwnership provides helper function controllerutil.SetControllerReference/controllerutil.SetOwnerReference
   243  // and controllerutil.AddFinalizer if not exists.
   244  func SetOwnership(owner, obj client.Object, scheme *runtime.Scheme, finalizer string, useOwnerReference ...bool) error {
   245  	if len(useOwnerReference) > 0 && useOwnerReference[0] {
   246  		if err := controllerutil.SetOwnerReference(owner, obj, scheme); err != nil {
   247  			return err
   248  		}
   249  	} else {
   250  		if err := controllerutil.SetControllerReference(owner, obj, scheme); err != nil {
   251  			return err
   252  		}
   253  	}
   254  	if !controllerutil.ContainsFinalizer(obj, finalizer) {
   255  		// pvc objects do not need to add finalizer
   256  		_, ok := obj.(*corev1.PersistentVolumeClaim)
   257  		if !ok {
   258  			if !controllerutil.AddFinalizer(obj, finalizer) {
   259  				return ErrFailedToAddFinalizer
   260  			}
   261  		}
   262  	}
   263  	return nil
   264  }
   265  
   266  // CheckResourceExists checks whether resource exist or not.
   267  func CheckResourceExists(
   268  	ctx context.Context,
   269  	cli client.Client,
   270  	key client.ObjectKey,
   271  	obj client.Object) (bool, error) {
   272  	if err := cli.Get(ctx, key, obj); err != nil {
   273  		return false, client.IgnoreNotFound(err)
   274  	}
   275  	// if found, return true
   276  	return true, nil
   277  }