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  }