github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/controllers/apps/transformer_halt_recovering.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  	"encoding/json"
    24  	"fmt"
    25  
    26  	"github.com/authzed/controller-idioms/hash"
    27  	corev1 "k8s.io/api/core/v1"
    28  	"k8s.io/apimachinery/pkg/api/meta"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"sigs.k8s.io/controller-runtime/pkg/client"
    31  
    32  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    33  	"github.com/1aal/kubeblocks/pkg/constant"
    34  	"github.com/1aal/kubeblocks/pkg/controller/graph"
    35  )
    36  
    37  type HaltRecoveryTransformer struct{}
    38  
    39  func (t *HaltRecoveryTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error {
    40  	transCtx, _ := ctx.(*clusterTransformContext)
    41  	cluster := transCtx.Cluster
    42  
    43  	if cluster.Status.ObservedGeneration != 0 {
    44  		// skip handling for cluster.status.observedGeneration > 0
    45  		return nil
    46  	}
    47  
    48  	listOptions := []client.ListOption{
    49  		client.InNamespace(cluster.Namespace),
    50  		client.MatchingLabels{
    51  			constant.AppInstanceLabelKey: cluster.Name,
    52  		},
    53  	}
    54  
    55  	pvcList := &corev1.PersistentVolumeClaimList{}
    56  	if err := transCtx.Client.List(transCtx.Context, pvcList, listOptions...); err != nil {
    57  		return newRequeueError(requeueDuration, err.Error())
    58  	}
    59  
    60  	if len(pvcList.Items) == 0 {
    61  		return nil
    62  	}
    63  
    64  	emitError := func(newCondition metav1.Condition) error {
    65  		if newCondition.LastTransitionTime.IsZero() {
    66  			newCondition.LastTransitionTime = metav1.Now()
    67  		}
    68  		newCondition.Status = metav1.ConditionFalse
    69  		oldCondition := meta.FindStatusCondition(cluster.Status.Conditions, newCondition.Type)
    70  		if oldCondition == nil {
    71  			cluster.Status.Conditions = append(cluster.Status.Conditions, newCondition)
    72  		} else {
    73  			*oldCondition = newCondition
    74  		}
    75  		transCtx.EventRecorder.Event(transCtx.Cluster, corev1.EventTypeWarning, newCondition.Reason, newCondition.Message)
    76  		return graph.ErrPrematureStop
    77  	}
    78  
    79  	// halt recovering from last applied record stored in pvc's annotation
    80  	l, ok := pvcList.Items[0].Annotations[constant.LastAppliedClusterAnnotationKey]
    81  	if !ok || l == "" {
    82  		return emitError(metav1.Condition{
    83  			Type:   appsv1alpha1.ConditionTypeHaltRecovery,
    84  			Reason: "UncleanedResources",
    85  			Message: fmt.Sprintf("found uncleaned resources, requires manual deletion, check with `kubectl -n %s get pvc,secret,cm -l %s=%s`",
    86  				cluster.Namespace, constant.AppInstanceLabelKey, cluster.Name),
    87  		})
    88  	}
    89  
    90  	lc := &appsv1alpha1.Cluster{}
    91  	if err := json.Unmarshal([]byte(l), lc); err != nil {
    92  		return newRequeueError(requeueDuration, err.Error())
    93  	}
    94  
    95  	// skip if same cluster UID
    96  	if lc.UID == cluster.UID {
    97  		return nil
    98  	}
    99  
   100  	// check clusterDefRef equality
   101  	if cluster.Spec.ClusterDefRef != lc.Spec.ClusterDefRef {
   102  		return emitError(metav1.Condition{
   103  			Type:    appsv1alpha1.ConditionTypeHaltRecovery,
   104  			Reason:  "HaltRecoveryFailed",
   105  			Message: fmt.Sprintf("not equal to last applied cluster.spec.clusterDefRef %s", lc.Spec.ClusterDefRef),
   106  		})
   107  	}
   108  
   109  	// check clusterVersionRef equality but allow clusters.apps.kubeblocks.io/allow-inconsistent-cv=true annotation override
   110  	if cluster.Spec.ClusterVersionRef != lc.Spec.ClusterVersionRef &&
   111  		cluster.Annotations[constant.HaltRecoveryAllowInconsistentCVAnnotKey] != trueVal {
   112  		return emitError(metav1.Condition{
   113  			Type:   appsv1alpha1.ConditionTypeHaltRecovery,
   114  			Reason: "HaltRecoveryFailed",
   115  			Message: fmt.Sprintf("not equal to last applied cluster.spec.clusterVersionRef %s; add '%s=true' annotation if void this check",
   116  				lc.Spec.ClusterVersionRef, constant.HaltRecoveryAllowInconsistentCVAnnotKey),
   117  		})
   118  	}
   119  
   120  	// check component len equality
   121  	if l := len(lc.Spec.ComponentSpecs); l != len(cluster.Spec.ComponentSpecs) {
   122  		return emitError(metav1.Condition{
   123  			Type:    appsv1alpha1.ConditionTypeHaltRecovery,
   124  			Reason:  "HaltRecoveryFailed",
   125  			Message: fmt.Sprintf("inconsistent spec.componentSpecs counts to last applied cluster.spec.componentSpecs (len=%d)", l),
   126  		})
   127  	}
   128  
   129  	// check every components' equality
   130  	for _, comp := range cluster.Spec.ComponentSpecs {
   131  		found := false
   132  		for _, lastUsedComp := range lc.Spec.ComponentSpecs {
   133  			// only need to verify [name, componentDefRef, replicas] for equality
   134  			if comp.Name != lastUsedComp.Name {
   135  				continue
   136  			}
   137  			if comp.ComponentDefRef != lastUsedComp.ComponentDefRef {
   138  				return emitError(metav1.Condition{
   139  					Type:   appsv1alpha1.ConditionTypeHaltRecovery,
   140  					Reason: "HaltRecoveryFailed",
   141  					Message: fmt.Sprintf("not equal to last applied cluster.spec.componentSpecs[%s].componentDefRef=%s",
   142  						comp.Name, lastUsedComp.ComponentDefRef),
   143  				})
   144  			}
   145  			if comp.Replicas != lastUsedComp.Replicas {
   146  				return emitError(metav1.Condition{
   147  					Type:   appsv1alpha1.ConditionTypeHaltRecovery,
   148  					Reason: "HaltRecoveryFailed",
   149  					Message: fmt.Sprintf("not equal to last applied cluster.spec.componentSpecs[%s].replicas=%d",
   150  						comp.Name, lastUsedComp.Replicas),
   151  				})
   152  			}
   153  
   154  			// following only check resource related spec., will skip check if HaltRecoveryAllowInconsistentResAnnotKey
   155  			// annotation is specified
   156  			if cluster.Annotations[constant.HaltRecoveryAllowInconsistentResAnnotKey] == trueVal {
   157  				found = true
   158  				break
   159  			}
   160  			if !isVolumeClaimTemplatesEqual(comp.VolumeClaimTemplates, lastUsedComp.VolumeClaimTemplates) {
   161  				objJSON, _ := json.Marshal(&lastUsedComp.VolumeClaimTemplates)
   162  				return emitError(metav1.Condition{
   163  					Type:   appsv1alpha1.ConditionTypeHaltRecovery,
   164  					Reason: "HaltRecoveryFailed",
   165  					Message: fmt.Sprintf("not equal to last applied cluster.spec.componentSpecs[%s].volumeClaimTemplates=%s; add '%s=true' annotation to void this check",
   166  						comp.Name, objJSON, constant.HaltRecoveryAllowInconsistentResAnnotKey),
   167  				})
   168  			}
   169  
   170  			if lastUsedComp.ClassDefRef != nil {
   171  				if comp.ClassDefRef == nil || hash.Object(*comp.ClassDefRef) != hash.Object(*lastUsedComp.ClassDefRef) {
   172  					objJSON, _ := json.Marshal(lastUsedComp.ClassDefRef)
   173  					return emitError(metav1.Condition{
   174  						Type:   appsv1alpha1.ConditionTypeHaltRecovery,
   175  						Reason: "HaltRecoveryFailed",
   176  						Message: fmt.Sprintf("not equal to last applied cluster.spec.componentSpecs[%s].classDefRef=%s; add '%s=true' annotation to void this check",
   177  							comp.Name, objJSON, constant.HaltRecoveryAllowInconsistentResAnnotKey),
   178  					})
   179  				}
   180  			} else if comp.ClassDefRef != nil {
   181  				return emitError(metav1.Condition{
   182  					Type:   appsv1alpha1.ConditionTypeHaltRecovery,
   183  					Reason: "HaltRecoveryFailed",
   184  					Message: fmt.Sprintf("not equal to last applied cluster.spec.componentSpecs[%s].classDefRef=null; add '%s=true' annotation to void this check",
   185  						comp.Name, constant.HaltRecoveryAllowInconsistentResAnnotKey),
   186  				})
   187  			}
   188  
   189  			if !isResourceRequirementsEqual(comp.Resources, lastUsedComp.Resources) {
   190  				objJSON, _ := json.Marshal(&lastUsedComp.Resources)
   191  				return emitError(metav1.Condition{
   192  					Type:   appsv1alpha1.ConditionTypeHaltRecovery,
   193  					Reason: "HaltRecoveryFailed",
   194  					Message: fmt.Sprintf("not equal to last applied cluster.spec.componentSpecs[%s].resources=%s; add '%s=true' annotation to void this check",
   195  						comp.Name, objJSON, constant.HaltRecoveryAllowInconsistentResAnnotKey),
   196  				})
   197  			}
   198  
   199  			found = true
   200  			break
   201  		}
   202  		if !found {
   203  			return emitError(metav1.Condition{
   204  				Type:   appsv1alpha1.ConditionTypeHaltRecovery,
   205  				Reason: "HaltRecoveryFailed",
   206  				Message: fmt.Sprintf("cluster.spec.componentSpecs[%s] not found in last applied cluster",
   207  					comp.Name),
   208  			})
   209  		}
   210  	}
   211  	return nil
   212  }
   213  
   214  var _ graph.Transformer = &HaltRecoveryTransformer{}