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 }