github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/report/report.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 "context" 24 "fmt" 25 "strings" 26 "time" 27 28 "github.com/spf13/cobra" 29 corev1 "k8s.io/api/core/v1" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 32 "k8s.io/apimachinery/pkg/runtime/schema" 33 utilerrors "k8s.io/apimachinery/pkg/util/errors" 34 "k8s.io/cli-runtime/pkg/genericclioptions" 35 "k8s.io/cli-runtime/pkg/genericiooptions" 36 "k8s.io/cli-runtime/pkg/printers" 37 "k8s.io/klog/v2" 38 cmdutil "k8s.io/kubectl/pkg/cmd/util" 39 "k8s.io/kubectl/pkg/util" 40 "k8s.io/kubectl/pkg/util/i18n" 41 "k8s.io/kubectl/pkg/util/templates" 42 "k8s.io/utils/strings/slices" 43 44 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 45 clischeme "github.com/1aal/kubeblocks/pkg/cli/scheme" 46 "github.com/1aal/kubeblocks/pkg/cli/spinner" 47 "github.com/1aal/kubeblocks/pkg/cli/types" 48 cliutil "github.com/1aal/kubeblocks/pkg/cli/util" 49 "github.com/1aal/kubeblocks/pkg/constant" 50 ) 51 52 const ( 53 versionFile = "version.txt" 54 manifestsFolder = "manifests" 55 eventsFolder = "events" 56 logsFolder = "logs" 57 58 kubeBlocksReport = "kubeblocks" 59 clusterReport = "cluster" 60 ) 61 62 var ( 63 reportClusterExamples = templates.Examples(` 64 # report KubeBlocks status 65 kbcli report cluster mycluster 66 67 # report KubeBlocks cluster information to file 68 kbcli report cluster mycluster -f filename 69 70 # report KubeBlocks cluster information with logs 71 kbcli report cluster mycluster --with-logs 72 73 # report KubeBlocks cluster information with logs and mask sensitive info 74 kbcli report cluster mycluster --with-logs --mask 75 76 # report KubeBlocks cluster information with logs since 1 hour ago 77 kbcli report cluster mycluster --with-logs --since 1h 78 79 # report KubeBlocks cluster information with logs since given time 80 kbcli report cluster mycluster --with-logs --since-time 2023-05-23T00:00:00Z 81 82 # report KubeBlocks cluster information with logs for all containers 83 kbcli report cluster mycluster --with-logs --all-containers 84 `) 85 86 reportKBExamples = templates.Examples(` 87 # report KubeBlocks status 88 kbcli report kubeblocks 89 90 # report KubeBlocks information to file 91 kbcli report kubeblocks -f filename 92 93 # report KubeBlocks information with logs 94 kbcli report kubeblocks --with-logs 95 96 # report KubeBlocks information with logs and mask sensitive info 97 kbcli report kubeblocks --with-logs --mask 98 `) 99 ) 100 101 var _ reportInterface = &reportKubeblocksOptions{} 102 var _ reportInterface = &reportClusterOptions{} 103 104 // NewReportCmd creates command for reports. 105 func NewReportCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 106 cmd := &cobra.Command{ 107 Use: "report [kubeblocks | cluster]", 108 Short: "report kubeblocks or cluster info.", 109 } 110 cmd.AddCommand( 111 newKubeblocksReportCmd(f, streams), 112 newClusterReportCmd(f, streams), 113 ) 114 return cmd 115 } 116 117 type reportInterface interface { 118 handleEvents(ctx context.Context) error 119 handleLogs(ctx context.Context) error 120 handleManifests(ctx context.Context) error 121 } 122 type reportOptions struct { 123 genericiooptions.IOStreams 124 // file name to output 125 file string 126 // namespace of resource 127 namespace string 128 // include withLogs or not 129 withLogs bool 130 // followings flags are for logs 131 // all containers or main container only 132 allContainers bool 133 // since time 134 sinceTime string 135 // since second 136 sinceDuration time.Duration 137 // log options 138 logOptions *corev1.PodLogOptions 139 // various clients 140 genericClientSet *genericClientSet 141 // enableMask sensitive info or not 142 mask bool 143 // resource printer, default to YAML printer without managed fields 144 resourcePrinter printers.ResourcePrinterFunc 145 // JSONYamlPrintFlags is used to print JSON or YAML 146 JSONYamlPrintFlags *genericclioptions.JSONYamlPrintFlags 147 // outpout format , default to YAML 148 outputFormat string 149 // reportWritter is used to write report to file 150 reportWritter reportWritter 151 } 152 153 type reportKubeblocksOptions struct { 154 reportOptions 155 kubeBlocksSelector metav1.ListOptions 156 } 157 158 type reportClusterOptions struct { 159 reportOptions 160 clusterName string 161 clusterSelector metav1.ListOptions 162 cluster *appsv1alpha1.Cluster 163 } 164 165 func newReportOptions(f genericiooptions.IOStreams) reportOptions { 166 return reportOptions{ 167 IOStreams: f, 168 JSONYamlPrintFlags: genericclioptions.NewJSONYamlPrintFlags(), 169 } 170 } 171 172 func (o *reportOptions) complete(f cmdutil.Factory) error { 173 var err error 174 175 if o.genericClientSet, err = NewGenericClientSet(f); err != nil { 176 return err 177 } 178 179 // complete log options 180 if o.logOptions, err = o.toLogOptions(); err != nil { 181 return err 182 } 183 184 // complete printer 185 if o.resourcePrinter, err = o.parsePrinter(); err != nil { 186 return err 187 } 188 189 o.reportWritter = &reportZipWritter{} 190 return nil 191 } 192 193 func (o *reportOptions) validate() error { 194 // make sure sinceTime and sinceSeconds are not both set 195 if len(o.sinceTime) > 0 && o.sinceDuration != 0 { 196 return fmt.Errorf("only one of --since-time / --since may be used") 197 } 198 if (!o.withLogs) && (len(o.sinceTime) > 0 || o.sinceDuration != 0 || o.allContainers) { 199 return fmt.Errorf("--since-time / --since / --all-contaiiners can only be used when --with-logs is set") 200 } 201 o.outputFormat = strings.ToLower(o.outputFormat) 202 if slices.Index(o.JSONYamlPrintFlags.AllowedFormats(), o.outputFormat) == -1 { 203 return fmt.Errorf("output format %s is not supported", o.outputFormat) 204 } 205 return nil 206 } 207 208 func (o *reportOptions) addFlags(cmd *cobra.Command) { 209 cmd.Flags().StringVarP(&o.file, "file", "f", "", "zip file for output") 210 cmd.Flags().BoolVar(&o.mask, "mask", true, "mask sensitive info for secrets and configmaps") 211 cmd.Flags().BoolVar(&o.withLogs, "with-logs", false, "include pod logs") 212 cmd.Flags().BoolVar(&o.allContainers, "all-containers", o.allContainers, "Get all containers' logs in the pod(s). Byt default, only the main container (the first container) will have logs recorded.") 213 cmd.Flags().StringVar(&o.sinceTime, "since-time", o.sinceTime, i18n.T("Only return logs after a specific date (RFC3339). Defaults to all logs. Only one of since-time / since may be used.")) 214 cmd.Flags().DurationVar(&o.sinceDuration, "since", o.sinceDuration, "Only return logs newer than a relative duration like 5s, 2m, or 3h. Defaults to all logs. Only one of since-time / since may be used.") 215 216 cmd.Flags().StringVarP(&o.outputFormat, "output", "o", "json", fmt.Sprintf("Output format. One of: %s.", strings.Join(o.JSONYamlPrintFlags.AllowedFormats(), "|"))) 217 cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc("output", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 218 return o.JSONYamlPrintFlags.AllowedFormats(), cobra.ShellCompDirectiveNoFileComp 219 })) 220 } 221 222 func (o *reportOptions) toLogOptions() (*corev1.PodLogOptions, error) { 223 logOptions := &corev1.PodLogOptions{} 224 225 if len(o.sinceTime) > 0 { 226 t, err := util.ParseRFC3339(o.sinceTime, metav1.Now) 227 if err != nil { 228 return nil, err 229 } 230 logOptions.SinceTime = &t 231 } 232 233 if o.sinceDuration != 0 { 234 sec := int64(o.sinceDuration.Round(time.Second).Seconds()) 235 logOptions.SinceSeconds = &sec 236 } 237 238 return logOptions, nil 239 } 240 241 func (o *reportOptions) parsePrinter() (printers.ResourcePrinterFunc, error) { 242 var err error 243 // by default, use YAML printer without managed fields 244 printer, err := o.JSONYamlPrintFlags.ToPrinter(o.outputFormat) 245 if err != nil { 246 return nil, err 247 } 248 // wrap printer with typesetter 249 typeSetterPrinter := printers.NewTypeSetter(clischeme.Scheme) 250 if printer, err = typeSetterPrinter.WrapToPrinter(printer, nil); err != nil { 251 return nil, err 252 } 253 // if mask is enabled, wrap printer with mask printer 254 if o.mask { 255 printer = &MaskPrinter{Delegate: printer} 256 } 257 return printer.PrintObj, nil 258 } 259 260 func newKubeblocksReportCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 261 o := &reportKubeblocksOptions{reportOptions: newReportOptions(streams)} 262 cmd := &cobra.Command{ 263 Use: "kubeblocks [-f file] [--with-logs] [--mask]", 264 Aliases: []string{"kb"}, 265 Short: "Report KubeBlocks information, including deployments, events, logs, etc.", 266 Args: cobra.NoArgs, 267 Example: reportKBExamples, 268 Run: func(cmd *cobra.Command, args []string) { 269 cliutil.CheckErr(o.validate()) 270 cliutil.CheckErr(o.complete(f)) 271 cliutil.CheckErr(o.run(f, streams)) 272 }, 273 } 274 o.addFlags(cmd) 275 return cmd 276 } 277 278 func (o *reportKubeblocksOptions) complete(f cmdutil.Factory) error { 279 if err := o.reportOptions.complete(f); err != nil { 280 return err 281 } 282 o.namespace, _ = cliutil.GetKubeBlocksNamespace(o.genericClientSet.client) 283 // complete file name 284 o.file = formatReportName(o.file, kubeBlocksReport) 285 if exists, _ := cliutil.FileExists(o.file); exists { 286 return fmt.Errorf("file already exist will not overwrite") 287 } 288 // complete kb selector 289 o.kubeBlocksSelector = metav1.ListOptions{LabelSelector: buildKubeBlocksSelector()} 290 return nil 291 } 292 293 func (o *reportKubeblocksOptions) run(f cmdutil.Factory, streams genericiooptions.IOStreams) error { 294 ctx, cancel := context.WithCancel(context.Background()) 295 defer cancel() 296 297 if err := o.reportWritter.Init(o.file, o.resourcePrinter); err != nil { 298 return err 299 } 300 defer func() { 301 if err := o.reportWritter.Close(); err != nil { 302 klog.Errorf("close zip file error: %v", err) 303 } 304 }() 305 306 fmt.Fprintf(o.Out, "reporting KubeBlocks information to %s\n", o.file) 307 308 if err := o.reportWritter.WriteKubeBlocksVersion(versionFile, o.genericClientSet.client); err != nil { 309 return err 310 } 311 if err := o.handleManifests(ctx); err != nil { 312 return err 313 } 314 if err := o.handleEvents(ctx); err != nil { 315 return err 316 } 317 if err := o.handleLogs(ctx); err != nil { 318 return err 319 } 320 321 return nil 322 } 323 324 func (o *reportKubeblocksOptions) handleManifests(ctx context.Context) error { 325 var ( 326 scopedgvrs = []schema.GroupVersionResource{ 327 types.DeployGVR(), 328 types.StatefulSetGVR(), 329 types.ConfigmapGVR(), 330 types.SecretGVR(), 331 types.ServiceGVR(), 332 types.RoleGVR(), 333 types.RoleBindingGVR(), 334 } 335 336 globalGvrs = []schema.GroupVersionResource{ 337 types.AddonGVR(), 338 types.ClusterDefGVR(), 339 types.ClusterRoleGVR(), 340 types.ClusterRoleBindingGVR(), 341 } 342 ) 343 // write manifest 344 s := spinner.New(o.Out, spinnerMsg("processing manifests")) 345 defer s.Fail() 346 // get namespaced resources 347 allErrors := make([]error, 0) 348 resourceLists := make([]*unstructured.UnstructuredList, 0) 349 resourceLists = append(resourceLists, cliutil.ListResourceByGVR(ctx, o.genericClientSet.dynamic, o.namespace, scopedgvrs, []metav1.ListOptions{o.kubeBlocksSelector}, &allErrors)...) 350 // get global resources 351 resourceLists = append(resourceLists, cliutil.ListResourceByGVR(ctx, o.genericClientSet.dynamic, metav1.NamespaceAll, globalGvrs, []metav1.ListOptions{o.kubeBlocksSelector}, &allErrors)...) 352 // get all storage class 353 resourceLists = append(resourceLists, cliutil.ListResourceByGVR(ctx, o.genericClientSet.dynamic, metav1.NamespaceAll, []schema.GroupVersionResource{types.StorageClassGVR()}, []metav1.ListOptions{{}}, &allErrors)...) 354 if err := o.reportWritter.WriteObjects(manifestsFolder, resourceLists, o.outputFormat); err != nil { 355 return err 356 } 357 s.Success() 358 return utilerrors.NewAggregate(allErrors) 359 } 360 361 func (o *reportKubeblocksOptions) handleEvents(ctx context.Context) error { 362 // write events 363 s := spinner.New(o.Out, spinnerMsg("processing events")) 364 defer s.Fail() 365 366 // get all events under kubeblocks namespace 367 if events, err := o.genericClientSet.client.CoreV1().Events(o.namespace).List(ctx, metav1.ListOptions{}); err != nil { 368 return err 369 } else { 370 eventMap := map[string][]corev1.Event{o.namespace + "-kubeblocks": events.Items} 371 if err := o.reportWritter.WriteEvents(eventsFolder, eventMap, o.outputFormat); err != nil { 372 return err 373 } 374 } 375 s.Success() 376 return nil 377 } 378 379 func (o *reportKubeblocksOptions) handleLogs(ctx context.Context) error { 380 if !o.withLogs { 381 return nil 382 } 383 s := spinner.New(o.Out, spinnerMsg("process pod logs")) 384 defer s.Fail() 385 // write logs 386 podList, err := o.genericClientSet.client.CoreV1().Pods(o.namespace).List(ctx, o.kubeBlocksSelector) 387 if err != nil { 388 return err 389 } 390 391 if err := o.reportWritter.WriteLogs(logsFolder, ctx, o.genericClientSet.client, podList, *o.logOptions, o.allContainers); err != nil { 392 return err 393 } 394 s.Success() 395 return nil 396 } 397 398 func newClusterReportCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 399 o := &reportClusterOptions{reportOptions: newReportOptions(streams)} 400 401 cmd := &cobra.Command{ 402 Use: "cluster NAME [-f file] [-with-logs] [-mask]", 403 Short: "Report Cluster information", 404 Example: reportClusterExamples, 405 ValidArgsFunction: cliutil.ResourceNameCompletionFunc(f, types.ClusterGVR()), 406 Run: func(cmd *cobra.Command, args []string) { 407 cliutil.CheckErr(o.validate(args)) 408 cliutil.CheckErr(o.complete(f)) 409 cliutil.CheckErr(o.run(f, streams)) 410 }, 411 } 412 o.addFlags(cmd) 413 return cmd 414 } 415 416 func (o *reportClusterOptions) validate(args []string) error { 417 if err := o.reportOptions.validate(); err != nil { 418 return err 419 } 420 if len(args) != 1 { 421 return fmt.Errorf("only ONE cluster name is allowed") 422 } 423 o.clusterName = args[0] 424 return nil 425 } 426 427 func (o *reportClusterOptions) complete(f cmdutil.Factory) error { 428 var err error 429 if err := o.reportOptions.complete(f); err != nil { 430 return err 431 } 432 // update namespace 433 o.namespace, _, err = f.ToRawKubeConfigLoader().Namespace() 434 if err != nil { 435 return err 436 } 437 // complete file name 438 439 o.file = formatReportName(o.file, fmt.Sprintf("%s-%s", clusterReport, o.clusterName)) 440 441 if exists, _ := cliutil.FileExists(o.file); exists { 442 return fmt.Errorf("file already exist will not overwrite") 443 } 444 445 o.clusterSelector = metav1.ListOptions{LabelSelector: buildClusterResourceSelector(o.clusterName)} 446 return nil 447 } 448 449 func (o *reportClusterOptions) run(f cmdutil.Factory, streams genericiooptions.IOStreams) error { 450 ctx, cancel := context.WithCancel(context.Background()) 451 defer cancel() 452 var err error 453 // make cluster exists before processing 454 if _, err = o.genericClientSet.kbClientSet.AppsV1alpha1().Clusters(o.namespace).Get(ctx, o.clusterName, metav1.GetOptions{}); err != nil { 455 return err 456 } 457 458 if err := o.reportWritter.Init(o.file, o.resourcePrinter); err != nil { 459 return err 460 } 461 defer func() { 462 if err := o.reportWritter.Close(); err != nil { 463 klog.Errorf("close zip file error: %v", err) 464 } 465 }() 466 467 fmt.Fprintf(o.Out, "reporting cluster information to %s\n", o.file) 468 469 if err := o.reportWritter.WriteKubeBlocksVersion(versionFile, o.genericClientSet.client); err != nil { 470 return err 471 } 472 if err := o.handleManifests(ctx); err != nil { 473 return err 474 } 475 if err := o.handleEvents(ctx); err != nil { 476 return err 477 } 478 if err := o.handleLogs(ctx); err != nil { 479 return err 480 } 481 return nil 482 } 483 484 func (o *reportClusterOptions) handleManifests(ctx context.Context) error { 485 var ( 486 scopedgvrs = []schema.GroupVersionResource{ 487 types.DeployGVR(), 488 types.StatefulSetGVR(), 489 types.ConfigmapGVR(), 490 types.SecretGVR(), 491 types.ServiceGVR(), 492 types.RoleGVR(), 493 types.RoleBindingGVR(), 494 types.BackupGVR(), 495 types.BackupPolicyGVR(), 496 types.BackupScheduleGVR(), 497 types.ActionSetGVR(), 498 types.RestoreGVR(), 499 types.PVCGVR(), 500 } 501 globalGvrs = []schema.GroupVersionResource{ 502 types.PVGVR(), 503 } 504 ) 505 506 var err error 507 if o.cluster, err = o.genericClientSet.kbClientSet.AppsV1alpha1().Clusters(o.namespace).Get(ctx, o.clusterName, metav1.GetOptions{}); err != nil { 508 return err 509 } 510 511 // write manifest 512 s := spinner.New(o.Out, spinnerMsg("processing manifests")) 513 defer s.Fail() 514 515 allErrors := make([]error, 0) 516 // get namespaced resources 517 resourceLists := make([]*unstructured.UnstructuredList, 0) 518 // write manifest 519 resourceLists = append(resourceLists, cliutil.ListResourceByGVR(ctx, o.genericClientSet.dynamic, o.namespace, scopedgvrs, []metav1.ListOptions{o.clusterSelector}, &allErrors)...) 520 resourceLists = append(resourceLists, cliutil.ListResourceByGVR(ctx, o.genericClientSet.dynamic, metav1.NamespaceAll, globalGvrs, []metav1.ListOptions{o.clusterSelector}, &allErrors)...) 521 if err := o.reportWritter.WriteObjects("manifests", resourceLists, o.outputFormat); err != nil { 522 return err 523 } 524 525 if err := o.reportWritter.WriteSingleObject(manifestsFolder, types.KindCluster, o.cluster.Name, o.cluster, o.outputFormat); err != nil { 526 return err 527 } 528 529 // get cluster definition 530 clusterDefName := o.cluster.Spec.ClusterDefRef 531 if clusterDef, err := o.genericClientSet.kbClientSet.AppsV1alpha1().ClusterDefinitions().Get(ctx, clusterDefName, metav1.GetOptions{}); err != nil { 532 return err 533 } else if err = o.reportWritter.WriteSingleObject(manifestsFolder, types.KindClusterDef, clusterDef.Name, clusterDef, o.outputFormat); err != nil { 534 return err 535 } 536 537 // get cluster version 538 clusterVersionName := o.cluster.Spec.ClusterVersionRef 539 if clusterVersion, err := o.genericClientSet.kbClientSet.AppsV1alpha1().ClusterVersions().Get(ctx, clusterVersionName, metav1.GetOptions{}); err != nil { 540 return err 541 } else if err = o.reportWritter.WriteSingleObject(manifestsFolder, types.KindClusterVersion, clusterVersion.Name, clusterVersion, o.outputFormat); err != nil { 542 return err 543 } 544 545 s.Success() 546 return nil 547 } 548 549 func (o *reportClusterOptions) handleEvents(ctx context.Context) error { 550 s := spinner.New(o.Out, spinnerMsg("processing events")) 551 defer s.Fail() 552 553 events := make(map[string][]corev1.Event, 0) 554 // get all events of cluster 555 clusterEvents, err := o.genericClientSet.client.CoreV1().Events(o.namespace).List(ctx, metav1.ListOptions{FieldSelector: fmt.Sprintf("involvedObject.name=%s", o.clusterName)}) 556 if err != nil { 557 return err 558 } 559 events["cluster-"+o.clusterName] = clusterEvents.Items 560 561 // get all events for pods 562 podList, err := o.genericClientSet.client.CoreV1().Pods(o.namespace).List(ctx, o.clusterSelector) 563 if err != nil { 564 return err 565 } 566 567 for _, pod := range podList.Items { 568 if podEvents, err := o.genericClientSet.client.CoreV1().Events(o.namespace).List(ctx, metav1.ListOptions{FieldSelector: fmt.Sprintf("involvedObject.name=%s", pod.Name)}); err != nil { 569 return err 570 } else { 571 events["pod-"+pod.Name] = podEvents.Items 572 } 573 } 574 575 if err = o.reportWritter.WriteEvents(eventsFolder, events, o.outputFormat); err != nil { 576 return err 577 } 578 s.Success() 579 return nil 580 } 581 582 func (o *reportClusterOptions) handleLogs(ctx context.Context) error { 583 if !o.withLogs { 584 return nil 585 } 586 587 s := spinner.New(o.Out, spinnerMsg("process pod logs")) 588 defer s.Fail() 589 590 // get all events for pods 591 if podList, err := o.genericClientSet.client.CoreV1().Pods(o.namespace).List(ctx, o.clusterSelector); err != nil { 592 return err 593 } else if err := o.reportWritter.WriteLogs(logsFolder, ctx, o.genericClientSet.client, podList, *o.logOptions, o.allContainers); err != nil { 594 return err 595 } 596 597 s.Success() 598 return nil 599 } 600 601 func spinnerMsg(format string, a ...any) spinner.Option { 602 return spinner.WithMessage(fmt.Sprintf("%-50s", fmt.Sprintf(format, a...))) 603 } 604 605 func formatReportName(fileName string, kind string) string { 606 if len(fileName) > 0 { 607 return fileName 608 } 609 return fmt.Sprintf("report-%s-%s.zip", kind, time.Now().Local().Format("2006-01-02-15-04-05")) 610 } 611 612 func buildClusterResourceSelector(clusterName string) string { 613 // app.kubernetes.io/instance: <clusterName> 614 // app.kubernetes.io/managed-by: kubeblocks 615 return fmt.Sprintf("%s=%s, %s=%s", constant.AppInstanceLabelKey, clusterName, constant.AppManagedByLabelKey, constant.AppName) 616 } 617 618 func buildKubeBlocksSelector() string { 619 // app.kubernetes.io/name: kubeblocks 620 // app.kubernetes.io/instance: kubeblocks 621 return fmt.Sprintf("%s=%s,%s=%s", 622 constant.AppInstanceLabelKey, types.KubeBlocksReleaseName, 623 constant.AppNameLabelKey, types.KubeBlocksChartName) 624 }