github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/cluster_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 cluster 21 22 import ( 23 "fmt" 24 "net/http" 25 26 . "github.com/onsi/ginkgo/v2" 27 . "github.com/onsi/gomega" 28 29 "k8s.io/apimachinery/pkg/runtime/schema" 30 "k8s.io/cli-runtime/pkg/genericiooptions" 31 "k8s.io/cli-runtime/pkg/resource" 32 "k8s.io/client-go/kubernetes/scheme" 33 clientfake "k8s.io/client-go/rest/fake" 34 cmdtesting "k8s.io/kubectl/pkg/cmd/testing" 35 36 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 37 "github.com/1aal/kubeblocks/pkg/cli/create" 38 kbclidelete "github.com/1aal/kubeblocks/pkg/cli/delete" 39 "github.com/1aal/kubeblocks/pkg/cli/testing" 40 "github.com/1aal/kubeblocks/pkg/cli/types" 41 testapps "github.com/1aal/kubeblocks/pkg/testutil/apps" 42 ) 43 44 var _ = Describe("Cluster", func() { 45 const ( 46 testComponentPath = "../../testing/testdata/component.yaml" 47 testComponentWithClassPath = "../../testing/testdata/component_with_class_1c1g.yaml" 48 testComponentWithInvalidClassPath = "../../testing/testdata/component_with_invalid_class.yaml" 49 testComponentWithResourcePath = "../../testing/testdata/component_with_resource_1c1g.yaml" 50 testComponentWithInvalidResourcePath = "../../testing/testdata/component_with_invalid_resource.yaml" 51 testClusterPath = "../../testing/testdata/cluster.yaml" 52 ) 53 54 const ( 55 clusterName = "test" 56 namespace = "default" 57 ) 58 var streams genericiooptions.IOStreams 59 var tf *cmdtesting.TestFactory 60 // test if DEFAULT_STORAGE_CLASS is not set in config.yaml 61 fakeNilConfigData := map[string]string{ 62 "config.yaml": `# the default storage class name. 63 #DEFAULT_STORAGE_CLASS: ""`, 64 } 65 fakeConfigData := map[string]string{ 66 "config.yaml": `# the default storage class name. 67 DEFAULT_STORAGE_CLASS: ""`, 68 } 69 fakeConfigDataWithDefaultSC := map[string]string{ 70 "config.yaml": `# the default storage class name. 71 DEFAULT_STORAGE_CLASS: kb-default-sc`, 72 } 73 BeforeEach(func() { 74 streams, _, _, _ = genericiooptions.NewTestIOStreams() 75 tf = cmdtesting.NewTestFactory().WithNamespace(namespace) 76 cd := testing.FakeClusterDef() 77 fakeDefaultStorageClass := testing.FakeStorageClass(testing.StorageClassName, testing.IsDefault) 78 tf.FakeDynamicClient = testing.FakeDynamicClient(cd, fakeDefaultStorageClass, testing.FakeClusterVersion(), testing.FakeConfigMap("kubeblocks-manager-config", types.DefaultNamespace, fakeConfigData), testing.FakeSecret(types.DefaultNamespace, clusterName)) 79 tf.Client = &clientfake.RESTClient{} 80 }) 81 82 AfterEach(func() { 83 tf.Cleanup() 84 }) 85 86 Context("create", func() { 87 It("without name", func() { 88 o := &CreateOptions{ 89 ClusterDefRef: testing.ClusterDefName, 90 ClusterVersionRef: testing.ClusterVersionName, 91 SetFile: testComponentPath, 92 UpdatableFlags: UpdatableFlags{ 93 TerminationPolicy: "Delete", 94 }, 95 CreateOptions: create.CreateOptions{ 96 Factory: tf, 97 Dynamic: tf.FakeDynamicClient, 98 IOStreams: streams, 99 }, 100 } 101 o.Options = o 102 Expect(o.Complete()).To(Succeed()) 103 Expect(o.Validate()).To(Succeed()) 104 Expect(o.Name).ShouldNot(BeEmpty()) 105 Expect(o.Run()).Should(HaveOccurred()) 106 }) 107 }) 108 109 Context("run", func() { 110 var o *CreateOptions 111 112 BeforeEach(func() { 113 clusterDef := testing.FakeClusterDef() 114 resourceConstraint := testapps.NewComponentResourceConstraintFactory(testapps.DefaultResourceConstraintName). 115 AddConstraints(testapps.ProductionResourceConstraint). 116 AddSelector(appsv1alpha1.ClusterResourceConstraintSelector{ 117 ClusterDefRef: clusterDef.Name, 118 Components: []appsv1alpha1.ComponentResourceConstraintSelector{ 119 { 120 ComponentDefRef: testing.ComponentDefName, 121 Rules: []string{"c1"}, 122 }, 123 }, 124 }). 125 GetObject() 126 127 tf.FakeDynamicClient = testing.FakeDynamicClient( 128 clusterDef, 129 testing.FakeStorageClass(testing.StorageClassName, testing.IsDefault), 130 testing.FakeClusterVersion(), 131 testing.FakeComponentClassDef(fmt.Sprintf("custom-%s", testing.ComponentDefName), clusterDef.Name, testing.ComponentDefName), 132 testing.FakeComponentClassDef("custom-mysql", clusterDef.Name, "mysql"), 133 testing.FakeConfigMap("kubeblocks-manager-config", types.DefaultNamespace, fakeConfigData), 134 testing.FakeSecret(types.DefaultNamespace, clusterName), 135 resourceConstraint, 136 ) 137 o = &CreateOptions{ 138 CreateOptions: create.CreateOptions{ 139 IOStreams: streams, 140 Name: clusterName, 141 Dynamic: tf.FakeDynamicClient, 142 CueTemplateName: CueTemplateName, 143 Factory: tf, 144 GVR: types.ClusterGVR(), 145 }, 146 SetFile: "", 147 ClusterDefRef: testing.ClusterDefName, 148 ClusterVersionRef: testing.ClusterVersionName, 149 UpdatableFlags: UpdatableFlags{ 150 PodAntiAffinity: "Preferred", 151 TopologyKeys: []string{"kubernetes.io/hostname"}, 152 NodeLabels: map[string]string{"testLabelKey": "testLabelValue"}, 153 TolerationsRaw: []string{"engineType=mongo:NoSchedule"}, 154 Tenancy: string(appsv1alpha1.SharedNode), 155 }, 156 } 157 o.TerminationPolicy = "WipeOut" 158 }) 159 160 Run := func() { 161 o.CreateOptions.Options = o 162 o.Args = []string{clusterName} 163 Expect(o.CreateOptions.Complete()).Should(Succeed()) 164 Expect(o.Namespace).To(Equal(namespace)) 165 Expect(o.Name).To(Equal(clusterName)) 166 Expect(o.Run()).Should(Succeed()) 167 } 168 169 It("validate tolerations", func() { 170 Expect(len(o.TolerationsRaw)).Should(Equal(1)) 171 Expect(o.Complete()).Should(Succeed()) 172 Expect(len(o.Tolerations)).Should(Equal(1)) 173 }) 174 175 It("validate termination policy should be set", func() { 176 o.TerminationPolicy = "" 177 Expect(o.Validate()).Should(HaveOccurred()) 178 }) 179 180 It("should succeed if component with valid class", func() { 181 o.Values = []string{fmt.Sprintf("type=%s,class=%s", testing.ComponentDefName, testapps.Class1c1gName)} 182 Expect(o.Complete()).Should(Succeed()) 183 Expect(o.Validate()).Should(Succeed()) 184 Run() 185 }) 186 187 It("should fail if component with invalid class", func() { 188 o.Values = []string{fmt.Sprintf("type=%s,class=class-not-exists", testing.ComponentDefName)} 189 Expect(o.Complete()).Should(HaveOccurred()) 190 }) 191 192 It("should succeed if component with resource meets the resource constraint", func() { 193 o.Values = []string{fmt.Sprintf("type=%s,cpu=1,memory=1Gi", testing.ComponentDefName)} 194 Expect(o.Complete()).Should(Succeed()) 195 Expect(o.Validate()).Should(Succeed()) 196 Run() 197 }) 198 199 It("should succeed if component with resource with smaller unit meets the constraint", func() { 200 o.Values = []string{fmt.Sprintf("type=%s,cpu=1000m,memory=1024Mi", testing.ComponentDefName)} 201 Expect(o.Complete()).Should(Succeed()) 202 Expect(o.Validate()).Should(Succeed()) 203 Run() 204 }) 205 206 It("should fail if component with resource not meets the constraint", func() { 207 o.Values = []string{fmt.Sprintf("type=%s,cpu=1,memory=100Gi", testing.ComponentDefName)} 208 Expect(o.Complete()).Should(HaveOccurred()) 209 }) 210 211 It("should succeed if component with cpu meets the constraint", func() { 212 o.Values = []string{fmt.Sprintf("type=%s,cpu=1", testing.ComponentDefName)} 213 Expect(o.Complete()).Should(Succeed()) 214 Expect(o.Validate()).Should(Succeed()) 215 Run() 216 }) 217 218 It("should fail if component with cpu not meets the constraint", func() { 219 o.Values = []string{fmt.Sprintf("type=%s,cpu=1024", testing.ComponentDefName)} 220 Expect(o.Complete()).Should(HaveOccurred()) 221 }) 222 223 It("should fail if component with memory not meets the constraint", func() { 224 o.Values = []string{fmt.Sprintf("type=%s,memory=1Ti", testing.ComponentDefName)} 225 Expect(o.Complete()).Should(HaveOccurred()) 226 }) 227 228 It("should succeed if component doesn't have class definition", func() { 229 o.Values = []string{fmt.Sprintf("type=%s,cpu=3,memory=7Gi", testing.ExtraComponentDefName)} 230 Expect(o.Complete()).Should(Succeed()) 231 Expect(o.Validate()).Should(Succeed()) 232 Run() 233 }) 234 235 It("should fail if component with storage not meets the constraint", func() { 236 o.Values = []string{fmt.Sprintf("type=%s,storage=500Mi", testing.ComponentDefName)} 237 Expect(o.Complete()).Should(HaveOccurred()) 238 239 o.Values = []string{fmt.Sprintf("type=%s,storage=1Pi", testing.ComponentDefName)} 240 Expect(o.Complete()).Should(HaveOccurred()) 241 }) 242 243 It("should fail if create cluster by non-existed file", func() { 244 o.SetFile = "test.yaml" 245 Expect(o.Complete()).Should(HaveOccurred()) 246 }) 247 248 It("should succeed if create cluster by empty file", func() { 249 o.SetFile = "" 250 Expect(o.Complete()).Should(Succeed()) 251 Expect(o.Validate()).Should(Succeed()) 252 Run() 253 }) 254 255 It("should succeed if create cluster by file without class and resource", func() { 256 o.SetFile = testComponentPath 257 Expect(o.Complete()).Should(Succeed()) 258 Expect(o.Validate()).Should(Succeed()) 259 Run() 260 }) 261 262 It("should succeed if create cluster by file with class", func() { 263 o.SetFile = testComponentWithClassPath 264 Expect(o.Complete()).Should(Succeed()) 265 Expect(o.Validate()).Should(Succeed()) 266 Run() 267 }) 268 269 It("should succeed if create cluster by file with resource", func() { 270 o.SetFile = testComponentWithResourcePath 271 Expect(o.Complete()).Should(Succeed()) 272 Expect(o.Validate()).Should(Succeed()) 273 Run() 274 }) 275 276 It("should fail if create cluster by file with non-existed class", func() { 277 o.SetFile = testComponentWithInvalidClassPath 278 Expect(o.Complete()).Should(HaveOccurred()) 279 }) 280 281 It("should succeed if create cluster with a complete config file", func() { 282 o.SetFile = testClusterPath 283 Expect(o.Complete()).Should(Succeed()) 284 Expect(o.Validate()).Should(Succeed()) 285 }) 286 }) 287 288 Context("create validate", func() { 289 var o *CreateOptions 290 BeforeEach(func() { 291 o = &CreateOptions{ 292 ClusterDefRef: testing.ClusterDefName, 293 ClusterVersionRef: testing.ClusterVersionName, 294 SetFile: testComponentPath, 295 UpdatableFlags: UpdatableFlags{ 296 TerminationPolicy: "Delete", 297 }, 298 CreateOptions: create.CreateOptions{ 299 Factory: tf, 300 Namespace: namespace, 301 Name: "mycluster", 302 Dynamic: tf.FakeDynamicClient, 303 IOStreams: streams, 304 }, 305 ComponentSpecs: make([]map[string]interface{}, 1), 306 } 307 o.ComponentSpecs[0] = make(map[string]interface{}) 308 o.ComponentSpecs[0]["volumeClaimTemplates"] = make([]interface{}, 1) 309 vct := o.ComponentSpecs[0]["volumeClaimTemplates"].([]interface{}) 310 vct[0] = make(map[string]interface{}) 311 vct[0].(map[string]interface{})["spec"] = make(map[string]interface{}) 312 spec := vct[0].(map[string]interface{})["spec"] 313 spec.(map[string]interface{})["storageClassName"] = testing.StorageClassName 314 }) 315 316 It("can validate whether the ClusterDefRef is null when create a new cluster ", func() { 317 Expect(o.ClusterDefRef).ShouldNot(BeEmpty()) 318 Expect(o.Validate()).Should(Succeed()) 319 o.ClusterDefRef = "" 320 Expect(o.Validate()).Should(HaveOccurred()) 321 }) 322 323 It("can validate whether the TerminationPolicy is null when create a new cluster ", func() { 324 Expect(o.TerminationPolicy).ShouldNot(BeEmpty()) 325 Expect(o.Validate()).Should(Succeed()) 326 o.TerminationPolicy = "" 327 Expect(o.Validate()).Should(HaveOccurred()) 328 }) 329 330 It("can validate whether the ClusterVersionRef is null and can't get latest version from client when create a new cluster ", func() { 331 Expect(o.ClusterVersionRef).ShouldNot(BeEmpty()) 332 Expect(o.Validate()).Should(Succeed()) 333 o.ClusterVersionRef = "" 334 Expect(o.Validate()).Should(Succeed()) 335 }) 336 337 It("can validate whether --set and --set-file both are specified when create a new cluster ", func() { 338 Expect(o.SetFile).ShouldNot(BeEmpty()) 339 Expect(o.Values).Should(BeNil()) 340 Expect(o.Validate()).Should(Succeed()) 341 o.Values = []string{"notEmpty"} 342 Expect(o.Validate()).Should(HaveOccurred()) 343 }) 344 345 It("can validate the cluster name must begin with a letter and can only contain lowercase letters, numbers, and '-'.", func() { 346 type fn func() 347 var succeed = func(name string) fn { 348 return func() { 349 o.Name = name 350 Expect(o.Validate()).Should(Succeed()) 351 } 352 } 353 var failed = func(name string) fn { 354 return func() { 355 o.Name = name 356 Expect(o.Validate()).Should(HaveOccurred()) 357 } 358 } 359 // more case to add 360 invalidCase := []string{ 361 "1abcd", "abcd-", "-abcd", "abc#d", "ABCD", "*&(&%", 362 } 363 364 validCase := []string{ 365 "abcd", "abcd1", "a1-2b-3d", 366 } 367 368 for i := range invalidCase { 369 failed(invalidCase[i]) 370 } 371 372 for i := range validCase { 373 succeed(validCase[i]) 374 } 375 376 }) 377 378 It("can validate whether the name is not longer than 16 characters when create a new cluster", func() { 379 Expect(len(o.Name)).Should(BeNumerically("<=", 16)) 380 Expect(o.Validate()).Should(Succeed()) 381 moreThan16 := 17 382 bytes := make([]byte, 0) 383 var clusterNameMoreThan16 string 384 for i := 0; i < moreThan16; i++ { 385 bytes = append(bytes, byte(i%26+'a')) 386 } 387 clusterNameMoreThan16 = string(bytes) 388 Expect(len(clusterNameMoreThan16)).Should(BeNumerically(">", 16)) 389 o.Name = clusterNameMoreThan16 390 Expect(o.Validate()).Should(HaveOccurred()) 391 }) 392 393 Context("validate storageClass", func() { 394 395 It("can get all StorageClasses in K8S and check out if the cluster have a default StorageClasses by GetStorageClasses()", func() { 396 storageClasses, existedDefault, err := getStorageClasses(o.Dynamic) 397 Expect(err).Should(Succeed()) 398 Expect(storageClasses).Should(HaveKey(testing.StorageClassName)) 399 Expect(existedDefault).Should(BeTrue()) 400 fakeNotDefaultStorageClass := testing.FakeStorageClass(testing.StorageClassName, testing.IsNotDefault) 401 tf.FakeDynamicClient = testing.FakeDynamicClient(testing.FakeClusterDef(), fakeNotDefaultStorageClass, testing.FakeClusterVersion(), testing.FakeConfigMap("kubeblocks-manager-config", types.DefaultNamespace, fakeConfigData), testing.FakeSecret(types.DefaultNamespace, clusterName)) 402 storageClasses, existedDefault, err = getStorageClasses(tf.FakeDynamicClient) 403 Expect(err).Should(Succeed()) 404 Expect(storageClasses).Should(HaveKey(testing.StorageClassName)) 405 Expect(existedDefault).ShouldNot(BeTrue()) 406 }) 407 408 It("can specify the StorageClass and the StorageClass must exist", func() { 409 Expect(validateStorageClass(o.Dynamic, o.ComponentSpecs)).Should(Succeed()) 410 fakeNotDefaultStorageClass := testing.FakeStorageClass(testing.StorageClassName+"-other", testing.IsNotDefault) 411 FakeDynamicClientWithNotDefaultSC := testing.FakeDynamicClient(testing.FakeClusterDef(), fakeNotDefaultStorageClass, testing.FakeClusterVersion(), testing.FakeConfigMap("kubeblocks-manager-config", types.DefaultNamespace, fakeConfigData), testing.FakeSecret(types.DefaultNamespace, clusterName)) 412 Expect(validateStorageClass(FakeDynamicClientWithNotDefaultSC, o.ComponentSpecs)).Should(HaveOccurred()) 413 }) 414 415 It("can get valiate the default StorageClasses", func() { 416 vct := o.ComponentSpecs[0]["volumeClaimTemplates"].([]interface{}) 417 spec := vct[0].(map[string]interface{})["spec"] 418 delete(spec.(map[string]interface{}), "storageClassName") 419 Expect(validateStorageClass(o.Dynamic, o.ComponentSpecs)).Should(Succeed()) 420 FakeDynamicClientWithNotDefaultSC := testing.FakeDynamicClient(testing.FakeClusterDef(), testing.FakeStorageClass(testing.StorageClassName+"-other", testing.IsNotDefault), testing.FakeClusterVersion(), testing.FakeConfigMap("kubeblocks-manager-config", types.DefaultNamespace, fakeConfigData), testing.FakeSecret(types.DefaultNamespace, clusterName)) 421 Expect(validateStorageClass(FakeDynamicClientWithNotDefaultSC, o.ComponentSpecs)).Should(HaveOccurred()) 422 // It can validate 'DEFAULT_STORAGE_CLASS' in ConfigMap for cloud K8S 423 FakeDynamicClientWithConfigDefaultSC := testing.FakeDynamicClient(testing.FakeClusterDef(), testing.FakeStorageClass(testing.StorageClassName+"-other", testing.IsNotDefault), testing.FakeClusterVersion(), testing.FakeConfigMap("kubeblocks-manager-config", types.DefaultNamespace, fakeConfigDataWithDefaultSC), testing.FakeSecret(types.DefaultNamespace, clusterName)) 424 Expect(validateStorageClass(FakeDynamicClientWithConfigDefaultSC, o.ComponentSpecs)).Should(Succeed()) 425 }) 426 427 It("validateDefaultSCInConfig test", func() { 428 have, err := validateDefaultSCInConfig(testing.FakeDynamicClient(testing.FakeConfigMap("kubeblocks-manager-config", types.DefaultNamespace, fakeConfigData), testing.FakeSecret(types.DefaultNamespace, clusterName))) 429 Expect(err).Should(Succeed()) 430 Expect(have).Should(BeFalse()) 431 have, err = validateDefaultSCInConfig(testing.FakeDynamicClient(testing.FakeConfigMap("kubeblocks-manager-config", types.DefaultNamespace, fakeConfigDataWithDefaultSC), testing.FakeSecret(types.DefaultNamespace, clusterName))) 432 Expect(err).Should(Succeed()) 433 Expect(have).Should(BeTrue()) 434 have, err = validateDefaultSCInConfig(testing.FakeDynamicClient(testing.FakeConfigMap("kubeblocks-manager-config", types.DefaultNamespace, fakeNilConfigData), testing.FakeSecret(types.DefaultNamespace, clusterName))) 435 Expect(err).Should(Succeed()) 436 Expect(have).Should(BeFalse()) 437 have, err = validateDefaultSCInConfig(testing.FakeDynamicClient(testing.FakeConfigMap("kubeblocks-manager-config", types.DefaultNamespace, nil), testing.FakeSecret(types.DefaultNamespace, clusterName))) 438 Expect(err).Should(Succeed()) 439 Expect(have).Should(BeFalse()) 440 have, err = validateDefaultSCInConfig(testing.FakeDynamicClient(testing.FakeConfigMap("kubeblocks-manager-config", types.DefaultNamespace, map[string]string{"not-config-yaml": "error situation"}), testing.FakeSecret(types.DefaultNamespace, clusterName))) 441 Expect(err).Should(Succeed()) 442 Expect(have).Should(BeFalse()) 443 444 }) 445 }) 446 447 }) 448 449 Context("delete cluster", func() { 450 var o *kbclidelete.DeleteOptions 451 452 BeforeEach(func() { 453 tf = testing.NewTestFactory(namespace) 454 455 _ = appsv1alpha1.AddToScheme(scheme.Scheme) 456 codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) 457 clusters := testing.FakeClusterList() 458 459 tf.UnstructuredClient = &clientfake.RESTClient{ 460 GroupVersion: schema.GroupVersion{Group: types.AppsAPIGroup, Version: types.AppsAPIVersion}, 461 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, 462 Client: clientfake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { 463 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &clusters.Items[0])}, nil 464 }), 465 } 466 467 tf.Client = tf.UnstructuredClient 468 o = &kbclidelete.DeleteOptions{ 469 Factory: tf, 470 IOStreams: streams, 471 GVR: types.ClusterGVR(), 472 AutoApprove: true, 473 } 474 }) 475 476 It("validata delete cluster by name", func() { 477 Expect(deleteCluster(o, []string{})).Should(HaveOccurred()) 478 Expect(deleteCluster(o, []string{clusterName})).Should(Succeed()) 479 o.LabelSelector = fmt.Sprintf("clusterdefinition.kubeblocks.io/name=%s", testing.ClusterDefName) 480 // todo: there is an issue with rendering the name of the "info" element, and efforts are being made to resolve it. 481 // Expect(deleteCluster(o, []string{})).Should(Succeed()) 482 Expect(deleteCluster(o, []string{clusterName})).Should(HaveOccurred()) 483 }) 484 485 }) 486 It("delete", func() { 487 cmd := NewDeleteCmd(tf, streams) 488 Expect(cmd).ShouldNot(BeNil()) 489 }) 490 491 It("cluster", func() { 492 cmd := NewClusterCmd(tf, streams) 493 Expect(cmd).ShouldNot(BeNil()) 494 Expect(cmd.HasSubCommands()).To(BeTrue()) 495 }) 496 497 It("connect", func() { 498 cmd := NewConnectCmd(tf, streams) 499 Expect(cmd).ShouldNot(BeNil()) 500 }) 501 502 It("list-logs-type", func() { 503 cmd := NewListLogsCmd(tf, streams) 504 Expect(cmd).ShouldNot(BeNil()) 505 }) 506 507 It("logs", func() { 508 cmd := NewLogsCmd(tf, streams) 509 Expect(cmd).ShouldNot(BeNil()) 510 }) 511 })