github.com/pingcap/tiup@v1.15.1/components/dm/spec/topology_dm.go (about) 1 // Copyright 2020 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 spec 15 16 import ( 17 "context" 18 "crypto/tls" 19 "fmt" 20 "path/filepath" 21 "reflect" 22 "strings" 23 "time" 24 25 "github.com/creasty/defaults" 26 "github.com/pingcap/errors" 27 "github.com/pingcap/tiup/pkg/cluster/api" 28 "github.com/pingcap/tiup/pkg/cluster/spec" 29 "github.com/pingcap/tiup/pkg/meta" 30 "github.com/pingcap/tiup/pkg/set" 31 "github.com/pingcap/tiup/pkg/utils" 32 ) 33 34 const ( 35 statusQueryTimeout = 2 * time.Second 36 ) 37 38 var ( 39 globalOptionTypeName = reflect.TypeOf(GlobalOptions{}).Name() 40 monitorOptionTypeName = reflect.TypeOf(MonitoredOptions{}).Name() 41 serverConfigsTypeName = reflect.TypeOf(DMServerConfigs{}).Name() 42 componentSourcesTypeName = reflect.TypeOf(ComponentSources{}).Name() 43 ) 44 45 func setDefaultDir(parent, role, port string, field reflect.Value) { 46 if field.String() != "" { 47 return 48 } 49 if defaults.CanUpdate(field.Interface()) { 50 dir := fmt.Sprintf("%s-%s", role, port) 51 field.Set(reflect.ValueOf(filepath.Join(parent, dir))) 52 } 53 } 54 55 func findField(v reflect.Value, fieldName string) (int, bool) { 56 for i := 0; i < v.NumField(); i++ { 57 if v.Type().Field(i).Name == fieldName { 58 return i, true 59 } 60 } 61 return -1, false 62 } 63 64 // Skip global/monitored/job options 65 func isSkipField(field reflect.Value) bool { 66 if field.Kind() == reflect.Ptr { 67 if field.IsZero() { 68 return true 69 } 70 field = field.Elem() 71 } 72 tp := field.Type().Name() 73 return tp == globalOptionTypeName || tp == monitorOptionTypeName || tp == serverConfigsTypeName || tp == componentSourcesTypeName 74 } 75 76 type ( 77 // GlobalOptions of spec. 78 GlobalOptions = spec.GlobalOptions 79 // MonitoredOptions is the spec of Monitored 80 MonitoredOptions = spec.MonitoredOptions 81 // PrometheusSpec is the spec of Prometheus 82 PrometheusSpec = spec.PrometheusSpec 83 // GrafanaSpec is the spec of Grafana 84 GrafanaSpec = spec.GrafanaSpec 85 // AlertmanagerSpec is the spec of Alertmanager 86 AlertmanagerSpec = spec.AlertmanagerSpec 87 // ResourceControl is the spec of ResourceControl 88 ResourceControl = meta.ResourceControl 89 ) 90 91 type ( 92 // DMServerConfigs represents the server runtime configuration 93 DMServerConfigs struct { 94 Master map[string]any `yaml:"master"` 95 Worker map[string]any `yaml:"worker"` 96 Grafana map[string]string `yaml:"grafana"` 97 } 98 99 // ComponentSources represents the source of components 100 ComponentSources struct { 101 Master string `yaml:"master,omitempty"` 102 Worker string `yaml:"worker,omitempty"` 103 } 104 105 // Specification represents the specification of topology.yaml 106 Specification struct { 107 GlobalOptions GlobalOptions `yaml:"global,omitempty" validate:"global:editable"` 108 MonitoredOptions *MonitoredOptions `yaml:"monitored,omitempty" validate:"monitored:editable"` 109 ComponentSources ComponentSources `yaml:"component_sources,omitempty" validate:"component_sources:editable"` 110 ServerConfigs DMServerConfigs `yaml:"server_configs,omitempty" validate:"server_configs:ignore"` 111 Masters []*MasterSpec `yaml:"master_servers"` 112 Workers []*WorkerSpec `yaml:"worker_servers"` 113 Monitors []*spec.PrometheusSpec `yaml:"monitoring_servers"` 114 Grafanas []*spec.GrafanaSpec `yaml:"grafana_servers,omitempty"` 115 Alertmanagers []*spec.AlertmanagerSpec `yaml:"alertmanager_servers,omitempty"` 116 } 117 ) 118 119 // AllDMComponentNames contains the names of all dm components. 120 // should include all components in ComponentsByStartOrder 121 func AllDMComponentNames() (roles []string) { 122 tp := &Specification{} 123 tp.IterComponent(func(c Component) { 124 roles = append(roles, c.Name()) 125 }) 126 127 return 128 } 129 130 // MasterSpec represents the Master topology specification in topology.yaml 131 type MasterSpec struct { 132 Host string `yaml:"host"` 133 ManageHost string `yaml:"manage_host,omitempty" validate:"manage_host:editable"` 134 SSHPort int `yaml:"ssh_port,omitempty" validate:"ssh_port:editable"` 135 Imported bool `yaml:"imported,omitempty"` 136 Patched bool `yaml:"patched,omitempty"` 137 IgnoreExporter bool `yaml:"ignore_exporter,omitempty"` 138 // Use Name to get the name with a default value if it's empty. 139 Name string `yaml:"name,omitempty"` 140 Port int `yaml:"port,omitempty" default:"8261"` 141 PeerPort int `yaml:"peer_port,omitempty" default:"8291"` 142 DeployDir string `yaml:"deploy_dir,omitempty"` 143 DataDir string `yaml:"data_dir,omitempty"` 144 LogDir string `yaml:"log_dir,omitempty"` 145 Source string `yaml:"source,omitempty" validate:"source:editable"` 146 NumaNode string `yaml:"numa_node,omitempty" validate:"numa_node:editable"` 147 Config map[string]any `yaml:"config,omitempty" validate:"config:ignore"` 148 ResourceControl ResourceControl `yaml:"resource_control,omitempty" validate:"resource_control:editable"` 149 Arch string `yaml:"arch,omitempty"` 150 OS string `yaml:"os,omitempty"` 151 V1SourcePath string `yaml:"v1_source_path,omitempty"` 152 } 153 154 // Status queries current status of the instance 155 func (s *MasterSpec) Status(_ context.Context, timeout time.Duration, tlsCfg *tls.Config, _ ...string) string { 156 if timeout < time.Second { 157 timeout = statusQueryTimeout 158 } 159 160 addr := utils.JoinHostPort(s.Host, s.Port) 161 dc := api.NewDMMasterClient([]string{addr}, timeout, tlsCfg) 162 isFound, isActive, isLeader, err := dc.GetMaster(s.Name) 163 if err != nil { 164 return "Down" 165 } 166 if !isFound { 167 return "N/A" 168 } 169 if !isActive { 170 return "Unhealthy" 171 } 172 res := "Healthy" 173 if isLeader { 174 res += "|L" 175 } 176 return res 177 } 178 179 // Role returns the component role of the instance 180 func (s *MasterSpec) Role() string { 181 return ComponentDMMaster 182 } 183 184 // SSH returns the host and SSH port of the instance 185 func (s *MasterSpec) SSH() (string, int) { 186 host := s.Host 187 if s.ManageHost != "" { 188 host = s.ManageHost 189 } 190 return host, s.SSHPort 191 } 192 193 // GetMainPort returns the main port of the instance 194 func (s *MasterSpec) GetMainPort() int { 195 return s.Port 196 } 197 198 // IsImported returns if the node is imported from TiDB-Ansible 199 func (s *MasterSpec) IsImported() bool { 200 return s.Imported 201 } 202 203 // IgnoreMonitorAgent returns if the node does not have monitor agents available 204 func (s *MasterSpec) IgnoreMonitorAgent() bool { 205 return s.IgnoreExporter 206 } 207 208 // GetAdvertisePeerURL returns AdvertisePeerURL 209 func (s *MasterSpec) GetAdvertisePeerURL(enableTLS bool) string { 210 scheme := utils.Ternary(enableTLS, "https", "http").(string) 211 return fmt.Sprintf("%s://%s", scheme, utils.JoinHostPort(s.Host, s.PeerPort)) 212 } 213 214 // WorkerSpec represents the Master topology specification in topology.yaml 215 type WorkerSpec struct { 216 Host string `yaml:"host"` 217 ManageHost string `yaml:"manage_host,omitempty" validate:"manage_host:editable"` 218 SSHPort int `yaml:"ssh_port,omitempty" validate:"ssh_port:editable"` 219 Imported bool `yaml:"imported,omitempty"` 220 Patched bool `yaml:"patched,omitempty"` 221 IgnoreExporter bool `yaml:"ignore_exporter,omitempty"` 222 // Use Name to get the name with a default value if it's empty. 223 Name string `yaml:"name,omitempty"` 224 Port int `yaml:"port,omitempty" default:"8262"` 225 DeployDir string `yaml:"deploy_dir,omitempty"` 226 DataDir string `yaml:"data_dir,omitempty"` 227 LogDir string `yaml:"log_dir,omitempty"` 228 Source string `yaml:"source,omitempty" validate:"source:editable"` 229 NumaNode string `yaml:"numa_node,omitempty" validate:"numa_node:editable"` 230 Config map[string]any `yaml:"config,omitempty" validate:"config:ignore"` 231 ResourceControl ResourceControl `yaml:"resource_control,omitempty" validate:"resource_control:editable"` 232 Arch string `yaml:"arch,omitempty"` 233 OS string `yaml:"os,omitempty"` 234 } 235 236 // Status queries current status of the instance 237 func (s *WorkerSpec) Status(_ context.Context, timeout time.Duration, tlsCfg *tls.Config, masterList ...string) string { 238 if len(masterList) < 1 { 239 return "N/A" 240 } 241 242 if timeout < time.Second { 243 timeout = statusQueryTimeout 244 } 245 dc := api.NewDMMasterClient(masterList, timeout, tlsCfg) 246 stage, err := dc.GetWorker(s.Name) 247 if err != nil { 248 return "Down" 249 } 250 if stage == "" { 251 return "N/A" 252 } 253 return stage 254 } 255 256 // Role returns the component role of the instance 257 func (s *WorkerSpec) Role() string { 258 return ComponentDMWorker 259 } 260 261 // SSH returns the host and SSH port of the instance 262 func (s *WorkerSpec) SSH() (string, int) { 263 host := s.Host 264 if s.ManageHost != "" { 265 host = s.ManageHost 266 } 267 return host, s.SSHPort 268 } 269 270 // GetMainPort returns the main port of the instance 271 func (s *WorkerSpec) GetMainPort() int { 272 return s.Port 273 } 274 275 // IsImported returns if the node is imported from TiDB-Ansible 276 func (s *WorkerSpec) IsImported() bool { 277 return s.Imported 278 } 279 280 // IgnoreMonitorAgent returns if the node does not have monitor agents available 281 func (s *WorkerSpec) IgnoreMonitorAgent() bool { 282 return s.IgnoreExporter 283 } 284 285 // UnmarshalYAML sets default values when unmarshaling the topology file 286 func (s *Specification) UnmarshalYAML(unmarshal func(any) error) error { 287 type topology Specification 288 if err := unmarshal((*topology)(s)); err != nil { 289 return err 290 } 291 292 if err := defaults.Set(s); err != nil { 293 return errors.Trace(err) 294 } 295 296 if s.MonitoredOptions != nil { 297 // Set monitored options 298 if s.MonitoredOptions.DeployDir == "" { 299 s.MonitoredOptions.DeployDir = filepath.Join(s.GlobalOptions.DeployDir, 300 fmt.Sprintf("%s-%d", spec.RoleMonitor, s.MonitoredOptions.NodeExporterPort)) 301 } 302 if s.MonitoredOptions.DataDir == "" { 303 s.MonitoredOptions.DataDir = filepath.Join(s.GlobalOptions.DataDir, 304 fmt.Sprintf("%s-%d", spec.RoleMonitor, s.MonitoredOptions.NodeExporterPort)) 305 } 306 if s.MonitoredOptions.LogDir == "" { 307 s.MonitoredOptions.LogDir = "log" 308 } 309 if !strings.HasPrefix(s.MonitoredOptions.LogDir, "/") && 310 !strings.HasPrefix(s.MonitoredOptions.LogDir, s.MonitoredOptions.DeployDir) { 311 s.MonitoredOptions.LogDir = filepath.Join(s.MonitoredOptions.DeployDir, s.MonitoredOptions.LogDir) 312 } 313 } 314 315 if err := fillDMCustomDefaults(&s.GlobalOptions, s); err != nil { 316 return err 317 } 318 319 return s.Validate() 320 } 321 322 // platformConflictsDetect checks for conflicts in topology for different OS / Arch 323 // for set to the same host / IP 324 func (s *Specification) platformConflictsDetect() error { 325 type ( 326 conflict struct { 327 os string 328 arch string 329 cfg string 330 } 331 ) 332 333 platformStats := map[string]conflict{} 334 topoSpec := reflect.ValueOf(s).Elem() 335 topoType := reflect.TypeOf(s).Elem() 336 337 for i := 0; i < topoSpec.NumField(); i++ { 338 if isSkipField(topoSpec.Field(i)) { 339 continue 340 } 341 342 compSpecs := topoSpec.Field(i) 343 for index := 0; index < compSpecs.Len(); index++ { 344 compSpec := reflect.Indirect(compSpecs.Index(index)) 345 // skip nodes imported from TiDB-Ansible 346 if compSpec.Addr().Interface().(InstanceSpec).IsImported() { 347 continue 348 } 349 // check hostname 350 host := compSpec.FieldByName("Host").String() 351 cfg := topoType.Field(i).Tag.Get("yaml") 352 if host == "" { 353 return errors.Errorf("`%s` contains empty host field", cfg) 354 } 355 356 // platform conflicts 357 stat := conflict{ 358 cfg: cfg, 359 } 360 if j, found := findField(compSpec, "OS"); found { 361 stat.os = compSpec.Field(j).String() 362 } 363 if j, found := findField(compSpec, "Arch"); found { 364 stat.arch = compSpec.Field(j).String() 365 } 366 367 prev, exist := platformStats[host] 368 if exist { 369 if prev.os != stat.os || prev.arch != stat.arch { 370 return &meta.ValidateErr{ 371 Type: meta.TypeMismatch, 372 Target: "platform", 373 LHS: fmt.Sprintf("%s:%s/%s", prev.cfg, prev.os, prev.arch), 374 RHS: fmt.Sprintf("%s:%s/%s", stat.cfg, stat.os, stat.arch), 375 Value: host, 376 } 377 } 378 } 379 platformStats[host] = stat 380 } 381 } 382 return nil 383 } 384 385 func (s *Specification) portConflictsDetect() error { 386 type ( 387 usedPort struct { 388 host string 389 port int 390 } 391 conflict struct { 392 tp string 393 cfg string 394 } 395 ) 396 397 portTypes := []string{ 398 "Port", 399 "StatusPort", 400 "PeerPort", 401 "ClientPort", 402 "WebPort", 403 "TCPPort", 404 "HTTPPort", 405 "ClusterPort", 406 } 407 408 portStats := map[usedPort]conflict{} 409 uniqueHosts := set.NewStringSet() 410 topoSpec := reflect.ValueOf(s).Elem() 411 topoType := reflect.TypeOf(s).Elem() 412 413 for i := 0; i < topoSpec.NumField(); i++ { 414 if isSkipField(topoSpec.Field(i)) { 415 continue 416 } 417 418 compSpecs := topoSpec.Field(i) 419 for index := 0; index < compSpecs.Len(); index++ { 420 compSpec := reflect.Indirect(compSpecs.Index(index)) 421 // skip nodes imported from TiDB-Ansible 422 if compSpec.Addr().Interface().(InstanceSpec).IsImported() { 423 continue 424 } 425 // check hostname 426 host := compSpec.FieldByName("Host").String() 427 cfg := topoType.Field(i).Tag.Get("yaml") 428 if host == "" { 429 return errors.Errorf("`%s` contains empty host field", cfg) 430 } 431 uniqueHosts.Insert(host) 432 433 // Ports conflicts 434 for _, portType := range portTypes { 435 if j, found := findField(compSpec, portType); found { 436 item := usedPort{ 437 host: host, 438 port: int(compSpec.Field(j).Int()), 439 } 440 tp := compSpec.Type().Field(j).Tag.Get("yaml") 441 prev, exist := portStats[item] 442 if exist { 443 return &meta.ValidateErr{ 444 Type: meta.TypeConflict, 445 Target: "port", 446 LHS: fmt.Sprintf("%s:%s.%s", prev.cfg, item.host, prev.tp), 447 RHS: fmt.Sprintf("%s:%s.%s", cfg, item.host, tp), 448 Value: item.port, 449 } 450 } 451 portStats[item] = conflict{ 452 tp: tp, 453 cfg: cfg, 454 } 455 } 456 } 457 } 458 } 459 460 // Port conflicts in monitored components 461 monitoredPortTypes := []string{ 462 "NodeExporterPort", 463 "BlackboxExporterPort", 464 } 465 monitoredOpt := topoSpec.FieldByName(monitorOptionTypeName) 466 if monitoredOpt.IsZero() { 467 return nil 468 } 469 monitoredOpt = monitoredOpt.Elem() 470 for host := range uniqueHosts { 471 cfg := "monitored" 472 for _, portType := range monitoredPortTypes { 473 f := monitoredOpt.FieldByName(portType) 474 item := usedPort{ 475 host: host, 476 port: int(f.Int()), 477 } 478 ft, found := monitoredOpt.Type().FieldByName(portType) 479 if !found { 480 return errors.Errorf("incompatible change `%s.%s`", monitorOptionTypeName, portType) 481 } 482 // `yaml:"node_exporter_port,omitempty"` 483 tp := strings.Split(ft.Tag.Get("yaml"), ",")[0] 484 prev, exist := portStats[item] 485 if exist { 486 return &meta.ValidateErr{ 487 Type: meta.TypeConflict, 488 Target: "port", 489 LHS: fmt.Sprintf("%s:%s.%s", prev.cfg, item.host, prev.tp), 490 RHS: fmt.Sprintf("%s:%s.%s", cfg, item.host, tp), 491 Value: item.port, 492 } 493 } 494 portStats[item] = conflict{ 495 tp: tp, 496 cfg: cfg, 497 } 498 } 499 } 500 501 return nil 502 } 503 504 func (s *Specification) dirConflictsDetect() error { 505 type ( 506 usedDir struct { 507 host string 508 dir string 509 } 510 conflict struct { 511 tp string 512 cfg string 513 } 514 ) 515 516 dirTypes := []string{ 517 "DataDir", 518 "DeployDir", 519 } 520 521 // usedInfo => type 522 var ( 523 dirStats = map[usedDir]conflict{} 524 uniqueHosts = set.NewStringSet() 525 ) 526 527 topoSpec := reflect.ValueOf(s).Elem() 528 topoType := reflect.TypeOf(s).Elem() 529 530 for i := 0; i < topoSpec.NumField(); i++ { 531 if isSkipField(topoSpec.Field(i)) { 532 continue 533 } 534 535 compSpecs := topoSpec.Field(i) 536 for index := 0; index < compSpecs.Len(); index++ { 537 compSpec := reflect.Indirect(compSpecs.Index(index)) 538 // skip nodes imported from TiDB-Ansible 539 if compSpec.Addr().Interface().(InstanceSpec).IsImported() { 540 continue 541 } 542 // check hostname 543 host := compSpec.FieldByName("Host").String() 544 cfg := topoType.Field(i).Tag.Get("yaml") 545 if host == "" { 546 return errors.Errorf("`%s` contains empty host field", cfg) 547 } 548 uniqueHosts.Insert(host) 549 550 // Directory conflicts 551 for _, dirType := range dirTypes { 552 if j, found := findField(compSpec, dirType); found { 553 item := usedDir{ 554 host: host, 555 dir: compSpec.Field(j).String(), 556 } 557 // data_dir is relative to deploy_dir by default, so they can be with 558 // same (sub) paths as long as the deploy_dirs are different 559 if item.dir != "" && !strings.HasPrefix(item.dir, "/") { 560 continue 561 } 562 // `yaml:"data_dir,omitempty"` 563 tp := strings.Split(compSpec.Type().Field(j).Tag.Get("yaml"), ",")[0] 564 prev, exist := dirStats[item] 565 if exist { 566 return &meta.ValidateErr{ 567 Type: meta.TypeConflict, 568 Target: "directory", 569 LHS: fmt.Sprintf("%s:%s.%s", prev.cfg, item.host, prev.tp), 570 RHS: fmt.Sprintf("%s:%s.%s", cfg, item.host, tp), 571 Value: item.dir, 572 } 573 } 574 dirStats[item] = conflict{ 575 tp: tp, 576 cfg: cfg, 577 } 578 } 579 } 580 } 581 } 582 583 return nil 584 } 585 586 // CountDir counts for dir paths used by any instance in the cluster with the same 587 // prefix, useful to find potential path conflicts 588 func (s *Specification) CountDir(targetHost, dirPrefix string) int { 589 dirTypes := []string{ 590 "DataDir", 591 "DeployDir", 592 "LogDir", 593 } 594 595 // host-path -> count 596 dirStats := make(map[string]int) 597 count := 0 598 topoSpec := reflect.ValueOf(s).Elem() 599 dirPrefix = spec.Abs(s.GlobalOptions.User, dirPrefix) 600 601 for i := 0; i < topoSpec.NumField(); i++ { 602 if isSkipField(topoSpec.Field(i)) { 603 continue 604 } 605 606 compSpecs := topoSpec.Field(i) 607 for index := 0; index < compSpecs.Len(); index++ { 608 compSpec := reflect.Indirect(compSpecs.Index(index)) 609 // Directory conflicts 610 for _, dirType := range dirTypes { 611 if j, found := findField(compSpec, dirType); found { 612 dir := compSpec.Field(j).String() 613 host := compSpec.FieldByName("Host").String() 614 615 switch dirType { // the same as in logic.go for (*instance) 616 case "DataDir": 617 deployDir := compSpec.FieldByName("DeployDir").String() 618 // the default data_dir is relative to deploy_dir 619 if dir != "" && !strings.HasPrefix(dir, "/") { 620 dir = filepath.Join(deployDir, dir) 621 } 622 case "LogDir": 623 deployDir := compSpec.FieldByName("DeployDir").String() 624 field := compSpec.FieldByName("LogDir") 625 if field.IsValid() { 626 dir = field.Interface().(string) 627 } 628 629 if dir == "" { 630 dir = "log" 631 } 632 if !strings.HasPrefix(dir, "/") { 633 dir = filepath.Join(deployDir, dir) 634 } 635 } 636 dir = spec.Abs(s.GlobalOptions.User, dir) 637 dirStats[host+dir]++ 638 } 639 } 640 } 641 } 642 643 for k, v := range dirStats { 644 if k == targetHost+dirPrefix || strings.HasPrefix(k, targetHost+dirPrefix+"/") { 645 count += v 646 } 647 } 648 649 return count 650 } 651 652 // TLSConfig generates a tls.Config for the specification as needed 653 func (s *Specification) TLSConfig(dir string) (*tls.Config, error) { 654 if !s.GlobalOptions.TLSEnabled { 655 return nil, nil 656 } 657 return spec.LoadClientCert(dir) 658 } 659 660 // Validate validates the topology specification and produce error if the 661 // specification invalid (e.g: port conflicts or directory conflicts) 662 func (s *Specification) Validate() error { 663 if err := s.platformConflictsDetect(); err != nil { 664 return err 665 } 666 667 if err := s.portConflictsDetect(); err != nil { 668 return err 669 } 670 671 if err := s.dirConflictsDetect(); err != nil { 672 return err 673 } 674 675 return spec.RelativePathDetect(s, isSkipField) 676 } 677 678 // Type implements Topology interface. 679 func (s *Specification) Type() string { 680 return spec.TopoTypeDM 681 } 682 683 // BaseTopo implements Topology interface. 684 func (s *Specification) BaseTopo() *spec.BaseTopo { 685 return &spec.BaseTopo{ 686 GlobalOptions: &s.GlobalOptions, 687 MonitoredOptions: s.GetMonitoredOptions(), 688 MasterList: s.GetMasterListWithManageHost(), 689 Monitors: s.Monitors, 690 Grafanas: s.Grafanas, 691 Alertmanagers: s.Alertmanagers, 692 } 693 } 694 695 // NewPart implements ScaleOutTopology interface. 696 func (s *Specification) NewPart() spec.Topology { 697 return &Specification{ 698 GlobalOptions: s.GlobalOptions, 699 MonitoredOptions: s.MonitoredOptions, 700 ServerConfigs: s.ServerConfigs, 701 } 702 } 703 704 // MergeTopo implements ScaleOutTopology interface. 705 func (s *Specification) MergeTopo(rhs spec.Topology) spec.Topology { 706 other, ok := rhs.(*Specification) 707 if !ok { 708 panic("topo should be DM Topology") 709 } 710 711 return s.Merge(other) 712 } 713 714 // GetMasterListWithManageHost returns a list of Master API hosts of the current cluster 715 func (s *Specification) GetMasterListWithManageHost() []string { 716 var masterList []string 717 718 for _, master := range s.Masters { 719 host := master.Host 720 if master.ManageHost != "" { 721 host = master.ManageHost 722 } 723 masterList = append(masterList, utils.JoinHostPort(host, master.Port)) 724 } 725 726 return masterList 727 } 728 729 // FillHostArchOrOS fills the topology with the given host->arch 730 func (s *Specification) FillHostArchOrOS(hostArch map[string]string, fullType spec.FullHostType) error { 731 return spec.FillHostArchOrOS(s, hostArch, fullType) 732 } 733 734 // Merge returns a new Topology which sum old ones 735 func (s *Specification) Merge(that spec.Topology) spec.Topology { 736 spec := that.(*Specification) 737 return &Specification{ 738 GlobalOptions: s.GlobalOptions, 739 MonitoredOptions: s.MonitoredOptions, 740 ServerConfigs: s.ServerConfigs, 741 Masters: append(s.Masters, spec.Masters...), 742 Workers: append(s.Workers, spec.Workers...), 743 Monitors: append(s.Monitors, spec.Monitors...), 744 Grafanas: append(s.Grafanas, spec.Grafanas...), 745 Alertmanagers: append(s.Alertmanagers, spec.Alertmanagers...), 746 } 747 } 748 749 // fillDefaults tries to fill custom fields to their default values 750 func fillDMCustomDefaults(globalOptions *GlobalOptions, data any) error { 751 v := reflect.ValueOf(data).Elem() 752 t := v.Type() 753 754 var err error 755 for i := 0; i < t.NumField(); i++ { 756 if err = setDMCustomDefaults(globalOptions, v.Field(i)); err != nil { 757 return err 758 } 759 } 760 761 return nil 762 } 763 764 func setDMCustomDefaults(globalOptions *GlobalOptions, field reflect.Value) error { 765 if !field.CanSet() || isSkipField(field) { 766 return nil 767 } 768 769 switch field.Kind() { 770 case reflect.Slice: 771 for i := 0; i < field.Len(); i++ { 772 if err := setDMCustomDefaults(globalOptions, field.Index(i)); err != nil { 773 return err 774 } 775 } 776 case reflect.Struct: 777 ref := reflect.New(field.Type()) 778 ref.Elem().Set(field) 779 if err := fillDMCustomDefaults(globalOptions, ref.Interface()); err != nil { 780 return err 781 } 782 field.Set(ref.Elem()) 783 case reflect.Ptr: 784 if err := setDMCustomDefaults(globalOptions, field.Elem()); err != nil { 785 return err 786 } 787 } 788 789 if field.Kind() != reflect.Struct { 790 return nil 791 } 792 793 for j := 0; j < field.NumField(); j++ { 794 switch field.Type().Field(j).Name { 795 case "SSHPort": 796 if field.Field(j).Int() != 0 { 797 continue 798 } 799 field.Field(j).Set(reflect.ValueOf(globalOptions.SSHPort)) 800 case "Name": 801 if field.Field(j).String() != "" { 802 continue 803 } 804 host := field.FieldByName("Host").String() 805 port := field.FieldByName("Port").Int() 806 field.Field(j).Set(reflect.ValueOf(fmt.Sprintf("dm-%s-%d", host, port))) 807 case "DataDir": 808 dataDir := field.Field(j).String() 809 if dataDir != "" { // already have a value, skip filling default values 810 continue 811 } 812 // If the data dir in global options is an obsolute path, it appends to 813 // the global and has a comp-port sub directory 814 if strings.HasPrefix(globalOptions.DataDir, "/") { 815 field.Field(j).Set(reflect.ValueOf(filepath.Join( 816 globalOptions.DataDir, 817 fmt.Sprintf("%s-%s", field.Addr().Interface().(InstanceSpec).Role(), getPort(field)), 818 ))) 819 continue 820 } 821 822 // If the data dir in global options is empty or a relative path, keep it be relative 823 // Our run_*.sh start scripts are run inside deploy_path, so the final location 824 // will be deploy_path/global.data_dir 825 // (the default value of global.data_dir is "data") 826 if globalOptions.DataDir == "" { 827 field.Field(j).Set(reflect.ValueOf("data")) 828 } else { 829 field.Field(j).Set(reflect.ValueOf(globalOptions.DataDir)) 830 } 831 case "DeployDir": 832 setDefaultDir(globalOptions.DeployDir, field.Addr().Interface().(InstanceSpec).Role(), getPort(field), field.Field(j)) 833 case "LogDir": 834 if field.Field(j).String() == "" && defaults.CanUpdate(field.Field(j).Interface()) { 835 field.Field(j).Set(reflect.ValueOf(globalOptions.LogDir)) 836 } 837 case "Arch": 838 switch strings.ToLower(field.Field(j).String()) { 839 // replace "x86_64" with amd64, they are the same in our repo 840 case "x86_64": 841 field.Field(j).Set(reflect.ValueOf("amd64")) 842 // replace "aarch64" with arm64 843 case "aarch64": 844 field.Field(j).Set(reflect.ValueOf("arm64")) 845 } 846 847 // convert to lower case 848 if field.Field(j).String() != "" { 849 field.Field(j).Set(reflect.ValueOf(strings.ToLower(field.Field(j).String()))) 850 } 851 case "OS": 852 // convert to lower case 853 if field.Field(j).String() != "" { 854 field.Field(j).Set(reflect.ValueOf(strings.ToLower(field.Field(j).String()))) 855 } 856 } 857 } 858 859 return nil 860 } 861 862 func getPort(v reflect.Value) string { 863 for i := 0; i < v.NumField(); i++ { 864 switch v.Type().Field(i).Name { 865 case "Port", "ClientPort", "WebPort", "TCPPort", "NodeExporterPort": 866 return fmt.Sprintf("%d", v.Field(i).Int()) 867 } 868 } 869 return "" 870 } 871 872 // GetGrafanaConfig returns global grafana configurations 873 func (s *Specification) GetGrafanaConfig() map[string]string { 874 return s.ServerConfigs.Grafana 875 }