github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/operations.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 "strings" 26 27 jsonpatch "github.com/evanphx/json-patch" 28 "github.com/spf13/cobra" 29 "golang.org/x/exp/slices" 30 corev1 "k8s.io/api/core/v1" 31 "k8s.io/apimachinery/pkg/api/resource" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/runtime" 34 "k8s.io/apimachinery/pkg/runtime/schema" 35 apitypes "k8s.io/apimachinery/pkg/types" 36 "k8s.io/apimachinery/pkg/util/json" 37 "k8s.io/cli-runtime/pkg/genericiooptions" 38 cmdutil "k8s.io/kubectl/pkg/cmd/util" 39 "k8s.io/kubectl/pkg/util/templates" 40 "sigs.k8s.io/controller-runtime/pkg/client" 41 42 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 43 "github.com/1aal/kubeblocks/pkg/cli/cluster" 44 classutil "github.com/1aal/kubeblocks/pkg/cli/cmd/class" 45 "github.com/1aal/kubeblocks/pkg/cli/create" 46 "github.com/1aal/kubeblocks/pkg/cli/printer" 47 "github.com/1aal/kubeblocks/pkg/cli/types" 48 "github.com/1aal/kubeblocks/pkg/cli/util" 49 "github.com/1aal/kubeblocks/pkg/cli/util/flags" 50 "github.com/1aal/kubeblocks/pkg/cli/util/prompt" 51 "github.com/1aal/kubeblocks/pkg/constant" 52 ) 53 54 type OperationsOptions struct { 55 create.CreateOptions `json:"-"` 56 HasComponentNamesFlag bool `json:"-"` 57 // autoApprove when set true, skip the double check. 58 autoApprove bool `json:"-"` 59 ComponentNames []string `json:"componentNames,omitempty"` 60 OpsRequestName string `json:"opsRequestName"` 61 TTLSecondsAfterSucceed int `json:"ttlSecondsAfterSucceed"` 62 63 // OpsType operation type 64 OpsType appsv1alpha1.OpsType `json:"type"` 65 66 // OpsTypeLower lower OpsType 67 OpsTypeLower string `json:"typeLower"` 68 69 // Upgrade options 70 ClusterVersionRef string `json:"clusterVersionRef"` 71 72 // VerticalScaling options 73 CPU string `json:"cpu"` 74 Memory string `json:"memory"` 75 Class string `json:"class"` 76 ClassDefRef appsv1alpha1.ClassDefRef `json:"classDefRef,omitempty"` 77 78 // HorizontalScaling options 79 Replicas int `json:"replicas"` 80 81 // Reconfiguring options 82 KeyValues map[string]*string `json:"keyValues"` 83 CfgTemplateName string `json:"cfgTemplateName"` 84 CfgFile string `json:"cfgFile"` 85 ForceRestart bool `json:"forceRestart"` 86 FileContent string `json:"fileContent"` 87 HasPatch bool `json:"hasPatch"` 88 89 // VolumeExpansion options. 90 // VCTNames VolumeClaimTemplate names 91 VCTNames []string `json:"vctNames,omitempty"` 92 Storage string `json:"storage"` 93 94 // Expose options 95 ExposeType string `json:"-"` 96 ExposeEnabled string `json:"-"` 97 Services []appsv1alpha1.ClusterComponentService `json:"services,omitempty"` 98 99 // Switchover options 100 Component string `json:"component"` 101 Instance string `json:"instance"` 102 } 103 104 func newBaseOperationsOptions(f cmdutil.Factory, streams genericiooptions.IOStreams, 105 opsType appsv1alpha1.OpsType, hasComponentNamesFlag bool) *OperationsOptions { 106 customOutPut := func(opt *create.CreateOptions) { 107 output := fmt.Sprintf("OpsRequest %s created successfully, you can view the progress:", opt.Name) 108 printer.PrintLine(output) 109 nextLine := fmt.Sprintf("\tkbcli cluster describe-ops %s -n %s", opt.Name, opt.Namespace) 110 printer.PrintLine(nextLine) 111 } 112 113 o := &OperationsOptions{ 114 // nil cannot be set to a map struct in CueLang, so init the map of KeyValues. 115 KeyValues: map[string]*string{}, 116 HasPatch: true, 117 OpsType: opsType, 118 HasComponentNamesFlag: hasComponentNamesFlag, 119 autoApprove: false, 120 CreateOptions: create.CreateOptions{ 121 Factory: f, 122 IOStreams: streams, 123 CueTemplateName: "cluster_operations_template.cue", 124 GVR: types.OpsGVR(), 125 CustomOutPut: customOutPut, 126 }, 127 } 128 129 o.OpsTypeLower = strings.ToLower(string(o.OpsType)) 130 o.CreateOptions.Options = o 131 return o 132 } 133 134 // addCommonFlags adds common flags for operations command 135 func (o *OperationsOptions) addCommonFlags(cmd *cobra.Command, f cmdutil.Factory) { 136 // add print flags 137 printer.AddOutputFlagForCreate(cmd, &o.Format, false) 138 139 cmd.Flags().StringVar(&o.OpsRequestName, "name", "", "OpsRequest name. if not specified, it will be randomly generated ") 140 cmd.Flags().IntVar(&o.TTLSecondsAfterSucceed, "ttlSecondsAfterSucceed", 0, "Time to live after the OpsRequest succeed") 141 cmd.Flags().StringVar(&o.DryRun, "dry-run", "none", `Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent.`) 142 cmd.Flags().Lookup("dry-run").NoOptDefVal = "unchanged" 143 if o.HasComponentNamesFlag { 144 flags.AddComponentsFlag(f, cmd, &o.ComponentNames, "Component names to this operations") 145 } 146 } 147 148 // CompleteRestartOps restarts all components of the cluster 149 // we should set all component names to ComponentNames flag. 150 func (o *OperationsOptions) CompleteRestartOps() error { 151 if o.Name == "" { 152 return makeMissingClusterNameErr() 153 } 154 if len(o.ComponentNames) != 0 { 155 return nil 156 } 157 clusterObj, err := cluster.GetClusterByName(o.Dynamic, o.Name, o.Namespace) 158 if err != nil { 159 return err 160 } 161 componentSpecs := clusterObj.Spec.ComponentSpecs 162 o.ComponentNames = make([]string, len(componentSpecs)) 163 for i := range componentSpecs { 164 o.ComponentNames[i] = componentSpecs[i].Name 165 } 166 return nil 167 } 168 169 // CompleteComponentsFlag when components flag is null and the cluster only has one component, auto complete it. 170 func (o *OperationsOptions) CompleteComponentsFlag() error { 171 if o.Name == "" { 172 return makeMissingClusterNameErr() 173 } 174 if len(o.ComponentNames) != 0 { 175 return nil 176 } 177 clusterObj, err := cluster.GetClusterByName(o.Dynamic, o.Name, o.Namespace) 178 if err != nil { 179 return err 180 } 181 if len(clusterObj.Spec.ComponentSpecs) == 1 { 182 o.ComponentNames = []string{clusterObj.Spec.ComponentSpecs[0].Name} 183 } 184 return nil 185 } 186 187 func (o *OperationsOptions) validateUpgrade() error { 188 if len(o.ClusterVersionRef) == 0 { 189 return fmt.Errorf("missing cluster-version") 190 } 191 return nil 192 } 193 194 func (o *OperationsOptions) validateVolumeExpansion() error { 195 if len(o.VCTNames) == 0 { 196 return fmt.Errorf("missing volume-claim-templates") 197 } 198 if len(o.Storage) == 0 { 199 return fmt.Errorf("missing storage") 200 } 201 202 for _, cName := range o.ComponentNames { 203 for _, vctName := range o.VCTNames { 204 labels := fmt.Sprintf("%s=%s,%s=%s,%s=%s", 205 constant.AppInstanceLabelKey, o.Name, 206 constant.KBAppComponentLabelKey, cName, 207 constant.VolumeClaimTemplateNameLabelKey, vctName, 208 ) 209 pvcs, err := o.Client.CoreV1().PersistentVolumeClaims(o.Namespace).List(context.Background(), 210 metav1.ListOptions{LabelSelector: labels, Limit: 1}) 211 if err != nil { 212 return err 213 } 214 if len(pvcs.Items) == 0 { 215 continue 216 } 217 pvc := pvcs.Items[0] 218 specStorage := pvc.Spec.Resources.Requests.Storage() 219 statusStorage := pvc.Status.Capacity.Storage() 220 targetStorage, err := resource.ParseQuantity(o.Storage) 221 if err != nil { 222 return fmt.Errorf("cannot parse '%v', %v", o.Storage, err) 223 } 224 // determine whether the opsRequest is a recovery action for volume expansion failure 225 if specStorage.Cmp(targetStorage) > 0 && 226 statusStorage.Cmp(targetStorage) <= 0 { 227 o.autoApprove = false 228 fmt.Fprintln(o.Out, printer.BoldYellow("Warning: this opsRequest is a recovery action for volume expansion failure and will re-create the PersistentVolumeClaims when RECOVER_VOLUME_EXPANSION_FAILURE=false")) 229 break 230 } 231 } 232 } 233 return nil 234 } 235 236 func (o *OperationsOptions) validateVScale(cluster *appsv1alpha1.Cluster) error { 237 if o.Class != "" && (o.CPU != "" || o.Memory != "") { 238 return fmt.Errorf("class and cpu/memory cannot be both specified") 239 } 240 if o.Class == "" && o.CPU == "" && o.Memory == "" { 241 return fmt.Errorf("class or cpu/memory must be specified") 242 } 243 244 clsMgr, err := classutil.GetManager(o.Dynamic, cluster.Spec.ClusterDefRef) 245 if err != nil { 246 return err 247 } 248 249 fillClassParams := func(comp *appsv1alpha1.ClusterComponentSpec) error { 250 if o.Class != "" { 251 clsDefRef := appsv1alpha1.ClassDefRef{} 252 parts := strings.SplitN(o.Class, ":", 2) 253 if len(parts) == 1 { 254 clsDefRef.Class = parts[0] 255 } else { 256 clsDefRef.Name = parts[0] 257 clsDefRef.Class = parts[1] 258 } 259 comp.ClassDefRef = &clsDefRef 260 comp.Resources = corev1.ResourceRequirements{} 261 } else { 262 comp.ClassDefRef = &appsv1alpha1.ClassDefRef{} 263 requests := make(corev1.ResourceList) 264 if o.CPU != "" { 265 cpu, err := resource.ParseQuantity(o.CPU) 266 if err != nil { 267 return fmt.Errorf("cannot parse '%v', %v", o.CPU, err) 268 } 269 requests[corev1.ResourceCPU] = cpu 270 } 271 if o.Memory != "" { 272 memory, err := resource.ParseQuantity(o.Memory) 273 if err != nil { 274 return fmt.Errorf("cannot parse '%v', %v", o.Memory, err) 275 } 276 requests[corev1.ResourceMemory] = memory 277 } 278 requests.DeepCopyInto(&comp.Resources.Requests) 279 requests.DeepCopyInto(&comp.Resources.Limits) 280 } 281 return nil 282 } 283 284 for _, name := range o.ComponentNames { 285 for _, comp := range cluster.Spec.ComponentSpecs { 286 if comp.Name != name { 287 continue 288 } 289 if err = fillClassParams(&comp); err != nil { 290 return err 291 } 292 if err = clsMgr.ValidateResources(cluster.Spec.ClusterDefRef, &comp); err != nil { 293 return err 294 } 295 } 296 } 297 298 return nil 299 } 300 301 // Validate command flags or args is legal 302 func (o *OperationsOptions) Validate() error { 303 if o.Name == "" { 304 return makeMissingClusterNameErr() 305 } 306 307 // check if cluster exist 308 obj, err := o.Dynamic.Resource(types.ClusterGVR()).Namespace(o.Namespace).Get(context.TODO(), o.Name, metav1.GetOptions{}) 309 if err != nil { 310 return err 311 } 312 var cluster appsv1alpha1.Cluster 313 if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &cluster); err != nil { 314 return err 315 } 316 317 // common validate for componentOps 318 if o.HasComponentNamesFlag && len(o.ComponentNames) == 0 { 319 return fmt.Errorf(`missing components, please specify the "--components" flag for multi-components cluster`) 320 } 321 322 switch o.OpsType { 323 case appsv1alpha1.VolumeExpansionType: 324 if err = o.validateVolumeExpansion(); err != nil { 325 return err 326 } 327 case appsv1alpha1.UpgradeType: 328 if err = o.validateUpgrade(); err != nil { 329 return err 330 } 331 case appsv1alpha1.VerticalScalingType: 332 if err = o.validateVScale(&cluster); err != nil { 333 return err 334 } 335 case appsv1alpha1.ExposeType: 336 if err = o.validateExpose(); err != nil { 337 return err 338 } 339 case appsv1alpha1.SwitchoverType: 340 if err = o.validatePromote(&cluster); err != nil { 341 return err 342 } 343 } 344 if !o.autoApprove && o.DryRun == "none" { 345 return prompt.Confirm([]string{o.Name}, o.In, "", "") 346 } 347 return nil 348 } 349 350 func (o *OperationsOptions) validatePromote(cluster *appsv1alpha1.Cluster) error { 351 var ( 352 clusterDefObj = appsv1alpha1.ClusterDefinition{} 353 podObj = &corev1.Pod{} 354 componentName string 355 ) 356 357 if len(cluster.Spec.ComponentSpecs) == 0 { 358 return fmt.Errorf("cluster.Spec.ComponentSpecs cannot be empty") 359 } 360 361 if o.Component != "" { 362 componentName = o.Component 363 } else { 364 if len(cluster.Spec.ComponentSpecs) > 1 { 365 return fmt.Errorf("there are multiple components in cluster, please use --component to specify the component for promote") 366 } 367 componentName = cluster.Spec.ComponentSpecs[0].Name 368 } 369 370 if o.Instance != "" { 371 // checks the validity of the instance whether it belongs to the current component and ensure it is not the primary or leader instance currently. 372 podKey := client.ObjectKey{ 373 Namespace: cluster.Namespace, 374 Name: o.Instance, 375 } 376 if err := util.GetResourceObjectFromGVR(types.PodGVR(), podKey, o.Dynamic, podObj); err != nil || podObj == nil { 377 return fmt.Errorf("instance %s not found, please check the validity of the instance using \"kbcli cluster list-instances\"", o.Instance) 378 } 379 v, ok := podObj.Labels[constant.RoleLabelKey] 380 if !ok || v == "" { 381 return fmt.Errorf("instance %s cannot be promoted because it had a invalid role label", o.Instance) 382 } 383 if v == constant.Primary || v == constant.Leader { 384 return fmt.Errorf("instance %s cannot be promoted because it is already the primary or leader instance", o.Instance) 385 } 386 if !strings.HasPrefix(podObj.Name, fmt.Sprintf("%s-%s", cluster.Name, componentName)) { 387 return fmt.Errorf("instance %s does not belong to the current component, please check the validity of the instance using \"kbcli cluster list-instances\"", o.Instance) 388 } 389 } 390 391 // check clusterDefinition switchoverSpec exist 392 clusterDefKey := client.ObjectKey{ 393 Namespace: "", 394 Name: cluster.Spec.ClusterDefRef, 395 } 396 if err := util.GetResourceObjectFromGVR(types.ClusterDefGVR(), clusterDefKey, o.Dynamic, &clusterDefObj); err != nil { 397 return err 398 } 399 var compDefObj *appsv1alpha1.ClusterComponentDefinition 400 for _, compDef := range clusterDefObj.Spec.ComponentDefs { 401 if compDef.Name == cluster.Spec.GetComponentDefRefName(componentName) { 402 compDefObj = &compDef 403 break 404 } 405 } 406 if compDefObj == nil { 407 return fmt.Errorf("cluster component %s is invalid", componentName) 408 } 409 if compDefObj.SwitchoverSpec == nil { 410 return fmt.Errorf("cluster component %s does not support switchover", componentName) 411 } 412 switch o.Instance { 413 case "": 414 if compDefObj.SwitchoverSpec.WithoutCandidate == nil { 415 return fmt.Errorf("cluster component %s does not support promote without specifying an instance. Please specify a specific instance for the promotion", componentName) 416 } 417 default: 418 if compDefObj.SwitchoverSpec.WithCandidate == nil { 419 return fmt.Errorf("cluster component %s does not support specifying an instance for promote. If you want to perform a promote operation, please do not specify an instance", componentName) 420 } 421 } 422 return nil 423 } 424 425 func (o *OperationsOptions) validateExpose() error { 426 switch util.ExposeType(o.ExposeType) { 427 case "", util.ExposeToVPC, util.ExposeToInternet: 428 default: 429 return fmt.Errorf("invalid expose type %q", o.ExposeType) 430 } 431 432 switch strings.ToLower(o.ExposeEnabled) { 433 case util.EnableValue, util.DisableValue: 434 default: 435 return fmt.Errorf("invalid value for enable flag: %s", o.ExposeEnabled) 436 } 437 return nil 438 } 439 440 func (o *OperationsOptions) fillExpose() error { 441 version, err := util.GetK8sVersion(o.Client.Discovery()) 442 if err != nil { 443 return err 444 } 445 provider, err := util.GetK8sProvider(version, o.Client) 446 if err != nil { 447 return err 448 } 449 if provider == util.UnknownProvider { 450 return fmt.Errorf("unknown k8s provider") 451 } 452 453 // default expose to internet 454 exposeType := util.ExposeType(o.ExposeType) 455 if exposeType == "" { 456 exposeType = util.ExposeToInternet 457 } 458 459 annotations, err := util.GetExposeAnnotations(provider, exposeType) 460 if err != nil { 461 return err 462 } 463 464 gvr := schema.GroupVersionResource{Group: types.AppsAPIGroup, Version: types.AppsAPIVersion, Resource: types.ResourceClusters} 465 unstructuredObj, err := o.Dynamic.Resource(gvr).Namespace(o.Namespace).Get(context.TODO(), o.Name, metav1.GetOptions{}) 466 if err != nil { 467 return err 468 } 469 cluster := appsv1alpha1.Cluster{} 470 if err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredObj.UnstructuredContent(), &cluster); err != nil { 471 return err 472 } 473 474 compMap := make(map[string]appsv1alpha1.ClusterComponentSpec) 475 for _, compSpec := range cluster.Spec.ComponentSpecs { 476 compMap[compSpec.Name] = compSpec 477 } 478 479 var ( 480 // currently, we use the expose type as service name 481 svcName = string(exposeType) 482 enabled = strings.ToLower(o.ExposeEnabled) == util.EnableValue 483 ) 484 for _, name := range o.ComponentNames { 485 comp, ok := compMap[name] 486 if !ok { 487 return fmt.Errorf("component %s not found", name) 488 } 489 490 for _, svc := range comp.Services { 491 if svc.Name != svcName { 492 o.Services = append(o.Services, svc) 493 } 494 } 495 496 if enabled { 497 o.Services = append(o.Services, appsv1alpha1.ClusterComponentService{ 498 Name: svcName, 499 ServiceType: corev1.ServiceTypeLoadBalancer, 500 Annotations: annotations, 501 }) 502 } 503 } 504 return nil 505 } 506 507 var restartExample = templates.Examples(` 508 # restart all components 509 kbcli cluster restart mycluster 510 511 # specified component to restart, separate with commas for multiple components 512 kbcli cluster restart mycluster --components=mysql 513 `) 514 515 // NewRestartCmd creates a restart command 516 func NewRestartCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 517 o := newBaseOperationsOptions(f, streams, appsv1alpha1.RestartType, true) 518 cmd := &cobra.Command{ 519 Use: "restart NAME", 520 Short: "Restart the specified components in the cluster.", 521 Example: restartExample, 522 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), 523 Run: func(cmd *cobra.Command, args []string) { 524 o.Args = args 525 cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) 526 cmdutil.CheckErr(o.Complete()) 527 cmdutil.CheckErr(o.CompleteRestartOps()) 528 cmdutil.CheckErr(o.Validate()) 529 cmdutil.CheckErr(o.Run()) 530 }, 531 } 532 o.addCommonFlags(cmd, f) 533 cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before restarting the cluster") 534 return cmd 535 } 536 537 var upgradeExample = templates.Examples(` 538 # upgrade the cluster to the target version 539 kbcli cluster upgrade mycluster --cluster-version=ac-mysql-8.0.30 540 `) 541 542 // NewUpgradeCmd creates an upgrade command 543 func NewUpgradeCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 544 o := newBaseOperationsOptions(f, streams, appsv1alpha1.UpgradeType, false) 545 cmd := &cobra.Command{ 546 Use: "upgrade NAME", 547 Short: "Upgrade the cluster version.", 548 Example: upgradeExample, 549 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), 550 Run: func(cmd *cobra.Command, args []string) { 551 o.Args = args 552 cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) 553 cmdutil.CheckErr(o.Complete()) 554 cmdutil.CheckErr(o.Validate()) 555 cmdutil.CheckErr(o.Run()) 556 }, 557 } 558 o.addCommonFlags(cmd, f) 559 cmd.Flags().StringVar(&o.ClusterVersionRef, "cluster-version", "", "Reference cluster version (required)") 560 cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before upgrading the cluster") 561 _ = cmd.MarkFlagRequired("cluster-version") 562 return cmd 563 } 564 565 var verticalScalingExample = templates.Examples(` 566 # scale the computing resources of specified components, separate with commas for multiple components 567 kbcli cluster vscale mycluster --components=mysql --cpu=500m --memory=500Mi 568 569 # scale the computing resources of specified components by class, run command 'kbcli class list --cluster-definition cluster-definition-name' to get available classes 570 kbcli cluster vscale mycluster --components=mysql --class=general-2c4g 571 `) 572 573 // NewVerticalScalingCmd creates a vertical scaling command 574 func NewVerticalScalingCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 575 o := newBaseOperationsOptions(f, streams, appsv1alpha1.VerticalScalingType, true) 576 cmd := &cobra.Command{ 577 Use: "vscale NAME", 578 Short: "Vertically scale the specified components in the cluster.", 579 Example: verticalScalingExample, 580 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), 581 Run: func(cmd *cobra.Command, args []string) { 582 o.Args = args 583 cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) 584 cmdutil.CheckErr(o.Complete()) 585 cmdutil.CheckErr(o.CompleteComponentsFlag()) 586 cmdutil.CheckErr(o.Validate()) 587 cmdutil.CheckErr(o.Run()) 588 }, 589 } 590 o.addCommonFlags(cmd, f) 591 cmd.Flags().StringVar(&o.CPU, "cpu", "", "Request and limit size of component cpu") 592 cmd.Flags().StringVar(&o.Memory, "memory", "", "Request and limit size of component memory") 593 cmd.Flags().StringVar(&o.Class, "class", "", "Component class") 594 cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before vertically scaling the cluster") 595 _ = cmd.MarkFlagRequired("components") 596 return cmd 597 } 598 599 var horizontalScalingExample = templates.Examples(` 600 # expand storage resources of specified components, separate with commas for multiple components 601 kbcli cluster hscale mycluster --components=mysql --replicas=3 602 `) 603 604 // NewHorizontalScalingCmd creates a horizontal scaling command 605 func NewHorizontalScalingCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 606 o := newBaseOperationsOptions(f, streams, appsv1alpha1.HorizontalScalingType, true) 607 cmd := &cobra.Command{ 608 Use: "hscale NAME", 609 Short: "Horizontally scale the specified components in the cluster.", 610 Example: horizontalScalingExample, 611 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), 612 Run: func(cmd *cobra.Command, args []string) { 613 o.Args = args 614 cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) 615 cmdutil.CheckErr(o.Complete()) 616 cmdutil.CheckErr(o.CompleteComponentsFlag()) 617 cmdutil.CheckErr(o.Validate()) 618 cmdutil.CheckErr(o.Run()) 619 }, 620 } 621 622 o.addCommonFlags(cmd, f) 623 cmd.Flags().IntVar(&o.Replicas, "replicas", o.Replicas, "Replicas with the specified components") 624 cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before horizontally scaling the cluster") 625 _ = cmd.MarkFlagRequired("replicas") 626 _ = cmd.MarkFlagRequired("components") 627 return cmd 628 } 629 630 var volumeExpansionExample = templates.Examples(` 631 # restart specifies the component, separate with commas for multiple components 632 kbcli cluster volume-expand mycluster --components=mysql --volume-claim-templates=data --storage=10Gi 633 `) 634 635 // NewVolumeExpansionCmd creates a volume expanding command 636 func NewVolumeExpansionCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 637 o := newBaseOperationsOptions(f, streams, appsv1alpha1.VolumeExpansionType, true) 638 cmd := &cobra.Command{ 639 Use: "volume-expand NAME", 640 Short: "Expand volume with the specified components and volumeClaimTemplates in the cluster.", 641 Example: volumeExpansionExample, 642 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), 643 Run: func(cmd *cobra.Command, args []string) { 644 o.Args = args 645 cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) 646 cmdutil.CheckErr(o.Complete()) 647 cmdutil.CheckErr(o.CompleteComponentsFlag()) 648 cmdutil.CheckErr(o.Validate()) 649 cmdutil.CheckErr(o.Run()) 650 }, 651 } 652 o.addCommonFlags(cmd, f) 653 cmd.Flags().StringSliceVarP(&o.VCTNames, "volume-claim-templates", "t", nil, "VolumeClaimTemplate names in components (required)") 654 cmd.Flags().StringVar(&o.Storage, "storage", "", "Volume storage size (required)") 655 cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before expanding the cluster volume") 656 _ = cmd.MarkFlagRequired("volume-claim-templates") 657 _ = cmd.MarkFlagRequired("storage") 658 _ = cmd.MarkFlagRequired("components") 659 return cmd 660 } 661 662 var ( 663 exposeExamples = templates.Examples(` 664 # Expose a cluster to vpc 665 kbcli cluster expose mycluster --type vpc --enable=true 666 667 # Expose a cluster to public internet 668 kbcli cluster expose mycluster --type internet --enable=true 669 670 # Stop exposing a cluster 671 kbcli cluster expose mycluster --type vpc --enable=false 672 `) 673 ) 674 675 // NewExposeCmd creates an expose command 676 func NewExposeCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 677 o := newBaseOperationsOptions(f, streams, appsv1alpha1.ExposeType, true) 678 cmd := &cobra.Command{ 679 Use: "expose NAME --enable=[true|false] --type=[vpc|internet]", 680 Short: "Expose a cluster with a new endpoint, the new endpoint can be found by executing 'kbcli cluster describe NAME'.", 681 Example: exposeExamples, 682 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), 683 Run: func(cmd *cobra.Command, args []string) { 684 o.Args = args 685 cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) 686 cmdutil.CheckErr(o.Complete()) 687 cmdutil.CheckErr(o.CompleteComponentsFlag()) 688 cmdutil.CheckErr(o.fillExpose()) 689 cmdutil.CheckErr(o.Validate()) 690 cmdutil.CheckErr(o.Run()) 691 }, 692 } 693 o.addCommonFlags(cmd, f) 694 cmd.Flags().StringVar(&o.ExposeType, "type", "", "Expose type, currently supported types are 'vpc', 'internet'") 695 cmd.Flags().StringVar(&o.ExposeEnabled, "enable", "", "Enable or disable the expose, values can be true or false") 696 cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before exposing the cluster") 697 698 util.CheckErr(cmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 699 return []string{string(util.ExposeToVPC), string(util.ExposeToInternet)}, cobra.ShellCompDirectiveNoFileComp 700 })) 701 util.CheckErr(cmd.RegisterFlagCompletionFunc("enable", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 702 return []string{"true", "false"}, cobra.ShellCompDirectiveNoFileComp 703 })) 704 705 _ = cmd.MarkFlagRequired("enable") 706 return cmd 707 } 708 709 var stopExample = templates.Examples(` 710 # stop the cluster and release all the pods of the cluster 711 kbcli cluster stop mycluster 712 `) 713 714 // NewStopCmd creates a stop command 715 func NewStopCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 716 o := newBaseOperationsOptions(f, streams, appsv1alpha1.StopType, false) 717 cmd := &cobra.Command{ 718 Use: "stop NAME", 719 Short: "Stop the cluster and release all the pods of the cluster.", 720 Example: stopExample, 721 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), 722 Run: func(cmd *cobra.Command, args []string) { 723 o.Args = args 724 cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) 725 cmdutil.CheckErr(o.Complete()) 726 cmdutil.CheckErr(o.Validate()) 727 cmdutil.CheckErr(o.Run()) 728 }, 729 } 730 o.addCommonFlags(cmd, f) 731 cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before stopping the cluster") 732 return cmd 733 } 734 735 var startExample = templates.Examples(` 736 # start the cluster when cluster is stopped 737 kbcli cluster start mycluster 738 `) 739 740 // NewStartCmd creates a start command 741 func NewStartCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 742 o := newBaseOperationsOptions(f, streams, appsv1alpha1.StartType, false) 743 o.autoApprove = true 744 cmd := &cobra.Command{ 745 Use: "start NAME", 746 Short: "Start the cluster if cluster is stopped.", 747 Example: startExample, 748 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), 749 Run: func(cmd *cobra.Command, args []string) { 750 o.Args = args 751 cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) 752 cmdutil.CheckErr(o.Complete()) 753 cmdutil.CheckErr(o.Validate()) 754 cmdutil.CheckErr(o.Run()) 755 }, 756 } 757 o.addCommonFlags(cmd, f) 758 return cmd 759 } 760 761 var cancelExample = templates.Examples(` 762 # cancel the opsRequest which is not completed. 763 kbcli cluster cancel-ops <opsRequestName> 764 `) 765 766 func cancelOps(o *OperationsOptions) error { 767 opsRequest := &appsv1alpha1.OpsRequest{} 768 if err := cluster.GetK8SClientObject(o.Dynamic, opsRequest, o.GVR, o.Namespace, o.Name); err != nil { 769 return err 770 } 771 notSupportedPhases := []appsv1alpha1.OpsPhase{appsv1alpha1.OpsFailedPhase, appsv1alpha1.OpsSucceedPhase, appsv1alpha1.OpsCancelledPhase} 772 if slices.Contains(notSupportedPhases, opsRequest.Status.Phase) { 773 return fmt.Errorf("can not cancel the opsRequest when phase is %s", opsRequest.Status.Phase) 774 } 775 if opsRequest.Status.Phase == appsv1alpha1.OpsCancellingPhase { 776 return fmt.Errorf(`opsRequest "%s" is cancelling`, opsRequest.Name) 777 } 778 supportedType := []appsv1alpha1.OpsType{appsv1alpha1.HorizontalScalingType, appsv1alpha1.VerticalScalingType} 779 if !slices.Contains(supportedType, opsRequest.Spec.Type) { 780 return fmt.Errorf("opsRequest type: %s not support cancel action", opsRequest.Spec.Type) 781 } 782 if !o.autoApprove { 783 if err := prompt.Confirm([]string{o.Name}, o.In, "", ""); err != nil { 784 return err 785 } 786 } 787 oldOps := opsRequest.DeepCopy() 788 opsRequest.Spec.Cancel = true 789 oldData, err := json.Marshal(oldOps) 790 if err != nil { 791 return err 792 } 793 newData, err := json.Marshal(opsRequest) 794 if err != nil { 795 return err 796 } 797 patchBytes, err := jsonpatch.CreateMergePatch(oldData, newData) 798 if err != nil { 799 return err 800 } 801 if _, err = o.Dynamic.Resource(types.OpsGVR()).Namespace(opsRequest.Namespace).Patch(context.TODO(), 802 opsRequest.Name, apitypes.MergePatchType, patchBytes, metav1.PatchOptions{}); err != nil { 803 return err 804 } 805 fmt.Fprintf(o.Out, "start to cancel opsRequest \"%s\", you can view the progress:\n\tkbcli cluster list-ops --name %s\n", o.Name, o.Name) 806 return nil 807 } 808 809 func NewCancelCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 810 o := newBaseOperationsOptions(f, streams, "", false) 811 cmd := &cobra.Command{ 812 Use: "cancel-ops NAME", 813 Short: "Cancel the pending/creating/running OpsRequest which type is vscale or hscale.", 814 Example: cancelExample, 815 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.OpsGVR()), 816 Run: func(cmd *cobra.Command, args []string) { 817 o.Args = args 818 cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) 819 cmdutil.CheckErr(o.Complete()) 820 cmdutil.CheckErr(cancelOps(o)) 821 }, 822 } 823 cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before cancel the opsRequest") 824 return cmd 825 } 826 827 var promoteExample = templates.Examples(` 828 # Promote the instance mycluster-mysql-1 as the new primary or leader. 829 kbcli cluster promote mycluster --instance mycluster-mysql-1 830 831 # Promote a non-primary or non-leader instance as the new primary or leader, the new primary or leader is determined by the system. 832 kbcli cluster promote mycluster 833 834 # If the cluster has multiple components, you need to specify a component, otherwise an error will be reported. 835 kbcli cluster promote mycluster --component=mysql --instance mycluster-mysql-1 836 `) 837 838 // NewPromoteCmd creates a promote command 839 func NewPromoteCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 840 o := newBaseOperationsOptions(f, streams, appsv1alpha1.SwitchoverType, false) 841 cmd := &cobra.Command{ 842 Use: "promote NAME [--component=<comp-name>] [--instance <instance-name>]", 843 Short: "Promote a non-primary or non-leader instance as the new primary or leader of the cluster", 844 Example: promoteExample, 845 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), 846 Run: func(cmd *cobra.Command, args []string) { 847 o.Args = args 848 cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) 849 cmdutil.CheckErr(o.Complete()) 850 cmdutil.CheckErr(o.CompleteComponentsFlag()) 851 cmdutil.CheckErr(o.Validate()) 852 cmdutil.CheckErr(o.Run()) 853 }, 854 } 855 cmd.Flags().StringVar(&o.Component, "component", "", "Specify the component name of the cluster, if the cluster has multiple components, you need to specify a component") 856 cmd.Flags().StringVar(&o.Instance, "instance", "", "Specify the instance name as the new primary or leader of the cluster, you can get the instance name by running \"kbcli cluster list-instances\"") 857 cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before promote the instance") 858 o.addCommonFlags(cmd, f) 859 return cmd 860 }