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  }