github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/dataprotection.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 "bytes" 24 "context" 25 "fmt" 26 "reflect" 27 "sort" 28 "strconv" 29 "strings" 30 "time" 31 32 "github.com/pkg/errors" 33 "github.com/spf13/cobra" 34 "golang.org/x/exp/maps" 35 batchv1 "k8s.io/api/batch/v1" 36 corev1 "k8s.io/api/core/v1" 37 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 38 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 39 "k8s.io/apimachinery/pkg/runtime" 40 "k8s.io/apimachinery/pkg/runtime/schema" 41 "k8s.io/apimachinery/pkg/util/duration" 42 "k8s.io/apimachinery/pkg/util/json" 43 "k8s.io/cli-runtime/pkg/genericiooptions" 44 "k8s.io/client-go/dynamic" 45 clientset "k8s.io/client-go/kubernetes" 46 "k8s.io/client-go/kubernetes/scheme" 47 "k8s.io/client-go/util/jsonpath" 48 "k8s.io/kubectl/pkg/cmd/get" 49 cmdutil "k8s.io/kubectl/pkg/cmd/util" 50 "k8s.io/kubectl/pkg/cmd/util/editor" 51 "k8s.io/kubectl/pkg/util/templates" 52 "sigs.k8s.io/controller-runtime/pkg/client" 53 54 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 55 dpv1alpha1 "github.com/1aal/kubeblocks/apis/dataprotection/v1alpha1" 56 "github.com/1aal/kubeblocks/pkg/cli/cluster" 57 "github.com/1aal/kubeblocks/pkg/cli/create" 58 "github.com/1aal/kubeblocks/pkg/cli/delete" 59 "github.com/1aal/kubeblocks/pkg/cli/list" 60 "github.com/1aal/kubeblocks/pkg/cli/printer" 61 "github.com/1aal/kubeblocks/pkg/cli/types" 62 "github.com/1aal/kubeblocks/pkg/cli/util" 63 "github.com/1aal/kubeblocks/pkg/constant" 64 dptypes "github.com/1aal/kubeblocks/pkg/dataprotection/types" 65 ) 66 67 var ( 68 listBackupPolicyExample = templates.Examples(` 69 # list all backup policies 70 kbcli cluster list-backup-policy 71 72 # using short cmd to list backup policy of the specified cluster 73 kbcli cluster list-bp mycluster 74 `) 75 editExample = templates.Examples(` 76 # edit backup policy 77 kbcli cluster edit-backup-policy <backup-policy-name> 78 79 # update backup Repo 80 kbcli cluster edit-backup-policy <backup-policy-name> --set backupRepoName=<backup-repo-name> 81 82 # using short cmd to edit backup policy 83 kbcli cluster edit-bp <backup-policy-name> 84 `) 85 createBackupExample = templates.Examples(` 86 # Create a backup for the cluster, use the default backup policy and volume snapshot backup method 87 kbcli cluster backup mycluster 88 89 # create a backup with a specified method, run "kbcli cluster desc-backup-policy mycluster" to show supported backup methods 90 kbcli cluster backup mycluster --method volume-snapshot 91 92 # create a backup with specified backup policy, run "kbcli cluster list-backup-policy mycluster" to show the cluster supported backup policies 93 kbcli cluster backup mycluster --method volume-snapshot --policy <backup-policy-name> 94 95 # create a backup from a parent backup 96 kbcli cluster backup mycluster --parent-backup parent-backup-name 97 `) 98 listBackupExample = templates.Examples(` 99 # list all backups 100 kbcli cluster list-backups 101 `) 102 deleteBackupExample = templates.Examples(` 103 # delete a backup named backup-name 104 kbcli cluster delete-backup cluster-name --name backup-name 105 `) 106 createRestoreExample = templates.Examples(` 107 # restore a new cluster from a backup 108 kbcli cluster restore new-cluster-name --backup backup-name 109 `) 110 describeBackupExample = templates.Examples(` 111 # describe a backup 112 kbcli cluster describe-backup backup-default-mycluster-20230616190023 113 `) 114 describeBackupPolicyExample = templates.Examples(` 115 # describe the default backup policy of the cluster 116 kbcli cluster describe-backup-policy cluster-name 117 118 # describe the backup policy of the cluster with specified name 119 kbcli cluster describe-backup-policy cluster-name --name backup-policy-name 120 `) 121 ) 122 123 const annotationTrueValue = "true" 124 125 type CreateBackupOptions struct { 126 BackupMethod string `json:"backupMethod"` 127 BackupName string `json:"backupName"` 128 BackupPolicy string `json:"backupPolicy"` 129 DeletionPolicy string `json:"deletionPolicy"` 130 RetentionPeriod string `json:"retentionPeriod"` 131 ParentBackupName string `json:"parentBackupName"` 132 133 create.CreateOptions `json:"-"` 134 } 135 136 type ListBackupOptions struct { 137 *list.ListOptions 138 BackupName string 139 } 140 141 type DescribeBackupOptions struct { 142 Factory cmdutil.Factory 143 client clientset.Interface 144 dynamic dynamic.Interface 145 namespace string 146 147 // resource type and names 148 Gvr schema.GroupVersionResource 149 names []string 150 151 genericiooptions.IOStreams 152 } 153 154 func (o *CreateBackupOptions) CompleteBackup() error { 155 if err := o.Complete(); err != nil { 156 return err 157 } 158 // generate backupName 159 if len(o.BackupName) == 0 { 160 o.BackupName = strings.Join([]string{"backup", o.Namespace, o.Name, time.Now().Format("20060102150405")}, "-") 161 } 162 163 return o.CreateOptions.Complete() 164 } 165 166 func (o *CreateBackupOptions) Validate() error { 167 if o.Name == "" { 168 return fmt.Errorf("missing cluster name") 169 } 170 171 // if backup policy is not specified, use the default backup policy 172 if o.BackupPolicy == "" { 173 if err := o.completeDefaultBackupPolicy(); err != nil { 174 return err 175 } 176 } 177 178 // check if backup policy exists 179 backupPolicyObj, err := o.Dynamic.Resource(types.BackupPolicyGVR()).Namespace(o.Namespace).Get(context.TODO(), o.BackupPolicy, metav1.GetOptions{}) 180 if err != nil { 181 return err 182 } 183 backupPolicy := &dpv1alpha1.BackupPolicy{} 184 if err := runtime.DefaultUnstructuredConverter.FromUnstructured(backupPolicyObj.Object, backupPolicy); err != nil { 185 return err 186 } 187 188 if o.BackupMethod == "" { 189 return fmt.Errorf("backup method can not be empty, you can specify it by --method") 190 } 191 // TODO: check if pvc exists 192 193 // valid retention period 194 if o.RetentionPeriod != "" { 195 _, err := dpv1alpha1.RetentionPeriod(o.RetentionPeriod).ToDuration() 196 if err != nil { 197 return fmt.Errorf("invalid retention period, please refer to examples [1y, 1m, 1d, 1h, 1m] or combine them [1y1m1d1h1m]") 198 } 199 } 200 201 // check if parent backup exists 202 if o.ParentBackupName != "" { 203 parentBackupObj, err := o.Dynamic.Resource(types.BackupGVR()).Namespace(o.Namespace).Get(context.TODO(), o.ParentBackupName, metav1.GetOptions{}) 204 if err != nil { 205 return err 206 } 207 parentBackup := &dpv1alpha1.Backup{} 208 if err := runtime.DefaultUnstructuredConverter.FromUnstructured(parentBackupObj.Object, parentBackup); err != nil { 209 return err 210 } 211 if parentBackup.Status.Phase != dpv1alpha1.BackupPhaseCompleted { 212 return fmt.Errorf("parent backup %s is not completed", o.ParentBackupName) 213 } 214 if parentBackup.Labels[constant.AppInstanceLabelKey] != o.Name { 215 return fmt.Errorf("parent backup %s is not belong to cluster %s", o.ParentBackupName, o.Name) 216 } 217 } 218 return nil 219 } 220 221 // completeDefaultBackupPolicy completes the default backup policy. 222 func (o *CreateBackupOptions) completeDefaultBackupPolicy() error { 223 defaultBackupPolicyName, err := o.getDefaultBackupPolicy() 224 if err != nil { 225 return err 226 } 227 o.BackupPolicy = defaultBackupPolicyName 228 return nil 229 } 230 231 func (o *CreateBackupOptions) getDefaultBackupPolicy() (string, error) { 232 clusterObj, err := o.Dynamic.Resource(types.ClusterGVR()).Namespace(o.Namespace).Get(context.TODO(), o.Name, metav1.GetOptions{}) 233 if err != nil { 234 return "", err 235 } 236 237 // TODO: support multiple components backup, add --componentDef flag 238 opts := metav1.ListOptions{ 239 LabelSelector: fmt.Sprintf("%s=%s", 240 constant.AppInstanceLabelKey, clusterObj.GetName()), 241 } 242 objs, err := o.Dynamic. 243 Resource(types.BackupPolicyGVR()).Namespace(o.Namespace). 244 List(context.TODO(), opts) 245 if err != nil { 246 return "", err 247 } 248 if len(objs.Items) == 0 { 249 return "", fmt.Errorf(`not found any backup policy for cluster "%s"`, o.Name) 250 } 251 var defaultBackupPolicies []unstructured.Unstructured 252 for _, obj := range objs.Items { 253 if obj.GetAnnotations()[dptypes.DefaultBackupPolicyAnnotationKey] == annotationTrueValue { 254 defaultBackupPolicies = append(defaultBackupPolicies, obj) 255 } 256 } 257 if len(defaultBackupPolicies) == 0 { 258 return "", fmt.Errorf(`not found any default backup policy for cluster "%s"`, o.Name) 259 } 260 if len(defaultBackupPolicies) > 1 { 261 return "", fmt.Errorf(`cluster "%s" has multiple default backup policies`, o.Name) 262 } 263 return defaultBackupPolicies[0].GetName(), nil 264 } 265 266 func NewCreateBackupCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 267 customOutPut := func(opt *create.CreateOptions) { 268 output := fmt.Sprintf("Backup %s created successfully, you can view the progress:", opt.Name) 269 printer.PrintLine(output) 270 nextLine := fmt.Sprintf("\tkbcli cluster list-backups --name=%s -n %s", opt.Name, opt.Namespace) 271 printer.PrintLine(nextLine) 272 } 273 274 o := &CreateBackupOptions{ 275 CreateOptions: create.CreateOptions{ 276 IOStreams: streams, 277 Factory: f, 278 GVR: types.BackupGVR(), 279 CueTemplateName: "backup_template.cue", 280 CustomOutPut: customOutPut, 281 }, 282 } 283 o.CreateOptions.Options = o 284 285 cmd := &cobra.Command{ 286 Use: "backup NAME", 287 Short: "Create a backup for the cluster.", 288 Example: createBackupExample, 289 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), 290 Run: func(cmd *cobra.Command, args []string) { 291 o.Args = args 292 cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) 293 cmdutil.CheckErr(o.CompleteBackup()) 294 cmdutil.CheckErr(o.Validate()) 295 cmdutil.CheckErr(o.Run()) 296 }, 297 } 298 299 cmd.Flags().StringVar(&o.BackupMethod, "method", "", "Backup methods are defined in backup policy (required), if only one backup method in backup policy, use it as default backup method, if multiple backup methods in backup policy, use method which volume snapshot is true as default backup method") 300 cmd.Flags().StringVar(&o.BackupName, "name", "", "Backup name") 301 cmd.Flags().StringVar(&o.BackupPolicy, "policy", "", "Backup policy name, if not specified, use the cluster default backup policy") 302 cmd.Flags().StringVar(&o.DeletionPolicy, "deletion-policy", "Delete", "Deletion policy for backup, determine whether the backup content in backup repo will be deleted after the backup is deleted, supported values: [Delete, Retain]") 303 cmd.Flags().StringVar(&o.RetentionPeriod, "retention-period", "", "Retention period for backup, supported values: [1y, 1mo, 1d, 1h, 1m] or combine them [1y1mo1d1h1m], if not specified, the backup will not be automatically deleted, you need to manually delete it.") 304 cmd.Flags().StringVar(&o.ParentBackupName, "parent-backup", "", "Parent backup name, used for incremental backup") 305 // register backup flag completion func 306 o.RegisterBackupFlagCompletionFunc(cmd, f) 307 return cmd 308 } 309 func (o *CreateBackupOptions) RegisterBackupFlagCompletionFunc(cmd *cobra.Command, f cmdutil.Factory) { 310 getClusterName := func(cmd *cobra.Command, args []string) string { 311 clusterName, _ := cmd.Flags().GetString("cluster") 312 if clusterName != "" { 313 return clusterName 314 } 315 if len(args) > 0 { 316 return args[0] 317 } 318 return "" 319 } 320 util.CheckErr(cmd.RegisterFlagCompletionFunc( 321 "deletion-policy", 322 func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 323 return []string{string(dpv1alpha1.BackupDeletionPolicyRetain), string(dpv1alpha1.BackupDeletionPolicyDelete)}, cobra.ShellCompDirectiveNoFileComp 324 })) 325 326 util.CheckErr(cmd.RegisterFlagCompletionFunc( 327 "policy", 328 func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 329 label := fmt.Sprintf("%s=%s", constant.AppInstanceLabelKey, getClusterName(cmd, args)) 330 return util.CompGetResourceWithLabels(f, cmd, util.GVRToString(types.BackupPolicyGVR()), []string{label}, toComplete), cobra.ShellCompDirectiveNoFileComp 331 })) 332 333 util.CheckErr(cmd.RegisterFlagCompletionFunc( 334 "method", 335 func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 336 namespace, _ := cmd.Flags().GetString("namespace") 337 if namespace == "" { 338 namespace, _, _ = f.ToRawKubeConfigLoader().Namespace() 339 } 340 var ( 341 labelSelector string 342 clusterName = getClusterName(cmd, args) 343 ) 344 if clusterName != "" { 345 labelSelector = fmt.Sprintf("%s=%s", constant.AppInstanceLabelKey, clusterName) 346 } 347 dynamicClient, _ := f.DynamicClient() 348 objs, _ := dynamicClient.Resource(types.BackupPolicyGVR()).Namespace(namespace).List(context.TODO(), metav1.ListOptions{ 349 LabelSelector: labelSelector, 350 }) 351 methodMap := map[string]struct{}{} 352 for _, v := range objs.Items { 353 backupPolicy := &dpv1alpha1.BackupPolicy{} 354 _ = runtime.DefaultUnstructuredConverter.FromUnstructured(v.Object, backupPolicy) 355 for _, m := range backupPolicy.Spec.BackupMethods { 356 methodMap[m.Name] = struct{}{} 357 } 358 } 359 return maps.Keys(methodMap), cobra.ShellCompDirectiveNoFileComp 360 })) 361 } 362 363 func PrintBackupList(o ListBackupOptions) error { 364 var backupNameMap = make(map[string]bool) 365 for _, name := range o.Names { 366 backupNameMap[name] = true 367 } 368 369 // if format is JSON or YAML, use default printer to output the result. 370 if o.Format == printer.JSON || o.Format == printer.YAML { 371 if o.BackupName != "" { 372 o.Names = []string{o.BackupName} 373 } 374 _, err := o.Run() 375 return err 376 } 377 dynamic, err := o.Factory.DynamicClient() 378 if err != nil { 379 return err 380 } 381 if o.AllNamespaces { 382 o.Namespace = "" 383 } 384 backupList, err := dynamic.Resource(types.BackupGVR()).Namespace(o.Namespace).List(context.TODO(), metav1.ListOptions{ 385 LabelSelector: o.LabelSelector, 386 FieldSelector: o.FieldSelector, 387 }) 388 if err != nil { 389 return err 390 } 391 392 if len(backupList.Items) == 0 { 393 o.PrintNotFoundResources() 394 return nil 395 } 396 397 // sort the unstructured objects with the creationTimestamp in positive order 398 sort.Sort(unstructuredList(backupList.Items)) 399 tbl := printer.NewTablePrinter(o.Out) 400 tbl.SetHeader("NAME", "NAMESPACE", "SOURCE-CLUSTER", "METHOD", "STATUS", "TOTAL-SIZE", "DURATION", "CREATE-TIME", "COMPLETION-TIME", "EXPIRATION") 401 for _, obj := range backupList.Items { 402 backup := &dpv1alpha1.Backup{} 403 if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, backup); err != nil { 404 return err 405 } 406 // TODO(ldm): find cluster from backup policy target spec. 407 sourceCluster := backup.Labels[constant.AppInstanceLabelKey] 408 durationStr := "" 409 if backup.Status.Duration != nil { 410 durationStr = duration.HumanDuration(backup.Status.Duration.Duration) 411 } 412 statusString := string(backup.Status.Phase) 413 if len(o.Names) > 0 && !backupNameMap[backup.Name] { 414 continue 415 } 416 tbl.AddRow(backup.Name, backup.Namespace, sourceCluster, backup.Spec.BackupMethod, statusString, backup.Status.TotalSize, 417 durationStr, util.TimeFormat(&backup.CreationTimestamp), util.TimeFormat(backup.Status.CompletionTimestamp), 418 util.TimeFormat(backup.Status.Expiration)) 419 } 420 tbl.Print() 421 return nil 422 } 423 424 func NewListBackupCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 425 o := &ListBackupOptions{ListOptions: list.NewListOptions(f, streams, types.BackupGVR())} 426 cmd := &cobra.Command{ 427 Use: "list-backups", 428 Short: "List backups.", 429 Aliases: []string{"ls-backups"}, 430 Example: listBackupExample, 431 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), 432 Run: func(cmd *cobra.Command, args []string) { 433 o.LabelSelector = util.BuildLabelSelectorByNames(o.LabelSelector, args) 434 if o.BackupName != "" { 435 o.Names = []string{o.BackupName} 436 } 437 cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) 438 util.CheckErr(o.Complete()) 439 util.CheckErr(PrintBackupList(*o)) 440 }, 441 } 442 o.AddFlags(cmd) 443 cmd.Flags().StringVar(&o.BackupName, "name", "", "The backup name to get the details.") 444 return cmd 445 } 446 447 func NewDescribeBackupCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 448 o := &DescribeBackupOptions{ 449 Factory: f, 450 IOStreams: streams, 451 Gvr: types.BackupGVR(), 452 } 453 cmd := &cobra.Command{ 454 Use: "describe-backup BACKUP-NAME", 455 Short: "Describe a backup.", 456 Aliases: []string{"desc-backup"}, 457 Example: describeBackupExample, 458 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.BackupGVR()), 459 Run: func(cmd *cobra.Command, args []string) { 460 cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) 461 util.CheckErr(o.Complete(args)) 462 util.CheckErr(o.Run()) 463 }, 464 } 465 return cmd 466 } 467 468 func NewDeleteBackupCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 469 o := delete.NewDeleteOptions(f, streams, types.BackupGVR()) 470 cmd := &cobra.Command{ 471 Use: "delete-backup", 472 Short: "Delete a backup.", 473 Example: deleteBackupExample, 474 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), 475 Run: func(cmd *cobra.Command, args []string) { 476 cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) 477 util.CheckErr(completeForDeleteBackup(o, args)) 478 util.CheckErr(o.Run()) 479 }, 480 } 481 cmd.Flags().StringSliceVar(&o.Names, "name", []string{}, "Backup names") 482 o.AddFlags(cmd) 483 return cmd 484 } 485 486 // completeForDeleteBackup completes cmd for delete backup 487 func completeForDeleteBackup(o *delete.DeleteOptions, args []string) error { 488 if len(args) == 0 { 489 return errors.New("Missing cluster name") 490 } 491 if len(args) > 1 { 492 return errors.New("Only supported delete the Backup of one cluster") 493 } 494 if !o.Force && len(o.Names) == 0 { 495 return errors.New("Missing --name as backup name.") 496 } 497 if o.Force && len(o.Names) == 0 { 498 // do force action, for --force and --name unset, delete all backups of the cluster 499 // if backup name unset and cluster name set, delete all backups of the cluster 500 o.LabelSelector = util.BuildLabelSelectorByNames(o.LabelSelector, args) 501 o.ConfirmedNames = args 502 } 503 o.ConfirmedNames = o.Names 504 return nil 505 } 506 507 type CreateRestoreOptions struct { 508 // backup name to restore in creation 509 Backup string `json:"backup,omitempty"` 510 511 // point in time recovery args 512 RestoreTime *time.Time `json:"restoreTime,omitempty"` 513 RestoreTimeStr string `json:"restoreTimeStr,omitempty"` 514 VolumeRestorePolicy string `json:"volumeRestorePolicy,omitempty"` 515 516 create.CreateOptions `json:"-"` 517 } 518 519 func (o *CreateRestoreOptions) getClusterObject(backup *dpv1alpha1.Backup) (*appsv1alpha1.Cluster, error) { 520 // use the cluster snapshot to restore firstly 521 clusterString, ok := backup.Annotations[constant.ClusterSnapshotAnnotationKey] 522 if ok { 523 clusterObj := &appsv1alpha1.Cluster{} 524 if err := json.Unmarshal([]byte(clusterString), &clusterObj); err != nil { 525 return nil, err 526 } 527 return clusterObj, nil 528 } 529 clusterName := backup.Labels[constant.AppInstanceLabelKey] 530 return cluster.GetClusterByName(o.Dynamic, clusterName, o.Namespace) 531 } 532 533 func (o *CreateRestoreOptions) Run() error { 534 if o.Backup != "" { 535 return o.runRestoreFromBackup() 536 } 537 return nil 538 } 539 540 func (o *CreateRestoreOptions) runRestoreFromBackup() error { 541 // get backup 542 backup := &dpv1alpha1.Backup{} 543 if err := cluster.GetK8SClientObject(o.Dynamic, backup, types.BackupGVR(), o.Namespace, o.Backup); err != nil { 544 return err 545 } 546 if backup.Status.Phase != dpv1alpha1.BackupPhaseCompleted { 547 return errors.Errorf(`backup "%s" is not completed.`, backup.Name) 548 } 549 if len(backup.Labels[constant.AppInstanceLabelKey]) == 0 { 550 return errors.Errorf(`missing source cluster in backup "%s", "app.kubernetes.io/instance" is empty in labels.`, o.Backup) 551 } 552 553 restoreTimeStr, err := formatRestoreTimeAndValidate(o.RestoreTimeStr, backup) 554 if err != nil { 555 return err 556 } 557 // get the cluster object and set the annotation for restore 558 clusterObj, err := o.getClusterObject(backup) 559 if err != nil { 560 return err 561 } 562 restoreAnnotation, err := getRestoreFromBackupAnnotation(backup, o.VolumeRestorePolicy, len(clusterObj.Spec.ComponentSpecs), clusterObj.Spec.ComponentSpecs[0].Name, restoreTimeStr) 563 if err != nil { 564 return err 565 } 566 clusterObj.ObjectMeta = metav1.ObjectMeta{ 567 Namespace: clusterObj.Namespace, 568 Name: o.Name, 569 Annotations: map[string]string{constant.RestoreFromBackupAnnotationKey: restoreAnnotation}, 570 } 571 return o.createCluster(clusterObj) 572 } 573 574 func (o *CreateRestoreOptions) createCluster(cluster *appsv1alpha1.Cluster) error { 575 clusterGVR := types.ClusterGVR() 576 cluster.Status = appsv1alpha1.ClusterStatus{} 577 cluster.TypeMeta = metav1.TypeMeta{ 578 Kind: types.KindCluster, 579 APIVersion: clusterGVR.Group + "/" + clusterGVR.Version, 580 } 581 // convert the cluster object and create it. 582 unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&cluster) 583 if err != nil { 584 return err 585 } 586 unstructuredObj := &unstructured.Unstructured{Object: unstructuredMap} 587 if unstructuredObj, err = o.Dynamic.Resource(clusterGVR).Namespace(o.Namespace).Create(context.TODO(), unstructuredObj, metav1.CreateOptions{}); err != nil { 588 return err 589 } 590 if !o.Quiet { 591 fmt.Fprintf(o.Out, "%s %s created\n", unstructuredObj.GetKind(), unstructuredObj.GetName()) 592 } 593 return nil 594 } 595 596 func isTimeInRange(t time.Time, start time.Time, end time.Time) bool { 597 return !t.Before(start) && !t.After(end) 598 } 599 600 func (o *CreateRestoreOptions) Validate() error { 601 if o.Backup == "" { 602 return fmt.Errorf("must be specified one of the --backup ") 603 } 604 605 if o.Name == "" { 606 name, err := generateClusterName(o.Dynamic, o.Namespace) 607 if err != nil { 608 return err 609 } 610 if name == "" { 611 return fmt.Errorf("failed to generate a random cluster name") 612 } 613 o.Name = name 614 } 615 return nil 616 } 617 618 func NewCreateRestoreCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 619 o := &CreateRestoreOptions{} 620 o.CreateOptions = create.CreateOptions{ 621 IOStreams: streams, 622 Factory: f, 623 Options: o, 624 } 625 626 cmd := &cobra.Command{ 627 Use: "restore", 628 Short: "Restore a new cluster from backup.", 629 Example: createRestoreExample, 630 Run: func(cmd *cobra.Command, args []string) { 631 o.Args = args 632 cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) 633 util.CheckErr(o.Complete()) 634 util.CheckErr(o.Validate()) 635 util.CheckErr(o.Run()) 636 }, 637 } 638 cmd.Flags().StringVar(&o.Backup, "backup", "", "Backup name") 639 cmd.Flags().StringVar(&o.RestoreTimeStr, "restore-to-time", "", "point in time recovery(PITR)") 640 cmd.Flags().StringVar(&o.VolumeRestorePolicy, "volume-restore-policy", "Parallel", "the volume claim restore policy, supported values: [Serial, Parallel]") 641 return cmd 642 } 643 644 func NewListBackupPolicyCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 645 o := list.NewListOptions(f, streams, types.BackupPolicyGVR()) 646 cmd := &cobra.Command{ 647 Use: "list-backup-policy", 648 Short: "List backups policies.", 649 Aliases: []string{"list-bp"}, 650 Example: listBackupPolicyExample, 651 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), 652 Run: func(cmd *cobra.Command, args []string) { 653 o.LabelSelector = util.BuildLabelSelectorByNames(o.LabelSelector, args) 654 o.Names = nil 655 cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) 656 util.CheckErr(o.Complete()) 657 util.CheckErr(PrintBackupPolicyList(*o)) 658 }, 659 } 660 o.AddFlags(cmd) 661 return cmd 662 } 663 664 // PrintBackupPolicyList prints the backup policy list. 665 func PrintBackupPolicyList(o list.ListOptions) error { 666 var backupPolicyNameMap = make(map[string]bool) 667 for _, name := range o.Names { 668 backupPolicyNameMap[name] = true 669 } 670 671 // if format is JSON or YAML, use default printer to output the result. 672 if o.Format == printer.JSON || o.Format == printer.YAML { 673 _, err := o.Run() 674 return err 675 } 676 dynamic, err := o.Factory.DynamicClient() 677 if err != nil { 678 return err 679 } 680 if o.AllNamespaces { 681 o.Namespace = "" 682 } 683 backupPolicyList, err := dynamic.Resource(types.BackupPolicyGVR()).Namespace(o.Namespace).List(context.TODO(), metav1.ListOptions{ 684 LabelSelector: o.LabelSelector, 685 FieldSelector: o.FieldSelector, 686 }) 687 if err != nil { 688 return err 689 } 690 691 if len(backupPolicyList.Items) == 0 { 692 o.PrintNotFoundResources() 693 return nil 694 } 695 696 tbl := printer.NewTablePrinter(o.Out) 697 tbl.SetHeader("NAME", "NAMESPACE", "DEFAULT", "CLUSTER", "CREATE-TIME", "STATUS") 698 for _, obj := range backupPolicyList.Items { 699 defaultPolicy, ok := obj.GetAnnotations()[dptypes.DefaultBackupPolicyAnnotationKey] 700 backupPolicy := &dpv1alpha1.BackupPolicy{} 701 if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, backupPolicy); err != nil { 702 return err 703 } 704 if !ok { 705 defaultPolicy = "false" 706 } 707 if len(o.Names) > 0 && !backupPolicyNameMap[backupPolicy.Name] { 708 continue 709 } 710 createTime := obj.GetCreationTimestamp() 711 tbl.AddRow(obj.GetName(), obj.GetNamespace(), defaultPolicy, obj.GetLabels()[constant.AppInstanceLabelKey], 712 util.TimeFormat(&createTime), backupPolicy.Status.Phase) 713 } 714 tbl.Print() 715 return nil 716 } 717 718 type updateBackupPolicyFieldFunc func(backupPolicy *dpv1alpha1.BackupPolicy, targetVal string) error 719 720 type editBackupPolicyOptions struct { 721 namespace string 722 name string 723 dynamic dynamic.Interface 724 Factory cmdutil.Factory 725 726 GVR schema.GroupVersionResource 727 genericiooptions.IOStreams 728 editContent []editorRow 729 editContentKeyMap map[string]updateBackupPolicyFieldFunc 730 original string 731 target string 732 values []string 733 isTest bool 734 } 735 736 type editorRow struct { 737 // key content key (required). 738 key string 739 // value jsonpath for backupPolicy.spec. 740 jsonpath string 741 // updateFunc applies the modified value to backupPolicy (required). 742 updateFunc updateBackupPolicyFieldFunc 743 } 744 745 func NewEditBackupPolicyCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 746 o := editBackupPolicyOptions{Factory: f, IOStreams: streams, GVR: types.BackupPolicyGVR()} 747 cmd := &cobra.Command{ 748 Use: "edit-backup-policy", 749 DisableFlagsInUseLine: true, 750 Aliases: []string{"edit-bp"}, 751 Short: "Edit backup policy", 752 Example: editExample, 753 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.BackupPolicyGVR()), 754 Run: func(cmd *cobra.Command, args []string) { 755 cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) 756 cmdutil.CheckErr(o.complete(args)) 757 cmdutil.CheckErr(o.runEditBackupPolicy()) 758 }, 759 } 760 cmd.Flags().StringArrayVar(&o.values, "set", []string{}, 761 "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") 762 return cmd 763 } 764 765 func (o *editBackupPolicyOptions) complete(args []string) error { 766 var err error 767 if len(args) == 0 { 768 return fmt.Errorf("missing backupPolicy name") 769 } 770 if len(args) > 1 { 771 return fmt.Errorf("only support to update one backupPolicy or quote cronExpression") 772 } 773 o.name = args[0] 774 if o.namespace, _, err = o.Factory.ToRawKubeConfigLoader().Namespace(); err != nil { 775 return err 776 } 777 if o.dynamic, err = o.Factory.DynamicClient(); err != nil { 778 return err 779 } 780 updateRepoName := func(backupPolicy *dpv1alpha1.BackupPolicy, targetVal string) error { 781 // check if the backup repo exists 782 if targetVal != "" { 783 _, err := o.dynamic.Resource(types.BackupRepoGVR()).Get(context.Background(), targetVal, metav1.GetOptions{}) 784 if err != nil { 785 return err 786 } 787 } 788 if backupPolicy != nil { 789 if targetVal != "" { 790 backupPolicy.Spec.BackupRepoName = &targetVal 791 } else { 792 backupPolicy.Spec.BackupRepoName = nil 793 } 794 } 795 return nil 796 } 797 798 o.editContent = []editorRow{ 799 { 800 key: "backupRepoName", 801 jsonpath: "backupRepoName", 802 updateFunc: func(backupPolicy *dpv1alpha1.BackupPolicy, targetVal string) error { 803 return updateRepoName(backupPolicy, targetVal) 804 }, 805 }, 806 } 807 o.editContentKeyMap = map[string]updateBackupPolicyFieldFunc{} 808 for _, v := range o.editContent { 809 if v.updateFunc == nil { 810 return fmt.Errorf("updateFunc can not be nil") 811 } 812 o.editContentKeyMap[v.key] = v.updateFunc 813 } 814 return nil 815 } 816 817 func (o *editBackupPolicyOptions) runEditBackupPolicy() error { 818 backupPolicy := &dpv1alpha1.BackupPolicy{} 819 key := client.ObjectKey{ 820 Name: o.name, 821 Namespace: o.namespace, 822 } 823 err := util.GetResourceObjectFromGVR(types.BackupPolicyGVR(), key, o.dynamic, &backupPolicy) 824 if err != nil { 825 return err 826 } 827 if len(o.values) == 0 { 828 edited, err := o.runWithEditor(backupPolicy) 829 if err != nil { 830 return err 831 } 832 o.values = strings.Split(edited, "\n") 833 } 834 return o.applyChanges(backupPolicy) 835 } 836 837 func (o *editBackupPolicyOptions) runWithEditor(backupPolicy *dpv1alpha1.BackupPolicy) (string, error) { 838 editor := editor.NewDefaultEditor([]string{ 839 "KUBE_EDITOR", 840 "EDITOR", 841 }) 842 contents, err := o.buildEditorContent(backupPolicy) 843 if err != nil { 844 return "", err 845 } 846 addHeader := func() string { 847 return fmt.Sprintf(`# Please edit the object below. Lines beginning with a '#' will be ignored, 848 # and an empty file will abort the edit. If an error occurs while saving this file will be 849 # reopened with the relevant failures. 850 # 851 %s 852 `, *contents) 853 } 854 if o.isTest { 855 // only for testing 856 return "", nil 857 } 858 edited, _, err := editor.LaunchTempFile(fmt.Sprintf("%s-edit-", backupPolicy.Name), "", bytes.NewBufferString(addHeader())) 859 if err != nil { 860 return "", err 861 } 862 return string(edited), nil 863 } 864 865 // buildEditorContent builds the editor content. 866 func (o *editBackupPolicyOptions) buildEditorContent(backPolicy *dpv1alpha1.BackupPolicy) (*string, error) { 867 var contents []string 868 for _, v := range o.editContent { 869 // get the value with jsonpath 870 val, err := o.getValueWithJsonpath(backPolicy.Spec, v.jsonpath) 871 if err != nil { 872 return nil, err 873 } 874 if val == nil { 875 continue 876 } 877 row := fmt.Sprintf("%s=%s", v.key, *val) 878 o.original += row 879 contents = append(contents, row) 880 } 881 result := strings.Join(contents, "\n") 882 return &result, nil 883 } 884 885 // getValueWithJsonpath gets the value with jsonpath. 886 func (o *editBackupPolicyOptions) getValueWithJsonpath(spec dpv1alpha1.BackupPolicySpec, path string) (*string, error) { 887 parser := jsonpath.New("edit-backup-policy").AllowMissingKeys(true) 888 pathExpression, err := get.RelaxedJSONPathExpression(path) 889 if err != nil { 890 return nil, err 891 } 892 if err = parser.Parse(pathExpression); err != nil { 893 return nil, err 894 } 895 values, err := parser.FindResults(spec) 896 if err != nil { 897 return nil, err 898 } 899 for _, v := range values { 900 if len(v) == 0 { 901 continue 902 } 903 v1 := v[0] 904 switch v1.Kind() { 905 case reflect.Ptr, reflect.Interface: 906 if v1.IsNil() { 907 return nil, nil 908 } 909 val := fmt.Sprintf("%v", v1.Elem()) 910 return &val, nil 911 default: 912 val := fmt.Sprintf("%v", v1.Interface()) 913 return &val, nil 914 } 915 } 916 return nil, nil 917 } 918 919 // applyChanges applies the changes of backupPolicy. 920 func (o *editBackupPolicyOptions) applyChanges(backupPolicy *dpv1alpha1.BackupPolicy) error { 921 for _, v := range o.values { 922 row := strings.TrimSpace(v) 923 if strings.HasPrefix(row, "#") || row == "" { 924 continue 925 } 926 o.target += row 927 arr := strings.Split(row, "=") 928 if len(arr) != 2 { 929 return fmt.Errorf(`invalid row: %s, format should be "key=value"`, v) 930 } 931 updateFn, ok := o.editContentKeyMap[arr[0]] 932 if !ok { 933 return fmt.Errorf(`invalid key: %s`, arr[0]) 934 } 935 arr[1] = strings.Trim(arr[1], `"`) 936 arr[1] = strings.Trim(arr[1], `'`) 937 if err := updateFn(backupPolicy, arr[1]); err != nil { 938 return err 939 } 940 } 941 // if no changes, return. 942 if o.original == o.target { 943 fmt.Fprintln(o.Out, "updated (no change)") 944 return nil 945 } 946 obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(backupPolicy) 947 if err != nil { 948 return err 949 } 950 if _, err = o.dynamic.Resource(types.BackupPolicyGVR()).Namespace(backupPolicy.Namespace).Update(context.TODO(), 951 &unstructured.Unstructured{Object: obj}, metav1.UpdateOptions{}); err != nil { 952 return err 953 } 954 fmt.Fprintln(o.Out, "updated") 955 return nil 956 } 957 958 type DescribeBackupPolicyOptions struct { 959 namespace string 960 dynamic dynamic.Interface 961 Factory cmdutil.Factory 962 client clientset.Interface 963 964 LabelSelector string 965 ClusterNames []string 966 Names []string 967 968 genericiooptions.IOStreams 969 } 970 971 func (o *DescribeBackupPolicyOptions) Complete() error { 972 var err error 973 974 if o.client, err = o.Factory.KubernetesClientSet(); err != nil { 975 return err 976 } 977 978 if o.dynamic, err = o.Factory.DynamicClient(); err != nil { 979 return err 980 } 981 982 if o.namespace, _, err = o.Factory.ToRawKubeConfigLoader().Namespace(); err != nil { 983 return err 984 } 985 return nil 986 } 987 988 func (o *DescribeBackupPolicyOptions) Validate() error { 989 // must specify one of the cluster name or backup policy name 990 if len(o.ClusterNames) == 0 && len(o.Names) == 0 { 991 return fmt.Errorf("missing cluster name or backup policy name") 992 } 993 994 return nil 995 } 996 997 func (o *DescribeBackupPolicyOptions) Run() error { 998 var backupPolicyNameMap = make(map[string]bool) 999 for _, name := range o.Names { 1000 backupPolicyNameMap[name] = true 1001 } 1002 1003 backupPolicyList, err := o.dynamic.Resource(types.BackupPolicyGVR()).Namespace(o.namespace).List(context.TODO(), metav1.ListOptions{ 1004 LabelSelector: o.LabelSelector, 1005 }) 1006 if err != nil { 1007 return err 1008 } 1009 1010 if len(backupPolicyList.Items) == 0 { 1011 fmt.Fprintf(o.Out, "No backup policy found\n") 1012 return nil 1013 } 1014 1015 for _, obj := range backupPolicyList.Items { 1016 backupPolicy := &dpv1alpha1.BackupPolicy{} 1017 if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, backupPolicy); err != nil { 1018 return err 1019 } 1020 isDefault := obj.GetAnnotations()[dptypes.DefaultBackupPolicyAnnotationKey] == "true" 1021 // if backup policy name is specified, only print the backup policy with the specified name 1022 if len(o.Names) > 0 && !backupPolicyNameMap[backupPolicy.Name] { 1023 continue 1024 } 1025 // if backup policy name is not specified, only print the default backup policy 1026 if len(o.Names) == 0 && !isDefault { 1027 continue 1028 } 1029 if err := o.printBackupPolicyObj(backupPolicy); err != nil { 1030 return err 1031 } 1032 } 1033 1034 return nil 1035 } 1036 1037 func (o *DescribeBackupPolicyOptions) printBackupPolicyObj(obj *dpv1alpha1.BackupPolicy) error { 1038 printer.PrintLine("Summary:") 1039 realPrintPairStringToLine("Name", obj.Name) 1040 realPrintPairStringToLine("Cluster", obj.Labels[constant.AppInstanceLabelKey]) 1041 realPrintPairStringToLine("Namespace", obj.Namespace) 1042 realPrintPairStringToLine("Default", strconv.FormatBool(obj.Annotations[dptypes.DefaultBackupPolicyAnnotationKey] == "true")) 1043 if obj.Spec.BackupRepoName != nil { 1044 realPrintPairStringToLine("Backup Repo Name", *obj.Spec.BackupRepoName) 1045 } 1046 1047 printer.PrintLine("\nBackup Methods:") 1048 p := printer.NewTablePrinter(o.Out) 1049 p.SetHeader("Name", "ActionSet", "snapshot-volumes") 1050 for _, v := range obj.Spec.BackupMethods { 1051 p.AddRow(v.Name, v.ActionSetName, strconv.FormatBool(*v.SnapshotVolumes)) 1052 } 1053 p.Print() 1054 1055 return nil 1056 } 1057 1058 func NewDescribeBackupPolicyCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 1059 o := &DescribeBackupPolicyOptions{ 1060 Factory: f, 1061 IOStreams: streams, 1062 } 1063 cmd := &cobra.Command{ 1064 Use: "describe-backup-policy", 1065 Aliases: []string{"desc-backup-policy"}, 1066 Short: "Describe backup policy", 1067 Example: describeBackupPolicyExample, 1068 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), 1069 Run: func(cmd *cobra.Command, args []string) { 1070 o.ClusterNames = args 1071 o.LabelSelector = util.BuildLabelSelectorByNames(o.LabelSelector, args) 1072 cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) 1073 util.CheckErr(o.Complete()) 1074 util.CheckErr(o.Validate()) 1075 util.CheckErr(o.Run()) 1076 }, 1077 } 1078 cmd.Flags().StringSliceVar(&o.Names, "name", []string{}, "Backup policy name") 1079 return cmd 1080 } 1081 1082 func (o *DescribeBackupOptions) Complete(args []string) error { 1083 var err error 1084 1085 if len(args) == 0 { 1086 return fmt.Errorf("backup name should be specified") 1087 } 1088 1089 o.names = args 1090 1091 if o.client, err = o.Factory.KubernetesClientSet(); err != nil { 1092 return err 1093 } 1094 1095 if o.dynamic, err = o.Factory.DynamicClient(); err != nil { 1096 return err 1097 } 1098 1099 if o.namespace, _, err = o.Factory.ToRawKubeConfigLoader().Namespace(); err != nil { 1100 return err 1101 } 1102 return nil 1103 } 1104 1105 func (o *DescribeBackupOptions) Run() error { 1106 for _, name := range o.names { 1107 backupObj := &dpv1alpha1.Backup{} 1108 if err := cluster.GetK8SClientObject(o.dynamic, backupObj, o.Gvr, o.namespace, name); err != nil { 1109 return err 1110 } 1111 if err := o.printBackupObj(backupObj); err != nil { 1112 return err 1113 } 1114 } 1115 return nil 1116 } 1117 1118 func (o *DescribeBackupOptions) printBackupObj(obj *dpv1alpha1.Backup) error { 1119 targetCluster := obj.Labels[constant.AppInstanceLabelKey] 1120 printer.PrintLineWithTabSeparator( 1121 printer.NewPair("Name", obj.Name), 1122 printer.NewPair("Cluster", targetCluster), 1123 printer.NewPair("Namespace", obj.Namespace), 1124 ) 1125 printer.PrintLine("\nSpec:") 1126 realPrintPairStringToLine("Method", obj.Spec.BackupMethod) 1127 realPrintPairStringToLine("Policy Name", obj.Spec.BackupPolicyName) 1128 1129 printer.PrintLine("\nStatus:") 1130 realPrintPairStringToLine("Phase", string(obj.Status.Phase)) 1131 realPrintPairStringToLine("Total Size", obj.Status.TotalSize) 1132 if obj.Status.BackupMethod != nil { 1133 realPrintPairStringToLine("ActionSet Name", obj.Status.BackupMethod.ActionSetName) 1134 } 1135 if obj.Status.BackupRepoName != "" { 1136 realPrintPairStringToLine("Repository", obj.Status.BackupRepoName) 1137 } 1138 if obj.Status.PersistentVolumeClaimName != "" { 1139 realPrintPairStringToLine("PVC Name", obj.Status.PersistentVolumeClaimName) 1140 } 1141 if obj.Status.Duration != nil { 1142 realPrintPairStringToLine("Duration", duration.HumanDuration(obj.Status.Duration.Duration)) 1143 } 1144 realPrintPairStringToLine("Expiration Time", util.TimeFormat(obj.Status.Expiration)) 1145 realPrintPairStringToLine("Start Time", util.TimeFormat(obj.Status.StartTimestamp)) 1146 realPrintPairStringToLine("Completion Time", util.TimeFormat(obj.Status.CompletionTimestamp)) 1147 // print failure reason, ignore error 1148 _ = o.enhancePrintFailureReason(obj.Name, obj.Status.FailureReason) 1149 1150 realPrintPairStringToLine("Path", obj.Status.Path) 1151 1152 if obj.Status.TimeRange != nil { 1153 realPrintPairStringToLine("Time Range Start", util.TimeFormat(obj.Status.TimeRange.Start)) 1154 realPrintPairStringToLine("Time Range End", util.TimeFormat(obj.Status.TimeRange.End)) 1155 } 1156 1157 if len(obj.Status.VolumeSnapshots) > 0 { 1158 printer.PrintLine("\nVolume Snapshots:") 1159 for _, v := range obj.Status.VolumeSnapshots { 1160 realPrintPairStringToLine("Name", v.Name) 1161 realPrintPairStringToLine("Content Name", v.ContentName) 1162 realPrintPairStringToLine("Volume Name:", v.VolumeName) 1163 realPrintPairStringToLine("Size", v.Size) 1164 } 1165 } 1166 1167 // get all events about backup 1168 events, err := o.client.CoreV1().Events(o.namespace).Search(scheme.Scheme, obj) 1169 if err != nil { 1170 return err 1171 } 1172 1173 // print the warning events 1174 printer.PrintAllWarningEvents(events, o.Out) 1175 1176 return nil 1177 } 1178 1179 func realPrintPairStringToLine(name, value string, spaceCount ...int) { 1180 if value != "" { 1181 printer.PrintPairStringToLine(name, value, spaceCount...) 1182 } 1183 } 1184 1185 // print the pod error logs if failure reason has occurred 1186 // TODO: the failure reason should be improved in the backup controller 1187 func (o *DescribeBackupOptions) enhancePrintFailureReason(backupName, failureReason string, spaceCount ...int) error { 1188 if failureReason == "" { 1189 return nil 1190 } 1191 ctx := context.Background() 1192 // get the latest job log details. 1193 labels := fmt.Sprintf("%s=%s", 1194 dptypes.BackupNameLabelKey, backupName, 1195 ) 1196 jobList, err := o.client.BatchV1().Jobs("").List(ctx, metav1.ListOptions{LabelSelector: labels}) 1197 if err != nil { 1198 return err 1199 } 1200 var failedJob *batchv1.Job 1201 for _, i := range jobList.Items { 1202 if i.Status.Failed > 0 { 1203 failedJob = &i 1204 break 1205 } 1206 } 1207 if failedJob != nil { 1208 podLabels := fmt.Sprintf("%s=%s", 1209 "controller-uid", failedJob.UID, 1210 ) 1211 podList, err := o.client.CoreV1().Pods(failedJob.Namespace).List(ctx, metav1.ListOptions{LabelSelector: podLabels}) 1212 if err != nil { 1213 return err 1214 } 1215 if len(podList.Items) > 0 { 1216 tailLines := int64(5) 1217 req := o.client.CoreV1(). 1218 Pods(podList.Items[0].Namespace). 1219 GetLogs(podList.Items[0].Name, &corev1.PodLogOptions{TailLines: &tailLines}) 1220 data, err := req.DoRaw(ctx) 1221 if err != nil { 1222 return err 1223 } 1224 failureReason = fmt.Sprintf("%s\n pod %s error logs:\n%s", 1225 failureReason, podList.Items[0].Name, string(data)) 1226 } 1227 } 1228 printer.PrintPairStringToLine("Failure Reason", failureReason, spaceCount...) 1229 1230 return nil 1231 }