github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/config/source_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 config
    15  
    16  import (
    17  	"bytes"
    18  	"context"
    19  	_ "embed"
    20  	"encoding/json"
    21  	"math"
    22  	"math/rand"
    23  	"os"
    24  	"path/filepath"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/BurntSushi/toml"
    29  	"github.com/go-mysql-org/go-mysql/mysql"
    30  	"github.com/pingcap/tiflow/dm/config/dbconfig"
    31  	"github.com/pingcap/tiflow/dm/pkg/conn"
    32  	tcontext "github.com/pingcap/tiflow/dm/pkg/context"
    33  	"github.com/pingcap/tiflow/dm/pkg/gtid"
    34  	"github.com/pingcap/tiflow/dm/pkg/log"
    35  	"github.com/pingcap/tiflow/dm/pkg/terror"
    36  	"github.com/pingcap/tiflow/dm/pkg/utils"
    37  	bf "github.com/pingcap/tiflow/pkg/binlog-filter"
    38  	"go.uber.org/zap"
    39  	"gopkg.in/yaml.v2"
    40  )
    41  
    42  const (
    43  	// the default base(min) server id generated by random.
    44  	defaultBaseServerID = math.MaxUint32 / 10
    45  	defaultRelayDir     = "relay-dir"
    46  )
    47  
    48  var getAllServerIDFunc = conn.GetAllServerID
    49  
    50  // SampleSourceConfig is sample config file of source.
    51  // The embed source.yaml is a copy of dm/master/source.yaml, because embed
    52  // can only match regular files in the current directory and subdirectories.
    53  //
    54  //go:embed source.yaml
    55  var SampleSourceConfig string
    56  
    57  // ObfuscatedPasswordForFeedback is the source encryption password that returns to the foreground.
    58  // PM's requirement, we always return obfuscated password to users.
    59  var ObfuscatedPasswordForFeedback string = "******"
    60  
    61  // PurgeConfig is the configuration for Purger.
    62  type PurgeConfig struct {
    63  	Interval    int64 `yaml:"interval" toml:"interval" json:"interval"`             // check whether need to purge at this @Interval (seconds)
    64  	Expires     int64 `yaml:"expires" toml:"expires" json:"expires"`                // if file's modified time is older than @Expires (hours), then it can be purged
    65  	RemainSpace int64 `yaml:"remain-space" toml:"remain-space" json:"remain-space"` // if remain space in @RelayBaseDir less than @RemainSpace (GB), then it can be purged
    66  }
    67  
    68  // SourceConfig is the configuration for source.
    69  type SourceConfig struct {
    70  	Enable     bool `yaml:"enable" toml:"enable" json:"enable"`
    71  	EnableGTID bool `yaml:"enable-gtid" toml:"enable-gtid" json:"enable-gtid"`
    72  	// deprecated
    73  	AutoFixGTID bool   `yaml:"auto-fix-gtid" toml:"auto-fix-gtid" json:"auto-fix-gtid"`
    74  	RelayDir    string `yaml:"relay-dir" toml:"relay-dir" json:"relay-dir"`
    75  	// deprecated
    76  	MetaDir string `yaml:"meta-dir" toml:"meta-dir" json:"meta-dir"`
    77  	Flavor  string `yaml:"flavor" toml:"flavor" json:"flavor"`
    78  	// deprecated
    79  	Charset string `yaml:"charset" toml:"charset" json:"charset"`
    80  
    81  	EnableRelay bool `yaml:"enable-relay" toml:"enable-relay" json:"enable-relay"`
    82  	// relay synchronous starting point (if specified)
    83  	RelayBinLogName string `yaml:"relay-binlog-name" toml:"relay-binlog-name" json:"relay-binlog-name"`
    84  	RelayBinlogGTID string `yaml:"relay-binlog-gtid" toml:"relay-binlog-gtid" json:"relay-binlog-gtid"`
    85  	// only use when the source is bound to a worker, do not marsh it
    86  	UUIDSuffix int `yaml:"-" toml:"-" json:"-"`
    87  
    88  	SourceID string            `yaml:"source-id" toml:"source-id" json:"source-id"`
    89  	From     dbconfig.DBConfig `yaml:"from" toml:"from" json:"from"`
    90  
    91  	// config items for purger
    92  	Purge PurgeConfig `yaml:"purge" toml:"purge" json:"purge"`
    93  
    94  	// config items for task status checker
    95  	Checker CheckerConfig `yaml:"checker" toml:"checker" json:"checker"`
    96  
    97  	// id of the worker on which this task run
    98  	ServerID uint32 `yaml:"server-id" toml:"server-id" json:"server-id"`
    99  
   100  	// deprecated tracer, to keep compatibility with older version
   101  	Tracer map[string]interface{} `yaml:"tracer" toml:"tracer" json:"-"`
   102  
   103  	CaseSensitive bool                  `yaml:"case-sensitive" toml:"case-sensitive" json:"case-sensitive"`
   104  	Filters       []*bf.BinlogEventRule `yaml:"filters" toml:"filters" json:"filters"`
   105  }
   106  
   107  // NewSourceConfig creates a new base config for upstream MySQL/MariaDB source.
   108  func NewSourceConfig() *SourceConfig {
   109  	c := newSourceConfig()
   110  	c.adjust()
   111  	return c
   112  }
   113  
   114  // NewSourceConfig creates a new base config without adjust.
   115  func newSourceConfig() *SourceConfig {
   116  	c := &SourceConfig{
   117  		Enable: true,
   118  		Purge: PurgeConfig{
   119  			Interval:    60 * 60,
   120  			Expires:     0,
   121  			RemainSpace: 15,
   122  		},
   123  		Checker: CheckerConfig{
   124  			CheckEnable:     true,
   125  			BackoffRollback: Duration{DefaultBackoffRollback},
   126  			BackoffMax:      Duration{DefaultBackoffMax},
   127  		},
   128  	}
   129  	return c
   130  }
   131  
   132  // Clone clones a config.
   133  func (c *SourceConfig) Clone() *SourceConfig {
   134  	clone := &SourceConfig{}
   135  	*clone = *c
   136  	return clone
   137  }
   138  
   139  // Toml returns TOML format representation of config.
   140  func (c *SourceConfig) Toml() (string, error) {
   141  	var b bytes.Buffer
   142  
   143  	err := toml.NewEncoder(&b).Encode(c)
   144  	if err != nil {
   145  		log.L().Error("fail to marshal config to toml", log.ShortError(err))
   146  	}
   147  
   148  	return b.String(), nil
   149  }
   150  
   151  // Yaml returns YAML format representation of config.
   152  func (c *SourceConfig) Yaml() (string, error) {
   153  	b, err := yaml.Marshal(c)
   154  	if err != nil {
   155  		log.L().Error("fail to marshal config to yaml", log.ShortError(err))
   156  	}
   157  
   158  	return string(b), nil
   159  }
   160  
   161  // FromToml parses flag definitions from the argument list.
   162  // accept toml content for legacy use (mainly used by etcd).
   163  func (c *SourceConfig) FromToml(content string) error {
   164  	// Parse first to get config file.
   165  	metaData, err := toml.Decode(content, c)
   166  	if err != nil {
   167  		return terror.ErrWorkerDecodeConfigFromFile.Delegate(err)
   168  	}
   169  	undecoded := metaData.Undecoded()
   170  	if len(undecoded) > 0 {
   171  		var undecodedItems []string
   172  		for _, item := range undecoded {
   173  			undecodedItems = append(undecodedItems, item.String())
   174  		}
   175  		return terror.ErrWorkerUndecodedItemFromFile.Generate(strings.Join(undecodedItems, ","))
   176  	}
   177  	c.adjust()
   178  	return c.Verify()
   179  }
   180  
   181  // SourceCfgFromYaml parses flag definitions from the argument list, content should be yaml format.
   182  func SourceCfgFromYaml(content string) (*SourceConfig, error) {
   183  	c := newSourceConfig()
   184  	if err := yaml.UnmarshalStrict([]byte(content), c); err != nil {
   185  		return nil, terror.ErrConfigYamlTransform.Delegate(err, "decode source config")
   186  	}
   187  	c.adjust()
   188  	return c, nil
   189  }
   190  
   191  // SourceCfgFromYamlAndVerify does SourceCfgFromYaml and Verify.
   192  func SourceCfgFromYamlAndVerify(content string) (*SourceConfig, error) {
   193  	c, err := SourceCfgFromYaml(content)
   194  	if err != nil {
   195  		return nil, err
   196  	}
   197  	if err = c.Verify(); err != nil {
   198  		return nil, err
   199  	}
   200  	return c, nil
   201  }
   202  
   203  // EncodeToml encodes config.
   204  func (c *SourceConfig) EncodeToml() (string, error) {
   205  	buf := new(bytes.Buffer)
   206  	if err := toml.NewEncoder(buf).Encode(c); err != nil {
   207  		return "", err
   208  	}
   209  	return buf.String(), nil
   210  }
   211  
   212  func (c *SourceConfig) String() string {
   213  	cfg, err := json.Marshal(c)
   214  	if err != nil {
   215  		log.L().Error("fail to marshal config to json", log.ShortError(err))
   216  	}
   217  	return string(cfg)
   218  }
   219  
   220  func (c *SourceConfig) adjust() {
   221  	c.From.Adjust()
   222  	c.Checker.Adjust()
   223  
   224  	if c.AutoFixGTID {
   225  		c.AutoFixGTID = false
   226  		log.L().Warn("auto-fix-gtid is deprecated, overwrite it to false")
   227  	}
   228  }
   229  
   230  // Verify verifies the config.
   231  func (c *SourceConfig) Verify() error {
   232  	if len(c.SourceID) == 0 {
   233  		return terror.ErrWorkerNeedSourceID.Generate()
   234  	}
   235  	if len(c.SourceID) > MaxSourceIDLength {
   236  		return terror.ErrWorkerTooLongSourceID.Generate(c.SourceID, MaxSourceIDLength)
   237  	}
   238  
   239  	var err error
   240  	if len(c.RelayBinLogName) > 0 {
   241  		if !utils.VerifyFilename(c.RelayBinLogName) {
   242  			return terror.ErrWorkerRelayBinlogName.Generate(c.RelayBinLogName)
   243  		}
   244  	}
   245  	if len(c.RelayBinlogGTID) > 0 {
   246  		_, err = gtid.ParserGTID(c.Flavor, c.RelayBinlogGTID)
   247  		if err != nil {
   248  			return terror.WithClass(terror.Annotatef(err, "relay-binlog-gtid %s", c.RelayBinlogGTID), terror.ClassDMWorker)
   249  		}
   250  	}
   251  
   252  	_, err = bf.NewBinlogEvent(c.CaseSensitive, c.Filters)
   253  	if err != nil {
   254  		return terror.ErrConfigBinlogEventFilter.Delegate(err)
   255  	}
   256  
   257  	if c.Checker.BackoffMax.Duration < c.Checker.BackoffMin.Duration {
   258  		return terror.ErrConfigCheckerMaxTooSmall.Generate(c.Checker.BackoffMax.Duration, c.Checker.BackoffMin.Duration)
   259  	}
   260  
   261  	return nil
   262  }
   263  
   264  // GetDecryptedClone returns a decrypted config replica in config.
   265  func (c *SourceConfig) GetDecryptedClone() *SourceConfig {
   266  	clone := c.Clone()
   267  	var pswdFrom string
   268  	if len(clone.From.Password) > 0 {
   269  		pswdFrom = utils.DecryptOrPlaintext(clone.From.Password)
   270  	}
   271  	clone.From.Password = pswdFrom
   272  	return clone
   273  }
   274  
   275  // GenerateDBConfig creates DBConfig for DB.
   276  func (c *SourceConfig) GenerateDBConfig() *dbconfig.DBConfig {
   277  	// decrypt password
   278  	clone := c.GetDecryptedClone()
   279  	from := &clone.From
   280  	from.RawDBCfg = dbconfig.DefaultRawDBConfig().SetReadTimeout(conn.DefaultDBTimeout.String())
   281  	return from
   282  }
   283  
   284  // Adjust flavor and server-id of SourceConfig.
   285  func (c *SourceConfig) Adjust(ctx context.Context, db *conn.BaseDB) (err error) {
   286  	c.From.Adjust()
   287  	c.Checker.Adjust()
   288  
   289  	// use one timeout for all following DB operations.
   290  	ctx2, cancel := context.WithTimeout(ctx, conn.DefaultDBTimeout)
   291  	defer cancel()
   292  	if c.Flavor == "" || c.ServerID == 0 {
   293  		err = c.AdjustFlavor(ctx2, db)
   294  		if err != nil {
   295  			return err
   296  		}
   297  
   298  		err = c.AdjustServerID(ctx2, db)
   299  		if err != nil {
   300  			return err
   301  		}
   302  	}
   303  
   304  	// MariaDB automatically enabled gtid after 10.0.2, refer to https://mariadb.com/kb/en/gtid/#using-global-transaction-ids
   305  	if c.EnableGTID && c.Flavor != mysql.MariaDBFlavor {
   306  		val, err := conn.GetGTIDMode(tcontext.NewContext(ctx2, log.L()), db)
   307  		if err != nil {
   308  			return err
   309  		}
   310  		if val != "ON" {
   311  			return terror.ErrSourceCheckGTID.Generate(c.SourceID, val)
   312  		}
   313  	}
   314  
   315  	if len(c.RelayDir) == 0 {
   316  		c.RelayDir = defaultRelayDir
   317  	}
   318  	if filepath.IsAbs(c.RelayDir) {
   319  		log.L().Warn("using an absolute relay path, relay log can't work when starting multiple relay worker")
   320  	}
   321  
   322  	return nil
   323  }
   324  
   325  // AdjustCaseSensitive adjust CaseSensitive from DB.
   326  func (c *SourceConfig) AdjustCaseSensitive(ctx context.Context, db *conn.BaseDB) (err error) {
   327  	caseSensitive, err2 := conn.GetDBCaseSensitive(ctx, db)
   328  	if err2 != nil {
   329  		return err2
   330  	}
   331  	c.CaseSensitive = caseSensitive
   332  	return nil
   333  }
   334  
   335  // AdjustFlavor adjust Flavor from DB.
   336  func (c *SourceConfig) AdjustFlavor(ctx context.Context, db *conn.BaseDB) (err error) {
   337  	if c.Flavor != "" {
   338  		switch c.Flavor {
   339  		case mysql.MariaDBFlavor, mysql.MySQLFlavor:
   340  			return nil
   341  		default:
   342  			return terror.ErrNotSupportedFlavor.Generate(c.Flavor)
   343  		}
   344  	}
   345  
   346  	c.Flavor, err = conn.GetFlavor(ctx, db)
   347  	if ctx.Err() != nil {
   348  		err = terror.Annotatef(err, "fail to get flavor info %v", ctx.Err())
   349  	}
   350  	return terror.WithScope(err, terror.ScopeUpstream)
   351  }
   352  
   353  // AdjustServerID adjust server id from DB.
   354  func (c *SourceConfig) AdjustServerID(ctx context.Context, db *conn.BaseDB) error {
   355  	if c.ServerID != 0 {
   356  		return nil
   357  	}
   358  
   359  	serverIDs, err := getAllServerIDFunc(tcontext.NewContext(ctx, log.L()), db)
   360  	if ctx.Err() != nil {
   361  		err = terror.Annotatef(err, "fail to get server-id info %v", ctx.Err())
   362  	}
   363  	if err != nil {
   364  		log.L().Error("failed to get server-id, will random choose a server-id for this source",
   365  			zap.Error(err))
   366  	}
   367  
   368  	rand.Seed(time.Now().UnixNano())
   369  	for i := 0; i < 5; i++ {
   370  		randomValue := uint32(rand.Intn(100000))
   371  		randomServerID := defaultBaseServerID + randomValue
   372  		if _, ok := serverIDs[randomServerID]; ok {
   373  			continue
   374  		}
   375  
   376  		c.ServerID = randomServerID
   377  		return nil
   378  	}
   379  
   380  	return terror.ErrInvalidServerID.Generatef("can't find a random available server ID")
   381  }
   382  
   383  // LoadFromFile loads config from file.
   384  func LoadFromFile(path string) (*SourceConfig, error) {
   385  	content, err := os.ReadFile(path)
   386  	if err != nil {
   387  		return nil, terror.ErrConfigReadCfgFromFile.Delegate(err, path)
   388  	}
   389  	return SourceCfgFromYaml(string(content))
   390  }
   391  
   392  // YamlForDowngrade returns YAML format represents of config for downgrade.
   393  func (c *SourceConfig) YamlForDowngrade() (string, error) {
   394  	s := NewSourceConfigForDowngrade(c)
   395  
   396  	// try to encrypt password
   397  	s.From.Password = utils.EncryptOrPlaintext(utils.DecryptOrPlaintext(c.From.Password))
   398  	s.omitDefaultVals()
   399  	return s.Yaml()
   400  }
   401  
   402  // SourceConfigForDowngrade is the base configuration for source in v2.0.
   403  // This config is used for downgrade(config export) from a higher dmctl version.
   404  // When we add any new config item into SourceConfig, we should update it also.
   405  type SourceConfigForDowngrade struct {
   406  	Enable          bool                   `yaml:"enable,omitempty"`
   407  	EnableGTID      bool                   `yaml:"enable-gtid"`
   408  	RelayDir        string                 `yaml:"relay-dir"`
   409  	Flavor          string                 `yaml:"flavor"`
   410  	Charset         string                 `yaml:"charset"`
   411  	EnableRelay     bool                   `yaml:"enable-relay"`
   412  	RelayBinLogName string                 `yaml:"relay-binlog-name"`
   413  	RelayBinlogGTID string                 `yaml:"relay-binlog-gtid"`
   414  	UUIDSuffix      int                    `yaml:"-"`
   415  	SourceID        string                 `yaml:"source-id"`
   416  	From            dbconfig.DBConfig      `yaml:"from"`
   417  	Purge           PurgeConfig            `yaml:"purge"`
   418  	Checker         CheckerConfig          `yaml:"checker"`
   419  	ServerID        uint32                 `yaml:"server-id"`
   420  	Tracer          map[string]interface{} `yaml:"tracer"`
   421  	// any new config item, we mark it omitempty
   422  	CaseSensitive bool                  `yaml:"case-sensitive,omitempty"`
   423  	Filters       []*bf.BinlogEventRule `yaml:"filters,omitempty"`
   424  }
   425  
   426  // NewSourceConfigForDowngrade creates a new base config for downgrade.
   427  func NewSourceConfigForDowngrade(sourceCfg *SourceConfig) *SourceConfigForDowngrade {
   428  	return &SourceConfigForDowngrade{
   429  		Enable:          sourceCfg.Enable,
   430  		EnableGTID:      sourceCfg.EnableGTID,
   431  		RelayDir:        sourceCfg.RelayDir,
   432  		Flavor:          sourceCfg.Flavor,
   433  		Charset:         sourceCfg.Charset,
   434  		EnableRelay:     sourceCfg.EnableRelay,
   435  		RelayBinLogName: sourceCfg.RelayBinLogName,
   436  		RelayBinlogGTID: sourceCfg.RelayBinlogGTID,
   437  		UUIDSuffix:      sourceCfg.UUIDSuffix,
   438  		SourceID:        sourceCfg.SourceID,
   439  		From:            sourceCfg.From,
   440  		Purge:           sourceCfg.Purge,
   441  		Checker:         sourceCfg.Checker,
   442  		ServerID:        sourceCfg.ServerID,
   443  		Tracer:          sourceCfg.Tracer,
   444  		CaseSensitive:   sourceCfg.CaseSensitive,
   445  		Filters:         sourceCfg.Filters,
   446  	}
   447  }
   448  
   449  // omitDefaultVals change default value to empty value for new config item.
   450  // If any default value for new config item is not empty(0 or false or nil),
   451  // we should change it to empty.
   452  func (c *SourceConfigForDowngrade) omitDefaultVals() {
   453  	c.Enable = false
   454  }
   455  
   456  // Yaml returns YAML format representation of the config.
   457  func (c *SourceConfigForDowngrade) Yaml() (string, error) {
   458  	b, err := yaml.Marshal(c)
   459  	return string(b), err
   460  }