github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/describe.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  	"context"
    24  	"fmt"
    25  	"io"
    26  	"strings"
    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/runtime"
    32  	"k8s.io/apimachinery/pkg/runtime/schema"
    33  	"k8s.io/cli-runtime/pkg/genericiooptions"
    34  	"k8s.io/client-go/dynamic"
    35  	clientset "k8s.io/client-go/kubernetes"
    36  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    37  	"k8s.io/kubectl/pkg/util/templates"
    38  
    39  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    40  	dpv1alpha1 "github.com/1aal/kubeblocks/apis/dataprotection/v1alpha1"
    41  	"github.com/1aal/kubeblocks/pkg/cli/cluster"
    42  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    43  	"github.com/1aal/kubeblocks/pkg/cli/types"
    44  	"github.com/1aal/kubeblocks/pkg/cli/util"
    45  	"github.com/1aal/kubeblocks/pkg/dataprotection/utils/boolptr"
    46  )
    47  
    48  var (
    49  	describeExample = templates.Examples(`
    50  		# describe a specified cluster
    51  		kbcli cluster describe mycluster`)
    52  
    53  	newTbl = func(out io.Writer, title string, header ...interface{}) *printer.TablePrinter {
    54  		fmt.Fprintln(out, title)
    55  		tbl := printer.NewTablePrinter(out)
    56  		tbl.SetHeader(header...)
    57  		return tbl
    58  	}
    59  )
    60  
    61  type describeOptions struct {
    62  	factory   cmdutil.Factory
    63  	client    clientset.Interface
    64  	dynamic   dynamic.Interface
    65  	namespace string
    66  
    67  	// resource type and names
    68  	gvr   schema.GroupVersionResource
    69  	names []string
    70  
    71  	*cluster.ClusterObjects
    72  	genericiooptions.IOStreams
    73  }
    74  
    75  func newOptions(f cmdutil.Factory, streams genericiooptions.IOStreams) *describeOptions {
    76  	return &describeOptions{
    77  		factory:   f,
    78  		IOStreams: streams,
    79  		gvr:       types.ClusterGVR(),
    80  	}
    81  }
    82  
    83  func NewDescribeCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
    84  	o := newOptions(f, streams)
    85  	cmd := &cobra.Command{
    86  		Use:               "describe NAME",
    87  		Short:             "Show details of a specific cluster.",
    88  		Example:           describeExample,
    89  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()),
    90  		Run: func(cmd *cobra.Command, args []string) {
    91  			util.CheckErr(o.complete(args))
    92  			util.CheckErr(o.run())
    93  		},
    94  	}
    95  	return cmd
    96  }
    97  
    98  func (o *describeOptions) complete(args []string) error {
    99  	var err error
   100  
   101  	if len(args) == 0 {
   102  		return fmt.Errorf("cluster name should be specified")
   103  	}
   104  	o.names = args
   105  
   106  	if o.client, err = o.factory.KubernetesClientSet(); err != nil {
   107  		return err
   108  	}
   109  
   110  	if o.dynamic, err = o.factory.DynamicClient(); err != nil {
   111  		return err
   112  	}
   113  
   114  	if o.namespace, _, err = o.factory.ToRawKubeConfigLoader().Namespace(); err != nil {
   115  		return err
   116  	}
   117  	return nil
   118  }
   119  
   120  func (o *describeOptions) run() error {
   121  	for _, name := range o.names {
   122  		if err := o.describeCluster(name); err != nil {
   123  			return err
   124  		}
   125  	}
   126  	return nil
   127  }
   128  
   129  func (o *describeOptions) describeCluster(name string) error {
   130  	clusterGetter := cluster.ObjectsGetter{
   131  		Client:    o.client,
   132  		Dynamic:   o.dynamic,
   133  		Name:      name,
   134  		Namespace: o.namespace,
   135  		GetOptions: cluster.GetOptions{
   136  			WithClusterDef:     true,
   137  			WithService:        true,
   138  			WithPod:            true,
   139  			WithPVC:            true,
   140  			WithDataProtection: true,
   141  		},
   142  	}
   143  
   144  	var err error
   145  	if o.ClusterObjects, err = clusterGetter.Get(); err != nil {
   146  		return err
   147  	}
   148  
   149  	// cluster summary
   150  	showCluster(o.Cluster, o.Out)
   151  
   152  	// show endpoints
   153  	showEndpoints(o.Cluster, o.Services, o.Out)
   154  
   155  	// topology
   156  	showTopology(o.ClusterObjects.GetInstanceInfo(), o.Out)
   157  
   158  	comps := o.ClusterObjects.GetComponentInfo()
   159  	// resources
   160  	showResource(comps, o.Out)
   161  
   162  	// images
   163  	showImages(comps, o.Out)
   164  
   165  	// data protection info
   166  	defaultBackupRepo, err := o.getDefaultBackupRepo()
   167  	if err != nil {
   168  		return err
   169  	}
   170  	showDataProtection(o.BackupPolicies, o.BackupSchedules, defaultBackupRepo, o.Out)
   171  
   172  	// events
   173  	showEvents(o.Cluster.Name, o.Cluster.Namespace, o.Out)
   174  	fmt.Fprintln(o.Out)
   175  
   176  	return nil
   177  }
   178  
   179  func (o *describeOptions) getDefaultBackupRepo() (string, error) {
   180  	backupRepoListObj, err := o.dynamic.Resource(types.BackupRepoGVR()).List(context.TODO(), metav1.ListOptions{})
   181  	if err != nil {
   182  		return printer.NoneString, err
   183  	}
   184  	for _, item := range backupRepoListObj.Items {
   185  		repo := dpv1alpha1.BackupRepo{}
   186  		if err := runtime.DefaultUnstructuredConverter.FromUnstructured(item.Object, &repo); err != nil {
   187  			return printer.NoneString, err
   188  		}
   189  		if repo.Status.IsDefault {
   190  			return repo.Name, nil
   191  		}
   192  	}
   193  	return printer.NoneString, nil
   194  }
   195  
   196  func showCluster(c *appsv1alpha1.Cluster, out io.Writer) {
   197  	if c == nil {
   198  		return
   199  	}
   200  	title := fmt.Sprintf("Name: %s\t Created Time: %s", c.Name, util.TimeFormat(&c.CreationTimestamp))
   201  	tbl := newTbl(out, title, "NAMESPACE", "CLUSTER-DEFINITION", "VERSION", "STATUS", "TERMINATION-POLICY")
   202  	tbl.AddRow(c.Namespace, c.Spec.ClusterDefRef, c.Spec.ClusterVersionRef, string(c.Status.Phase), string(c.Spec.TerminationPolicy))
   203  	tbl.Print()
   204  }
   205  
   206  func showTopology(instances []*cluster.InstanceInfo, out io.Writer) {
   207  	tbl := newTbl(out, "\nTopology:", "COMPONENT", "INSTANCE", "ROLE", "STATUS", "AZ", "NODE", "CREATED-TIME")
   208  	for _, ins := range instances {
   209  		tbl.AddRow(ins.Component, ins.Name, ins.Role, ins.Status, ins.AZ, ins.Node, ins.CreatedTime)
   210  	}
   211  	tbl.Print()
   212  }
   213  
   214  func showResource(comps []*cluster.ComponentInfo, out io.Writer) {
   215  	tbl := newTbl(out, "\nResources Allocation:", "COMPONENT", "DEDICATED", "CPU(REQUEST/LIMIT)", "MEMORY(REQUEST/LIMIT)", "STORAGE-SIZE", "STORAGE-CLASS")
   216  	for _, c := range comps {
   217  		tbl.AddRow(c.Name, "false", c.CPU, c.Memory, cluster.BuildStorageSize(c.Storage), cluster.BuildStorageClass(c.Storage))
   218  	}
   219  	tbl.Print()
   220  }
   221  
   222  func showImages(comps []*cluster.ComponentInfo, out io.Writer) {
   223  	tbl := newTbl(out, "\nImages:", "COMPONENT", "TYPE", "IMAGE")
   224  	for _, c := range comps {
   225  		tbl.AddRow(c.Name, c.Type, c.Image)
   226  	}
   227  	tbl.Print()
   228  }
   229  
   230  func showEvents(name string, namespace string, out io.Writer) {
   231  	// hint user how to get events
   232  	fmt.Fprintf(out, "\nShow cluster events: kbcli cluster list-events -n %s %s", namespace, name)
   233  }
   234  
   235  func showEndpoints(c *appsv1alpha1.Cluster, svcList *corev1.ServiceList, out io.Writer) {
   236  	if c == nil {
   237  		return
   238  	}
   239  
   240  	tbl := newTbl(out, "\nEndpoints:", "COMPONENT", "MODE", "INTERNAL", "EXTERNAL")
   241  	for _, comp := range c.Spec.ComponentSpecs {
   242  		internalEndpoints, externalEndpoints := cluster.GetComponentEndpoints(svcList, &comp)
   243  		if len(internalEndpoints) == 0 && len(externalEndpoints) == 0 {
   244  			continue
   245  		}
   246  		tbl.AddRow(comp.Name, "ReadWrite", util.CheckEmpty(strings.Join(internalEndpoints, "\n")),
   247  			util.CheckEmpty(strings.Join(externalEndpoints, "\n")))
   248  	}
   249  	tbl.Print()
   250  }
   251  
   252  func showDataProtection(backupPolicies []dpv1alpha1.BackupPolicy, backupSchedules []dpv1alpha1.BackupSchedule, defaultBackupRepo string, out io.Writer) {
   253  	if len(backupPolicies) == 0 || len(backupSchedules) == 0 {
   254  		return
   255  	}
   256  	tbl := newTbl(out, "\nData Protection:", "BACKUP-REPO", "AUTO-BACKUP", "BACKUP-SCHEDULE", "BACKUP-METHOD", "BACKUP-RETENTION")
   257  	for _, schedule := range backupSchedules {
   258  		backupRepo := defaultBackupRepo
   259  		for _, policy := range backupPolicies {
   260  			if policy.Name != schedule.Spec.BackupPolicyName {
   261  				continue
   262  			}
   263  			if policy.Spec.BackupRepoName != nil {
   264  				backupRepo = *policy.Spec.BackupRepoName
   265  			}
   266  		}
   267  		for _, schedulePolicy := range schedule.Spec.Schedules {
   268  			if !boolptr.IsSetToTrue(schedulePolicy.Enabled) {
   269  				continue
   270  			}
   271  
   272  			tbl.AddRow(backupRepo, "Enabled", schedulePolicy.CronExpression, schedulePolicy.BackupMethod, schedulePolicy.RetentionPeriod.String())
   273  		}
   274  	}
   275  	tbl.Print()
   276  }
   277  
   278  //	 getBackupRecoverableTime returns the recoverable time range string
   279  //	func getBackupRecoverableTime(backups []dpv1alpha1.Backup) string {
   280  //	recoverabelTime := dpv1alpha1.GetRecoverableTimeRange(backups)
   281  //	var result string
   282  //	for _, i := range recoverabelTime {
   283  //		result = addTimeRange(result, i.StartTime, i.StopTime)
   284  //	}
   285  //	if result == "" {
   286  //		return printer.NoneString
   287  //	}
   288  //	return result
   289  //	}
   290  
   291  //	func addTimeRange(result string, start, end *metav1.Time) string {
   292  //		if result != "" {
   293  //			result += ", "
   294  //		}
   295  //		result += fmt.Sprintf("%s ~ %s", util.TimeFormatWithDuration(start, time.Second),
   296  //			util.TimeFormatWithDuration(end, time.Second))
   297  //		return result
   298  //	}