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 }