github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/describe_ops.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 "fmt" 24 "reflect" 25 "sort" 26 "strings" 27 28 "github.com/spf13/cobra" 29 "golang.org/x/exp/maps" 30 corev1 "k8s.io/api/core/v1" 31 "k8s.io/apimachinery/pkg/api/resource" 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 "k8s.io/client-go/kubernetes/scheme" 37 cmdutil "k8s.io/kubectl/pkg/cmd/util" 38 "k8s.io/kubectl/pkg/util/templates" 39 40 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/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 ) 46 47 var ( 48 describeOpsExample = templates.Examples(` 49 # describe a specified OpsRequest 50 kbcli cluster describe-ops mysql-restart-82zxv`) 51 ) 52 53 type describeOpsOptions struct { 54 factory cmdutil.Factory 55 client clientset.Interface 56 dynamic dynamic.Interface 57 namespace string 58 59 // resource type and names 60 gvr schema.GroupVersionResource 61 names []string 62 63 genericiooptions.IOStreams 64 } 65 66 type opsObject interface { 67 appsv1alpha1.VerticalScaling | appsv1alpha1.HorizontalScaling | appsv1alpha1.OpsRequestVolumeClaimTemplate | appsv1alpha1.VolumeExpansion 68 } 69 70 func newDescribeOpsOptions(f cmdutil.Factory, streams genericiooptions.IOStreams) *describeOpsOptions { 71 return &describeOpsOptions{ 72 factory: f, 73 IOStreams: streams, 74 gvr: types.OpsGVR(), 75 } 76 } 77 78 func NewDescribeOpsCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 79 o := newDescribeOpsOptions(f, streams) 80 cmd := &cobra.Command{ 81 Use: "describe-ops", 82 Short: "Show details of a specific OpsRequest.", 83 Aliases: []string{"desc-ops"}, 84 Example: describeOpsExample, 85 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.OpsGVR()), 86 Run: func(cmd *cobra.Command, args []string) { 87 util.CheckErr(o.complete(args)) 88 util.CheckErr(o.run()) 89 }, 90 } 91 return cmd 92 } 93 94 // getCommandFlagsSlice returns the targetName slice by getName function and opsObject slice, their lengths are equal. 95 func getCommandFlagsSlice[T opsObject](opsSt []T, 96 convertObject func(t T) any, 97 getName func(t T) string) ([][]string, []any) { 98 // returns the index of the first occurrence of v in s,s or -1 if not present. 99 indexFromAnySlice := func(s []any, v any) int { 100 for i := range s { 101 if reflect.DeepEqual(s[i], v) { 102 return i 103 } 104 } 105 return -1 106 } 107 opsObjectSlice := make([]any, 0, len(opsSt)) 108 targetNameSlice := make([][]string, 0, len(opsSt)) 109 for _, v := range opsSt { 110 index := indexFromAnySlice(opsObjectSlice, convertObject(v)) 111 if index == -1 { 112 opsObjectSlice = append(opsObjectSlice, convertObject(v)) 113 targetNameSlice = append(targetNameSlice, []string{getName(v)}) 114 continue 115 } 116 targetNameSlice[index] = append(targetNameSlice[index], getName(v)) 117 } 118 return targetNameSlice, opsObjectSlice 119 } 120 121 func (o *describeOpsOptions) complete(args []string) error { 122 var err error 123 124 if len(args) == 0 { 125 return fmt.Errorf("OpsRequest name should be specified") 126 } 127 128 o.names = args 129 130 if o.client, err = o.factory.KubernetesClientSet(); err != nil { 131 return err 132 } 133 134 if o.dynamic, err = o.factory.DynamicClient(); err != nil { 135 return err 136 } 137 138 if o.namespace, _, err = o.factory.ToRawKubeConfigLoader().Namespace(); err != nil { 139 return err 140 } 141 return nil 142 } 143 144 func (o *describeOpsOptions) run() error { 145 for _, name := range o.names { 146 if err := o.describeOps(name); err != nil { 147 return err 148 } 149 } 150 return nil 151 } 152 153 // describeOps gets the OpsRequest by name and describes it. 154 func (o *describeOpsOptions) describeOps(name string) error { 155 opsRequest := &appsv1alpha1.OpsRequest{} 156 if err := cluster.GetK8SClientObject(o.dynamic, opsRequest, o.gvr, o.namespace, name); err != nil { 157 return err 158 } 159 return o.printOpsRequest(opsRequest) 160 } 161 162 // printOpsRequest prints the information of OpsRequest for describing command. 163 func (o *describeOpsOptions) printOpsRequest(ops *appsv1alpha1.OpsRequest) error { 164 printer.PrintLine("Spec:") 165 printer.PrintLineWithTabSeparator( 166 // first pair string 167 printer.NewPair(" Name", ops.Name), 168 printer.NewPair("NameSpace", ops.Namespace), 169 printer.NewPair("Cluster", ops.Spec.ClusterRef), 170 printer.NewPair("Type", string(ops.Spec.Type)), 171 ) 172 173 o.printOpsCommand(ops) 174 175 // print the last configuration of the cluster. 176 o.printLastConfiguration(ops.Status.LastConfiguration, ops.Spec.Type) 177 178 // print the OpsRequest.status 179 o.printOpsRequestStatus(&ops.Status) 180 181 // print the OpsRequest.status.conditions 182 printer.PrintConditions(ops.Status.Conditions, o.Out) 183 184 // get all events about cluster 185 events, err := o.client.CoreV1().Events(o.namespace).Search(scheme.Scheme, ops) 186 if err != nil { 187 return err 188 } 189 190 // print the warning events 191 printer.PrintAllWarningEvents(events, o.Out) 192 193 return nil 194 } 195 196 // printOpsCommand prints the kbcli command by OpsRequest.spec. 197 func (o *describeOpsOptions) printOpsCommand(opsRequest *appsv1alpha1.OpsRequest) { 198 if opsRequest == nil { 199 return 200 } 201 var commands []string 202 switch opsRequest.Spec.Type { 203 case appsv1alpha1.RestartType: 204 commands = o.getRestartCommand(opsRequest.Spec) 205 case appsv1alpha1.UpgradeType: 206 commands = o.getUpgradeCommand(opsRequest.Spec) 207 case appsv1alpha1.HorizontalScalingType: 208 commands = o.getHorizontalScalingCommand(opsRequest.Spec) 209 case appsv1alpha1.VerticalScalingType: 210 commands = o.getVerticalScalingCommand(opsRequest.Spec) 211 case appsv1alpha1.VolumeExpansionType: 212 commands = o.getVolumeExpansionCommand(opsRequest.Spec) 213 case appsv1alpha1.ReconfiguringType: 214 commands = o.getReconfiguringCommand(opsRequest.Spec) 215 } 216 if len(commands) == 0 { 217 printer.PrintLine("\nCommand: " + printer.NoneString) 218 return 219 } 220 printer.PrintTitle("Command") 221 for i := range commands { 222 command := fmt.Sprintf("%s --namespace=%s", commands[i], opsRequest.Namespace) 223 printer.PrintLine(" " + command) 224 } 225 } 226 227 // getRestartCommand gets the command of the Restart OpsRequest. 228 func (o *describeOpsOptions) getRestartCommand(spec appsv1alpha1.OpsRequestSpec) []string { 229 if len(spec.RestartList) == 0 { 230 return nil 231 } 232 componentNames := make([]string, len(spec.RestartList)) 233 for i, v := range spec.RestartList { 234 componentNames[i] = v.ComponentName 235 } 236 return []string{ 237 fmt.Sprintf("kbcli cluster restart %s --components=%s", spec.ClusterRef, 238 strings.Join(componentNames, ",")), 239 } 240 } 241 242 // getUpgradeCommand gets the command of the Upgrade OpsRequest. 243 func (o *describeOpsOptions) getUpgradeCommand(spec appsv1alpha1.OpsRequestSpec) []string { 244 return []string{ 245 fmt.Sprintf("kbcli cluster upgrade %s --cluster-version=%s", spec.ClusterRef, 246 spec.Upgrade.ClusterVersionRef), 247 } 248 } 249 250 // addResourceFlag adds resource flag for VerticalScaling OpsRequest. 251 func (o *describeOpsOptions) addResourceFlag(key string, value *resource.Quantity) string { 252 if !value.IsZero() { 253 return fmt.Sprintf(" --%s=%s", key, value) 254 } 255 return "" 256 } 257 258 // getVerticalScalingCommand gets the command of the VerticalScaling OpsRequest 259 func (o *describeOpsOptions) getVerticalScalingCommand(spec appsv1alpha1.OpsRequestSpec) []string { 260 if len(spec.VerticalScalingList) == 0 { 261 return nil 262 } 263 convertObject := func(h appsv1alpha1.VerticalScaling) any { 264 return h.ResourceRequirements 265 } 266 getCompName := func(h appsv1alpha1.VerticalScaling) string { 267 return h.ComponentName 268 } 269 componentNameSlice, resourceSlice := getCommandFlagsSlice[appsv1alpha1.VerticalScaling]( 270 spec.VerticalScalingList, convertObject, getCompName) 271 commands := make([]string, len(componentNameSlice)) 272 for i := range componentNameSlice { 273 commands[i] = fmt.Sprintf("kbcli cluster vscale %s --components=%s", 274 spec.ClusterRef, strings.Join(componentNameSlice[i], ",")) 275 clsRef := spec.VerticalScalingList[i].ClassDefRef 276 if clsRef != nil { 277 class := clsRef.Class 278 if clsRef.Name != "" { 279 class = fmt.Sprintf("%s:%s", clsRef.Name, class) 280 } 281 commands[i] += fmt.Sprintf("--class=%s", class) 282 } else { 283 resource := resourceSlice[i].(corev1.ResourceRequirements) 284 commands[i] += o.addResourceFlag("cpu", resource.Limits.Cpu()) 285 commands[i] += o.addResourceFlag("memory", resource.Limits.Memory()) 286 } 287 } 288 return commands 289 } 290 291 // getHorizontalScalingCommand gets the command of the HorizontalScaling OpsRequest. 292 func (o *describeOpsOptions) getHorizontalScalingCommand(spec appsv1alpha1.OpsRequestSpec) []string { 293 if len(spec.HorizontalScalingList) == 0 { 294 return nil 295 } 296 convertObject := func(h appsv1alpha1.HorizontalScaling) any { 297 return h.Replicas 298 } 299 getCompName := func(h appsv1alpha1.HorizontalScaling) string { 300 return h.ComponentName 301 } 302 componentNameSlice, replicasSlice := getCommandFlagsSlice[appsv1alpha1.HorizontalScaling]( 303 spec.HorizontalScalingList, convertObject, getCompName) 304 commands := make([]string, len(componentNameSlice)) 305 for i := range componentNameSlice { 306 commands[i] = fmt.Sprintf("kbcli cluster hscale %s --components=%s --replicas=%d", 307 spec.ClusterRef, strings.Join(componentNameSlice[i], ","), replicasSlice[i].(int32)) 308 } 309 return commands 310 } 311 312 // getVolumeExpansionCommand gets the command of the VolumeExpansion command. 313 func (o *describeOpsOptions) getVolumeExpansionCommand(spec appsv1alpha1.OpsRequestSpec) []string { 314 convertObject := func(v appsv1alpha1.OpsRequestVolumeClaimTemplate) any { 315 return v.Storage 316 } 317 getVCTName := func(v appsv1alpha1.OpsRequestVolumeClaimTemplate) string { 318 return v.Name 319 } 320 commands := make([]string, 0) 321 for _, v := range spec.VolumeExpansionList { 322 vctNameSlice, storageSlice := getCommandFlagsSlice[appsv1alpha1.OpsRequestVolumeClaimTemplate]( 323 v.VolumeClaimTemplates, convertObject, getVCTName) 324 for i := range vctNameSlice { 325 storage := storageSlice[i].(resource.Quantity) 326 commands = append(commands, fmt.Sprintf("kbcli cluster volume-expand %s --components=%s --volume-claim-template-names=%s --storage=%s", 327 spec.ClusterRef, v.ComponentName, strings.Join(vctNameSlice[i], ","), storage.String())) 328 } 329 } 330 return commands 331 } 332 333 // getReconfiguringCommand gets the command of the VolumeExpansion command. 334 func (o *describeOpsOptions) getReconfiguringCommand(spec appsv1alpha1.OpsRequestSpec) []string { 335 var ( 336 updatedParams = spec.Reconfigure 337 componentName = updatedParams.ComponentName 338 ) 339 340 if len(updatedParams.Configurations) == 0 { 341 return nil 342 } 343 344 configuration := updatedParams.Configurations[0] 345 if len(configuration.Keys) == 0 { 346 return nil 347 } 348 349 commandArgs := make([]string, 0) 350 commandArgs = append(commandArgs, "kbcli") 351 commandArgs = append(commandArgs, "cluster") 352 commandArgs = append(commandArgs, "configure") 353 commandArgs = append(commandArgs, spec.ClusterRef) 354 commandArgs = append(commandArgs, fmt.Sprintf("--components=%s", componentName)) 355 commandArgs = append(commandArgs, fmt.Sprintf("--config-spec=%s", configuration.Name)) 356 357 config := configuration.Keys[0] 358 commandArgs = append(commandArgs, fmt.Sprintf("--config-file=%s", config.Key)) 359 for _, p := range config.Parameters { 360 if p.Value == nil { 361 continue 362 } 363 commandArgs = append(commandArgs, fmt.Sprintf("--set %s=%s", p.Key, *p.Value)) 364 } 365 return []string{strings.Join(commandArgs, " ")} 366 } 367 368 // printOpsRequestStatus prints the OpsRequest status infos. 369 func (o *describeOpsOptions) printOpsRequestStatus(opsStatus *appsv1alpha1.OpsRequestStatus) { 370 printer.PrintTitle("Status") 371 startTime := opsStatus.StartTimestamp 372 if !startTime.IsZero() { 373 printer.PrintPairStringToLine("Start Time", util.TimeFormat(&startTime)) 374 } 375 completeTime := opsStatus.CompletionTimestamp 376 if !completeTime.IsZero() { 377 printer.PrintPairStringToLine("Completion Time", util.TimeFormat(&completeTime)) 378 } 379 if !startTime.IsZero() { 380 printer.PrintPairStringToLine("Duration", util.GetHumanReadableDuration(startTime, completeTime)) 381 } 382 printer.PrintPairStringToLine("Status", string(opsStatus.Phase)) 383 o.printProgressDetails(opsStatus) 384 } 385 386 // printLastConfiguration prints the last configuration of the cluster before doing the OpsRequest. 387 func (o *describeOpsOptions) printLastConfiguration(configuration appsv1alpha1.LastConfiguration, opsType appsv1alpha1.OpsType) { 388 if reflect.DeepEqual(configuration, appsv1alpha1.LastConfiguration{}) { 389 return 390 } 391 printer.PrintTitle("Last Configuration") 392 switch opsType { 393 case appsv1alpha1.UpgradeType: 394 printer.PrintPairStringToLine("Cluster Version", configuration.ClusterVersionRef) 395 case appsv1alpha1.VerticalScalingType: 396 handleVScale := func(tbl *printer.TablePrinter, cName string, compConf appsv1alpha1.LastComponentConfiguration) { 397 tbl.AddRow(cName, compConf.Requests.Cpu().String(), compConf.Requests.Memory().String(), compConf.Limits.Cpu().String(), compConf.Limits.Memory().String()) 398 } 399 headers := []interface{}{"COMPONENT", "REQUEST-CPU", "REQUEST-MEMORY", "LIMIT-CPU", "LIMIT-MEMORY"} 400 o.printLastConfigurationByOpsType(configuration, headers, handleVScale) 401 case appsv1alpha1.HorizontalScalingType: 402 handleHScale := func(tbl *printer.TablePrinter, cName string, compConf appsv1alpha1.LastComponentConfiguration) { 403 tbl.AddRow(cName, *compConf.Replicas) 404 } 405 headers := []interface{}{"COMPONENT", "REPLICAS"} 406 o.printLastConfigurationByOpsType(configuration, headers, handleHScale) 407 case appsv1alpha1.VolumeExpansionType: 408 handleVolumeExpansion := func(tbl *printer.TablePrinter, cName string, compConf appsv1alpha1.LastComponentConfiguration) { 409 vcts := compConf.VolumeClaimTemplates 410 for _, v := range vcts { 411 tbl.AddRow(cName, v.Name, v.Storage.String()) 412 } 413 } 414 headers := []interface{}{"COMPONENT", "VOLUME-CLAIM-TEMPLATE", "STORAGE"} 415 o.printLastConfigurationByOpsType(configuration, headers, handleVolumeExpansion) 416 } 417 } 418 419 // printLastConfigurationByOpsType prints the last configuration by ops type. 420 func (o *describeOpsOptions) printLastConfigurationByOpsType(configuration appsv1alpha1.LastConfiguration, 421 headers []interface{}, 422 handleOpsObject func(tbl *printer.TablePrinter, cName string, compConf appsv1alpha1.LastComponentConfiguration), 423 ) { 424 tbl := printer.NewTablePrinter(o.Out) 425 tbl.SetHeader(headers...) 426 keys := maps.Keys(configuration.Components) 427 sort.Strings(keys) 428 for _, cName := range keys { 429 handleOpsObject(tbl, cName, configuration.Components[cName]) 430 } 431 tbl.Print() 432 } 433 434 // printProgressDetails prints the progressDetails of all components in this OpsRequest. 435 func (o *describeOpsOptions) printProgressDetails(opsStatus *appsv1alpha1.OpsRequestStatus) { 436 printer.PrintPairStringToLine("Progress", opsStatus.Progress) 437 keys := maps.Keys(opsStatus.Components) 438 sort.Strings(keys) 439 tbl := printer.NewTablePrinter(o.Out) 440 tbl.SetHeader(fmt.Sprintf("%-22s%s", "", "OBJECT-KEY"), "STATUS", "DURATION", "MESSAGE") 441 for _, cName := range keys { 442 progressDetails := opsStatus.Components[cName].ProgressDetails 443 for _, v := range progressDetails { 444 var groupStr string 445 if len(v.Group) > 0 { 446 groupStr = fmt.Sprintf("(%s)", v.Group) 447 } 448 tbl.AddRow(fmt.Sprintf("%-22s%s%s", "", v.ObjectKey, groupStr), 449 v.Status, util.GetHumanReadableDuration(v.StartTime, v.EndTime), v.Message) 450 } 451 } 452 // "-/-" is the progress default value. 453 if opsStatus.Progress != "-/-" { 454 tbl.Print() 455 } 456 }