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(), &copySecret)).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(), &copyConfigMap)).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(), &copySecret)).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(), &copyConfigMap)).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(), &copySecret)).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(), &copyConfigMap)).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  }