github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/testutil/apps/common_util.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  	"fmt"
    24  	"reflect"
    25  	"strings"
    26  
    27  	"github.com/onsi/ginkgo/v2"
    28  	"github.com/onsi/gomega"
    29  	"github.com/sethvargo/go-password/password"
    30  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    31  	"k8s.io/apimachinery/pkg/types"
    32  	"k8s.io/apimachinery/pkg/util/yaml"
    33  	"sigs.k8s.io/controller-runtime/pkg/client"
    34  	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
    35  
    36  	"github.com/1aal/kubeblocks/pkg/constant"
    37  	intctrlutil "github.com/1aal/kubeblocks/pkg/generics"
    38  	"github.com/1aal/kubeblocks/pkg/testutil"
    39  	"github.com/1aal/kubeblocks/test/testdata"
    40  )
    41  
    42  var ToIgnoreFinalizers []string
    43  
    44  func init() {
    45  	ResetToIgnoreFinalizers()
    46  }
    47  
    48  func ResetToIgnoreFinalizers() {
    49  	ToIgnoreFinalizers = []string{
    50  		"orphan",
    51  		"kubernetes.io/pvc-protection",
    52  		// REVIEW: adding following is a hack, if tests are running as
    53  		// controller-runtime manager setup.
    54  		constant.ConfigurationTemplateFinalizerName,
    55  		constant.DBClusterFinalizerName,
    56  	}
    57  }
    58  
    59  // Helper functions to change object's fields in input closure and then update it.
    60  // Each helper is a wrapper of k8sClient.Patch.
    61  // Example:
    62  // Expect(ChangeObj(testCtx, obj, func() {
    63  //		// modify input obj
    64  // })).Should(Succeed())
    65  
    66  func ChangeObj[T intctrlutil.Object, PT intctrlutil.PObject[T]](testCtx *testutil.TestContext,
    67  	pobj PT, action func(PT)) error {
    68  	patch := client.MergeFrom(PT(pobj.DeepCopy()))
    69  	action(pobj)
    70  	return testCtx.Cli.Patch(testCtx.Ctx, pobj, patch)
    71  }
    72  
    73  func ChangeObjStatus[T intctrlutil.Object, PT intctrlutil.PObject[T]](testCtx *testutil.TestContext,
    74  	pobj PT, action func()) error {
    75  	patch := client.MergeFrom(PT(pobj.DeepCopy()))
    76  	action()
    77  	return testCtx.Cli.Status().Patch(testCtx.Ctx, pobj, patch)
    78  }
    79  
    80  // Helper functions to get object, change its fields in input closure and update it.
    81  // Each helper is a wrapper of client.Get and client.Patch.
    82  // Each helper returns a Gomega assertion function, which should be passed into
    83  // Eventually() or Consistently() as the first parameter.
    84  // Example:
    85  // Eventually(GetAndChangeObj(testCtx, key, func(fetched *appsv1alpha1.ClusterDefinition) {
    86  //		    // modify fetched clusterDef
    87  //      })).Should(Succeed())
    88  // Warning: these functions should NOT be used together with Expect().
    89  // BAD Example:
    90  // Expect(GetAndChangeObj(testCtx, key, ...)).Should(Succeed())
    91  // Although it compiles, and test may also pass, it makes no sense and doesn't work as you expect.
    92  
    93  func GetAndChangeObj[T intctrlutil.Object, PT intctrlutil.PObject[T]](
    94  	testCtx *testutil.TestContext, namespacedName types.NamespacedName, action func(PT)) func() error {
    95  	return func() error {
    96  		var obj T
    97  		pobj := PT(&obj)
    98  		if err := testCtx.Cli.Get(testCtx.Ctx, namespacedName, pobj); err != nil {
    99  			return err
   100  		}
   101  		return ChangeObj(testCtx, pobj, func(lobj PT) {
   102  			action(lobj)
   103  		})
   104  	}
   105  }
   106  
   107  func GetAndChangeObjStatus[T intctrlutil.Object, PT intctrlutil.PObject[T]](
   108  	testCtx *testutil.TestContext, namespacedName types.NamespacedName, action func(pobj PT)) func() error {
   109  	return func() error {
   110  		var obj T
   111  		pobj := PT(&obj)
   112  		if err := testCtx.Cli.Get(testCtx.Ctx, namespacedName, pobj); err != nil {
   113  			return err
   114  		}
   115  		return ChangeObjStatus(testCtx, pobj, func() { action(pobj) })
   116  	}
   117  }
   118  
   119  // Helper functions to check fields of resources when writing unit tests.
   120  // Each helper returns a Gomega assertion function, which should be passed into
   121  // Eventually() or Consistently() as the first parameter.
   122  // Example:
   123  // Eventually(CheckObj(testCtx, key, func(g Gomega, fetched *appsv1alpha1.Cluster) {
   124  //   g.Expect(..).To(BeTrue()) // do some check
   125  // })).Should(Succeed())
   126  // Warning: these functions should NOT be used together with Expect().
   127  // BAD Example:
   128  // Expect(CheckObj(testCtx, key, ...)).Should(Succeed())
   129  // Although it compiles, and test may also pass, it makes no sense and doesn't work as you expect.
   130  
   131  func CheckObjExists(testCtx *testutil.TestContext, namespacedName types.NamespacedName,
   132  	obj client.Object, expectExisted bool) func(g gomega.Gomega) {
   133  	return func(g gomega.Gomega) {
   134  		err := testCtx.Cli.Get(testCtx.Ctx, namespacedName, obj)
   135  		if expectExisted {
   136  			g.Expect(err).To(gomega.Not(gomega.HaveOccurred()))
   137  		} else {
   138  			g.Expect(err).To(gomega.Satisfy(apierrors.IsNotFound))
   139  		}
   140  	}
   141  }
   142  
   143  func CheckObj[T intctrlutil.Object, PT intctrlutil.PObject[T]](testCtx *testutil.TestContext,
   144  	namespacedName types.NamespacedName, check func(g gomega.Gomega, pobj PT)) func(g gomega.Gomega) {
   145  	return func(g gomega.Gomega) {
   146  		var obj T
   147  		pobj := PT(&obj)
   148  		g.Expect(testCtx.Cli.Get(testCtx.Ctx, namespacedName, pobj)).To(gomega.Succeed())
   149  		check(g, pobj)
   150  	}
   151  }
   152  
   153  // Helper functions to check fields of resource lists when writing unit tests.
   154  
   155  func List[T intctrlutil.Object, PT intctrlutil.PObject[T],
   156  	L intctrlutil.ObjList[T], PL intctrlutil.PObjList[T, L]](
   157  	testCtx *testutil.TestContext, _ func(T, PT, L, PL), opt ...client.ListOption) func(gomega.Gomega) []T {
   158  	return func(g gomega.Gomega) []T {
   159  		var objList L
   160  		g.Expect(testCtx.Cli.List(testCtx.Ctx, PL(&objList), opt...)).To(gomega.Succeed())
   161  		return reflect.ValueOf(&objList).Elem().FieldByName("Items").Interface().([]T)
   162  	}
   163  }
   164  
   165  // Helper functions to create object from testdata files.
   166  
   167  func CustomizeObjYAML(a ...any) func(string) string {
   168  	return func(inputYAML string) string {
   169  		return fmt.Sprintf(inputYAML, a...)
   170  	}
   171  }
   172  
   173  func GetRandomizedKey(namespace, prefix string) types.NamespacedName {
   174  	randomStr, _ := password.Generate(6, 0, 0, true, false)
   175  	return types.NamespacedName{
   176  		Name:      prefix + randomStr,
   177  		Namespace: namespace,
   178  	}
   179  }
   180  
   181  func RandomizedObjName() func(client.Object) {
   182  	return func(obj client.Object) {
   183  		randomStr, _ := password.Generate(6, 0, 0, true, false)
   184  		obj.SetName(obj.GetName() + randomStr)
   185  	}
   186  }
   187  
   188  func WithName(name string) func(client.Object) {
   189  	return func(obj client.Object) {
   190  		obj.SetName(name)
   191  	}
   192  }
   193  
   194  func WithNamespace(namespace string) func(client.Object) {
   195  	return func(obj client.Object) {
   196  		obj.SetNamespace(namespace)
   197  	}
   198  }
   199  
   200  func WithNamespacedName(resourceName, ns string) func(client.Object) {
   201  	return func(obj client.Object) {
   202  		obj.SetNamespace(ns)
   203  		obj.SetName(resourceName)
   204  	}
   205  }
   206  
   207  func WithMap(keysAndValues ...string) map[string]string {
   208  	// ignore mismatching for kvs
   209  	m := make(map[string]string, len(keysAndValues)/2)
   210  	for i := 0; i+1 < len(keysAndValues); i += 2 {
   211  		m[keysAndValues[i]] = keysAndValues[i+1]
   212  	}
   213  	return m
   214  }
   215  
   216  func WithLabels(keysAndValues ...string) func(client.Object) {
   217  	return func(obj client.Object) {
   218  		obj.SetLabels(WithMap(keysAndValues...))
   219  	}
   220  }
   221  
   222  func WithAnnotations(keysAndValues ...string) func(client.Object) {
   223  	return func(obj client.Object) {
   224  		obj.SetAnnotations(WithMap(keysAndValues...))
   225  	}
   226  }
   227  
   228  // CreateObj calls CreateCustomizedObj with CustomizeObjYAML wrapper for any optional modify actions.
   229  func CreateObj[T intctrlutil.Object, PT intctrlutil.PObject[T]](testCtx *testutil.TestContext,
   230  	filePath string, pobj PT, actions ...any) PT {
   231  	return CreateCustomizedObj(testCtx, filePath, pobj, CustomizeObjYAML(actions...))
   232  }
   233  
   234  func NewCustomizedObj[T intctrlutil.Object, PT intctrlutil.PObject[T]](
   235  	filePath string, pobj PT, actions ...any) PT {
   236  	objBytes, err := testdata.GetTestDataFileContent(filePath)
   237  	gomega.Expect(err).NotTo(gomega.HaveOccurred())
   238  	objYAML := string(objBytes)
   239  	for _, action := range actions {
   240  		if action == nil {
   241  			continue
   242  		}
   243  		switch f := action.(type) {
   244  		case func(string) string:
   245  			objYAML = f(objYAML)
   246  		default:
   247  		}
   248  	}
   249  	gomega.Expect(yaml.Unmarshal([]byte(objYAML), pobj)).Should(gomega.Succeed())
   250  	for _, action := range actions {
   251  		if action == nil {
   252  			continue
   253  		}
   254  		switch f := action.(type) {
   255  		case func(client.Object):
   256  			f(pobj)
   257  		case func(PT):
   258  			f(pobj)
   259  		}
   260  	}
   261  	return pobj
   262  }
   263  
   264  func CreateCustomizedObj[T intctrlutil.Object, PT intctrlutil.PObject[T]](testCtx *testutil.TestContext,
   265  	filePath string, pobj PT, actions ...any) PT {
   266  	pobj = NewCustomizedObj(filePath, pobj, actions...)
   267  	return CreateK8sResource(testCtx, pobj).(PT)
   268  }
   269  
   270  func CheckedCreateCustomizedObj[T intctrlutil.Object, PT intctrlutil.PObject[T]](testCtx *testutil.TestContext,
   271  	filePath string, pobj PT, actions ...any) PT {
   272  	pobj = NewCustomizedObj(filePath, pobj, actions...)
   273  	return CheckedCreateK8sResource(testCtx, pobj).(PT)
   274  }
   275  
   276  // Helper functions to delete object.
   277  
   278  func DeleteObject[T intctrlutil.Object, PT intctrlutil.PObject[T]](
   279  	testCtx *testutil.TestContext, key types.NamespacedName, pobj PT) {
   280  	gomega.Expect(func() error {
   281  		if err := testCtx.Cli.Get(testCtx.Ctx, key, pobj); err != nil {
   282  			return client.IgnoreNotFound(err)
   283  		}
   284  		return testCtx.Cli.Delete(testCtx.Ctx, pobj)
   285  	}()).Should(gomega.Succeed())
   286  }
   287  
   288  // Helper functions to delete a list of resources when writing unit tests.
   289  
   290  // ClearResources clears all resources of the given type T satisfying the input ListOptions.
   291  func ClearResources[T intctrlutil.Object, PT intctrlutil.PObject[T],
   292  	L intctrlutil.ObjList[T], PL intctrlutil.PObjList[T, L]](
   293  	testCtx *testutil.TestContext, funcSig func(T, PT, L, PL), opts ...client.DeleteAllOfOption) {
   294  	ClearResourcesWithRemoveFinalizerOption[T, PT, L, PL](testCtx, funcSig, false, opts...)
   295  }
   296  
   297  // ClearResourcesWithRemoveFinalizerOption clears all resources of the given type T with
   298  // removeFinalizer specifier, and satisfying the input ListOptions.
   299  func ClearResourcesWithRemoveFinalizerOption[T intctrlutil.Object, PT intctrlutil.PObject[T],
   300  	L intctrlutil.ObjList[T], PL intctrlutil.PObjList[T, L]](
   301  	testCtx *testutil.TestContext, _ func(T, PT, L, PL), removeFinalizer bool, opts ...client.DeleteAllOfOption) {
   302  	var (
   303  		obj     T
   304  		objList L
   305  	)
   306  
   307  	listOptions := make([]client.ListOption, 0)
   308  	for _, opt := range opts {
   309  		applyToListFunc := reflect.ValueOf(opt).MethodByName("ApplyToList")
   310  		if applyToListFunc.IsValid() {
   311  			listOptions = append(listOptions, opt.(client.ListOption))
   312  		}
   313  	}
   314  
   315  	gvk, _ := apiutil.GVKForObject(PL(&objList), testCtx.Cli.Scheme())
   316  	ginkgo.By("clear resources " + strings.TrimSuffix(gvk.Kind, "List"))
   317  	gomega.Eventually(func(g gomega.Gomega) {
   318  		g.Expect(testCtx.Cli.DeleteAllOf(testCtx.Ctx, PT(&obj), opts...)).ShouldNot(gomega.HaveOccurred())
   319  		g.Expect(testCtx.Cli.List(testCtx.Ctx, PL(&objList), listOptions...)).Should(gomega.Succeed())
   320  		items := reflect.ValueOf(&objList).Elem().FieldByName("Items").Interface().([]T)
   321  		for _, obj := range items {
   322  			pobj := PT(&obj)
   323  			if pobj.GetDeletionTimestamp().IsZero() {
   324  				panic("expected DeletionTimestamp is not nil")
   325  			}
   326  			finalizers := pobj.GetFinalizers()
   327  			if len(finalizers) > 0 {
   328  				if removeFinalizer {
   329  					g.Expect(ChangeObj(testCtx, pobj, func(lobj PT) {
   330  						pobj.SetFinalizers([]string{})
   331  					})).To(gomega.Succeed())
   332  				} else {
   333  					g.Expect(finalizers).Should(gomega.BeEmpty())
   334  				}
   335  			}
   336  		}
   337  		g.Expect(items).Should(gomega.BeEmpty())
   338  	}, testCtx.ClearResourceTimeout, testCtx.ClearResourcePollingInterval).Should(gomega.Succeed())
   339  }
   340  
   341  // ClearClusterResources clears all dependent resources belonging to existing clusters.
   342  // The function is intended to be called to clean resources created by cluster controller in envtest
   343  // environment without UseExistingCluster set, where garbage collection lacks.
   344  func ClearClusterResources(testCtx *testutil.TestContext) {
   345  	inNS := client.InNamespace(testCtx.DefaultNamespace)
   346  	ClearResources(testCtx, intctrlutil.ClusterSignature, inNS,
   347  		client.HasLabels{testCtx.TestObjLabelKey})
   348  	// finalizer of ConfigMap are deleted in ClusterDef&ClusterVersion controller
   349  	ClearResources(testCtx, intctrlutil.ClusterVersionSignature,
   350  		client.HasLabels{testCtx.TestObjLabelKey})
   351  	ClearResources(testCtx, intctrlutil.ClusterDefinitionSignature,
   352  		client.HasLabels{testCtx.TestObjLabelKey})
   353  }
   354  
   355  // ClearClusterResourcesWithRemoveFinalizerOption clears all dependent resources belonging to existing clusters.
   356  func ClearClusterResourcesWithRemoveFinalizerOption(testCtx *testutil.TestContext) {
   357  	inNs := client.InNamespace(testCtx.DefaultNamespace)
   358  	hasLabels := client.HasLabels{testCtx.TestObjLabelKey}
   359  	ClearResourcesWithRemoveFinalizerOption(testCtx, intctrlutil.ClusterSignature, true, inNs, hasLabels)
   360  	// finalizer of ConfigMap are deleted in ClusterDef & ClusterVersion controller
   361  	ClearResourcesWithRemoveFinalizerOption(testCtx, intctrlutil.ClusterVersionSignature, true, hasLabels)
   362  	ClearResourcesWithRemoveFinalizerOption(testCtx, intctrlutil.ClusterDefinitionSignature, true, hasLabels)
   363  }