github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/migration/create.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 "fmt" 24 "strings" 25 "time" 26 27 "github.com/spf13/cobra" 28 v1 "k8s.io/api/core/v1" 29 "k8s.io/apimachinery/pkg/util/rand" 30 "k8s.io/cli-runtime/pkg/genericiooptions" 31 cmdutil "k8s.io/kubectl/pkg/cmd/util" 32 33 "github.com/1aal/kubeblocks/pkg/cli/create" 34 "github.com/1aal/kubeblocks/pkg/cli/types" 35 migrationv1 "github.com/1aal/kubeblocks/pkg/cli/types/migrationapi" 36 "github.com/1aal/kubeblocks/pkg/cli/util" 37 ) 38 39 var ( 40 AllStepsArr = []string{ 41 migrationv1.CliStepGlobal.String(), 42 migrationv1.CliStepPreCheck.String(), 43 migrationv1.CliStepCdc.String(), 44 migrationv1.CliStepInitStruct.String(), 45 migrationv1.CliStepInitData.String(), 46 } 47 ) 48 49 const ( 50 StringBoolTrue = "true" 51 StringBoolFalse = "false" 52 ) 53 54 type CreateMigrationOptions struct { 55 Template string `json:"template"` 56 TaskType string `json:"taskType,omitempty"` 57 Source string `json:"source"` 58 SourceEndpointModel EndpointModel `json:"sourceEndpointModel,omitempty"` 59 Sink string `json:"sink"` 60 SinkEndpointModel EndpointModel `json:"sinkEndpointModel,omitempty"` 61 MigrationObject []string `json:"migrationObject"` 62 MigrationObjectModel MigrationObjectModel `json:"migrationObjectModel,omitempty"` 63 Steps []string `json:"steps,omitempty"` 64 StepsModel []string `json:"stepsModel,omitempty"` 65 Tolerations []string `json:"tolerations,omitempty"` 66 TolerationModel map[string][]interface{} `json:"tolerationModel,omitempty"` 67 Resources []string `json:"resources,omitempty"` 68 ResourceModel map[string]interface{} `json:"resourceModel,omitempty"` 69 ServerID uint32 `json:"serverId,omitempty"` 70 create.CreateOptions `json:"-"` 71 } 72 73 func NewMigrationCreateCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { 74 o := &CreateMigrationOptions{ 75 CreateOptions: create.CreateOptions{ 76 Factory: f, 77 IOStreams: streams, 78 CueTemplateName: "migration_template.cue", 79 GVR: types.MigrationTaskGVR(), 80 }} 81 o.CreateOptions.Options = o 82 83 cmd := &cobra.Command{ 84 Use: "create NAME", 85 Short: "Create a migration task.", 86 Example: CreateTemplate, 87 ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.MigrationTaskGVR()), 88 Run: func(cmd *cobra.Command, args []string) { 89 o.Args = args 90 cmdutil.CheckErr(o.Complete()) 91 cmdutil.CheckErr(o.Validate()) 92 cmdutil.CheckErr(o.Run()) 93 }, 94 } 95 96 cmd.Flags().StringVar(&o.Template, "template", "", "Specify migration template, run \"kbcli migration templates\" to show all available migration templates") 97 cmd.Flags().StringVar(&o.Source, "source", "", "Set the source database information for migration.such as '{username}:{password}@{connection_address}:{connection_port}/[{database}]'") 98 cmd.Flags().StringVar(&o.Sink, "sink", "", "Set the sink database information for migration.such as '{username}:{password}@{connection_address}:{connection_port}/[{database}]") 99 cmd.Flags().StringSliceVar(&o.MigrationObject, "migration-object", []string{}, "Set the data objects that need to be migrated,such as '\"db1.table1\",\"db2\"'") 100 cmd.Flags().StringSliceVar(&o.Steps, "steps", []string{}, "Set up migration steps,such as: precheck=true,init-struct=true,init-data=true,cdc=true") 101 cmd.Flags().StringSliceVar(&o.Tolerations, "tolerations", []string{}, "Tolerations for migration, such as '\"key=engineType,value=pg,operator=Equal,effect=NoSchedule\"'") 102 cmd.Flags().StringSliceVar(&o.Resources, "resources", []string{}, "Resources limit for migration, such as '\"cpu=3000m,memory=3Gi\"'") 103 104 util.CheckErr(cmd.MarkFlagRequired("template")) 105 util.CheckErr(cmd.MarkFlagRequired("source")) 106 util.CheckErr(cmd.MarkFlagRequired("sink")) 107 util.CheckErr(cmd.MarkFlagRequired("migration-object")) 108 return cmd 109 } 110 111 func (o *CreateMigrationOptions) Validate() error { 112 var err error 113 114 if _, err = IsMigrationCrdValidWithDynamic(&o.Dynamic); err != nil { 115 PrintCrdInvalidError(err) 116 } 117 118 if o.Template == "" { 119 return fmt.Errorf("migration template is needed, use \"kbcli migration templates\" to check and special one") 120 } 121 122 errMsgArr := make([]string, 0) 123 // Source 124 o.SourceEndpointModel = EndpointModel{} 125 if err = o.SourceEndpointModel.BuildFromStr(&errMsgArr, o.Source); err != nil { 126 return err 127 } 128 // Sink 129 o.SinkEndpointModel = EndpointModel{} 130 if err = o.SinkEndpointModel.BuildFromStr(&errMsgArr, o.Sink); err != nil { 131 return err 132 } 133 134 // MigrationObject 135 if err = o.MigrationObjectModel.BuildFromStrs(&errMsgArr, o.MigrationObject); err != nil { 136 return err 137 } 138 139 // Steps & taskType 140 if err = o.BuildWithSteps(&errMsgArr); err != nil { 141 return err 142 } 143 144 // Tolerations 145 if err = o.BuildWithTolerations(); err != nil { 146 return err 147 } 148 149 // Resources 150 if err = o.BuildWithResources(); err != nil { 151 return err 152 } 153 154 // RuntimeParams 155 if err = o.BuildWithRuntimeParams(); err != nil { 156 return err 157 } 158 159 // Log errors if necessary 160 if len(errMsgArr) > 0 { 161 return fmt.Errorf(strings.Join(errMsgArr, ";\n")) 162 } 163 return nil 164 } 165 166 func (o *CreateMigrationOptions) BuildWithSteps(errMsgArr *[]string) error { 167 taskType := InitializationAndCdc.String() 168 validStepMap, validStepKey := CliStepChangeToStructure() 169 enableCdc, enablePreCheck, enableInitStruct, enableInitData := StringBoolTrue, StringBoolTrue, StringBoolTrue, StringBoolTrue 170 if len(o.Steps) > 0 { 171 for _, step := range o.Steps { 172 stepArr := strings.Split(step, "=") 173 if len(stepArr) != 2 { 174 BuildErrorMsg(errMsgArr, fmt.Sprintf("[%s] in steps setting is invalid", step)) 175 return nil 176 } 177 stepName := strings.ToLower(strings.TrimSpace(stepArr[0])) 178 enable := strings.ToLower(strings.TrimSpace(stepArr[1])) 179 if validStepMap[stepName] == "" { 180 BuildErrorMsg(errMsgArr, fmt.Sprintf("[%s] in steps settings is invalid, the name should be one of: (%s)", step, validStepKey)) 181 return nil 182 } 183 if enable != StringBoolTrue && enable != StringBoolFalse { 184 BuildErrorMsg(errMsgArr, fmt.Sprintf("[%s] in steps settings is invalid, the value should be one of: (true false)", step)) 185 return nil 186 } 187 switch stepName { 188 case migrationv1.CliStepCdc.String(): 189 enableCdc = enable 190 case migrationv1.CliStepPreCheck.String(): 191 enablePreCheck = enable 192 case migrationv1.CliStepInitStruct.String(): 193 enableInitStruct = enable 194 case migrationv1.CliStepInitData.String(): 195 enableInitData = enable 196 } 197 } 198 199 if enableInitData != StringBoolTrue { 200 BuildErrorMsg(errMsgArr, "step init-data is needed") 201 return nil 202 } 203 if enableCdc == StringBoolTrue { 204 taskType = InitializationAndCdc.String() 205 } else { 206 taskType = Initialization.String() 207 } 208 } 209 o.TaskType = taskType 210 o.StepsModel = []string{} 211 if enablePreCheck == StringBoolTrue { 212 o.StepsModel = append(o.StepsModel, migrationv1.StepPreCheck.String()) 213 } 214 if enableInitStruct == StringBoolTrue { 215 o.StepsModel = append(o.StepsModel, migrationv1.StepStructPreFullLoad.String()) 216 } 217 if enableInitData == StringBoolTrue { 218 o.StepsModel = append(o.StepsModel, migrationv1.StepFullLoad.String()) 219 } 220 return nil 221 } 222 223 func (o *CreateMigrationOptions) BuildWithTolerations() error { 224 o.TolerationModel = o.buildTolerationOrResources(o.Tolerations) 225 tmp := make([]interface{}, 0) 226 for _, step := range AllStepsArr { 227 if o.TolerationModel[step] == nil { 228 o.TolerationModel[step] = tmp 229 } 230 } 231 return nil 232 } 233 234 func (o *CreateMigrationOptions) BuildWithResources() error { 235 o.ResourceModel = make(map[string]interface{}) 236 for k, v := range o.buildTolerationOrResources(o.Resources) { 237 if len(v) >= 1 { 238 o.ResourceModel[k] = v[0] 239 } 240 } 241 for _, step := range AllStepsArr { 242 if o.ResourceModel[step] == nil { 243 o.ResourceModel[step] = v1.ResourceList{} 244 } 245 } 246 return nil 247 } 248 249 func (o *CreateMigrationOptions) BuildWithRuntimeParams() error { 250 template := migrationv1.MigrationTemplate{} 251 templateGvr := types.MigrationTemplateGVR() 252 if err := APIResource(&o.CreateOptions.Dynamic, &templateGvr, o.Template, "", &template); err != nil { 253 return err 254 } 255 256 // Generate random serverId for MySQL type database. Possible values are between 10001 and 2^32-10001 257 if template.Spec.Source.DBType == migrationv1.MigrationDBTypeMySQL { 258 o.ServerID = o.generateRandomMySQLServerID() 259 } else { 260 o.ServerID = 10001 261 } 262 263 return nil 264 } 265 266 func (o *CreateMigrationOptions) buildTolerationOrResources(raws []string) map[string][]interface{} { 267 results := make(map[string][]interface{}) 268 for _, raw := range raws { 269 step := migrationv1.CliStepGlobal.String() 270 tmpMap := map[string]interface{}{} 271 rawLoop: 272 for _, entries := range strings.Split(raw, ",") { 273 parts := strings.SplitN(entries, "=", 2) 274 k := strings.TrimSpace(parts[0]) 275 v := strings.TrimSpace(parts[1]) 276 if k == "step" { 277 switch v { 278 case migrationv1.CliStepPreCheck.String(), migrationv1.CliStepCdc.String(), migrationv1.CliStepInitStruct.String(), migrationv1.CliStepInitData.String(): 279 step = v 280 } 281 continue rawLoop 282 } 283 tmpMap[k] = v 284 } 285 results[step] = append(results[step], tmpMap) 286 } 287 return results 288 } 289 290 func (o *CreateMigrationOptions) generateRandomMySQLServerID() uint32 { 291 rand.Seed(time.Now().UnixNano()) 292 return uint32(rand.Int63nRange(10001, 1<<32-10001)) 293 }