github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/master/config.go (about) 1 // Copyright 2019 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 "bytes" 18 _ "embed" 19 "encoding/hex" 20 "encoding/json" 21 "flag" 22 "fmt" 23 "net" 24 "net/url" 25 "os" 26 "strings" 27 "time" 28 29 "github.com/BurntSushi/toml" 30 "github.com/pingcap/tiflow/dm/config/security" 31 "github.com/pingcap/tiflow/dm/pkg/log" 32 "github.com/pingcap/tiflow/dm/pkg/terror" 33 "github.com/pingcap/tiflow/dm/pkg/utils" 34 "github.com/pingcap/tiflow/pkg/version" 35 "go.etcd.io/etcd/server/v3/embed" 36 "go.uber.org/zap" 37 ) 38 39 const ( 40 defaultRPCTimeout = "30s" 41 defaultNamePrefix = "dm-master" 42 defaultDataDirPrefix = "default" 43 defaultPeerUrls = "http://127.0.0.1:8291" 44 defaultInitialClusterState = embed.ClusterStateFlagNew 45 defaultAutoCompactionMode = "periodic" 46 defaultAutoCompactionRetention = "1h" 47 defaultMaxTxnOps = 2048 48 defaultQuotaBackendBytes = 2 * 1024 * 1024 * 1024 // 2GB 49 quotaBackendBytesLowerBound = 500 * 1024 * 1024 // 500MB 50 ) 51 52 // SampleConfig is sample config of dm-master. 53 // 54 //go:embed dm-master.toml 55 var SampleConfig string 56 57 // NewConfig creates a config for dm-master. 58 func NewConfig() *Config { 59 cfg := &Config{} 60 cfg.flagSet = flag.NewFlagSet("dm-master", flag.ContinueOnError) 61 fs := cfg.flagSet 62 63 fs.BoolVar(&cfg.printVersion, "V", false, "prints version and exit") 64 fs.BoolVar(&cfg.printSampleConfig, "print-sample-config", false, "print sample config file of dm-worker") 65 fs.BoolVar(&cfg.OpenAPI, "openapi", false, "enable openapi") 66 fs.StringVar(&cfg.ConfigFile, "config", "", "path to config file") 67 fs.StringVar(&cfg.MasterAddr, "master-addr", "", "master API server and status addr") 68 fs.StringVar(&cfg.AdvertiseAddr, "advertise-addr", "", `advertise address for client traffic (default "${master-addr}")`) 69 fs.StringVar(&cfg.LogLevel, "L", "info", "log level: debug, info, warn, error, fatal") 70 fs.StringVar(&cfg.LogFile, "log-file", "", "log file path") 71 fs.StringVar(&cfg.LogFormat, "log-format", "text", `the format of the log, "text" or "json"`) 72 // fs.StringVar(&cfg.LogRotate, "log-rotate", "day", "log file rotate type, hour/day") 73 74 fs.StringVar(&cfg.Name, "name", "", "human-readable name for this DM-master member") 75 fs.StringVar(&cfg.DataDir, "data-dir", "", `path to the data directory (default "default.${name}")`) 76 fs.StringVar(&cfg.InitialCluster, "initial-cluster", "", fmt.Sprintf("initial cluster configuration for bootstrapping, e.g. dm-master=%s", defaultPeerUrls)) 77 fs.StringVar(&cfg.PeerUrls, "peer-urls", defaultPeerUrls, "URLs for peer traffic") 78 fs.StringVar(&cfg.AdvertisePeerUrls, "advertise-peer-urls", "", `advertise URLs for peer traffic (default "${peer-urls}")`) 79 fs.StringVar(&cfg.Join, "join", "", `join to an existing cluster (usage: cluster's "${master-addr}" list, e.g. "127.0.0.1:8261,127.0.0.1:18261"`) 80 fs.UintVar(&cfg.MaxTxnOps, "max-txn-ops", defaultMaxTxnOps, `etcd's max-txn-ops, default value is 2048`) 81 fs.UintVar(&cfg.MaxRequestBytes, "max-request-bytes", embed.DefaultMaxRequestBytes, `etcd's max-request-bytes`) 82 fs.StringVar(&cfg.AutoCompactionMode, "auto-compaction-mode", defaultAutoCompactionMode, `etcd's auto-compaction-mode, either 'periodic' or 'revision'`) 83 fs.StringVar(&cfg.AutoCompactionRetention, "auto-compaction-retention", defaultAutoCompactionRetention, `etcd's auto-compaction-retention, accept values like '5h' or '5' (5 hours in 'periodic' mode or 5 revisions in 'revision')`) 84 fs.Int64Var(&cfg.QuotaBackendBytes, "quota-backend-bytes", defaultQuotaBackendBytes, `etcd's storage quota in bytes`) 85 86 fs.StringVar(&cfg.SSLCA, "ssl-ca", "", "path of file that contains list of trusted SSL CAs for connection") 87 fs.StringVar(&cfg.SSLCert, "ssl-cert", "", "path of file that contains X509 certificate in PEM format for connection") 88 fs.StringVar(&cfg.SSLKey, "ssl-key", "", "path of file that contains X509 key in PEM format for connection") 89 fs.Var(&cfg.CertAllowedCN, "cert-allowed-cn", "the trusted common name that allowed to visit") 90 91 fs.StringVar(&cfg.V1SourcesPath, "v1-sources-path", "", "directory path used to store source config files when upgrading from v1.0.x") 92 fs.StringVar(&cfg.SecretKeyPath, "secret-key-path", "", "path of file that contains secret key for encrypting and decrypting password, the secret key should be a hex AES-256 key of length 64") 93 94 return cfg 95 } 96 97 type ExperimentalFeatures struct { 98 OpenAPI bool `toml:"openapi,omitempty"` // OpenAPI is available in v5.4 as default. 99 } 100 101 // Config is the configuration for dm-master. 102 type Config struct { 103 flagSet *flag.FlagSet 104 105 LogLevel string `toml:"log-level" json:"log-level"` 106 LogFile string `toml:"log-file" json:"log-file"` 107 LogFormat string `toml:"log-format" json:"log-format"` 108 LogRotate string `toml:"log-rotate" json:"log-rotate"` 109 110 RPCTimeoutStr string `toml:"rpc-timeout" json:"rpc-timeout"` 111 RPCTimeout time.Duration `toml:"-" json:"-"` 112 RPCRateLimit float64 `toml:"rpc-rate-limit" json:"rpc-rate-limit"` 113 RPCRateBurst int `toml:"rpc-rate-burst" json:"rpc-rate-burst"` 114 115 MasterAddr string `toml:"master-addr" json:"master-addr"` 116 AdvertiseAddr string `toml:"advertise-addr" json:"advertise-addr"` 117 118 ConfigFile string `toml:"config-file" json:"config-file"` 119 120 // etcd relative config items 121 // NOTE: we use `MasterAddr` to generate `ClientUrls` and `AdvertiseClientUrls` 122 // NOTE: more items will be add when adding leader election 123 Name string `toml:"name" json:"name"` 124 DataDir string `toml:"data-dir" json:"data-dir"` 125 PeerUrls string `toml:"peer-urls" json:"peer-urls"` 126 AdvertisePeerUrls string `toml:"advertise-peer-urls" json:"advertise-peer-urls"` 127 InitialCluster string `toml:"initial-cluster" json:"initial-cluster"` 128 InitialClusterState string `toml:"initial-cluster-state" json:"initial-cluster-state"` 129 Join string `toml:"join" json:"join"` // cluster's client address (endpoints), not peer address 130 MaxTxnOps uint `toml:"max-txn-ops" json:"max-txn-ops"` 131 MaxRequestBytes uint `toml:"max-request-bytes" json:"max-request-bytes"` 132 AutoCompactionMode string `toml:"auto-compaction-mode" json:"auto-compaction-mode"` 133 AutoCompactionRetention string `toml:"auto-compaction-retention" json:"auto-compaction-retention"` 134 QuotaBackendBytes int64 `toml:"quota-backend-bytes" json:"quota-backend-bytes"` 135 OpenAPI bool `toml:"openapi" json:"openapi"` 136 137 // directory path used to store source config files when upgrading from v1.0.x. 138 // if this path set, DM-master leader will try to upgrade from v1.0.x to the current version. 139 V1SourcesPath string `toml:"v1-sources-path" json:"v1-sources-path"` 140 141 // tls config 142 security.Security 143 SecretKeyPath string `toml:"secret-key-path" json:"secret-key-path" yaml:"secret-key-path"` 144 SecretKey []byte `toml:"-" json:"-" yaml:"-"` 145 146 printVersion bool 147 printSampleConfig bool 148 149 ExperimentalFeatures ExperimentalFeatures `toml:"experimental"` 150 } 151 152 func (c *Config) String() string { 153 cfg, err := json.Marshal(c) 154 if err != nil { 155 log.L().Error("marshal to json", zap.Reflect("master config", c), log.ShortError(err)) 156 } 157 return string(cfg) 158 } 159 160 // Toml returns TOML format representation of config. 161 func (c *Config) Toml() (string, error) { 162 var b bytes.Buffer 163 164 err := toml.NewEncoder(&b).Encode(c) 165 if err != nil { 166 log.L().Error("fail to marshal config to toml", log.ShortError(err)) 167 } 168 169 return b.String(), nil 170 } 171 172 // Parse parses flag definitions from the argument list. 173 func (c *Config) Parse(arguments []string) error { 174 // Parse first to get config file. 175 err := c.flagSet.Parse(arguments) 176 if err != nil { 177 return terror.ErrMasterConfigParseFlagSet.Delegate(err) 178 } 179 180 if c.printVersion { 181 fmt.Println(version.GetRawInfo()) 182 return flag.ErrHelp 183 } 184 185 if c.printSampleConfig { 186 fmt.Println(SampleConfig) 187 return flag.ErrHelp 188 } 189 190 // Load config file if specified. 191 if c.ConfigFile != "" { 192 err = c.configFromFile(c.ConfigFile) 193 if err != nil { 194 return err 195 } 196 } 197 198 // Parse again to replace with command line options. 199 err = c.flagSet.Parse(arguments) 200 if err != nil { 201 return terror.ErrMasterConfigParseFlagSet.Delegate(err) 202 } 203 204 if len(c.flagSet.Args()) != 0 { 205 return terror.ErrMasterConfigInvalidFlag.Generate(c.flagSet.Arg(0)) 206 } 207 208 return c.adjust() 209 } 210 211 // configFromFile loads config from file. 212 func (c *Config) configFromFile(path string) error { 213 metaData, err := toml.DecodeFile(path, c) 214 if err != nil { 215 return terror.ErrMasterConfigTomlTransform.Delegate(err) 216 } 217 undecoded := metaData.Undecoded() 218 if len(undecoded) > 0 { 219 var undecodedItems []string 220 for _, item := range undecoded { 221 undecodedItems = append(undecodedItems, item.String()) 222 } 223 return terror.ErrMasterConfigUnknownItem.Generate(strings.Join(undecodedItems, ",")) 224 } 225 return nil 226 } 227 228 // FromContent loads config from TOML format content. 229 func (c *Config) FromContent(content string) error { 230 metaData, err := toml.Decode(content, c) 231 if err != nil { 232 return terror.ErrMasterConfigTomlTransform.Delegate(err) 233 } 234 undecoded := metaData.Undecoded() 235 if len(undecoded) > 0 { 236 var undecodedItems []string 237 for _, item := range undecoded { 238 undecodedItems = append(undecodedItems, item.String()) 239 } 240 return terror.ErrMasterConfigUnknownItem.Generate(strings.Join(undecodedItems, ",")) 241 } 242 return c.adjust() 243 } 244 245 // adjust adjusts configs. 246 func (c *Config) adjust() error { 247 c.MasterAddr = utils.UnwrapScheme(c.MasterAddr) 248 // MasterAddr's format may be "host:port" or ":port" 249 host, port, err := net.SplitHostPort(c.MasterAddr) 250 if err != nil { 251 return terror.ErrMasterHostPortNotValid.Delegate(err, c.MasterAddr) 252 } 253 254 if c.AdvertiseAddr == "" { 255 if host == "" || host == "0.0.0.0" || len(port) == 0 { 256 return terror.ErrMasterHostPortNotValid.Generatef("master-addr (%s) must include the 'host' part (should not be '0.0.0.0') when advertise-addr is not set", c.MasterAddr) 257 } 258 c.AdvertiseAddr = c.MasterAddr 259 } else { 260 c.AdvertiseAddr = utils.UnwrapScheme(c.AdvertiseAddr) 261 // AdvertiseAddr's format should be "host:port" 262 host, port, err = net.SplitHostPort(c.AdvertiseAddr) 263 if err != nil { 264 return terror.ErrMasterAdvertiseAddrNotValid.Delegate(err, c.AdvertiseAddr) 265 } 266 if len(host) == 0 || host == "0.0.0.0" || len(port) == 0 { 267 return terror.ErrMasterAdvertiseAddrNotValid.Generate(c.AdvertiseAddr) 268 } 269 } 270 271 if c.RPCTimeoutStr == "" { 272 c.RPCTimeoutStr = defaultRPCTimeout 273 } 274 timeout, err := time.ParseDuration(c.RPCTimeoutStr) 275 if err != nil { 276 return terror.ErrMasterConfigTimeoutParse.Delegate(err) 277 } 278 c.RPCTimeout = timeout 279 280 // for backward compatibility 281 if c.RPCRateLimit <= 0 { 282 log.L().Warn("invalid rpc-rate-limit, default value used", zap.Float64("specified rpc-rate-limit", c.RPCRateLimit), zap.Float64("default rpc-rate-limit", DefaultRate)) 283 c.RPCRateLimit = DefaultRate 284 } 285 if c.RPCRateBurst <= 0 { 286 log.L().Warn("invalid rpc-rate-burst, default value use", zap.Int("specified rpc-rate-burst", c.RPCRateBurst), zap.Int("default rpc-rate-burst", DefaultBurst)) 287 c.RPCRateBurst = DefaultBurst 288 } 289 290 if c.Name == "" { 291 var hostname string 292 hostname, err = os.Hostname() 293 if err != nil { 294 return terror.ErrMasterGetHostnameFail.Delegate(err) 295 } 296 c.Name = fmt.Sprintf("%s-%s", defaultNamePrefix, hostname) 297 } 298 299 if c.DataDir == "" { 300 c.DataDir = fmt.Sprintf("%s.%s", defaultDataDirPrefix, c.Name) 301 } 302 303 if c.PeerUrls == "" { 304 c.PeerUrls = defaultPeerUrls 305 } else { 306 c.PeerUrls = utils.WrapSchemes(c.PeerUrls, c.SSLCA != "") 307 } 308 309 if c.AdvertisePeerUrls == "" { 310 c.AdvertisePeerUrls = c.PeerUrls 311 } else { 312 c.AdvertisePeerUrls = utils.WrapSchemes(c.AdvertisePeerUrls, c.SSLCA != "") 313 } 314 315 if c.InitialCluster == "" { 316 items := strings.Split(c.AdvertisePeerUrls, ",") 317 for i, item := range items { 318 items[i] = fmt.Sprintf("%s=%s", c.Name, item) 319 } 320 c.InitialCluster = strings.Join(items, ",") 321 } else { 322 c.InitialCluster = utils.WrapSchemesForInitialCluster(c.InitialCluster, c.SSLCA != "") 323 } 324 325 if c.InitialClusterState == "" { 326 c.InitialClusterState = defaultInitialClusterState 327 } 328 329 if c.Join != "" { 330 c.Join = utils.WrapSchemes(c.Join, c.SSLCA != "") 331 } 332 333 if c.QuotaBackendBytes < quotaBackendBytesLowerBound { 334 log.L().Warn("quota-backend-bytes is too low, will adjust it", 335 zap.Int64("from", c.QuotaBackendBytes), 336 zap.Int64("to", quotaBackendBytesLowerBound)) 337 c.QuotaBackendBytes = quotaBackendBytesLowerBound 338 } 339 340 if c.ExperimentalFeatures.OpenAPI { 341 c.OpenAPI = true 342 c.ExperimentalFeatures.OpenAPI = false 343 log.L().Warn("openapi is a GA feature and removed from experimental features, so this configuration may have no affect in feature release, please set openapi=true in dm-master config file") 344 } 345 346 return c.adjustSecretKeyPath() 347 } 348 349 func (c *Config) adjustSecretKeyPath() error { 350 if c.SecretKeyPath == "" { 351 return nil 352 } 353 354 content, err := os.ReadFile(c.SecretKeyPath) 355 if err != nil { 356 return terror.ErrConfigSecretKeyPath.Generate(err) 357 } 358 contentStr := strings.TrimSpace(string(content)) 359 decodeContent, err := hex.DecodeString(contentStr) 360 if err != nil { 361 return terror.ErrConfigSecretKeyPath.Generate(err) 362 } 363 if len(decodeContent) != 32 { 364 return terror.ErrConfigSecretKeyPath.Generate("the secret key must be a hex AES-256 key of length 64") 365 } 366 c.SecretKey = decodeContent 367 return nil 368 } 369 370 // Reload load config from local file. 371 func (c *Config) Reload() error { 372 if c.ConfigFile != "" { 373 err := c.configFromFile(c.ConfigFile) 374 if err != nil { 375 return err 376 } 377 } 378 379 return c.adjust() 380 } 381 382 // genEmbedEtcdConfig generates the configuration needed by embed etcd. 383 // This method should be called after logger initialized and before any concurrent gRPC calls. 384 func (c *Config) genEmbedEtcdConfig(cfg *embed.Config) (*embed.Config, error) { 385 cfg.Name = c.Name 386 cfg.Dir = c.DataDir 387 388 // reuse the previous master-addr as the client listening URL. 389 var err error 390 cfg.ListenClientUrls, err = parseURLs(c.MasterAddr) 391 if err != nil { 392 return nil, terror.ErrMasterGenEmbedEtcdConfigFail.Delegate(err, "invalid master-addr") 393 } 394 cfg.AdvertiseClientUrls, err = parseURLs(c.AdvertiseAddr) 395 if err != nil { 396 return nil, terror.ErrMasterGenEmbedEtcdConfigFail.Delegate(err, "invalid advertise-addr") 397 } 398 399 cfg.ListenPeerUrls, err = parseURLs(c.PeerUrls) 400 if err != nil { 401 return nil, terror.ErrMasterGenEmbedEtcdConfigFail.Delegate(err, "invalid peer-urls") 402 } 403 404 cfg.AdvertisePeerUrls, err = parseURLs(c.AdvertisePeerUrls) 405 if err != nil { 406 return nil, terror.ErrMasterGenEmbedEtcdConfigFail.Delegate(err, "invalid advertise-peer-urls") 407 } 408 409 cfg.InitialCluster = c.InitialCluster 410 cfg.ClusterState = c.InitialClusterState 411 cfg.AutoCompactionMode = c.AutoCompactionMode 412 cfg.AutoCompactionRetention = c.AutoCompactionRetention 413 cfg.QuotaBackendBytes = c.QuotaBackendBytes 414 cfg.MaxTxnOps = c.MaxTxnOps 415 cfg.MaxRequestBytes = c.MaxRequestBytes 416 417 err = cfg.Validate() // verify & trigger the builder 418 if err != nil { 419 return nil, terror.ErrMasterGenEmbedEtcdConfigFail.AnnotateDelegate(err, "fail to validate embed etcd config") 420 } 421 422 // security config 423 if len(c.SSLCA) != 0 { 424 cfg.ClientTLSInfo.TrustedCAFile = c.SSLCA 425 cfg.ClientTLSInfo.CertFile = c.SSLCert 426 cfg.ClientTLSInfo.KeyFile = c.SSLKey 427 428 cfg.PeerTLSInfo.TrustedCAFile = c.SSLCA 429 cfg.PeerTLSInfo.CertFile = c.SSLCert 430 cfg.PeerTLSInfo.KeyFile = c.SSLKey 431 432 // NOTE: etcd only support one allowed CN 433 if len(c.CertAllowedCN) > 0 { 434 cfg.ClientTLSInfo.AllowedCN = c.CertAllowedCN[0] 435 cfg.PeerTLSInfo.AllowedCN = c.CertAllowedCN[0] 436 cfg.PeerTLSInfo.ClientCertAuth = len(c.SSLCA) != 0 437 cfg.ClientTLSInfo.ClientCertAuth = len(c.SSLCA) != 0 438 } 439 } 440 441 return cfg, nil 442 } 443 444 // parseURLs parse a string into multiple urls. 445 // if the URL in the string without protocol scheme, use `http` as the default. 446 // if no IP exists in the address, `0.0.0.0` is used. 447 func parseURLs(s string) ([]url.URL, error) { 448 if s == "" { 449 return nil, nil 450 } 451 452 items := strings.Split(s, ",") 453 urls := make([]url.URL, 0, len(items)) 454 for _, item := range items { 455 // tolerate valid `master-addr`, but invalid URL format. mainly caused by no protocol scheme 456 if !(strings.HasPrefix(item, "http://") || strings.HasPrefix(item, "https://")) { 457 prefix := "http://" 458 if useTLS.Load() { 459 prefix = "https://" 460 } 461 item = prefix + item 462 } 463 u, err := url.Parse(item) 464 if err != nil { 465 return nil, terror.ErrMasterParseURLFail.Delegate(err, item) 466 } 467 if strings.Index(u.Host, ":") == 0 { 468 u.Host = "0.0.0.0" + u.Host 469 } 470 urls = append(urls, *u) 471 } 472 return urls, nil 473 } 474 475 func genEmbedEtcdConfigWithLogger(logLevel string) *embed.Config { 476 cfg := embed.NewConfig() 477 // disable grpc gateway because https://github.com/etcd-io/etcd/issues/12713 478 // TODO: wait above issue fixed 479 cfg.EnableGRPCGateway = false // enable gRPC gateway for the internal etcd. 480 481 // use zap as the logger for embed etcd 482 // NOTE: `genEmbedEtcdConfig` can only be called after logger initialized. 483 // NOTE: if using zap logger for etcd, must build it before any concurrent gRPC calls, 484 // otherwise, DATA RACE occur in NewZapCoreLoggerBuilder and gRPC. 485 logger := log.L().WithFields(zap.String("component", "embed etcd")) 486 // if logLevel is info, set etcd log level to WARN to reduce log 487 if strings.ToLower(logLevel) == "info" { 488 log.L().Info("Set log level of etcd to `warn`, if you want to log more message about etcd, change log-level to `debug` in master configuration file") 489 logger.Logger = logger.WithOptions(zap.IncreaseLevel(zap.WarnLevel)) 490 } 491 cfg.ZapLoggerBuilder = embed.NewZapCoreLoggerBuilder(logger.Logger, logger.Core(), log.Props().Syncer) // use global app props. 492 cfg.Logger = "zap" 493 // TODO: we run ZapLoggerBuilder to set SetLoggerV2 before we do some etcd operations 494 // otherwise we will meet data race while running `grpclog.SetLoggerV2` 495 // It's vert tricky here, we should use a better way to avoid this in the future. 496 err := cfg.ZapLoggerBuilder(cfg) 497 if err != nil { 498 panic(err) // we must ensure we can generate embed etcd config 499 } 500 501 return cfg 502 }