github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/controller/plan/prepare_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 plan 21 22 import ( 23 "fmt" 24 "reflect" 25 26 . "github.com/onsi/ginkgo/v2" 27 . "github.com/onsi/gomega" 28 29 appsv1 "k8s.io/api/apps/v1" 30 corev1 "k8s.io/api/core/v1" 31 "k8s.io/apimachinery/pkg/types" 32 "sigs.k8s.io/controller-runtime/pkg/client" 33 34 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 35 cfgcore "github.com/1aal/kubeblocks/pkg/configuration/core" 36 "github.com/1aal/kubeblocks/pkg/controller/component" 37 "github.com/1aal/kubeblocks/pkg/controller/configuration" 38 "github.com/1aal/kubeblocks/pkg/controller/factory" 39 intctrlutil "github.com/1aal/kubeblocks/pkg/controllerutil" 40 "github.com/1aal/kubeblocks/pkg/generics" 41 testapps "github.com/1aal/kubeblocks/pkg/testutil/apps" 42 ) 43 44 const ( 45 mysqlCompDefName = "replicasets" 46 mysqlCompName = "mysql" 47 nginxCompDefName = "nginx" 48 nginxCompName = "nginx" 49 redisCompDefName = "replicasets" 50 redisCompName = "redis" 51 ) 52 53 // buildComponentResources generate all necessary sub-resources objects used in component, 54 // like Secret, ConfigMap, Service, StatefulSet, Deployment, Volume, PodDisruptionBudget etc. 55 func buildComponentResources(reqCtx intctrlutil.RequestCtx, cli client.Client, 56 clusterDef *appsv1alpha1.ClusterDefinition, 57 clusterVer *appsv1alpha1.ClusterVersion, 58 cluster *appsv1alpha1.Cluster, 59 component *component.SynthesizedComponent) ([]client.Object, error) { 60 resources := make([]client.Object, 0) 61 if cluster.UID == "" { 62 cluster.UID = types.UID("test-uid") 63 } 64 workloadProcessor := func(customSetup func(*corev1.ConfigMap) (client.Object, error)) error { 65 envConfig := factory.BuildEnvConfig(cluster, component) 66 resources = append(resources, envConfig) 67 68 workload, err := customSetup(envConfig) 69 if err != nil { 70 return err 71 } 72 73 defer func() { 74 // workload object should be appended last 75 resources = append(resources, workload) 76 }() 77 78 var podSpec *corev1.PodSpec 79 sts, ok := workload.(*appsv1.StatefulSet) 80 if ok { 81 podSpec = &sts.Spec.Template.Spec 82 } else { 83 deploy, ok := workload.(*appsv1.Deployment) 84 if ok { 85 podSpec = &deploy.Spec.Template.Spec 86 } 87 } 88 if podSpec == nil { 89 return nil 90 } 91 92 defer func() { 93 for _, cc := range []*[]corev1.Container{ 94 &podSpec.Containers, 95 &podSpec.InitContainers, 96 } { 97 volumes := podSpec.Volumes 98 for _, c := range *cc { 99 for _, v := range c.VolumeMounts { 100 // if persistence is not found, add emptyDir pod.spec.volumes[] 101 volumes, _ = intctrlutil.CreateOrUpdateVolume(volumes, v.Name, func(volumeName string) corev1.Volume { 102 return corev1.Volume{ 103 Name: v.Name, 104 VolumeSource: corev1.VolumeSource{ 105 EmptyDir: &corev1.EmptyDirVolumeSource{}, 106 }, 107 } 108 }, nil) 109 } 110 } 111 podSpec.Volumes = volumes 112 } 113 }() 114 115 // render config template 116 return RenderConfigNScriptFiles( 117 &intctrlutil.ResourceCtx{ 118 Context: reqCtx.Ctx, 119 Client: cli, 120 Namespace: cluster.GetNamespace(), 121 ClusterName: cluster.GetNamespace(), 122 ComponentName: component.Name, 123 }, 124 clusterVer, cluster, component, workload, podSpec, nil) 125 } 126 127 // TODO: may add a PDB transform to Create/Update/Delete. 128 // if no these handle, the cluster controller will occur an error during reconciling. 129 // conditional build PodDisruptionBudget 130 if component.MinAvailable != nil { 131 pdb := factory.BuildPDB(cluster, component) 132 resources = append(resources, pdb) 133 } else { 134 panic("this shouldn't happen") 135 } 136 137 // REVIEW/TODO: 138 // - need higher level abstraction handling 139 // - or move this module to part operator controller handling 140 switch component.WorkloadType { 141 case appsv1alpha1.Stateful, appsv1alpha1.Consensus, appsv1alpha1.Replication: 142 if err := workloadProcessor( 143 func(envConfig *corev1.ConfigMap) (client.Object, error) { 144 return factory.BuildSts(reqCtx, cluster, component, envConfig.Name) 145 }); err != nil { 146 return nil, err 147 } 148 } 149 150 return resources, nil 151 } 152 153 var _ = Describe("Cluster Controller", func() { 154 155 cleanEnv := func() { 156 // must wait until resources deleted and no longer exist before the testcases start, 157 // otherwise if later it needs to create some new resource objects with the same name, 158 // in race conditions, it will find the existence of old objects, resulting failure to 159 // create the new objects. 160 By("clean resources") 161 162 inNS := client.InNamespace(testCtx.DefaultNamespace) 163 ml := client.HasLabels{testCtx.TestObjLabelKey} 164 165 // non-namespaced 166 testapps.ClearResources(&testCtx, generics.ConfigConstraintSignature, ml) 167 168 // namespaced 169 testapps.ClearResources(&testCtx, generics.ConfigMapSignature, inNS, ml) 170 } 171 172 BeforeEach(func() { 173 cleanEnv() 174 }) 175 176 AfterEach(func() { 177 cleanEnv() 178 }) 179 180 const ( 181 clusterDefName = "test-clusterdef" 182 clusterVersionName = "test-clusterversion" 183 clusterName = "test-cluster" 184 ) 185 var ( 186 clusterDef *appsv1alpha1.ClusterDefinition 187 clusterVersion *appsv1alpha1.ClusterVersion 188 cluster *appsv1alpha1.Cluster 189 configSpecName string 190 ) 191 192 isStatefulSet := func(v string) bool { 193 return v == "StatefulSet" 194 } 195 196 Context("with Deployment workload", func() { 197 BeforeEach(func() { 198 clusterDef = testapps.NewClusterDefFactory(clusterDefName). 199 AddComponentDef(testapps.StatelessNginxComponent, nginxCompDefName). 200 GetObject() 201 clusterVersion = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). 202 AddComponentVersion(nginxCompDefName). 203 AddContainerShort("nginx", testapps.NginxImage). 204 GetObject() 205 cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, 206 clusterDef.Name, clusterVersion.Name). 207 AddComponent(nginxCompDefName, nginxCompName). 208 GetObject() 209 }) 210 211 It("should construct pdb", func() { 212 reqCtx := intctrlutil.RequestCtx{ 213 Ctx: ctx, 214 Log: logger, 215 } 216 component, err := component.BuildComponent( 217 reqCtx, 218 nil, 219 cluster, 220 clusterDef, 221 &clusterDef.Spec.ComponentDefs[0], 222 &cluster.Spec.ComponentSpecs[0], 223 nil, 224 &clusterVersion.Spec.ComponentVersions[0]) 225 Expect(err).Should(Succeed()) 226 227 resources, err := buildComponentResources(reqCtx, testCtx.Cli, clusterDef, clusterVersion, cluster, component) 228 Expect(err).Should(Succeed()) 229 230 expects := []string{ 231 "PodDisruptionBudget", 232 } 233 Expect(resources).Should(HaveLen(len(expects))) 234 for i, v := range expects { 235 Expect(reflect.TypeOf(resources[i]).String()).Should(ContainSubstring(v), fmt.Sprintf("failed at idx %d", i)) 236 } 237 }) 238 }) 239 240 Context("with Stateful workload and without config template", func() { 241 BeforeEach(func() { 242 clusterDef = testapps.NewClusterDefFactory(clusterDefName). 243 AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompDefName). 244 GetObject() 245 clusterVersion = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). 246 AddComponentVersion(mysqlCompDefName). 247 AddContainerShort("mysql", testapps.ApeCloudMySQLImage). 248 GetObject() 249 pvcSpec := testapps.NewPVCSpec("1Gi") 250 cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, 251 clusterDef.Name, clusterVersion.Name). 252 AddComponent(mysqlCompName, mysqlCompDefName). 253 AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). 254 GetObject() 255 }) 256 257 It("should construct env, default ClusterIP service, headless service and statefuset objects and should not render config template", func() { 258 reqCtx := intctrlutil.RequestCtx{ 259 Ctx: ctx, 260 Log: logger, 261 } 262 component, err := component.BuildComponent( 263 reqCtx, 264 nil, 265 cluster, 266 clusterDef, 267 &clusterDef.Spec.ComponentDefs[0], 268 &cluster.Spec.ComponentSpecs[0], 269 nil, 270 &clusterVersion.Spec.ComponentVersions[0], 271 ) 272 Expect(err).Should(Succeed()) 273 274 resources, err := buildComponentResources(reqCtx, testCtx.Cli, clusterDef, clusterVersion, cluster, component) 275 Expect(err).Should(Succeed()) 276 277 expects := []string{ 278 "PodDisruptionBudget", 279 "ConfigMap", 280 "StatefulSet", 281 } 282 Expect(resources).Should(HaveLen(len(expects))) 283 for i, v := range expects { 284 Expect(reflect.TypeOf(resources[i]).String()).Should(ContainSubstring(v), fmt.Sprintf("failed at idx %d", i)) 285 if isStatefulSet(v) { 286 container := clusterDef.Spec.ComponentDefs[0].PodSpec.Containers[0] 287 sts := resources[i].(*appsv1.StatefulSet) 288 Expect(len(sts.Spec.Template.Spec.Volumes)).Should(Equal(len(container.VolumeMounts))) 289 } 290 } 291 }) 292 }) 293 294 Context("with Stateful workload and with config template", func() { 295 BeforeEach(func() { 296 cm := testapps.CreateCustomizedObj(&testCtx, "config/envfrom-config.yaml", &corev1.ConfigMap{}, 297 testCtx.UseDefaultNamespace()) 298 299 cfgTpl := testapps.CreateCustomizedObj(&testCtx, "config/envfrom-constraint.yaml", 300 &appsv1alpha1.ConfigConstraint{}) 301 302 configSpecName = cm.Name 303 clusterDef = testapps.NewClusterDefFactory(clusterDefName). 304 AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompDefName). 305 AddConfigTemplate(cm.Name, cm.Name, cfgTpl.Name, testCtx.DefaultNamespace, "mysql-config", testapps.DefaultMySQLContainerName, "not-exist"). 306 GetObject() 307 clusterVersion = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). 308 AddComponentVersion(mysqlCompDefName). 309 AddContainerShort("mysql", testapps.ApeCloudMySQLImage). 310 GetObject() 311 pvcSpec := testapps.NewPVCSpec("1Gi") 312 cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, 313 clusterDef.Name, clusterVersion.Name). 314 AddComponent(mysqlCompName, mysqlCompDefName). 315 AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). 316 GetObject() 317 }) 318 319 It("should render config template", func() { 320 reqCtx := intctrlutil.RequestCtx{ 321 Ctx: ctx, 322 Log: logger, 323 } 324 component, err := component.BuildComponent( 325 reqCtx, 326 nil, 327 cluster, 328 clusterDef, 329 &clusterDef.Spec.ComponentDefs[0], 330 &cluster.Spec.ComponentSpecs[0], 331 nil, 332 &clusterVersion.Spec.ComponentVersions[0]) 333 Expect(err).Should(Succeed()) 334 335 resources, err := buildComponentResources(reqCtx, testCtx.Cli, clusterDef, clusterVersion, cluster, component) 336 Expect(err).Should(Succeed()) 337 338 expects := []string{ 339 "PodDisruptionBudget", 340 "ConfigMap", 341 "StatefulSet", 342 } 343 Expect(resources).Should(HaveLen(len(expects))) 344 for i, v := range expects { 345 Expect(reflect.TypeOf(resources[i]).String()).Should(ContainSubstring(v), fmt.Sprintf("failed at idx %d", i)) 346 if isStatefulSet(v) { 347 sts := resources[i].(*appsv1.StatefulSet) 348 Expect(configuration.CheckEnvFrom(&sts.Spec.Template.Spec.Containers[0], cfgcore.GenerateEnvFromName(cfgcore.GetComponentCfgName(cluster.Name, component.Name, configSpecName)))).Should(BeTrue()) 349 } 350 } 351 }) 352 }) 353 354 Context("with Stateful workload and with config template and with config volume mount", func() { 355 BeforeEach(func() { 356 cm := testapps.CreateCustomizedObj(&testCtx, "config/config-template.yaml", &corev1.ConfigMap{}, 357 testCtx.UseDefaultNamespace()) 358 359 cfgTpl := testapps.CreateCustomizedObj(&testCtx, "config/config-constraint.yaml", 360 &appsv1alpha1.ConfigConstraint{}) 361 362 clusterDef = testapps.NewClusterDefFactory(clusterDefName). 363 AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompDefName). 364 AddConfigTemplate(cm.Name, cm.Name, cfgTpl.Name, testCtx.DefaultNamespace, "mysql-config"). 365 AddContainerVolumeMounts("mysql", []corev1.VolumeMount{{Name: "mysql-config", MountPath: "/mnt/config"}}). 366 GetObject() 367 clusterVersion = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). 368 AddComponentVersion(mysqlCompDefName). 369 AddContainerShort("mysql", testapps.ApeCloudMySQLImage). 370 GetObject() 371 pvcSpec := testapps.NewPVCSpec("1Gi") 372 cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, 373 clusterDef.Name, clusterVersion.Name). 374 AddComponent(mysqlCompName, mysqlCompDefName). 375 AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). 376 GetObject() 377 }) 378 379 It("should add config manager sidecar container", func() { 380 reqCtx := intctrlutil.RequestCtx{ 381 Ctx: ctx, 382 Log: logger, 383 } 384 component, err := component.BuildComponent( 385 reqCtx, 386 nil, 387 cluster, 388 clusterDef, 389 &clusterDef.Spec.ComponentDefs[0], 390 &cluster.Spec.ComponentSpecs[0], 391 nil, 392 &clusterVersion.Spec.ComponentVersions[0]) 393 Expect(err).Should(Succeed()) 394 395 resources, err := buildComponentResources(reqCtx, testCtx.Cli, clusterDef, clusterVersion, cluster, component) 396 Expect(err).Should(Succeed()) 397 398 expects := []string{ 399 "PodDisruptionBudget", 400 "ConfigMap", 401 "StatefulSet", 402 } 403 Expect(resources).Should(HaveLen(len(expects))) 404 for i, v := range expects { 405 Expect(reflect.TypeOf(resources[i]).String()).Should(ContainSubstring(v), fmt.Sprintf("failed at idx %d", i)) 406 if isStatefulSet(v) { 407 sts := resources[i].(*appsv1.StatefulSet) 408 podSpec := sts.Spec.Template.Spec 409 Expect(len(podSpec.Containers) >= 3).Should(BeTrue()) 410 } 411 } 412 originPodSpec := clusterDef.Spec.ComponentDefs[0].PodSpec 413 Expect(len(originPodSpec.Containers)).Should(Equal(1)) 414 }) 415 }) 416 417 // for test GetContainerWithVolumeMount 418 Context("with Consensus workload and with external service", func() { 419 var ( 420 clusterDef *appsv1alpha1.ClusterDefinition 421 clusterVersion *appsv1alpha1.ClusterVersion 422 cluster *appsv1alpha1.Cluster 423 ) 424 425 BeforeEach(func() { 426 clusterDef = testapps.NewClusterDefFactory(clusterDefName). 427 AddComponentDef(testapps.ConsensusMySQLComponent, mysqlCompDefName). 428 AddComponentDef(testapps.StatelessNginxComponent, nginxCompDefName). 429 GetObject() 430 clusterVersion = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). 431 AddComponentVersion(mysqlCompDefName). 432 AddContainerShort("mysql", testapps.ApeCloudMySQLImage). 433 AddComponentVersion(nginxCompDefName). 434 AddContainerShort("nginx", testapps.NginxImage). 435 GetObject() 436 pvcSpec := testapps.NewPVCSpec("1Gi") 437 cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, 438 clusterDef.Name, clusterVersion.Name). 439 AddComponent(mysqlCompName, mysqlCompDefName). 440 AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). 441 GetObject() 442 }) 443 444 It("should construct env, headless service, statefuset and external service objects", func() { 445 reqCtx := intctrlutil.RequestCtx{ 446 Ctx: ctx, 447 Log: logger, 448 } 449 component, err := component.BuildComponent( 450 reqCtx, 451 nil, 452 cluster, 453 clusterDef, 454 &clusterDef.Spec.ComponentDefs[0], 455 &cluster.Spec.ComponentSpecs[0], 456 nil, 457 &clusterVersion.Spec.ComponentVersions[0]) 458 Expect(err).Should(Succeed()) 459 resources, err := buildComponentResources(reqCtx, testCtx.Cli, clusterDef, clusterVersion, cluster, component) 460 Expect(err).Should(Succeed()) 461 expects := []string{ 462 "PodDisruptionBudget", 463 "ConfigMap", 464 "StatefulSet", 465 } 466 Expect(resources).Should(HaveLen(len(expects))) 467 for i, v := range expects { 468 Expect(reflect.TypeOf(resources[i]).String()).Should(ContainSubstring(v), fmt.Sprintf("failed at idx %d", i)) 469 } 470 }) 471 }) 472 473 // for test GetContainerWithVolumeMount 474 Context("with Replications workload without pvc", func() { 475 var ( 476 clusterDef *appsv1alpha1.ClusterDefinition 477 clusterVersion *appsv1alpha1.ClusterVersion 478 cluster *appsv1alpha1.Cluster 479 ) 480 481 BeforeEach(func() { 482 clusterDef = testapps.NewClusterDefFactory(clusterDefName). 483 AddComponentDef(testapps.ReplicationRedisComponent, redisCompDefName). 484 AddComponentDef(testapps.StatelessNginxComponent, nginxCompDefName). 485 GetObject() 486 clusterVersion = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). 487 AddComponentVersion(redisCompDefName). 488 AddContainerShort("redis", testapps.DefaultRedisImageName). 489 AddComponentVersion(nginxCompDefName). 490 AddContainerShort("nginx", testapps.NginxImage). 491 GetObject() 492 cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, 493 clusterDef.Name, clusterVersion.Name). 494 AddComponent(redisCompName, redisCompDefName). 495 SetReplicas(2). 496 GetObject() 497 }) 498 499 It("should construct env, statefuset object", func() { 500 reqCtx := intctrlutil.RequestCtx{ 501 Ctx: ctx, 502 Log: logger, 503 } 504 component, err := component.BuildComponent( 505 reqCtx, 506 nil, 507 cluster, 508 clusterDef, 509 &clusterDef.Spec.ComponentDefs[0], 510 &cluster.Spec.ComponentSpecs[0], 511 nil, 512 &clusterVersion.Spec.ComponentVersions[0]) 513 Expect(err).Should(Succeed()) 514 515 resources, err := buildComponentResources(reqCtx, testCtx.Cli, clusterDef, clusterVersion, cluster, component) 516 Expect(err).Should(Succeed()) 517 518 Expect(resources).Should(HaveLen(3)) 519 Expect(reflect.TypeOf(resources[0]).String()).Should(ContainSubstring("PodDisruptionBudget")) 520 Expect(reflect.TypeOf(resources[1]).String()).Should(ContainSubstring("ConfigMap")) 521 Expect(reflect.TypeOf(resources[2]).String()).Should(ContainSubstring("StatefulSet")) 522 }) 523 }) 524 525 // TODO: (free6om) 526 // uncomment following test case until pre-provisoned PVC work begin 527 // // for test GetContainerWithVolumeMount 528 // Context("with Replications workload with pvc", func() { 529 // var ( 530 // clusterDef *appsv1alpha1.ClusterDefinition 531 // clusterVersion *appsv1alpha1.ClusterVersion 532 // cluster *appsv1alpha1.Cluster 533 // ) 534 // 535 // BeforeEach(func() { 536 // clusterDef = testapps.NewClusterDefFactory(clusterDefName). 537 // AddComponentDef(testapps.ReplicationRedisComponent, redisCompDefName). 538 // AddComponentDef(testapps.StatelessNginxComponent, nginxCompDefName). 539 // GetObject() 540 // clusterVersion = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). 541 // AddComponentVersion(redisCompDefName). 542 // AddContainerShort("redis", testapps.DefaultRedisImageName). 543 // AddComponentVersion(nginxCompDefName). 544 // AddContainerShort("nginx", testapps.NginxImage). 545 // GetObject() 546 // pvcSpec := testapps.NewPVCSpec("1Gi") 547 // cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, 548 // clusterDef.Name, clusterVersion.Name). 549 // AddComponentVersion(redisCompName, redisCompDefName). 550 // SetReplicas(2). 551 // AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). 552 // GetObject() 553 // }) 554 // 555 // It("should construct pvc objects for each replica", func() { 556 // reqCtx := intctrlutil.RequestCtx{ 557 // Ctx: ctx, 558 // Log: logger, 559 // } 560 // component := component.BuildComponent( 561 // reqCtx, 562 // *cluster, 563 // *clusterDef, 564 // clusterDef.Spec.ComponentDefs[0], 565 // cluster.Spec.ComponentSpecs[0], 566 // &clusterVersion.Spec.ComponentVersions[0]) 567 // task := types.InitReconcileTask(clusterDef, clusterVersion, cluster, component) 568 // Expect(PrepareComponentResources(reqCtx, testCtx.Cli, task)).Should(Succeed()) 569 // 570 // resources := *task.Resources 571 // Expect(resources).Should(HaveLen(6)) 572 // Expect(reflect.TypeOf(resources[0]).String()).Should(ContainSubstring("ConfigMap")) 573 // Expect(reflect.TypeOf(resources[1]).String()).Should(ContainSubstring("Service")) 574 // Expect(reflect.TypeOf(resources[2]).String()).Should(ContainSubstring("PersistentVolumeClaim")) 575 // Expect(reflect.TypeOf(resources[3]).String()).Should(ContainSubstring("PersistentVolumeClaim")) 576 // Expect(reflect.TypeOf(resources[4]).String()).Should(ContainSubstring("StatefulSet")) 577 // Expect(reflect.TypeOf(resources[5]).String()).Should(ContainSubstring("Service")) 578 // }) 579 // }) 580 581 })