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  })