github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/ctl/master/config.go (about) 1 // Copyright 2021 PingCAP, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package master 15 16 import ( 17 "context" 18 "encoding/json" 19 "os" 20 "path" 21 "sort" 22 "strings" 23 24 "github.com/pingcap/errors" 25 "github.com/pingcap/tiflow/dm/ctl/common" 26 "github.com/pingcap/tiflow/dm/pb" 27 "github.com/pingcap/tiflow/dm/pkg/ha" 28 "github.com/pingcap/tiflow/dm/pkg/utils" 29 "github.com/spf13/cobra" 30 clientv3 "go.etcd.io/etcd/client/v3" 31 "google.golang.org/protobuf/types/known/emptypb" 32 ) 33 34 var ( 35 taskDirname = "tasks" 36 sourceDirname = "sources" 37 relayWorkersFilename = "relay_workers.json" 38 yamlSuffix = ".yaml" 39 ) 40 41 // NewConfigCmd creates a Config command. 42 func NewConfigCmd() *cobra.Command { 43 cmd := &cobra.Command{ 44 Use: "config <command>", 45 Short: "manage config operations", 46 } 47 cmd.AddCommand( 48 newConfigTaskCmd(), 49 newConfigSourceCmd(), 50 newConfigMasterCmd(), 51 newConfigWorkerCmd(), 52 newExportCfgsCmd(), 53 newImportCfgsCmd(), 54 newConfigTaskTemplateCmd(), 55 ) 56 cmd.PersistentFlags().StringP("path", "p", "", "specify the file path to export/import`") 57 return cmd 58 } 59 60 func newConfigTaskTemplateCmd() *cobra.Command { 61 cmd := &cobra.Command{ 62 Use: "task-template [task-name]", 63 Short: "show task template which is created by WebUI with task config format", 64 RunE: func(cmd *cobra.Command, args []string) error { 65 if len(args) == 0 || len(args) > 1 { 66 return cmd.Help() 67 } 68 name := args[0] 69 output, err := cmd.Flags().GetString("path") 70 if err != nil { 71 return err 72 } 73 return sendGetConfigRequest(pb.CfgType_TaskTemplateType, name, output) 74 }, 75 } 76 return cmd 77 } 78 79 func newConfigTaskCmd() *cobra.Command { 80 cmd := &cobra.Command{ 81 Use: "task [task-name]", 82 Short: "manage or show task configs", 83 RunE: func(cmd *cobra.Command, args []string) error { 84 if len(args) == 0 || len(args) > 1 { 85 return cmd.Help() 86 } 87 name := args[0] 88 output, err := cmd.Flags().GetString("path") 89 if err != nil { 90 return err 91 } 92 return sendGetConfigRequest(pb.CfgType_TaskType, name, output) 93 }, 94 } 95 cmd.AddCommand( 96 newConfigTaskUpdateCmd(), 97 ) 98 return cmd 99 } 100 101 // FIXME: implement this later. 102 func newConfigTaskUpdateCmd() *cobra.Command { 103 cmd := &cobra.Command{ 104 Use: "update <command>", 105 Short: "update config task", 106 Hidden: true, 107 RunE: func(cmd *cobra.Command, args []string) error { 108 return errors.Errorf("this function will be supported later") 109 }, 110 } 111 return cmd 112 } 113 114 func newConfigSourceCmd() *cobra.Command { 115 cmd := &cobra.Command{ 116 Use: "source [source-name]", 117 Short: "manage or show source config", 118 RunE: configSourceList, 119 } 120 cmd.AddCommand( 121 newConfigSourceUpdateCmd(), 122 ) 123 return cmd 124 } 125 126 func configSourceList(cmd *cobra.Command, args []string) error { 127 if len(args) != 1 { 128 return cmd.Help() 129 } 130 name := args[0] 131 output, err := cmd.Flags().GetString("path") 132 if err != nil { 133 return err 134 } 135 return sendGetConfigRequest(pb.CfgType_SourceType, name, output) 136 } 137 138 // FIXME: implement this later. 139 func newConfigSourceUpdateCmd() *cobra.Command { 140 cmd := &cobra.Command{ 141 Use: "update <command>", 142 Short: "update config source", 143 Hidden: true, 144 RunE: func(cmd *cobra.Command, args []string) error { 145 return errors.Errorf("this function will be supported later") 146 }, 147 } 148 return cmd 149 } 150 151 func newConfigMasterCmd() *cobra.Command { 152 cmd := &cobra.Command{ 153 Use: "master [master-name]", 154 Short: "manage or show master configs", 155 RunE: configMasterList, 156 } 157 return cmd 158 } 159 160 func configMasterList(cmd *cobra.Command, args []string) error { 161 if len(args) != 1 { 162 return cmd.Help() 163 } 164 name := args[0] 165 output, err := cmd.Flags().GetString("path") 166 if err != nil { 167 return err 168 } 169 return sendGetConfigRequest(pb.CfgType_MasterType, name, output) 170 } 171 172 func newConfigWorkerCmd() *cobra.Command { 173 cmd := &cobra.Command{ 174 Use: "worker [worker-name]", 175 Short: "manage or show worker configs", 176 RunE: configWorkerList, 177 } 178 return cmd 179 } 180 181 func configWorkerList(cmd *cobra.Command, args []string) error { 182 if len(args) == 0 || len(args) > 1 { 183 return cmd.Help() 184 } 185 name := args[0] 186 output, err := cmd.Flags().GetString("path") 187 if err != nil { 188 return err 189 } 190 return sendGetConfigRequest(pb.CfgType_WorkerType, name, output) 191 } 192 193 // newExportCfgsCmd creates a exportCfg command. 194 func newExportCfgsCmd() *cobra.Command { 195 cmd := &cobra.Command{ 196 Use: "export", 197 Short: "Export the configurations of sources and tasks", 198 RunE: exportCfgsFunc, 199 } 200 cmd.Flags().StringP("dir", "d", "", "specify the configs directory, default is `./configs`") 201 _ = cmd.Flags().MarkHidden("dir") 202 return cmd 203 } 204 205 // newImportCfgsCmd creates a importCfg command. 206 func newImportCfgsCmd() *cobra.Command { 207 cmd := &cobra.Command{ 208 Use: "import", 209 Short: "Import the configurations of sources and tasks", 210 RunE: importCfgsFunc, 211 } 212 cmd.Flags().StringP("dir", "d", "", "specify the configs directory, default is `./configs`") 213 _ = cmd.Flags().MarkHidden("dir") 214 return cmd 215 } 216 217 // exportCfgsFunc exports configs. 218 func exportCfgsFunc(cmd *cobra.Command, args []string) error { 219 filePath, err := cmd.Flags().GetString("path") 220 if err != nil { 221 common.PrintLinesf("can not get path") 222 return err 223 } else if filePath == "" { 224 filePath, err = cmd.Flags().GetString("dir") 225 if err != nil { 226 common.PrintLinesf("can not get directory") 227 return err 228 } 229 } 230 if filePath == "" { 231 filePath = "configs" 232 } 233 234 // get all configs 235 sourceCfgContents, taskCfgContents, relayWorkersSet, err := getAllCfgs(common.GlobalCtlClient.EtcdClient) 236 if err != nil { 237 return err 238 } 239 // create directory 240 taskDir, sourceDir, err := createDirectory(filePath) 241 if err != nil { 242 return err 243 } 244 // write sourceCfg files 245 if err = writeSourceCfgs(sourceDir, sourceCfgContents); err != nil { 246 return err 247 } 248 // write taskCfg files 249 if err = writeTaskCfgs(taskDir, taskCfgContents); err != nil { 250 return err 251 } 252 // write relayWorkers 253 if err = writeRelayWorkers(path.Join(filePath, relayWorkersFilename), relayWorkersSet); err != nil { 254 return err 255 } 256 257 common.PrintLinesf("export configs to directory `%s` succeed", filePath) 258 return nil 259 } 260 261 // importCfgsFunc imports configs. 262 func importCfgsFunc(cmd *cobra.Command, args []string) error { 263 filePath, err := cmd.Flags().GetString("path") 264 if err != nil { 265 common.PrintLinesf("can not get path") 266 return err 267 } else if filePath == "" { 268 filePath, err = cmd.Flags().GetString("dir") 269 if err != nil { 270 common.PrintLinesf("can not get directory") 271 return err 272 } 273 } 274 if filePath == "" { 275 filePath = "configs" 276 } 277 278 sourceCfgs, taskCfgs, relayWorkers, err := collectCfgs(filePath) 279 if err != nil { 280 return err 281 } 282 283 ctx, cancel := context.WithCancel(context.Background()) 284 defer cancel() 285 if err := createSources(ctx, sourceCfgs); err != nil { 286 return err 287 } 288 if err := createTasks(ctx, taskCfgs); err != nil { 289 return err 290 } 291 if len(relayWorkers) > 0 { 292 common.PrintLinesf("The original relay workers have been exported to `%s`.", path.Join(filePath, relayWorkersFilename)) 293 common.PrintLinesf("Currently DM doesn't support recover relay workers. You may need to execute `transfer-source` and `start-relay` command manually.") 294 } 295 296 common.PrintLinesf("import configs from directory `%s` succeed", filePath) 297 return nil 298 } 299 300 func collectDirCfgs(dir string) ([]string, error) { 301 files, err := os.ReadDir(dir) 302 if err != nil { 303 return nil, err 304 } 305 306 cfgs := make([]string, 0, len(files)) 307 for _, f := range files { 308 cfg, err2 := common.GetFileContent(path.Join(dir, f.Name())) 309 if err2 != nil { 310 return nil, err2 311 } 312 cfgs = append(cfgs, string(cfg)) 313 } 314 return cfgs, nil 315 } 316 317 func getAllCfgs(cli *clientv3.Client) (map[string]string, map[string]string, map[string]map[string]struct{}, error) { 318 listSourceResp := &pb.ListSourceConfigsResponse{} 319 err2 := common.SendRequest( 320 context.Background(), 321 "ListSourceConfigs", 322 &emptypb.Empty{}, 323 &listSourceResp, 324 ) 325 if err2 != nil { 326 return nil, nil, nil, err2 327 } 328 if !listSourceResp.Result { 329 return nil, nil, nil, errors.New(listSourceResp.Msg) 330 } 331 332 listTaskResp := &pb.ListTaskConfigsResponse{} 333 err2 = common.SendRequest( 334 context.Background(), 335 "ListTaskConfigs", 336 &emptypb.Empty{}, 337 &listTaskResp, 338 ) 339 if err2 != nil { 340 return nil, nil, nil, err2 341 } 342 if !listTaskResp.Result { 343 return nil, nil, nil, errors.New(listTaskResp.Msg) 344 } 345 346 // get all relay configs. 347 relayWorkers, _, err := ha.GetAllRelayConfig(cli) 348 if err != nil { 349 common.PrintLinesf("can not get relay workers from etcd") 350 return nil, nil, nil, err 351 } 352 return listSourceResp.SourceConfigs, listTaskResp.TaskConfigs, relayWorkers, nil 353 } 354 355 func createDirectory(dir string) (string, string, error) { 356 taskDir := path.Join(dir, taskDirname) 357 if err := os.MkdirAll(taskDir, 0o700); err != nil { 358 common.PrintLinesf("can not create directory of task configs `%s`", taskDir) 359 return "", "", err 360 } 361 sourceDir := path.Join(dir, sourceDirname) 362 if err := os.MkdirAll(sourceDir, 0o700); err != nil { 363 common.PrintLinesf("can not create directory of source configs `%s`", sourceDir) 364 return "", "", err 365 } 366 return taskDir, sourceDir, nil 367 } 368 369 func writeSourceCfgs(sourceDir string, sourceCfgContents map[string]string) error { 370 for source, fileContent := range sourceCfgContents { 371 sourceFile := path.Join(sourceDir, source) 372 sourceFile += yamlSuffix 373 err := os.WriteFile(sourceFile, []byte(fileContent), 0o600) 374 if err != nil { 375 common.PrintLinesf("fail to write source config to file `%s`", sourceFile) 376 return err 377 } 378 } 379 return nil 380 } 381 382 func writeTaskCfgs(taskDir string, taskCfgContents map[string]string) error { 383 // from task => subtask to task => taskCfg 384 for task, content := range taskCfgContents { 385 taskFile := path.Join(taskDir, task) 386 taskFile += yamlSuffix 387 if err := os.WriteFile(taskFile, []byte(content), 0o600); err != nil { 388 common.PrintLinesf("can not write task config to file `%s`", taskFile) 389 return err 390 } 391 } 392 return nil 393 } 394 395 func writeRelayWorkers(relayWorkersFile string, relayWorkersSet map[string]map[string]struct{}) error { 396 if len(relayWorkersSet) == 0 { 397 return nil 398 } 399 400 // from source => workerSet to source => workerList 401 relayWorkers := make(map[string][]string, len(relayWorkersSet)) 402 for source, workerSet := range relayWorkersSet { 403 workers := make([]string, 0, len(workerSet)) 404 for worker := range workerSet { 405 workers = append(workers, worker) 406 } 407 sort.Strings(workers) 408 relayWorkers[source] = workers 409 } 410 411 content, err := json.Marshal(relayWorkers) 412 if err != nil { 413 common.PrintLinesf("fail to marshal relay workers") 414 return err 415 } 416 417 err = os.WriteFile(relayWorkersFile, content, 0o600) 418 if err != nil { 419 common.PrintLinesf("can not write relay workers to file `%s`", relayWorkersFile) 420 return err 421 } 422 return nil 423 } 424 425 func collectCfgs(dir string) (sourceCfgs []string, taskCfgs []string, relayWorkers map[string][]string, err error) { 426 var ( 427 sourceDir = path.Join(dir, sourceDirname) 428 taskDir = path.Join(dir, taskDirname) 429 relayWorkersFile = path.Join(dir, relayWorkersFilename) 430 content []byte 431 ) 432 if !utils.IsDirExists(dir) { 433 return nil, nil, nil, errors.Errorf("config directory `%s` not exists", dir) 434 } 435 436 if utils.IsDirExists(sourceDir) { 437 if sourceCfgs, err = collectDirCfgs(sourceDir); err != nil { 438 common.PrintLinesf("fail to collect source config files from source configs directory `%s`", sourceDir) 439 return 440 } 441 } 442 if utils.IsDirExists(taskDir) { 443 if taskCfgs, err = collectDirCfgs(taskDir); err != nil { 444 common.PrintLinesf("fail to collect task config files from task configs directory `%s`", taskDir) 445 return 446 } 447 } 448 if utils.IsFileExists(relayWorkersFile) { 449 content, err = common.GetFileContent(relayWorkersFile) 450 if err != nil { 451 common.PrintLinesf("fail to read relay workers config `%s`", relayWorkersFile) 452 return 453 } 454 err = json.Unmarshal(content, &relayWorkers) 455 if err != nil { 456 common.PrintLinesf("fail to unmarshal relay workers config `%s`", relayWorkersFile) 457 return 458 } 459 } 460 // nolint:nakedret 461 return 462 } 463 464 func createSources(ctx context.Context, sourceCfgs []string) error { 465 if len(sourceCfgs) == 0 { 466 return nil 467 } 468 common.PrintLinesf("start creating sources") 469 470 sourceResp := &pb.OperateSourceResponse{} 471 // Do not use batch for `operate-source start source1, source2` if we want to support idemponent config import. 472 // Because `operate-source start` will revert all batch sources if any source error. 473 // e.g. ErrSchedulerSourceCfgExist 474 for _, sourceCfg := range sourceCfgs { 475 err := common.SendRequest( 476 ctx, 477 "OperateSource", 478 &pb.OperateSourceRequest{ 479 Config: []string{sourceCfg}, 480 Op: pb.SourceOp_StartSource, 481 }, 482 &sourceResp, 483 ) 484 if err != nil { 485 common.PrintLinesf("fail to create sources") 486 return err 487 } 488 489 if !sourceResp.Result && !strings.Contains(sourceResp.Msg, "already exist") { 490 common.PrettyPrintResponse(sourceResp) 491 return errors.Errorf("fail to create sources") 492 } 493 } 494 return nil 495 } 496 497 func createTasks(ctx context.Context, taskCfgs []string) error { 498 if len(taskCfgs) == 0 { 499 return nil 500 } 501 common.PrintLinesf("start creating tasks") 502 503 taskResp := &pb.StartTaskResponse{} 504 for _, taskCfg := range taskCfgs { 505 err := common.SendRequest( 506 ctx, 507 "StartTask", 508 &pb.StartTaskRequest{ 509 Task: taskCfg, 510 }, 511 &taskResp, 512 ) 513 if err != nil { 514 common.PrintLinesf("fail to create tasks") 515 return err 516 } 517 if !taskResp.Result && !strings.Contains(taskResp.Msg, "already exist") { 518 common.PrettyPrintResponse(taskResp) 519 return errors.Errorf("fail to create tasks") 520 } 521 } 522 return nil 523 }