github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/controllers/apps/operations/volume_expansion.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 operations
    21  
    22  import (
    23  	"fmt"
    24  	"reflect"
    25  	"regexp"
    26  	"strconv"
    27  	"time"
    28  
    29  	"github.com/pkg/errors"
    30  	corev1 "k8s.io/api/core/v1"
    31  	"k8s.io/apimachinery/pkg/api/resource"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"sigs.k8s.io/controller-runtime/pkg/client"
    34  
    35  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    36  	"github.com/1aal/kubeblocks/pkg/constant"
    37  	intctrlutil "github.com/1aal/kubeblocks/pkg/controllerutil"
    38  )
    39  
    40  type volumeExpansionOpsHandler struct{}
    41  
    42  var _ OpsHandler = volumeExpansionOpsHandler{}
    43  
    44  var pvcNameRegex = regexp.MustCompile("(.*)-([0-9]+)$")
    45  
    46  const (
    47  	// VolumeExpansionTimeOut volume expansion timeout.
    48  	VolumeExpansionTimeOut = 30 * time.Minute
    49  )
    50  
    51  func init() {
    52  	// the volume expansion operation only supports online expansion now
    53  	volumeExpansionBehaviour := OpsBehaviour{
    54  		OpsHandler: volumeExpansionOpsHandler{},
    55  	}
    56  
    57  	opsMgr := GetOpsManager()
    58  	opsMgr.RegisterOps(appsv1alpha1.VolumeExpansionType, volumeExpansionBehaviour)
    59  }
    60  
    61  // ActionStartedCondition the started condition when handle the volume expansion request.
    62  func (ve volumeExpansionOpsHandler) ActionStartedCondition(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource) (*metav1.Condition, error) {
    63  	return appsv1alpha1.NewVolumeExpandingCondition(opsRes.OpsRequest), nil
    64  }
    65  
    66  // Action modifies Cluster.spec.components[*].VolumeClaimTemplates[*].spec.resources
    67  func (ve volumeExpansionOpsHandler) Action(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource) error {
    68  	var (
    69  		volumeExpansionMap = opsRes.OpsRequest.Spec.ToVolumeExpansionListToMap()
    70  		volumeExpansionOps appsv1alpha1.VolumeExpansion
    71  		ok                 bool
    72  	)
    73  	for index, component := range opsRes.Cluster.Spec.ComponentSpecs {
    74  		if volumeExpansionOps, ok = volumeExpansionMap[component.Name]; !ok {
    75  			continue
    76  		}
    77  		compSpec := &opsRes.Cluster.Spec.ComponentSpecs[index]
    78  		for _, v := range volumeExpansionOps.VolumeClaimTemplates {
    79  			for i, vct := range component.VolumeClaimTemplates {
    80  				if vct.Name != v.Name {
    81  					continue
    82  				}
    83  				compSpec.VolumeClaimTemplates[i].
    84  					Spec.Resources.Requests[corev1.ResourceStorage] = v.Storage
    85  			}
    86  		}
    87  	}
    88  	return cli.Update(reqCtx.Ctx, opsRes.Cluster)
    89  }
    90  
    91  // ReconcileAction will be performed when action is done and loops till OpsRequest.status.phase is Succeed/Failed.
    92  // the Reconcile function for volume expansion opsRequest.
    93  func (ve volumeExpansionOpsHandler) ReconcileAction(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource) (appsv1alpha1.OpsPhase, time.Duration, error) {
    94  	var (
    95  		opsRequest             = opsRes.OpsRequest
    96  		requeueAfter           time.Duration
    97  		err                    error
    98  		opsRequestPhase        = appsv1alpha1.OpsRunningPhase
    99  		oldOpsRequestStatus    = opsRequest.Status.DeepCopy()
   100  		expectProgressCount    int
   101  		succeedProgressCount   int
   102  		completedProgressCount int
   103  	)
   104  
   105  	patch := client.MergeFrom(opsRequest.DeepCopy())
   106  	if opsRequest.Status.Components == nil {
   107  		ve.initComponentStatus(opsRequest)
   108  	}
   109  	storageMap := ve.getRequestStorageMap(opsRequest)
   110  	// reconcile the status.components. when the volume expansion is successful,
   111  	// sync the volumeClaimTemplate status and component phase On the OpsRequest and Cluster.
   112  	for _, v := range opsRequest.Spec.VolumeExpansionList {
   113  		compStatus := opsRequest.Status.Components[v.ComponentName]
   114  		for _, vct := range v.VolumeClaimTemplates {
   115  			succeedCount, expectCount, completedCount, err := ve.handleVCTExpansionProgress(reqCtx, cli, opsRes,
   116  				&compStatus, storageMap, v.ComponentName, vct.Name)
   117  			if err != nil {
   118  				return "", requeueAfter, err
   119  			}
   120  			expectProgressCount += expectCount
   121  			succeedProgressCount += succeedCount
   122  			completedProgressCount += completedCount
   123  		}
   124  		opsRequest.Status.Components[v.ComponentName] = compStatus
   125  	}
   126  	if completedProgressCount != expectProgressCount {
   127  		requeueAfter = time.Minute
   128  	}
   129  	opsRequest.Status.Progress = fmt.Sprintf("%d/%d", completedProgressCount, expectProgressCount)
   130  	// patch OpsRequest.status.components
   131  	if !reflect.DeepEqual(*oldOpsRequestStatus, opsRequest.Status) {
   132  		if err = cli.Status().Patch(reqCtx.Ctx, opsRequest, patch); err != nil {
   133  			return opsRequestPhase, requeueAfter, err
   134  		}
   135  	}
   136  
   137  	// check all PVCs of volumeClaimTemplate are successful
   138  	if expectProgressCount == completedProgressCount {
   139  		if expectProgressCount == succeedProgressCount {
   140  			opsRequestPhase = appsv1alpha1.OpsSucceedPhase
   141  		} else {
   142  			opsRequestPhase = appsv1alpha1.OpsFailedPhase
   143  		}
   144  	} else {
   145  		// check whether the volume expansion operation has timed out
   146  		if time.Now().After(opsRequest.Status.StartTimestamp.Add(VolumeExpansionTimeOut)) {
   147  			// if volume expansion timed out
   148  			opsRequestPhase = appsv1alpha1.OpsFailedPhase
   149  			err = errors.New(fmt.Sprintf("Timed out waiting for volume expansion to complete, the timeout value is %g minutes", VolumeExpansionTimeOut.Minutes()))
   150  		}
   151  	}
   152  	return opsRequestPhase, requeueAfter, err
   153  }
   154  
   155  // SaveLastConfiguration records last configuration to the OpsRequest.status.lastConfiguration
   156  func (ve volumeExpansionOpsHandler) SaveLastConfiguration(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource) error {
   157  	opsRequest := opsRes.OpsRequest
   158  	componentNameSet := opsRequest.GetComponentNameSet()
   159  	storageMap := ve.getRequestStorageMap(opsRequest)
   160  	lastComponentInfo := map[string]appsv1alpha1.LastComponentConfiguration{}
   161  	for _, v := range opsRes.Cluster.Spec.ComponentSpecs {
   162  		if _, ok := componentNameSet[v.Name]; !ok {
   163  			continue
   164  		}
   165  		lastVCTs := make([]appsv1alpha1.OpsRequestVolumeClaimTemplate, 0)
   166  		for _, vct := range v.VolumeClaimTemplates {
   167  			key := getComponentVCTKey(v.Name, vct.Name)
   168  			if _, ok := storageMap[key]; !ok {
   169  				continue
   170  			}
   171  			lastVCTs = append(lastVCTs, appsv1alpha1.OpsRequestVolumeClaimTemplate{
   172  				Name:    vct.Name,
   173  				Storage: vct.Spec.Resources.Requests[corev1.ResourceStorage],
   174  			})
   175  		}
   176  		lastComponentInfo[v.Name] = appsv1alpha1.LastComponentConfiguration{
   177  			VolumeClaimTemplates: lastVCTs,
   178  		}
   179  	}
   180  	opsRequest.Status.LastConfiguration.Components = lastComponentInfo
   181  	return nil
   182  }
   183  
   184  // pvcIsResizing when pvc start resizing, it will set conditions type to Resizing/FileSystemResizePending
   185  func (ve volumeExpansionOpsHandler) pvcIsResizing(pvc *corev1.PersistentVolumeClaim) bool {
   186  	var isResizing bool
   187  	for _, condition := range pvc.Status.Conditions {
   188  		if condition.Type == corev1.PersistentVolumeClaimResizing || condition.Type == corev1.PersistentVolumeClaimFileSystemResizePending {
   189  			isResizing = true
   190  			break
   191  		}
   192  	}
   193  	return isResizing
   194  }
   195  
   196  func (ve volumeExpansionOpsHandler) getRequestStorageMap(opsRequest *appsv1alpha1.OpsRequest) map[string]resource.Quantity {
   197  	storageMap := map[string]resource.Quantity{}
   198  	for _, v := range opsRequest.Spec.VolumeExpansionList {
   199  		for _, vct := range v.VolumeClaimTemplates {
   200  			key := getComponentVCTKey(v.ComponentName, vct.Name)
   201  			storageMap[key] = vct.Storage
   202  		}
   203  	}
   204  	return storageMap
   205  }
   206  
   207  // initComponentStatus inits status.components for the VolumeExpansion OpsRequest
   208  func (ve volumeExpansionOpsHandler) initComponentStatus(opsRequest *appsv1alpha1.OpsRequest) {
   209  	opsRequest.Status.Components = map[string]appsv1alpha1.OpsRequestComponentStatus{}
   210  	for _, v := range opsRequest.Spec.VolumeExpansionList {
   211  		opsRequest.Status.Components[v.ComponentName] = appsv1alpha1.OpsRequestComponentStatus{}
   212  	}
   213  }
   214  
   215  // handleVCTExpansionProgress checks whether the pvc of the volume claim template is in (resizing, expansion succeeded, expansion completed).
   216  func (ve volumeExpansionOpsHandler) handleVCTExpansionProgress(reqCtx intctrlutil.RequestCtx,
   217  	cli client.Client,
   218  	opsRes *OpsResource,
   219  	compStatus *appsv1alpha1.OpsRequestComponentStatus,
   220  	storageMap map[string]resource.Quantity,
   221  	componentName, vctName string) (int, int, int, error) {
   222  	var (
   223  		succeedCount   int
   224  		expectCount    int
   225  		completedCount int
   226  		err            error
   227  	)
   228  	pvcList := &corev1.PersistentVolumeClaimList{}
   229  	if err = cli.List(reqCtx.Ctx, pvcList, client.MatchingLabels{
   230  		constant.AppInstanceLabelKey:    opsRes.Cluster.Name,
   231  		constant.KBAppComponentLabelKey: componentName,
   232  	}, client.InNamespace(opsRes.Cluster.Namespace)); err != nil {
   233  		return 0, 0, 0, err
   234  	}
   235  	comp := opsRes.Cluster.Spec.GetComponentByName(componentName)
   236  	if comp == nil {
   237  		err = fmt.Errorf("comp %s of cluster %s not found", componentName, opsRes.Cluster.Name)
   238  		return 0, 0, 0, err
   239  	}
   240  	expectCount = int(comp.Replicas)
   241  	vctKey := getComponentVCTKey(componentName, vctName)
   242  	requestStorage := storageMap[vctKey]
   243  	var ordinal int
   244  	for _, v := range pvcList.Items {
   245  		// VolumeClaimTemplateNameLabelKeyForLegacy is deprecated: only compatible with version 0.5, will be removed in 0.7?
   246  		if v.Labels[constant.VolumeClaimTemplateNameLabelKey] != vctName &&
   247  			v.Labels[constant.VolumeClaimTemplateNameLabelKeyForLegacy] != vctName {
   248  			continue
   249  		}
   250  		// filter PVC(s) with ordinal no larger than comp.Replicas - 1, which left by scale-in
   251  		ordinal, err = getPVCOrdinal(v.Name)
   252  		if err != nil {
   253  			return 0, 0, 0, err
   254  		}
   255  		if ordinal > expectCount-1 {
   256  			continue
   257  		}
   258  		objectKey := getPVCProgressObjectKey(v.Name)
   259  		progressDetail := findStatusProgressDetail(compStatus.ProgressDetails, objectKey)
   260  		if progressDetail == nil {
   261  			progressDetail = &appsv1alpha1.ProgressStatusDetail{ObjectKey: objectKey, Group: vctName}
   262  		}
   263  		if progressDetail.Status == appsv1alpha1.FailedProgressStatus {
   264  			completedCount += 1
   265  			continue
   266  		}
   267  		currStorageSize := v.Status.Capacity.Storage()
   268  		// should check if the spec.resources.requests.storage equals to the requested storage
   269  		// and pvc is bound if the pvc is re-created for recovery.
   270  		if currStorageSize.Cmp(requestStorage) == 0 &&
   271  			v.Spec.Resources.Requests.Storage().Cmp(requestStorage) == 0 &&
   272  			v.Status.Phase == corev1.ClaimBound {
   273  			succeedCount += 1
   274  			completedCount += 1
   275  			message := fmt.Sprintf("Successfully expand volume: %s in Component: %s", objectKey, componentName)
   276  			progressDetail.SetStatusAndMessage(appsv1alpha1.SucceedProgressStatus, message)
   277  			setComponentStatusProgressDetail(opsRes.Recorder, opsRes.OpsRequest, &compStatus.ProgressDetails, *progressDetail)
   278  			continue
   279  		}
   280  		if ve.pvcIsResizing(&v) {
   281  			message := fmt.Sprintf("Start expanding volume: %s in Component: %s ", objectKey, componentName)
   282  			progressDetail.SetStatusAndMessage(appsv1alpha1.ProcessingProgressStatus, message)
   283  		} else {
   284  			message := fmt.Sprintf("Waiting for an external controller to process the pvc: %s in Component: %s ", objectKey, componentName)
   285  			progressDetail.SetStatusAndMessage(appsv1alpha1.PendingProgressStatus, message)
   286  		}
   287  		setComponentStatusProgressDetail(opsRes.Recorder, opsRes.OpsRequest, &compStatus.ProgressDetails, *progressDetail)
   288  	}
   289  	return succeedCount, expectCount, completedCount, nil
   290  }
   291  
   292  func getComponentVCTKey(componentName, vctName string) string {
   293  	return fmt.Sprintf("%s/%s", componentName, vctName)
   294  }
   295  
   296  func getPVCProgressObjectKey(pvcName string) string {
   297  	return fmt.Sprintf("PVC/%s", pvcName)
   298  }
   299  
   300  func getPVCOrdinal(pvcName string) (int, error) {
   301  	subMatches := pvcNameRegex.FindStringSubmatch(pvcName)
   302  	if len(subMatches) < 3 {
   303  		return 0, fmt.Errorf("wrong pvc name: %s", pvcName)
   304  	}
   305  	return strconv.Atoi(subMatches[2])
   306  }