github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/controllers/apps/operations/volume_expansion_test.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 "time" 24 25 . "github.com/onsi/ginkgo/v2" 26 . "github.com/onsi/gomega" 27 corev1 "k8s.io/api/core/v1" 28 storagev1 "k8s.io/api/storage/v1" 29 apierrors "k8s.io/apimachinery/pkg/api/errors" 30 "k8s.io/apimachinery/pkg/api/resource" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/kubectl/pkg/util/storage" 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 "github.com/1aal/kubeblocks/pkg/generics" 39 testapps "github.com/1aal/kubeblocks/pkg/testutil/apps" 40 ) 41 42 var _ = Describe("OpsRequest Controller Volume Expansion Handler", func() { 43 44 var ( 45 // waitDuration = time.Second * 3 46 randomStr = testCtx.GetRandomStr() 47 clusterDefinitionName = "cluster-definition-for-ops-" + randomStr 48 clusterVersionName = "clusterversion-for-ops-" + randomStr 49 clusterName = "cluster-for-ops-" + randomStr 50 storageClassName = "csi-hostpath-sc-" + randomStr 51 ) 52 53 const ( 54 vctName = "data" 55 consensusCompName = "consensus" 56 ) 57 58 cleanEnv := func() { 59 // must wait till resources deleted and no longer existed before the testcases start, 60 // otherwise if later it needs to create some new resource objects with the same name, 61 // in race conditions, it will find the existence of old objects, resulting failure to 62 // create the new objects. 63 By("clean resources") 64 65 // delete cluster(and all dependent sub-resources), clusterversion and clusterdef 66 testapps.ClearClusterResources(&testCtx) 67 68 // delete rest resources 69 inNS := client.InNamespace(testCtx.DefaultNamespace) 70 ml := client.HasLabels{testCtx.TestObjLabelKey} 71 // namespaced 72 testapps.ClearResources(&testCtx, generics.OpsRequestSignature, inNS, ml) 73 // non-namespaced 74 testapps.ClearResources(&testCtx, generics.StorageClassSignature, ml) 75 } 76 77 BeforeEach(cleanEnv) 78 79 AfterEach(cleanEnv) 80 81 createPVC := func(clusterName, scName, vctName, pvcName string) { 82 // Note: in real k8s cluster, it maybe fails when pvc created by k8s controller. 83 testapps.NewPersistentVolumeClaimFactory(testCtx.DefaultNamespace, pvcName, clusterName, 84 consensusCompName, testapps.DataVolumeName).AddLabels(constant.AppInstanceLabelKey, clusterName, 85 constant.VolumeClaimTemplateNameLabelKey, testapps.DataVolumeName, 86 constant.KBAppComponentLabelKey, consensusCompName).SetStorage("2Gi").SetStorageClass(storageClassName).CheckedCreate(&testCtx) 87 } 88 89 initResourcesForVolumeExpansion := func(clusterObject *appsv1alpha1.Cluster, opsRes *OpsResource, storage string, replicas int) (*appsv1alpha1.OpsRequest, []string) { 90 pvcNames := opsRes.Cluster.GetVolumeClaimNames(consensusCompName) 91 for _, pvcName := range pvcNames { 92 createPVC(clusterObject.Name, storageClassName, vctName, pvcName) 93 // mock pvc is Bound 94 Expect(testapps.GetAndChangeObjStatus(&testCtx, client.ObjectKey{Name: pvcName, Namespace: testCtx.DefaultNamespace}, func(pvc *corev1.PersistentVolumeClaim) { 95 pvc.Status.Phase = corev1.ClaimBound 96 })()).ShouldNot(HaveOccurred()) 97 98 } 99 currRandomStr := testCtx.GetRandomStr() 100 ops := testapps.NewOpsRequestObj("volumeexpansion-ops-"+currRandomStr, testCtx.DefaultNamespace, 101 clusterObject.Name, appsv1alpha1.VolumeExpansionType) 102 ops.Spec.VolumeExpansionList = []appsv1alpha1.VolumeExpansion{ 103 { 104 ComponentOps: appsv1alpha1.ComponentOps{ComponentName: consensusCompName}, 105 VolumeClaimTemplates: []appsv1alpha1.OpsRequestVolumeClaimTemplate{ 106 { 107 Name: vctName, 108 Storage: resource.MustParse(storage), 109 }, 110 }, 111 }, 112 } 113 opsRes.OpsRequest = ops 114 115 // create opsRequest 116 ops = testapps.CreateOpsRequest(ctx, testCtx, ops) 117 return ops, pvcNames 118 } 119 120 mockVolumeExpansionActionAndReconcile := func(reqCtx intctrlutil.RequestCtx, opsRes *OpsResource, newOps *appsv1alpha1.OpsRequest, pvcNames []string) { 121 // first step, validate ops and update phase to Creating 122 _, err := GetOpsManager().Do(reqCtx, k8sClient, opsRes) 123 Expect(err).Should(BeNil()) 124 125 // next step, do volume-expand action 126 _, err = GetOpsManager().Do(reqCtx, k8sClient, opsRes) 127 Expect(err).Should(BeNil()) 128 129 By("mock pvc.spec.resources.request.storage has applied by cluster controller") 130 for _, pvcName := range pvcNames { 131 Expect(testapps.GetAndChangeObj(&testCtx, client.ObjectKey{Name: pvcName, Namespace: testCtx.DefaultNamespace}, func(pvc *corev1.PersistentVolumeClaim) { 132 pvc.Spec.Resources.Requests[corev1.ResourceStorage] = newOps.Spec.VolumeExpansionList[0].VolumeClaimTemplates[0].Storage 133 })()).ShouldNot(HaveOccurred()) 134 } 135 136 By("mock opsRequest is Running") 137 Expect(testapps.ChangeObjStatus(&testCtx, newOps, func() { 138 newOps.Status.Phase = appsv1alpha1.OpsRunningPhase 139 newOps.Status.StartTimestamp = metav1.Time{Time: time.Now()} 140 })).ShouldNot(HaveOccurred()) 141 142 // reconcile ops status 143 opsRes.OpsRequest = newOps 144 _, err = GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) 145 Expect(err).Should(BeNil()) 146 } 147 148 testVolumeExpansion := func(reqCtx intctrlutil.RequestCtx, clusterObject *appsv1alpha1.Cluster, opsRes *OpsResource, randomStr string) { 149 // mock cluster is Running to support volume expansion ops 150 Expect(testapps.ChangeObjStatus(&testCtx, clusterObject, func() { 151 clusterObject.Status.Phase = appsv1alpha1.RunningClusterPhase 152 })).ShouldNot(HaveOccurred()) 153 154 // init resources for volume expansion 155 comp := clusterObject.Spec.GetComponentByName(consensusCompName) 156 newOps, pvcNames := initResourcesForVolumeExpansion(clusterObject, opsRes, "3Gi", int(comp.Replicas)) 157 158 By("mock run volumeExpansion action and reconcileAction") 159 mockVolumeExpansionActionAndReconcile(reqCtx, opsRes, newOps, pvcNames) 160 161 By("mock pvc is resizing") 162 for _, pvcName := range pvcNames { 163 pvcKey := client.ObjectKey{Name: pvcName, Namespace: testCtx.DefaultNamespace} 164 Expect(testapps.GetAndChangeObjStatus(&testCtx, pvcKey, func(pvc *corev1.PersistentVolumeClaim) { 165 pvc.Status.Conditions = []corev1.PersistentVolumeClaimCondition{{ 166 Type: corev1.PersistentVolumeClaimResizing, 167 Status: corev1.ConditionTrue, 168 LastTransitionTime: metav1.Now(), 169 }, 170 } 171 pvc.Status.Phase = corev1.ClaimBound 172 })()).ShouldNot(HaveOccurred()) 173 174 Eventually(testapps.CheckObj(&testCtx, pvcKey, func(g Gomega, tmpPVC *corev1.PersistentVolumeClaim) { 175 conditions := tmpPVC.Status.Conditions 176 g.Expect(len(conditions) > 0 && conditions[0].Type == corev1.PersistentVolumeClaimResizing).Should(BeTrue()) 177 })).Should(Succeed()) 178 179 // waiting OpsRequest.status.components["consensus"].vct["data"] is running 180 _, _ = GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) 181 Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(newOps), func(g Gomega, tmpOps *appsv1alpha1.OpsRequest) { 182 progressDetails := tmpOps.Status.Components[consensusCompName].ProgressDetails 183 progressDetail := findStatusProgressDetail(progressDetails, getPVCProgressObjectKey(pvcName)) 184 g.Expect(progressDetail != nil && progressDetail.Status == appsv1alpha1.ProcessingProgressStatus).Should(BeTrue()) 185 })).Should(Succeed()) 186 187 By("mock pvc resizing succeed") 188 // mock pvc volumeExpansion succeed 189 Expect(testapps.GetAndChangeObjStatus(&testCtx, pvcKey, func(pvc *corev1.PersistentVolumeClaim) { 190 pvc.Status.Capacity = corev1.ResourceList{corev1.ResourceStorage: resource.MustParse("3Gi")} 191 })()).ShouldNot(HaveOccurred()) 192 193 Eventually(testapps.CheckObj(&testCtx, pvcKey, func(g Gomega, tmpPVC *corev1.PersistentVolumeClaim) { 194 g.Expect(tmpPVC.Status.Capacity[corev1.ResourceStorage] == resource.MustParse("3Gi")).Should(BeTrue()) 195 })).Should(Succeed()) 196 } 197 198 // waiting for OpsRequest.status.phase is succeed 199 _, err := GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) 200 Expect(err).Should(BeNil()) 201 Expect(opsRes.OpsRequest.Status.Phase).Should(Equal(appsv1alpha1.OpsSucceedPhase)) 202 } 203 204 testDeleteRunningVolumeExpansion := func(clusterObject *appsv1alpha1.Cluster, opsRes *OpsResource) { 205 // init resources for volume expansion 206 newOps, pvcNames := initResourcesForVolumeExpansion(clusterObject, opsRes, "5Gi", 1) 207 Expect(k8sClient.Delete(ctx, newOps)).Should(Succeed()) 208 Eventually(func() error { 209 return k8sClient.Get(ctx, client.ObjectKey{Name: newOps.Name, Namespace: testCtx.DefaultNamespace}, &appsv1alpha1.OpsRequest{}) 210 }).Should(Satisfy(apierrors.IsNotFound)) 211 212 By("test handle the invalid volumeExpansion OpsRequest") 213 pvc := &corev1.PersistentVolumeClaim{} 214 Expect(k8sClient.Get(ctx, client.ObjectKey{Name: pvcNames[0], Namespace: testCtx.DefaultNamespace}, pvc)).Should(Succeed()) 215 216 Eventually(testapps.GetClusterPhase(&testCtx, client.ObjectKeyFromObject(clusterObject))).Should(Equal(appsv1alpha1.RunningClusterPhase)) 217 } 218 219 Context("Test VolumeExpansion", func() { 220 It("VolumeExpansion should work", func() { 221 reqCtx := intctrlutil.RequestCtx{Ctx: ctx} 222 _, _, clusterObject := testapps.InitConsensusMysql(&testCtx, clusterDefinitionName, 223 clusterVersionName, clusterName, "consensus", consensusCompName) 224 // init storageClass 225 sc := testapps.CreateStorageClass(&testCtx, storageClassName, true) 226 Expect(testapps.ChangeObj(&testCtx, sc, func(lsc *storagev1.StorageClass) { 227 lsc.Annotations = map[string]string{storage.IsDefaultStorageClassAnnotation: "true"} 228 })).ShouldNot(HaveOccurred()) 229 230 opsRes := &OpsResource{ 231 Cluster: clusterObject, 232 Recorder: k8sManager.GetEventRecorderFor("opsrequest-controller"), 233 } 234 235 By("Test OpsManager.MainEnter function with ClusterOps") 236 Expect(testapps.ChangeObjStatus(&testCtx, clusterObject, func() { 237 clusterObject.Status.Phase = appsv1alpha1.RunningClusterPhase 238 clusterObject.Status.Components = map[string]appsv1alpha1.ClusterComponentStatus{ 239 consensusCompName: { 240 Phase: appsv1alpha1.RunningClusterCompPhase, 241 }, 242 } 243 })).ShouldNot(HaveOccurred()) 244 245 By("Test VolumeExpansion") 246 testVolumeExpansion(reqCtx, clusterObject, opsRes, randomStr) 247 248 By("Test delete the Running VolumeExpansion OpsRequest") 249 testDeleteRunningVolumeExpansion(clusterObject, opsRes) 250 }) 251 }) 252 })