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 // }