github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/controllers/apps/clusterversion_controller.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 apps
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"runtime"
    26  	"strings"
    27  	"time"
    28  
    29  	"golang.org/x/exp/slices"
    30  	corev1 "k8s.io/api/core/v1"
    31  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    32  	"k8s.io/apimachinery/pkg/labels"
    33  	k8sruntime "k8s.io/apimachinery/pkg/runtime"
    34  	"k8s.io/apimachinery/pkg/types"
    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"
    39  	"sigs.k8s.io/controller-runtime/pkg/log"
    40  
    41  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    42  	appsconfig "github.com/1aal/kubeblocks/controllers/apps/configuration"
    43  	"github.com/1aal/kubeblocks/pkg/constant"
    44  	intctrlutil "github.com/1aal/kubeblocks/pkg/controllerutil"
    45  	viper "github.com/1aal/kubeblocks/pkg/viperx"
    46  )
    47  
    48  // +kubebuilder:rbac:groups=apps.kubeblocks.io,resources=clusterversions,verbs=get;list;watch;create;update;patch;delete
    49  // +kubebuilder:rbac:groups=apps.kubeblocks.io,resources=clusterversions/status,verbs=get;update;patch
    50  // +kubebuilder:rbac:groups=apps.kubeblocks.io,resources=clusterversions/finalizers,verbs=update
    51  
    52  // ClusterVersionReconciler reconciles a ClusterVersion object
    53  type ClusterVersionReconciler struct {
    54  	client.Client
    55  	Scheme   *k8sruntime.Scheme
    56  	Recorder record.EventRecorder
    57  }
    58  
    59  func init() {
    60  	clusterDefUpdateHandlers["clusterVersion"] = clusterVersionUpdateHandler
    61  	viper.SetDefault(maxConcurReconClusterVersionKey, runtime.NumCPU())
    62  }
    63  
    64  // Reconcile is part of the main kubernetes reconciliation loop which aims to
    65  // move the current state of the cluster closer to the desired state.
    66  //
    67  // For more details, check Reconcile and its Result here:
    68  // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.4/pkg/reconcile
    69  func (r *ClusterVersionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    70  	reqCtx := intctrlutil.RequestCtx{
    71  		Ctx:      ctx,
    72  		Req:      req,
    73  		Log:      log.FromContext(ctx).WithValues("clusterDefinition", req.NamespacedName),
    74  		Recorder: r.Recorder,
    75  	}
    76  
    77  	clusterVersion := &appsv1alpha1.ClusterVersion{}
    78  	if err := r.Client.Get(reqCtx.Ctx, reqCtx.Req.NamespacedName, clusterVersion); err != nil {
    79  		return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "")
    80  	}
    81  
    82  	res, err := intctrlutil.HandleCRDeletion(reqCtx, r, clusterVersion, clusterVersionFinalizerName, func() (*ctrl.Result, error) {
    83  		recordEvent := func() {
    84  			r.Recorder.Event(clusterVersion, corev1.EventTypeWarning, constant.ReasonRefCRUnavailable,
    85  				"cannot be deleted because of existing referencing Cluster.")
    86  		}
    87  		if res, err := intctrlutil.ValidateReferenceCR(reqCtx, r.Client, clusterVersion,
    88  			constant.ClusterVerLabelKey, recordEvent, &appsv1alpha1.ClusterList{}); res != nil || err != nil {
    89  			return res, err
    90  		}
    91  		return nil, r.deleteExternalResources(reqCtx, clusterVersion)
    92  	})
    93  	if res != nil {
    94  		return *res, err
    95  	}
    96  
    97  	if clusterVersion.Status.ObservedGeneration == clusterVersion.Generation &&
    98  		slices.Contains(clusterVersion.Status.GetTerminalPhases(), clusterVersion.Status.Phase) {
    99  		return intctrlutil.Reconciled()
   100  	}
   101  
   102  	clusterdefinition := &appsv1alpha1.ClusterDefinition{}
   103  	if err := r.Client.Get(reqCtx.Ctx, types.NamespacedName{
   104  		Name: clusterVersion.Spec.ClusterDefinitionRef,
   105  	}, clusterdefinition); err != nil {
   106  		if apierrors.IsNotFound(err) {
   107  			if res, patchErr := r.patchClusterDefLabel(reqCtx, clusterVersion); res != nil {
   108  				return *res, patchErr
   109  			}
   110  			if err = r.handleClusterDefNotFound(reqCtx, clusterVersion, err.Error()); err != nil {
   111  				return intctrlutil.RequeueWithErrorAndRecordEvent(clusterVersion, r.Recorder, err, reqCtx.Log)
   112  			}
   113  			return intctrlutil.Reconciled()
   114  		}
   115  		return intctrlutil.RequeueWithErrorAndRecordEvent(clusterVersion, r.Recorder, err, reqCtx.Log)
   116  	}
   117  
   118  	patchStatus := func(phase appsv1alpha1.Phase, message string) error {
   119  		patch := client.MergeFrom(clusterVersion.DeepCopy())
   120  		clusterVersion.Status.Phase = phase
   121  		clusterVersion.Status.Message = message
   122  		clusterVersion.Status.ObservedGeneration = clusterVersion.Generation
   123  		clusterVersion.Status.ClusterDefGeneration = clusterdefinition.Generation
   124  		return r.Client.Status().Patch(ctx, clusterVersion, patch)
   125  	}
   126  
   127  	if statusMsg := validateClusterVersion(clusterVersion, clusterdefinition); statusMsg != "" {
   128  		if err := patchStatus(appsv1alpha1.UnavailablePhase, statusMsg); err != nil {
   129  			return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "")
   130  		}
   131  		return intctrlutil.Reconciled()
   132  	}
   133  
   134  	if err = appsconfig.ReconcileConfigSpecsForReferencedCR(r.Client, reqCtx, clusterVersion); err != nil {
   135  		return intctrlutil.RequeueAfter(time.Second, reqCtx.Log, err.Error())
   136  	}
   137  
   138  	if res, err = r.patchClusterDefLabel(reqCtx, clusterVersion); res != nil {
   139  		return *res, err
   140  	}
   141  
   142  	if err = patchStatus(appsv1alpha1.AvailablePhase, ""); err != nil {
   143  		return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "")
   144  	}
   145  	intctrlutil.RecordCreatedEvent(r.Recorder, clusterVersion)
   146  	return intctrlutil.Reconciled()
   147  }
   148  
   149  // SetupWithManager sets up the controller with the Manager.
   150  func (r *ClusterVersionReconciler) SetupWithManager(mgr ctrl.Manager) error {
   151  	return ctrl.NewControllerManagedBy(mgr).
   152  		For(&appsv1alpha1.ClusterVersion{}).
   153  		WithOptions(controller.Options{
   154  			MaxConcurrentReconciles: viper.GetInt(maxConcurReconClusterVersionKey),
   155  		}).
   156  		Complete(r)
   157  }
   158  
   159  func (r *ClusterVersionReconciler) patchClusterDefLabel(reqCtx intctrlutil.RequestCtx,
   160  	clusterVersion *appsv1alpha1.ClusterVersion) (*ctrl.Result, error) {
   161  	if v, ok := clusterVersion.ObjectMeta.Labels[constant.ClusterDefLabelKey]; !ok || v != clusterVersion.Spec.ClusterDefinitionRef {
   162  		patch := client.MergeFrom(clusterVersion.DeepCopy())
   163  		if clusterVersion.ObjectMeta.Labels == nil {
   164  			clusterVersion.ObjectMeta.Labels = map[string]string{}
   165  		}
   166  		clusterVersion.ObjectMeta.Labels[constant.ClusterDefLabelKey] = clusterVersion.Spec.ClusterDefinitionRef
   167  		if err := r.Client.Patch(reqCtx.Ctx, clusterVersion, patch); err != nil {
   168  			return intctrlutil.ResultToP(intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, ""))
   169  		}
   170  		return intctrlutil.ResultToP(intctrlutil.Reconciled())
   171  	}
   172  	return nil, nil
   173  }
   174  
   175  // handleClusterDefNotFound handles clusterVersion status when clusterDefinition not found.
   176  func (r *ClusterVersionReconciler) handleClusterDefNotFound(reqCtx intctrlutil.RequestCtx,
   177  	clusterVersion *appsv1alpha1.ClusterVersion, message string) error {
   178  	if clusterVersion.Status.Message == message {
   179  		return nil
   180  	}
   181  	patch := client.MergeFrom(clusterVersion.DeepCopy())
   182  	clusterVersion.Status.Phase = appsv1alpha1.UnavailablePhase
   183  	clusterVersion.Status.Message = message
   184  	return r.Client.Status().Patch(reqCtx.Ctx, clusterVersion, patch)
   185  }
   186  
   187  func validateClusterVersion(clusterVersion *appsv1alpha1.ClusterVersion, clusterDef *appsv1alpha1.ClusterDefinition) string {
   188  	notFoundComponentDefNames, noContainersComponents := clusterVersion.GetInconsistentComponentsInfo(clusterDef)
   189  	var statusMsgs []string
   190  	if len(notFoundComponentDefNames) > 0 {
   191  		statusMsgs = append(statusMsgs, fmt.Sprintf("spec.componentSpecs[*].componentDefRef %v not found in ClusterDefinition.spec.componentDefs[*].name", notFoundComponentDefNames))
   192  	} else if len(noContainersComponents) > 0 {
   193  		statusMsgs = append(statusMsgs, fmt.Sprintf("spec.componentSpecs[*].componentDefRef %v missing spec.componentSpecs[*].containers in ClusterDefinition.spec.componentDefs[*] and ClusterVersion.spec.componentVersions[*]", noContainersComponents))
   194  	}
   195  	return strings.Join(statusMsgs, ";")
   196  }
   197  
   198  func (r *ClusterVersionReconciler) deleteExternalResources(reqCtx intctrlutil.RequestCtx, clusterVersion *appsv1alpha1.ClusterVersion) error {
   199  	//
   200  	// delete any external resources associated with the cronJob
   201  	//
   202  	// Ensure that delete implementation is idempotent and safe to invoke
   203  	// multiple times for same object.
   204  	return appsconfig.DeleteConfigMapFinalizer(r.Client, reqCtx, clusterVersion)
   205  }
   206  
   207  func clusterVersionUpdateHandler(cli client.Client, ctx context.Context, clusterDef *appsv1alpha1.ClusterDefinition) error {
   208  	labelSelector, err := labels.Parse(constant.ClusterDefLabelKey + "=" + clusterDef.GetName())
   209  	if err != nil {
   210  		return err
   211  	}
   212  	o := &client.ListOptions{LabelSelector: labelSelector}
   213  
   214  	list := &appsv1alpha1.ClusterVersionList{}
   215  	if err := cli.List(ctx, list, o); err != nil {
   216  		return err
   217  	}
   218  	for _, item := range list.Items {
   219  		if item.Status.ClusterDefGeneration != clusterDef.Generation {
   220  			patch := client.MergeFrom(item.DeepCopy())
   221  			if statusMsg := validateClusterVersion(&item, clusterDef); statusMsg != "" {
   222  				item.Status.Phase = appsv1alpha1.UnavailablePhase
   223  				item.Status.Message = statusMsg
   224  			} else {
   225  				item.Status.Phase = appsv1alpha1.AvailablePhase
   226  				item.Status.Message = ""
   227  				item.Status.ClusterDefGeneration = clusterDef.Generation
   228  			}
   229  			if err = cli.Status().Patch(ctx, &item, patch); err != nil {
   230  				return err
   231  			}
   232  		}
   233  	}
   234  
   235  	return nil
   236  }