diff --git a/components/playground/instance/pd.go b/components/playground/instance/pd.go index 52cb360d28..13bf0a5fe3 100644 --- a/components/playground/instance/pd.go +++ b/components/playground/instance/pd.go @@ -21,6 +21,7 @@ import ( "github.com/pingcap/errors" tiupexec "github.com/pingcap/tiup/pkg/exec" + "github.com/pingcap/tiup/pkg/tidbver" "github.com/pingcap/tiup/pkg/utils" ) @@ -84,7 +85,14 @@ func (inst *PDInstance) InitCluster(pds []*PDInstance) *PDInstance { // Name return the name of pd. func (inst *PDInstance) Name() string { - return fmt.Sprintf("pd-%d", inst.ID) + switch inst.Role { + case PDRoleTSO: + return fmt.Sprintf("tso-%d", inst.ID) + case PDRoleScheduling: + return fmt.Sprintf("scheduling-%d", inst.ID) + default: + return fmt.Sprintf("pd-%d", inst.ID) + } } // Start calls set inst.cmd and Start @@ -143,6 +151,9 @@ func (inst *PDInstance) Start(ctx context.Context, version utils.Version) error fmt.Sprintf("--log-file=%s", inst.LogFile()), fmt.Sprintf("--config=%s", configPath), } + if tidbver.PDSupportMicroServicesWithName(version.String()) { + args = append(args, fmt.Sprintf("--name=%s", uid)) + } case PDRoleScheduling: endpoints := pdEndpoints(inst.pds, true) args = []string{ @@ -154,6 +165,9 @@ func (inst *PDInstance) Start(ctx context.Context, version utils.Version) error fmt.Sprintf("--log-file=%s", inst.LogFile()), fmt.Sprintf("--config=%s", configPath), } + if tidbver.PDSupportMicroServicesWithName(version.String()) { + args = append(args, fmt.Sprintf("--name=%s", uid)) + } } var err error diff --git a/embed/templates/scripts/run_scheduling.sh.tpl b/embed/templates/scripts/run_scheduling.sh.tpl index a15b1ba4f7..2ba72fed0f 100644 --- a/embed/templates/scripts/run_scheduling.sh.tpl +++ b/embed/templates/scripts/run_scheduling.sh.tpl @@ -11,6 +11,9 @@ cd "${DEPLOY_DIR}" || exit 1 exec numactl --cpunodebind={{.NumaNode}} --membind={{.NumaNode}} env GODEBUG=madvdontneed=1 bin/pd-server services scheduling\ {{- else}} exec env GODEBUG=madvdontneed=1 bin/pd-server services scheduling \ +{{- end}} +{{- if .Name}} + --name="{{.Name}}" \ {{- end}} --backend-endpoints="{{.BackendEndpoints}}" \ --listen-addr="{{.ListenURL}}" \ diff --git a/embed/templates/scripts/run_tso.sh.tpl b/embed/templates/scripts/run_tso.sh.tpl index 0d6486d73e..177b676aff 100644 --- a/embed/templates/scripts/run_tso.sh.tpl +++ b/embed/templates/scripts/run_tso.sh.tpl @@ -11,6 +11,9 @@ cd "${DEPLOY_DIR}" || exit 1 exec numactl --cpunodebind={{.NumaNode}} --membind={{.NumaNode}} env GODEBUG=madvdontneed=1 bin/pd-server services tso\ {{- else}} exec env GODEBUG=madvdontneed=1 bin/pd-server services tso \ +{{- end}} +{{- if .Name}} + --name="{{.Name}}" \ {{- end}} --backend-endpoints="{{.BackendEndpoints}}" \ --listen-addr="{{.ListenURL}}" \ diff --git a/go.mod b/go.mod index 2d3137fd7c..da2dafbc79 100644 --- a/go.mod +++ b/go.mod @@ -99,6 +99,7 @@ require ( github.com/rivo/uniseg v0.4.4 // indirect github.com/rs/xid v1.4.0 // indirect github.com/sirupsen/logrus v1.9.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/tklauser/go-sysconf v0.3.11 // indirect github.com/tklauser/numcpus v0.6.0 // indirect github.com/vishvananda/netlink v0.0.0-20210530105856-14e832ae1e8f // indirect diff --git a/go.sum b/go.sum index b3001521ff..5316ac9032 100644 --- a/go.sum +++ b/go.sum @@ -214,6 +214,8 @@ github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUq github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= diff --git a/pkg/cluster/manager/manager_test.go b/pkg/cluster/manager/manager_test.go index e80d1d1c64..b82b6eb785 100644 --- a/pkg/cluster/manager/manager_test.go +++ b/pkg/cluster/manager/manager_test.go @@ -85,6 +85,21 @@ pd_servers: assert.Nil(err) err = validateNewTopo(&topo) assert.NotNil(err) + + topo = spec.Specification{} + err = yaml.Unmarshal([]byte(` +global: + user: "test4" + deploy_dir: "test-deploy" + data_dir: "test-data" +tso_servers: + - host: 172.16.5.53 +scheduling_servers: + - host: 172.16.5.54 +`), &topo) + assert.Nil(err) + err = validateNewTopo(&topo) + assert.Nil(err) } func TestDeduplicateCheckResult(t *testing.T) { diff --git a/pkg/cluster/manager/transfer_test.go b/pkg/cluster/manager/transfer_test.go index bef179dd0b..d4b4581f6a 100644 --- a/pkg/cluster/manager/transfer_test.go +++ b/pkg/cluster/manager/transfer_test.go @@ -53,4 +53,32 @@ func TestRenderSpec(t *testing.T) { dir, err = renderSpec("{{.DataDir}}", s, "test-pd") assert.Nil(t, err) assert.NotEmpty(t, dir) + + s = &spec.TSOInstance{BaseInstance: spec.BaseInstance{ + InstanceSpec: &spec.TSOSpec{ + Host: "172.16.5.140", + SSHPort: 22, + Name: "tso-1", + DeployDir: "/home/test/deploy/tso-3379", + DataDir: "/home/test/deploy/tso-3379/data", + }, + }} + // s.BaseInstance.InstanceSpec + dir, err = renderSpec("{{.DataDir}}", s, "test-tso") + assert.Nil(t, err) + assert.NotEmpty(t, dir) + + s = &spec.SchedulingInstance{BaseInstance: spec.BaseInstance{ + InstanceSpec: &spec.SchedulingSpec{ + Host: "172.16.5.140", + SSHPort: 22, + Name: "scheduling-1", + DeployDir: "/home/test/deploy/scheduling-3379", + DataDir: "/home/test/deploy/scheduling-3379/data", + }, + }} + // s.BaseInstance.InstanceSpec + dir, err = renderSpec("{{.DataDir}}", s, "test-scheduling") + assert.Nil(t, err) + assert.NotEmpty(t, dir) } diff --git a/pkg/cluster/operation/upgrade.go b/pkg/cluster/operation/upgrade.go index 5c4c420858..a5eea3fc18 100644 --- a/pkg/cluster/operation/upgrade.go +++ b/pkg/cluster/operation/upgrade.go @@ -18,6 +18,7 @@ import ( "crypto/tls" "fmt" "os" + "path/filepath" "reflect" "strings" "time" @@ -25,6 +26,7 @@ import ( perrs "github.com/pingcap/errors" "github.com/pingcap/tiup/pkg/checkpoint" "github.com/pingcap/tiup/pkg/cluster/api" + "github.com/pingcap/tiup/pkg/cluster/ctxt" "github.com/pingcap/tiup/pkg/cluster/spec" logprinter "github.com/pingcap/tiup/pkg/logger/printer" "github.com/pingcap/tiup/pkg/set" @@ -144,6 +146,52 @@ func Upgrade( logger.Debugf("Deferred upgrading of PD leader %s", instance.ID()) continue } + case spec.ComponentTSO: + // if component version is not specified, use the cluster version or latest("") + ins := instance.(*spec.TSOInstance) + version, err := getVersion(ctx, ins.DeployDir(), ins.GetManageHost()) + if err != nil { + return err + } + logger.Infof(fmt.Sprintf("tso instance change startScript %s, %s", version, ins.Name)) + if tidbver.PDSupportMicroServicesWithName(version) && ins.Name != "" { + tsos := topo.(*spec.Specification).TSOServers + var name string + for _, s := range tsos { + if s.Host == ins.Host && s.Port == ins.Port { + name = s.Name + break + } + } + logger.Infof(fmt.Sprintf("tso instance change startScript %s", name)) + ins := instance.(*spec.TSOInstance) + err := spec.ModifyPDStartScriptPath(ctx, ins.Role(), ins.Host, ins.GetMainPort(), name) + if err != nil { + return err + } + } + case spec.ComponentScheduling: + ins := instance.(*spec.SchedulingInstance) + version, err := getVersion(ctx, ins.DeployDir(), ins.GetManageHost()) + if err != nil { + return err + } + logger.Infof(fmt.Sprintf("scheduling instance change startScript %s, %s", version, ins.Name)) + if tidbver.PDSupportMicroServicesWithName(version) && ins.Name != "" { + schedulings := topo.(*spec.Specification).SchedulingServers + var name string + for _, s := range schedulings { + if s.Host == ins.Host && s.Port == ins.Port { + name = s.Name + break + } + } + logger.Infof(fmt.Sprintf("scheduling instance change startScript %s", name)) + err := spec.ModifyPDStartScriptPath(ctx, ins.Role(), ins.Host, ins.GetMainPort(), name) + if err != nil { + return err + } + } case spec.ComponentCDC: ins := instance.(*spec.CDCInstance) address := ins.GetAddr() @@ -362,3 +410,25 @@ func decreaseScheduleLimit(pc *api.PDClient, origLeaderScheduleLimit, origRegion } return pc.SetReplicationConfig("region-schedule-limit", origRegionScheduleLimit) } + +func getVersion(ctx context.Context, deployDir, host string) (string, error) { + binPath := filepath.Join(deployDir, "bin/pd-server") + e, found := ctxt.GetInner(ctx).GetExecutor(host) + if !found { + return "", fmt.Errorf("no executor") + } + stdout, stderr, err := e.Execute(ctx, binPath+" --version", false) + if err != nil { + return "", perrs.Annotatef(err, "stderr: %s", string(stderr)) + } + + var version string + for _, line := range strings.Split(string(stdout), "\n") { + if strings.HasPrefix(line, "Release Version:") { + // Extract version number from the line + version = strings.TrimSpace(strings.TrimPrefix(line, "Release Version:")) + break + } + } + return version, nil +} diff --git a/pkg/cluster/operation/upgrade_test.go b/pkg/cluster/operation/upgrade_test.go new file mode 100644 index 0000000000..98469994e0 --- /dev/null +++ b/pkg/cluster/operation/upgrade_test.go @@ -0,0 +1,74 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package operator + +import ( + "context" + "testing" + "time" + + "github.com/pingcap/tiup/pkg/cluster/ctxt" + logprinter "github.com/pingcap/tiup/pkg/logger/printer" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// MockExecutor is a mock of Executor interface +type MockExecutor struct { + mock.Mock +} + +func (m *MockExecutor) Execute(ctx context.Context, cmd string, sudo bool, timeout ...time.Duration) (stdout []byte, stderr []byte, err error) { + args := m.Called(ctx, cmd, sudo) + return args.Get(0).([]byte), args.Get(1).([]byte), args.Error(2) +} + +func (m *MockExecutor) Transfer(ctx context.Context, src, dst string, download bool, limit int, compress bool) error { + panic("implement me") +} + +func TestGetVersion(t *testing.T) { + ctx := ctxt.New(context.Background(), 0, logprinter.NewLogger("")) + deployDir := "/fake/deploy/dir" + host := "localhost" + expectedVersion := "v8.3.0-abcdef" + mockExecutor := new(MockExecutor) + mockExecutor.On("Execute", ctx, "/fake/deploy/dir/bin/pd-server --version", false).Return([]byte("Release Version: v8.3.0-abcdef\nEdition: Community\nGit Commit Hash: b235bc1152a7942d71d42222b0e078d91ccd0106\nGit Branch: heads/refs/tags/v8.3.0\nUTC Build Time: 2024-07-01 05:08:57"), []byte(""), nil) + ctxt.GetInner(ctx).SetExecutor(host, mockExecutor) + // Execute + version, err := getVersion(ctx, deployDir, host) + assert.NoError(t, err) + assert.Equal(t, expectedVersion, version) + + expectedVersion = "v8.2.0-abcdef" + mockExecutor = new(MockExecutor) + mockExecutor.On("Execute", ctx, "/fake/deploy/dir/bin/pd-server --version", false).Return([]byte("Release Version: v8.2.0-abcdef\nEdition: Community\nGit Commit Hash: b235bc1152a7942d71d42222b0e078d91ccd0106\nGit Branch: heads/refs/tags/v8.3.0\nUTC Build Time: 2024-07-01 05:08:57"), []byte(""), nil) + ctxt.GetInner(ctx).SetExecutor(host, mockExecutor) + // Execute + version, err = getVersion(ctx, deployDir, host) + assert.NoError(t, err) + assert.Equal(t, expectedVersion, version) + + expectedVersion = "nightly-abcdef" + mockExecutor = new(MockExecutor) + mockExecutor.On("Execute", ctx, "/fake/deploy/dir/bin/pd-server --version", false).Return([]byte("Release Version: nightly-abcdef\nEdition: Community\nGit Commit Hash: b235bc1152a7942d71d42222b0e078d91ccd0106\nGit Branch: heads/refs/tags/v8.3.0\nUTC Build Time: 2024-07-01 05:08:57"), []byte(""), nil) + ctxt.GetInner(ctx).SetExecutor(host, mockExecutor) + // Execute + version, err = getVersion(ctx, deployDir, host) + assert.NoError(t, err) + assert.Equal(t, expectedVersion, version) + + // Teardown + mockExecutor.AssertExpectations(t) +} diff --git a/pkg/cluster/spec/scheduling.go b/pkg/cluster/spec/scheduling.go index efbe7def00..3da3975f2d 100644 --- a/pkg/cluster/spec/scheduling.go +++ b/pkg/cluster/spec/scheduling.go @@ -25,26 +25,29 @@ import ( "github.com/pingcap/tiup/pkg/cluster/ctxt" "github.com/pingcap/tiup/pkg/cluster/template/scripts" "github.com/pingcap/tiup/pkg/meta" + "github.com/pingcap/tiup/pkg/tidbver" "github.com/pingcap/tiup/pkg/utils" ) // SchedulingSpec represents the scheduling topology specification in topology.yaml type SchedulingSpec struct { - Host string `yaml:"host"` - ManageHost string `yaml:"manage_host,omitempty" validate:"manage_host:editable"` - ListenHost string `yaml:"listen_host,omitempty"` - AdvertiseListenAddr string `yaml:"advertise_listen_addr,omitempty"` - SSHPort int `yaml:"ssh_port,omitempty" validate:"ssh_port:editable"` - IgnoreExporter bool `yaml:"ignore_exporter,omitempty"` - Port int `yaml:"port" default:"3379"` - DeployDir string `yaml:"deploy_dir,omitempty"` - DataDir string `yaml:"data_dir,omitempty"` - LogDir string `yaml:"log_dir,omitempty"` - Source string `yaml:"source,omitempty" validate:"source:editable"` - NumaNode string `yaml:"numa_node,omitempty" validate:"numa_node:editable"` - Config map[string]any `yaml:"config,omitempty" validate:"config:ignore"` - Arch string `yaml:"arch,omitempty"` - OS string `yaml:"os,omitempty"` + Host string `yaml:"host"` + ManageHost string `yaml:"manage_host,omitempty" validate:"manage_host:editable"` + ListenHost string `yaml:"listen_host,omitempty"` + AdvertiseListenAddr string `yaml:"advertise_listen_addr,omitempty"` + SSHPort int `yaml:"ssh_port,omitempty" validate:"ssh_port:editable"` + IgnoreExporter bool `yaml:"ignore_exporter,omitempty"` + // Use Name to get the name with a default value if it's empty. + Name string `yaml:"name,omitempty"` + Port int `yaml:"port" default:"3379"` + DeployDir string `yaml:"deploy_dir,omitempty"` + DataDir string `yaml:"data_dir,omitempty"` + LogDir string `yaml:"log_dir,omitempty"` + Source string `yaml:"source,omitempty" validate:"source:editable"` + NumaNode string `yaml:"numa_node,omitempty" validate:"numa_node:editable"` + Config map[string]any `yaml:"config,omitempty" validate:"config:ignore"` + Arch string `yaml:"arch,omitempty"` + OS string `yaml:"os,omitempty"` } // Status queries current status of the instance @@ -200,7 +203,6 @@ func (c *SchedulingComponent) Instances() []Instance { // SchedulingInstance represent the scheduling instance type SchedulingInstance struct { - Name string BaseInstance topo Topology } @@ -229,6 +231,7 @@ func (i *SchedulingInstance) InitConfig( pds = append(pds, pdspec.GetAdvertiseClientURL(enableTLS)) } cfg := &scripts.SchedulingScript{ + Name: spec.Name, ListenURL: fmt.Sprintf("%s://%s", scheme, utils.JoinHostPort(i.GetListenHost(), spec.Port)), AdvertiseListenURL: spec.GetAdvertiseListenURL(enableTLS), BackendEndpoints: strings.Join(pds, ","), @@ -237,6 +240,9 @@ func (i *SchedulingInstance) InitConfig( LogDir: paths.Log, NumaNode: spec.NumaNode, } + if !tidbver.PDSupportMicroServicesWithName(version) { + cfg.Name = "" + } fp := filepath.Join(paths.Cache, fmt.Sprintf("run_scheduling_%s_%d.sh", i.GetHost(), i.GetPort())) if err := cfg.ConfigToFile(fp); err != nil { diff --git a/pkg/cluster/spec/spec.go b/pkg/cluster/spec/spec.go index ffef492dda..99bd0e87dd 100644 --- a/pkg/cluster/spec/spec.go +++ b/pkg/cluster/spec/spec.go @@ -679,10 +679,20 @@ func setCustomDefaults(globalOptions *GlobalOptions, field reflect.Value) error } field.Field(j).Set(reflect.ValueOf(globalOptions.SSHPort)) case "Name": + // Only PD related components have `Name` field, if field.Field(j).String() != "" { continue } host := reflect.Indirect(field).FieldByName("Host").String() + // `TSO` and `Scheduling` components use `Port` filed + if reflect.Indirect(field).FieldByName("Port").IsValid() { + port := reflect.Indirect(field).FieldByName("Port").Int() + // field.String() is + role := strings.Split(strings.Split(field.Type().String(), ".")[1], "Spec")[0] + component := strings.ToLower(role) + field.Field(j).Set(reflect.ValueOf(fmt.Sprintf("%s-%s-%d", component, host, port))) + continue + } clientPort := reflect.Indirect(field).FieldByName("ClientPort").Int() field.Field(j).Set(reflect.ValueOf(fmt.Sprintf("pd-%s-%d", host, clientPort))) case "DataDir": diff --git a/pkg/cluster/spec/tso.go b/pkg/cluster/spec/tso.go index a84069f6ee..e309f1d652 100644 --- a/pkg/cluster/spec/tso.go +++ b/pkg/cluster/spec/tso.go @@ -25,26 +25,29 @@ import ( "github.com/pingcap/tiup/pkg/cluster/ctxt" "github.com/pingcap/tiup/pkg/cluster/template/scripts" "github.com/pingcap/tiup/pkg/meta" + "github.com/pingcap/tiup/pkg/tidbver" "github.com/pingcap/tiup/pkg/utils" ) // TSOSpec represents the TSO topology specification in topology.yaml type TSOSpec struct { - Host string `yaml:"host"` - ManageHost string `yaml:"manage_host,omitempty" validate:"manage_host:editable"` - ListenHost string `yaml:"listen_host,omitempty"` - AdvertiseListenAddr string `yaml:"advertise_listen_addr,omitempty"` - SSHPort int `yaml:"ssh_port,omitempty" validate:"ssh_port:editable"` - IgnoreExporter bool `yaml:"ignore_exporter,omitempty"` - Port int `yaml:"port" default:"3379"` - DeployDir string `yaml:"deploy_dir,omitempty"` - DataDir string `yaml:"data_dir,omitempty"` - LogDir string `yaml:"log_dir,omitempty"` - Source string `yaml:"source,omitempty" validate:"source:editable"` - NumaNode string `yaml:"numa_node,omitempty" validate:"numa_node:editable"` - Config map[string]any `yaml:"config,omitempty" validate:"config:ignore"` - Arch string `yaml:"arch,omitempty"` - OS string `yaml:"os,omitempty"` + Host string `yaml:"host"` + ManageHost string `yaml:"manage_host,omitempty" validate:"manage_host:editable"` + ListenHost string `yaml:"listen_host,omitempty"` + AdvertiseListenAddr string `yaml:"advertise_listen_addr,omitempty"` + SSHPort int `yaml:"ssh_port,omitempty" validate:"ssh_port:editable"` + IgnoreExporter bool `yaml:"ignore_exporter,omitempty"` + // Use Name to get the name with a default value if it's empty. + Name string `yaml:"name,omitempty"` + Port int `yaml:"port" default:"3379"` + DeployDir string `yaml:"deploy_dir,omitempty"` + DataDir string `yaml:"data_dir,omitempty"` + LogDir string `yaml:"log_dir,omitempty"` + Source string `yaml:"source,omitempty" validate:"source:editable"` + NumaNode string `yaml:"numa_node,omitempty" validate:"numa_node:editable"` + Config map[string]any `yaml:"config,omitempty" validate:"config:ignore"` + Arch string `yaml:"arch,omitempty"` + OS string `yaml:"os,omitempty"` } // Status queries current status of the instance @@ -200,7 +203,6 @@ func (c *TSOComponent) Instances() []Instance { // TSOInstance represent the TSO instance type TSOInstance struct { - Name string BaseInstance topo Topology } @@ -229,6 +231,7 @@ func (i *TSOInstance) InitConfig( pds = append(pds, pdspec.GetAdvertiseClientURL(enableTLS)) } cfg := &scripts.TSOScript{ + Name: spec.Name, ListenURL: fmt.Sprintf("%s://%s", scheme, utils.JoinHostPort(i.GetListenHost(), spec.Port)), AdvertiseListenURL: spec.GetAdvertiseListenURL(enableTLS), BackendEndpoints: strings.Join(pds, ","), @@ -237,6 +240,9 @@ func (i *TSOInstance) InitConfig( LogDir: paths.Log, NumaNode: spec.NumaNode, } + if !tidbver.PDSupportMicroServicesWithName(version) { + cfg.Name = "" + } fp := filepath.Join(paths.Cache, fmt.Sprintf("run_tso_%s_%d.sh", i.GetHost(), i.GetPort())) if err := cfg.ConfigToFile(fp); err != nil { diff --git a/pkg/cluster/spec/util.go b/pkg/cluster/spec/util.go index 26ba27c5ed..e3abb495f0 100644 --- a/pkg/cluster/spec/util.go +++ b/pkg/cluster/spec/util.go @@ -23,6 +23,8 @@ import ( "strings" "time" + "github.com/pingcap/errors" + "github.com/pingcap/tiup/pkg/cluster/ctxt" "github.com/pingcap/tiup/pkg/utils" "github.com/pingcap/tiup/pkg/version" "github.com/prometheus/common/expfmt" @@ -231,3 +233,82 @@ func GetDMMasterPackageName(topo Topology) string { } return ComponentDMMaster } + +var systemdUnitPath = "/etc/systemd/system" + +// ModifyPDStartScriptPath modify the start *PD* script path to add `--name` flag +func ModifyPDStartScriptPath(ctx context.Context, component, host string, port int, name string) error { + e, found := ctxt.GetInner(ctx).GetExecutor(host) + if !found { + return fmt.Errorf("no executor") + } + // 1. find the script path + serviceFile := fmt.Sprintf("%s/%s-%d.service", + systemdUnitPath, + component, + port) + cmd := fmt.Sprintf("grep 'ExecStart' %s | sed 's/ExecStart=//'", serviceFile) + stdout, stderr, err := e.Execute(ctx, cmd, false) + if err != nil { + return err + } + if len(stderr) > 0 { + return errors.Errorf( + "can not detect dir paths of %s %s:%d, %s", + component, + host, + port, + stderr, + ) + } + // 2. check script content if contains `name` + path := extractScriptPath(string(stdout)) + cmd = fmt.Sprintf("cat %s", path) + stdout, stderr, err = e.Execute(ctx, cmd, false) + if err != nil { + return err + } + if len(stderr) > 0 { + return errors.Errorf( + "can not read script path of %s %s:%d, %s", + component, + host, + port, + stderr, + ) + } + + if strings.Contains(string(stdout), "name") { + return nil + } + + // 3. writing the name to the script + content := fmt.Sprintf(" \\\n --name='%s'", name) + cmd = fmt.Sprintf("sed -i '$ s|$| %s|' %s", content, path) + stdout, stderr, err = e.Execute(ctx, cmd, false) + if err != nil { + return err + } + if len(stderr) > 0 { + return errors.Errorf( + "can not modify script path of %s %s:%d, %s", + component, + host, + port, + stderr, + ) + } + + return nil +} + +// path like `/bin/bash -c '/root/tidb-deploy/scheduling-3399/scripts/run_scheduling.sh'\n` +func extractScriptPath(command string) string { + command = strings.TrimSuffix(command, "\n") + parts := strings.Split(command, " ") + if len(parts) < 3 { + return "Invalid command structure" + } + + return strings.Trim(parts[2], "'") +} diff --git a/pkg/cluster/spec/util_test.go b/pkg/cluster/spec/util_test.go index fb5d487d79..69a33b1a80 100644 --- a/pkg/cluster/spec/util_test.go +++ b/pkg/cluster/spec/util_test.go @@ -14,7 +14,18 @@ package spec import ( + "context" + "fmt" + "os" + "testing" + "time" + "github.com/pingcap/check" + "github.com/pingcap/tiup/pkg/cluster/ctxt" + logprinter "github.com/pingcap/tiup/pkg/logger/printer" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" ) type utilSuite struct{} @@ -61,3 +72,58 @@ func (s *utilSuite) TestMultiDirAbs(c *check.C) { c.Assert(paths[0], check.Equals, "/home/tidb/a") c.Assert(paths[1], check.Equals, "/tmp/b") } + +func TestExtractScriptPath(t *testing.T) { + command := "/bin/bash -c '/root/tidb-deploy/scheduling-3399/scripts/run_scheduling.sh'\n" + scriptPath := extractScriptPath(command) + println("scriptPath:", scriptPath) + assert.Equal(t, "/root/tidb-deploy/scheduling-3399/scripts/run_scheduling.sh", scriptPath) +} + +// MockExecutor simulates command execution +type MockExecutor struct { + mock.Mock +} + +func (m *MockExecutor) Execute(ctx context.Context, cmd string, sudo bool, timeout ...time.Duration) (stdout []byte, stderr []byte, err error) { + args := m.Called(ctx, cmd, sudo) + return args.Get(0).([]byte), args.Get(1).([]byte), args.Error(2) +} + +func (m *MockExecutor) Transfer(ctx context.Context, src, dst string, download bool, limit int, compress bool) error { + panic("implement me") +} + +func TestModifyStartScriptPath(t *testing.T) { + ctx := ctxt.New(context.Background(), 0, logprinter.NewLogger("")) + component := "test-component" + host := "localhost" + port := 8080 + content := fmt.Sprintf(" \\\n --name='%s'", "scheudling-0") + + // Setup temporary file for testing + assert := require.New(t) + conf, err := os.CreateTemp("", "scheduling.conf") + defer os.Remove(conf.Name()) + assert.Nil(err) + scriptPath := conf.Name() + err = os.WriteFile(scriptPath, []byte(content), 0644) + assert.Nil(err) + + // Mock executor + mockExec := new(MockExecutor) + shPath := fmt.Sprintf("/bin/bash -c '%s'", conf.Name()) + mockExec.On("Execute", ctx, mock.Anything, false).Return([]byte(shPath), []byte(""), nil) + + // Inject mock executor + ctxt.GetInner(ctx).SetExecutor(host, mockExec) + + // Test function + err = ModifyStartScriptPath(ctx, component, host, port, content) + assert.Nil(err) + + // Verify file content + resultContent, err := os.ReadFile(scriptPath) + assert.Nil(err) + assert.Contains(string(resultContent), content) +} diff --git a/pkg/cluster/spec/validate.go b/pkg/cluster/spec/validate.go index 99384cbec7..ce73682c01 100644 --- a/pkg/cluster/spec/validate.go +++ b/pkg/cluster/spec/validate.go @@ -984,6 +984,38 @@ func (s *Specification) validatePDNames() error { return nil } +func (s *Specification) validateTSONames() error { + // check tso server name + tsoNames := set.NewStringSet() + for _, tso := range s.TSOServers { + if tso.Name == "" { + continue + } + + if tsoNames.Exist(tso.Name) { + return errors.Errorf("component tso_servers.name is not supported duplicated, the name %s is duplicated", tso.Name) + } + tsoNames.Insert(tso.Name) + } + return nil +} + +func (s *Specification) validateSchedulingNames() error { + // check scheduling server name + schedulingNames := set.NewStringSet() + for _, scheduling := range s.SchedulingServers { + if scheduling.Name == "" { + continue + } + + if schedulingNames.Exist(scheduling.Name) { + return errors.Errorf("component scheduling_servers.name is not supported duplicated, the name %s is duplicated", scheduling.Name) + } + schedulingNames.Insert(scheduling.Name) + } + return nil +} + func (s *Specification) validateTiFlashConfigs() error { c := FindComponent(s, ComponentTiFlash) for _, ins := range c.Instances() { @@ -1063,6 +1095,8 @@ func (s *Specification) Validate() error { s.dirConflictsDetect, s.validateUserGroup, s.validatePDNames, + s.validateTSONames, + s.validateSchedulingNames, s.validateTiSparkSpec, s.validateTiFlashConfigs, s.validateMonitorAgent, diff --git a/pkg/cluster/template/scripts/pdms_test.go b/pkg/cluster/template/scripts/pdms_test.go new file mode 100644 index 0000000000..414f693f1f --- /dev/null +++ b/pkg/cluster/template/scripts/pdms_test.go @@ -0,0 +1,80 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package scripts + +import ( + "os" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestScheduling(t *testing.T) { + assert := require.New(t) + conf, err := os.CreateTemp("", "scheduling.conf") + assert.Nil(err) + defer os.Remove(conf.Name()) + + cfg := &SchedulingScript{ + Name: "scheduling-0", + ListenURL: "127.0.0.1", + AdvertiseListenURL: "127.0.0.2", + BackendEndpoints: "127.0.0.3", + DeployDir: "/deploy", + DataDir: "/data", + LogDir: "/log", + } + err = cfg.ConfigToFile(conf.Name()) + assert.Nil(err) + content, err := os.ReadFile(conf.Name()) + assert.Nil(err) + assert.True(strings.Contains(string(content), "--name")) + + cfg.Name = "" + err = cfg.ConfigToFile(conf.Name()) + assert.Nil(err) + content, err = os.ReadFile(conf.Name()) + assert.Nil(err) + assert.False(strings.Contains(string(content), "--name")) +} + +func TestTSO(t *testing.T) { + assert := require.New(t) + conf, err := os.CreateTemp("", "tso.conf") + assert.Nil(err) + defer os.Remove(conf.Name()) + + cfg := &TSOScript{ + Name: "tso-0", + ListenURL: "127.0.0.1", + AdvertiseListenURL: "127.0.0.2", + BackendEndpoints: "127.0.0.3", + DeployDir: "/deploy", + DataDir: "/data", + LogDir: "/log", + } + err = cfg.ConfigToFile(conf.Name()) + assert.Nil(err) + content, err := os.ReadFile(conf.Name()) + assert.Nil(err) + assert.True(strings.Contains(string(content), "--name")) + + cfg.Name = "" + err = cfg.ConfigToFile(conf.Name()) + assert.Nil(err) + content, err = os.ReadFile(conf.Name()) + assert.Nil(err) + assert.False(strings.Contains(string(content), "--name")) +} diff --git a/pkg/cluster/template/scripts/scheduling.go b/pkg/cluster/template/scripts/scheduling.go index 6167d9336e..76142485a2 100644 --- a/pkg/cluster/template/scripts/scheduling.go +++ b/pkg/cluster/template/scripts/scheduling.go @@ -24,6 +24,7 @@ import ( // SchedulingScript represent the data to generate scheduling config type SchedulingScript struct { + Name string ListenURL string AdvertiseListenURL string BackendEndpoints string diff --git a/pkg/cluster/template/scripts/tso.go b/pkg/cluster/template/scripts/tso.go index 0197b82c38..91c3bfe1d0 100644 --- a/pkg/cluster/template/scripts/tso.go +++ b/pkg/cluster/template/scripts/tso.go @@ -24,6 +24,7 @@ import ( // TSOScript represent the data to generate tso config type TSOScript struct { + Name string ListenURL string AdvertiseListenURL string BackendEndpoints string diff --git a/pkg/tidbver/tidbver.go b/pkg/tidbver/tidbver.go index c659304a85..c811510cfc 100644 --- a/pkg/tidbver/tidbver.go +++ b/pkg/tidbver/tidbver.go @@ -104,6 +104,11 @@ func PDSupportMicroServices(version string) bool { return semver.Compare(version, "v7.3.0") >= 0 || strings.Contains(version, "nightly") } +// PDSupportMicroServicesWithName return if the given version of PD supports micro services with name. +func PDSupportMicroServicesWithName(version string) bool { + return semver.Compare(version, "v8.3.0") >= 0 || strings.Contains(version, "nightly") +} + // TiCDCSupportConfigFile return if given version of TiCDC support config file func TiCDCSupportConfigFile(version string) bool { // config support since v4.0.13, ignore v5.0.0-rc