github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/migration/describe.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package migration
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"io"
    26  	"sort"
    27  	"strconv"
    28  	"strings"
    29  	"time"
    30  
    31  	"github.com/spf13/cobra"
    32  	appv1 "k8s.io/api/apps/v1"
    33  	batchv1 "k8s.io/api/batch/v1"
    34  	v1 "k8s.io/api/core/v1"
    35  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    36  	"k8s.io/apimachinery/pkg/runtime/schema"
    37  	"k8s.io/cli-runtime/pkg/genericiooptions"
    38  	"k8s.io/client-go/dynamic"
    39  	clientset "k8s.io/client-go/kubernetes"
    40  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    41  
    42  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    43  	"github.com/1aal/kubeblocks/pkg/cli/types"
    44  	v1alpha1 "github.com/1aal/kubeblocks/pkg/cli/types/migrationapi"
    45  	"github.com/1aal/kubeblocks/pkg/cli/util"
    46  )
    47  
    48  var (
    49  	newTbl = func(out io.Writer, title string, header ...interface{}) *printer.TablePrinter {
    50  		fmt.Fprintln(out, title)
    51  		tbl := printer.NewTablePrinter(out)
    52  		tbl.SetHeader(header...)
    53  		return tbl
    54  	}
    55  )
    56  
    57  type describeOptions struct {
    58  	factory   cmdutil.Factory
    59  	client    clientset.Interface
    60  	dynamic   dynamic.Interface
    61  	namespace string
    62  
    63  	// resource type and names
    64  	gvr   schema.GroupVersionResource
    65  	names []string
    66  
    67  	*v1alpha1.MigrationObjects
    68  	genericiooptions.IOStreams
    69  }
    70  
    71  func newOptions(f cmdutil.Factory, streams genericiooptions.IOStreams) *describeOptions {
    72  	return &describeOptions{
    73  		factory:   f,
    74  		IOStreams: streams,
    75  		gvr:       types.MigrationTaskGVR(),
    76  	}
    77  }
    78  
    79  func NewMigrationDescribeCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
    80  	o := newOptions(f, streams)
    81  	cmd := &cobra.Command{
    82  		Use:               "describe NAME",
    83  		Short:             "Show details of a specific migration task.",
    84  		Example:           DescribeExample,
    85  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.MigrationTaskGVR()),
    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  func (o *describeOptions) complete(args []string) error {
    95  	var err error
    96  
    97  	if o.client, err = o.factory.KubernetesClientSet(); err != nil {
    98  		return err
    99  	}
   100  
   101  	if o.dynamic, err = o.factory.DynamicClient(); err != nil {
   102  		return err
   103  	}
   104  
   105  	if o.namespace, _, err = o.factory.ToRawKubeConfigLoader().Namespace(); err != nil {
   106  		return err
   107  	}
   108  
   109  	if _, err = IsMigrationCrdValidWithDynamic(&o.dynamic); err != nil {
   110  		PrintCrdInvalidError(err)
   111  	}
   112  
   113  	if len(args) == 0 {
   114  		return fmt.Errorf("migration task name should be specified")
   115  	}
   116  	o.names = args
   117  	return nil
   118  }
   119  
   120  func (o *describeOptions) run() error {
   121  	for _, name := range o.names {
   122  		if err := o.describeMigration(name); err != nil {
   123  			return err
   124  		}
   125  	}
   126  	return nil
   127  }
   128  
   129  func (o *describeOptions) describeMigration(name string) error {
   130  	var err error
   131  	if o.MigrationObjects, err = getMigrationObjects(o, name); err != nil {
   132  		return err
   133  	}
   134  
   135  	// MigrationTask Summary
   136  	showTaskSummary(o.Task, o.Out)
   137  
   138  	// MigrationTask Config
   139  	showTaskConfig(o.Task, o.Out)
   140  
   141  	// MigrationTemplate Summary
   142  	showTemplateSummary(o.Template, o.Out)
   143  
   144  	// Initialization Detail
   145  	showInitialization(o.Task, o.Template, o.Jobs, o.Out)
   146  
   147  	switch o.Task.Spec.TaskType {
   148  	case v1alpha1.InitializationAndCdc, v1alpha1.CDC:
   149  		// Cdc Detail
   150  		showCdc(o.StatefulSets, o.Pods, o.Out)
   151  
   152  		// Cdc Metrics
   153  		showCdcMetrics(o.Task, o.Out)
   154  	}
   155  
   156  	fmt.Fprintln(o.Out)
   157  
   158  	return nil
   159  }
   160  
   161  func getMigrationObjects(o *describeOptions, taskName string) (*v1alpha1.MigrationObjects, error) {
   162  	obj := &v1alpha1.MigrationObjects{
   163  		Task:     &v1alpha1.MigrationTask{},
   164  		Template: &v1alpha1.MigrationTemplate{},
   165  	}
   166  	var err error
   167  	taskGvr := types.MigrationTaskGVR()
   168  	if err = APIResource(&o.dynamic, &taskGvr, taskName, o.namespace, obj.Task); err != nil {
   169  		return nil, err
   170  	}
   171  	templateGvr := types.MigrationTemplateGVR()
   172  	if err = APIResource(&o.dynamic, &templateGvr, obj.Task.Spec.Template, "", obj.Template); err != nil {
   173  		return nil, err
   174  	}
   175  	listOpts := func() metav1.ListOptions {
   176  		return metav1.ListOptions{
   177  			LabelSelector: fmt.Sprintf("%s=%s", MigrationTaskLabel, taskName),
   178  		}
   179  	}
   180  	if obj.Jobs, err = o.client.BatchV1().Jobs(o.namespace).List(context.Background(), listOpts()); err != nil {
   181  		return nil, err
   182  	}
   183  	if obj.Pods, err = o.client.CoreV1().Pods(o.namespace).List(context.Background(), listOpts()); err != nil {
   184  		return nil, err
   185  	}
   186  	if obj.StatefulSets, err = o.client.AppsV1().StatefulSets(o.namespace).List(context.Background(), listOpts()); err != nil {
   187  		return nil, err
   188  	}
   189  	return obj, nil
   190  }
   191  
   192  func showTaskSummary(task *v1alpha1.MigrationTask, out io.Writer) {
   193  	if task == nil {
   194  		return
   195  	}
   196  	title := fmt.Sprintf("Name: %s\t Status: %s", task.Name, task.Status.TaskStatus)
   197  	tbl := newTbl(out, title, "NAMESPACE", "CREATED-TIME", "START-TIME", "FINISHED-TIME")
   198  	tbl.AddRow(task.Namespace, util.TimeFormatWithDuration(&task.CreationTimestamp, time.Second), util.TimeFormatWithDuration(task.Status.StartTime, time.Second), util.TimeFormatWithDuration(task.Status.FinishTime, time.Second))
   199  	tbl.Print()
   200  }
   201  
   202  func showTaskConfig(task *v1alpha1.MigrationTask, out io.Writer) {
   203  	if task == nil {
   204  		return
   205  	}
   206  	tbl := newTbl(out, "\nMigration Config:")
   207  	tbl.AddRow("source", fmt.Sprintf("%s:%s@%s/%s",
   208  		task.Spec.SourceEndpoint.UserName,
   209  		task.Spec.SourceEndpoint.Password,
   210  		task.Spec.SourceEndpoint.Address,
   211  		task.Spec.SourceEndpoint.DatabaseName,
   212  	))
   213  	tbl.AddRow("sink", fmt.Sprintf("%s:%s@%s/%s",
   214  		task.Spec.SinkEndpoint.UserName,
   215  		task.Spec.SinkEndpoint.Password,
   216  		task.Spec.SinkEndpoint.Address,
   217  		task.Spec.SinkEndpoint.DatabaseName,
   218  	))
   219  	tbl.AddRow("migration objects", task.Spec.MigrationObj.String(true))
   220  	tbl.Print()
   221  }
   222  
   223  func showTemplateSummary(template *v1alpha1.MigrationTemplate, out io.Writer) {
   224  	if template == nil {
   225  		return
   226  	}
   227  	title := fmt.Sprintf("\nTemplate: %s\t", template.Name)
   228  	tbl := newTbl(out, title, "DATABASE-MAPPING", "STATUS")
   229  	tbl.AddRow(template.Spec.Description, template.Status.Phase)
   230  	tbl.Print()
   231  }
   232  
   233  func showInitialization(task *v1alpha1.MigrationTask, template *v1alpha1.MigrationTemplate, jobList *batchv1.JobList, out io.Writer) {
   234  	if len(jobList.Items) == 0 {
   235  		return
   236  	}
   237  	sort.SliceStable(jobList.Items, func(i, j int) bool {
   238  		jobName1 := jobList.Items[i].Name
   239  		jobName2 := jobList.Items[j].Name
   240  		order1, _ := strconv.ParseInt(string([]byte(jobName1)[strings.LastIndex(jobName1, "-")+1:]), 10, 8)
   241  		order2, _ := strconv.ParseInt(string([]byte(jobName2)[strings.LastIndex(jobName2, "-")+1:]), 10, 8)
   242  		return order1 < order2
   243  	})
   244  	cliStepOrder := BuildInitializationStepsOrder(task, template)
   245  	tbl := newTbl(out, "\nInitialization:", "STEP", "NAMESPACE", "STATUS", "CREATED_TIME", "START-TIME", "FINISHED-TIME")
   246  	if len(cliStepOrder) != len(jobList.Items) {
   247  		return
   248  	}
   249  	for i, job := range jobList.Items {
   250  		tbl.AddRow(cliStepOrder[i], job.Namespace, getJobStatus(job.Status.Conditions), util.TimeFormatWithDuration(&job.CreationTimestamp, time.Second), util.TimeFormatWithDuration(job.Status.StartTime, time.Second), util.TimeFormatWithDuration(job.Status.CompletionTime, time.Second))
   251  	}
   252  	tbl.Print()
   253  }
   254  
   255  func showCdc(statefulSets *appv1.StatefulSetList, pods *v1.PodList, out io.Writer) {
   256  	if len(pods.Items) == 0 || len(statefulSets.Items) == 0 {
   257  		return
   258  	}
   259  	tbl := newTbl(out, "\nCdc:", "NAMESPACE", "STATUS", "CREATED_TIME", "START-TIME")
   260  	for _, pod := range pods.Items {
   261  		if pod.Annotations[MigrationTaskStepAnnotation] != v1alpha1.StepCdc.String() {
   262  			continue
   263  		}
   264  		tbl.AddRow(pod.Namespace, getCdcStatus(&statefulSets.Items[0], &pod), util.TimeFormatWithDuration(&pod.CreationTimestamp, time.Second), util.TimeFormatWithDuration(pod.Status.StartTime, time.Second))
   265  	}
   266  	tbl.Print()
   267  }
   268  
   269  func showCdcMetrics(task *v1alpha1.MigrationTask, out io.Writer) {
   270  	if task.Status.Cdc.Metrics == nil || len(task.Status.Cdc.Metrics) == 0 {
   271  		return
   272  	}
   273  	arr := make([]string, 0)
   274  	for mKey := range task.Status.Cdc.Metrics {
   275  		arr = append(arr, mKey)
   276  	}
   277  	sort.Strings(arr)
   278  	tbl := newTbl(out, "\nCdc Metrics:")
   279  	for _, k := range arr {
   280  		tbl.AddRow(k, task.Status.Cdc.Metrics[k])
   281  	}
   282  	tbl.Print()
   283  }
   284  
   285  func getJobStatus(conditions []batchv1.JobCondition) string {
   286  	if len(conditions) == 0 {
   287  		return "-"
   288  	} else {
   289  		return string(conditions[len(conditions)-1].Type)
   290  	}
   291  }
   292  
   293  func getCdcStatus(statefulSet *appv1.StatefulSet, cdcPod *v1.Pod) v1.PodPhase {
   294  	if cdcPod.Status.Phase == v1.PodRunning &&
   295  		statefulSet.Status.Replicas > statefulSet.Status.AvailableReplicas {
   296  		if time.Now().Unix()-statefulSet.CreationTimestamp.Time.Unix() < 10*60 {
   297  			return v1.PodPending
   298  		} else {
   299  			return v1.PodFailed
   300  		}
   301  	} else {
   302  		return cdcPod.Status.Phase
   303  	}
   304  }