github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/report/report_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 report 21 22 import ( 23 "bytes" 24 "context" 25 "encoding/json" 26 "net/http" 27 "time" 28 29 . "github.com/onsi/ginkgo/v2" 30 . "github.com/onsi/gomega" 31 32 appsv1 "k8s.io/api/apps/v1" 33 corev1 "k8s.io/api/core/v1" 34 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 35 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 36 "k8s.io/apimachinery/pkg/runtime" 37 "k8s.io/apimachinery/pkg/runtime/schema" 38 "k8s.io/cli-runtime/pkg/genericiooptions" 39 "k8s.io/cli-runtime/pkg/printers" 40 "k8s.io/cli-runtime/pkg/resource" 41 "k8s.io/client-go/kubernetes" 42 clientfake "k8s.io/client-go/rest/fake" 43 cmdtesting "k8s.io/kubectl/pkg/cmd/testing" 44 45 kbclischeme "github.com/1aal/kubeblocks/pkg/cli/scheme" 46 "github.com/1aal/kubeblocks/pkg/cli/testing" 47 "github.com/1aal/kubeblocks/pkg/cli/types" 48 kbclientset "github.com/1aal/kubeblocks/pkg/client/clientset/versioned/fake" 49 "github.com/1aal/kubeblocks/pkg/constant" 50 ) 51 52 var _ = Describe("report", func() { 53 var ( 54 namespace = "test" 55 streams genericiooptions.IOStreams 56 tf *cmdtesting.TestFactory 57 ) 58 59 const ( 60 jsonFormat = "json" 61 yamlFormat = "yaml" 62 ) 63 64 buildClusterResourceSelectorMap := func(clusterName string) map[string]string { 65 // app.kubernetes.io/instance: <clusterName> 66 // app.kubernetes.io/managed-by: kubeblocks 67 return map[string]string{ 68 constant.AppInstanceLabelKey: clusterName, 69 constant.AppManagedByLabelKey: constant.AppName, 70 } 71 } 72 73 BeforeEach(func() { 74 tf = cmdtesting.NewTestFactory().WithNamespace(namespace) 75 tf.Client = &clientfake.RESTClient{} 76 tf.FakeDynamicClient = testing.FakeDynamicClient() 77 streams = genericiooptions.NewTestIOStreamsDiscard() 78 }) 79 80 AfterEach(func() { 81 tf.Cleanup() 82 }) 83 84 Context("report options", func() { 85 It("check outputformat", func() { 86 o := newReportOptions(streams) 87 Expect(o.validate()).Should(HaveOccurred()) 88 o.outputFormat = jsonFormat 89 Expect(o.validate()).ShouldNot(HaveOccurred()) 90 o.outputFormat = yamlFormat 91 Expect(o.validate()).ShouldNot(HaveOccurred()) 92 o.outputFormat = "text" 93 Expect(o.validate()).Should(HaveOccurred()) 94 }) 95 96 It("check toLogOptions", func() { 97 o := newReportOptions(streams) 98 o.outputFormat = jsonFormat 99 logOptions, err := o.toLogOptions() 100 Expect(err).ShouldNot(HaveOccurred()) 101 Expect(logOptions).ShouldNot(BeNil()) 102 Expect(logOptions.SinceTime).Should(BeNil()) 103 Expect(logOptions.SinceSeconds).Should(BeNil()) 104 }) 105 106 It("check toLogOptions with since-time", func() { 107 o := newReportOptions(streams) 108 o.outputFormat = jsonFormat 109 o.sinceDuration = time.Hour 110 logOptions, err := o.toLogOptions() 111 Expect(err).ShouldNot(HaveOccurred()) 112 Expect(logOptions).ShouldNot(BeNil()) 113 Expect(logOptions.SinceTime).Should(BeNil()) 114 Expect(logOptions.SinceSeconds).ShouldNot(BeNil()) 115 sinceSeconds := logOptions.SinceSeconds 116 Expect(*sinceSeconds).Should(Equal(int64(3600))) 117 }) 118 119 It("check toLogOptions with since", func() { 120 o := newReportOptions(streams) 121 o.outputFormat = jsonFormat 122 o.sinceTime = "2023-05-23T00:00:00Z" 123 logOptions, err := o.toLogOptions() 124 Expect(err).ShouldNot(HaveOccurred()) 125 Expect(logOptions).ShouldNot(BeNil()) 126 Expect(logOptions.SinceTime).ShouldNot(BeNil()) 127 Expect(logOptions.SinceSeconds).Should(BeNil()) 128 129 sicneTime := logOptions.SinceTime 130 Expect(sicneTime.Format(time.RFC3339)).Should(Equal("2023-05-23T00:00:00Z")) 131 }) 132 133 It("validate report options", func() { 134 var err error 135 o := newReportOptions(streams) 136 o.outputFormat = jsonFormat 137 Expect(o.validate()).ShouldNot(HaveOccurred()) 138 139 // set since-time 140 o.sinceTime = "2023-05-23T00:00:00Z" 141 // disable log 142 o.withLogs = false 143 Expect(o.validate()).Should(HaveOccurred()) 144 145 // enable log 146 o.withLogs = true 147 Expect(o.validate()).Should(Succeed()) 148 149 // set since 150 o.sinceDuration = time.Hour 151 err = o.validate() 152 Expect(err).Should(HaveOccurred()) 153 Expect(err.Error()).Should(Equal("only one of --since-time / --since may be used")) 154 155 o.withLogs = false 156 o.sinceDuration = 0 157 o.allContainers = true 158 err = o.validate() 159 Expect(err).Should(HaveOccurred()) 160 Expect(err.Error()).Should(MatchRegexp("can only be used when --with-logs is set")) 161 162 o.withLogs = true 163 err = o.validate() 164 Expect(err).Should(Succeed()) 165 }) 166 }) 167 168 Context("parse printer", func() { 169 const charName = "JohnSnow" 170 // check default printer 171 secret := &corev1.Secret{ 172 TypeMeta: metav1.TypeMeta{ 173 Kind: "Secret", 174 APIVersion: "v1", 175 }, 176 ObjectMeta: metav1.ObjectMeta{ 177 Name: "test-secret", 178 }, 179 Data: map[string][]byte{ 180 "test": []byte(charName), 181 }, 182 } 183 configMap := &corev1.ConfigMap{ 184 TypeMeta: metav1.TypeMeta{ 185 Kind: "ConfigMap", 186 APIVersion: "v1", 187 }, 188 ObjectMeta: metav1.ObjectMeta{ 189 Name: "test-configmap", 190 }, 191 Data: map[string]string{ 192 "test": charName, 193 }, 194 } 195 196 It("use default printer", func() { 197 var err error 198 var strBuffer bytes.Buffer 199 200 o := newReportOptions(streams) 201 o.outputFormat = jsonFormat 202 Expect(o.validate()).ShouldNot(HaveOccurred()) 203 print, err := o.parsePrinter() 204 Expect(err).ShouldNot(HaveOccurred()) 205 Expect(print).ShouldNot(BeNil()) 206 207 // test default printer, print secret 208 err = print.PrintObj(secret, &strBuffer) 209 Expect(err).Should(Succeed()) 210 copySecret := &corev1.Secret{} 211 Expect(json.Unmarshal(strBuffer.Bytes(), ©Secret)).Should(Succeed()) 212 for _, v := range copySecret.Data { 213 Expect(v).Should(Equal([]byte(charName))) 214 } 215 // test default printer, print config map 216 strBuffer.Reset() 217 err = print.PrintObj(configMap, &strBuffer) 218 Expect(err).Should(Succeed()) 219 copyConfigMap := &corev1.ConfigMap{} 220 Expect(json.Unmarshal(strBuffer.Bytes(), ©ConfigMap)).Should(Succeed()) 221 for _, v := range copyConfigMap.Data { 222 Expect(v).Should(Equal(charName)) 223 } 224 }) 225 226 It("use mask printer", func() { 227 var err error 228 var strBuffer bytes.Buffer 229 230 o := newReportOptions(streams) 231 o.outputFormat = jsonFormat 232 o.mask = true 233 234 Expect(o.validate()).ShouldNot(HaveOccurred()) 235 print, err := o.parsePrinter() 236 Expect(err).ShouldNot(HaveOccurred()) 237 Expect(print).ShouldNot(BeNil()) 238 239 // test default printer, print secret 240 err = print.PrintObj(secret, &strBuffer) 241 Expect(err).Should(Succeed()) 242 copySecret := &corev1.Secret{} 243 Expect(json.Unmarshal(strBuffer.Bytes(), ©Secret)).Should(Succeed()) 244 for _, v := range copySecret.Data { 245 Expect(v).Should(Equal([]byte(EncryptedData))) 246 } 247 // test default printer, print config map 248 strBuffer.Reset() 249 err = print.PrintObj(configMap, &strBuffer) 250 Expect(err).Should(Succeed()) 251 copyConfigMap := &corev1.ConfigMap{} 252 Expect(json.Unmarshal(strBuffer.Bytes(), ©ConfigMap)).Should(Succeed()) 253 for _, v := range copyConfigMap.Data { 254 Expect(v).Should(Equal(EncryptedData)) 255 } 256 }) 257 258 It("use mask printer for unstructured data", func() { 259 var err error 260 var strBuffer bytes.Buffer 261 262 o := newReportOptions(streams) 263 o.outputFormat = jsonFormat 264 o.mask = true 265 266 Expect(o.validate()).ShouldNot(HaveOccurred()) 267 print, err := o.parsePrinter() 268 Expect(err).ShouldNot(HaveOccurred()) 269 Expect(print).ShouldNot(BeNil()) 270 271 unstructuredSecret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(secret) 272 Expect(err).ShouldNot(HaveOccurred()) 273 unstructuredConfigMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(configMap) 274 Expect(err).ShouldNot(HaveOccurred()) 275 276 unstructuredSecretObj := &unstructured.Unstructured{Object: unstructuredSecret} 277 unstructuredConfigMapObj := &unstructured.Unstructured{Object: unstructuredConfigMap} 278 279 // test default printer, print secret 280 strBuffer.Reset() 281 err = print.PrintObj(unstructuredSecretObj, &strBuffer) 282 Expect(err).Should(Succeed()) 283 copySecret := &corev1.Secret{} 284 Expect(json.Unmarshal(strBuffer.Bytes(), ©Secret)).Should(Succeed()) 285 for _, v := range copySecret.Data { 286 Expect((string)(v)).Should(Equal(EncryptedData)) 287 } 288 // test default printer, print config map 289 strBuffer.Reset() 290 err = print.PrintObj(unstructuredConfigMapObj, &strBuffer) 291 Expect(err).Should(Succeed()) 292 copyConfigMap := &corev1.ConfigMap{} 293 Expect(json.Unmarshal(strBuffer.Bytes(), ©ConfigMap)).Should(Succeed()) 294 for _, v := range copyConfigMap.Data { 295 Expect(v).Should(Equal(EncryptedData)) 296 } 297 }) 298 }) 299 300 Context("report-kubeblocks options", func() { 301 const ( 302 namespace = "test" 303 ) 304 305 BeforeEach(func() { 306 tf = cmdtesting.NewTestFactory().WithNamespace(namespace) 307 codec := kbclischeme.Codecs.LegacyCodec(kbclischeme.Scheme.PrioritizedVersionsAllGroups()...) 308 309 // mock a secret for helm chart 310 secrets := testing.FakeSecretsWithLabels(namespace, map[string]string{ 311 "name": types.KubeBlocksChartName, 312 "owner": "helm", 313 }) 314 315 deploy := testing.FakeKBDeploy("0.5.23") 316 deploy.Namespace = namespace 317 deploymentList := &appsv1.DeploymentList{} 318 deploymentList.Items = []appsv1.Deployment{*deploy} 319 // mock events 320 events := &corev1.EventList{} 321 pods := testing.FakePods(1, namespace, deploy.Name) 322 podEvent := testing.FakeEventForObject("test-event-pod", namespace, pods.Items[0].Name) 323 events.Items = append(events.Items, *podEvent) 324 325 httpResp := func(obj runtime.Object) *http.Response { 326 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj)} 327 } 328 329 tf.UnstructuredClient = &clientfake.RESTClient{ 330 GroupVersion: schema.GroupVersion{Group: types.AppsAPIGroup, Version: types.AppsAPIVersion}, 331 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, 332 Client: clientfake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { 333 urlPrefix := "/api/v1/namespaces/" + namespace 334 return map[string]*http.Response{ 335 urlPrefix + "/deployments": httpResp(deploymentList), 336 urlPrefix + "/events": httpResp(events), 337 urlPrefix + "/pods": httpResp(pods), 338 "/api/v1/secrets": httpResp(secrets), 339 }[req.URL.Path], nil 340 }), 341 } 342 343 tf.Client = tf.UnstructuredClient 344 tf.FakeDynamicClient = testing.FakeDynamicClient(deploy, podEvent, &secrets.Items[0]) 345 streams = genericiooptions.NewTestIOStreamsDiscard() 346 }) 347 348 It("complete kb-report options", func() { 349 o := reportKubeblocksOptions{reportOptions: newReportOptions(streams)} 350 o.outputFormat = jsonFormat 351 Expect(o.complete(tf)).To(Succeed()) 352 Expect(o.genericClientSet).ShouldNot(BeNil()) 353 Expect(o.namespace).Should(Equal(namespace)) 354 Expect(o.file).Should(MatchRegexp("report-kubeblocks-.*.zip")) 355 }) 356 357 It("complete kb-report manifest", func() { 358 o := reportKubeblocksOptions{reportOptions: newReportOptions(streams)} 359 o.outputFormat = jsonFormat 360 Expect(o.complete(tf)).To(Succeed()) 361 362 By("use fake zip writter to test handleManifests") 363 o.reportWritter = &fakeZipWritter{} 364 ctx := context.Background() 365 Expect(o.handleManifests(ctx)).To(Succeed()) 366 Expect(o.handleEvents(ctx)).To(Succeed()) 367 Expect(o.handleLogs(ctx)).To(Succeed()) 368 }) 369 }) 370 371 Context("report-cluster options", func() { 372 const ( 373 namespace = "test" 374 clusterName = "test-cluster" 375 deployName = "test-deploy" 376 statefulSetName = "test-statefulset" 377 ) 378 var ( 379 kbfakeclient *kbclientset.Clientset 380 ) 381 BeforeEach(func() { 382 clusterLabels := buildClusterResourceSelectorMap(clusterName) 383 384 tf = cmdtesting.NewTestFactory().WithNamespace(namespace) 385 codec := kbclischeme.Codecs.LegacyCodec(kbclischeme.Scheme.PrioritizedVersionsAllGroups()...) 386 387 cluster := testing.FakeCluster(clusterName, namespace) 388 clusterDef := testing.FakeClusterDef() 389 clusterVersion := testing.FakeClusterVersion() 390 391 deploy := testing.FakeDeploy(deployName, namespace, clusterLabels) 392 deploymentList := &appsv1.DeploymentList{} 393 deploymentList.Items = []appsv1.Deployment{*deploy} 394 395 // mock events 396 events := &corev1.EventList{} 397 event := testing.FakeEventForObject("test-event-cluster", namespace, clusterName) 398 events.Items = []corev1.Event{*event} 399 400 pods := testing.FakePods(1, namespace, clusterName) 401 event = testing.FakeEventForObject("test-event-pod", namespace, pods.Items[0].Name) 402 events.Items = append(events.Items, *event) 403 404 sts := testing.FakeStatefulSet(statefulSetName, namespace, clusterLabels) 405 statefulSetList := &appsv1.StatefulSetList{} 406 statefulSetList.Items = []appsv1.StatefulSet{*sts} 407 408 httpResp := func(obj runtime.Object) *http.Response { 409 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj)} 410 } 411 412 tf.UnstructuredClient = &clientfake.RESTClient{ 413 GroupVersion: schema.GroupVersion{Group: types.AppsAPIGroup, Version: types.AppsAPIVersion}, 414 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, 415 Client: clientfake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { 416 urlPrefix := "/api/v1/namespaces/" + namespace 417 return map[string]*http.Response{ 418 urlPrefix + "/deployments": httpResp(deploymentList), 419 urlPrefix + "/statefulsets": httpResp(statefulSetList), 420 urlPrefix + "/events": httpResp(events), 421 urlPrefix + "/pods": httpResp(pods), 422 }[req.URL.Path], nil 423 }), 424 } 425 426 tf.Client = tf.UnstructuredClient 427 tf.FakeDynamicClient = testing.FakeDynamicClient(deploy, sts, event) 428 kbfakeclient = testing.FakeKBClientSet(cluster, clusterDef, clusterVersion) 429 streams = genericiooptions.NewTestIOStreamsDiscard() 430 }) 431 432 It("validate cluster-report options", func() { 433 o := reportClusterOptions{reportOptions: newReportOptions(streams)} 434 o.outputFormat = jsonFormat 435 args := make([]string, 0) 436 Expect(o.validate(args)).Should(HaveOccurred()) 437 args = append(args, "mycluster") 438 Expect(o.validate(args)).Should(Succeed()) 439 args = append(args, "mycluster2") 440 Expect(o.validate(args)).Should(HaveOccurred()) 441 }) 442 443 It("complete cluster-report options", func() { 444 o := reportClusterOptions{reportOptions: newReportOptions(streams)} 445 o.outputFormat = jsonFormat 446 args := make([]string, 0) 447 args = append(args, "mycluster") 448 Expect(o.validate(args)).Should(Succeed()) 449 Expect(o.complete(tf)).To(Succeed()) 450 Expect(o.genericClientSet).ShouldNot(BeNil()) 451 Expect(len(o.namespace)).ShouldNot(Equal(0)) 452 Expect(o.file).Should(MatchRegexp("report-cluster-.*.zip")) 453 }) 454 455 It("handle cluster-report manifests", func() { 456 o := reportClusterOptions{reportOptions: newReportOptions(streams)} 457 o.outputFormat = jsonFormat 458 args := make([]string, 0) 459 args = append(args, clusterName) 460 Expect(o.validate(args)).Should(Succeed()) 461 Expect(o.complete(tf)).To(Succeed()) 462 o.genericClientSet.kbClientSet = kbfakeclient 463 464 By("use fake zip writter to test handleManifests") 465 o.reportWritter = &fakeZipWritter{} 466 ctx := context.Background() 467 Expect(o.handleManifests(ctx)).To(Succeed()) 468 Expect(o.handleEvents(ctx)).To(Succeed()) 469 Expect(o.handleLogs(ctx)).To(Succeed()) 470 }) 471 }) 472 }) 473 474 type fakeZipWritter struct { 475 printer printers.ResourcePrinter 476 } 477 478 var _ reportWritter = &fakeZipWritter{} 479 480 func (w *fakeZipWritter) Init(file string, printer printers.ResourcePrinterFunc) error { 481 w.printer = printer 482 return nil 483 } 484 485 func (w *fakeZipWritter) Close() error { 486 return nil 487 } 488 func (w *fakeZipWritter) WriteKubeBlocksVersion(fileName string, client kubernetes.Interface) error { 489 return nil 490 } 491 func (w *fakeZipWritter) WriteObjects(folderName string, objects []*unstructured.UnstructuredList, format string) error { 492 return nil 493 } 494 func (w *fakeZipWritter) WriteSingleObject(prefix string, kind string, name string, object runtime.Object, format string) error { 495 return nil 496 } 497 func (w *fakeZipWritter) WriteEvents(folderName string, events map[string][]corev1.Event, format string) error { 498 return nil 499 } 500 501 func (w *fakeZipWritter) WriteLogs(folderName string, ctx context.Context, client kubernetes.Interface, 502 pods *corev1.PodList, logOptions corev1.PodLogOptions, 503 allContainers bool) error { 504 return nil 505 }