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 }