From d2789bfb745f3321e54af5f19edbcbc36db18962 Mon Sep 17 00:00:00 2001 From: Hangjie Mo Date: Thu, 27 Aug 2026 15:59:19 +0800 Subject: [PATCH 1/6] cluster: fix Grafana VM datasource replacement (#2732) (cherry picked from commit 17b30ee5886fac2023502a1aeedb245cf393db58) --- pkg/cluster/spec/grafana.go | 7 ++-- pkg/cluster/spec/grafana_test.go | 55 ++++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/pkg/cluster/spec/grafana.go b/pkg/cluster/spec/grafana.go index 13ddaed8ab..97b037dce7 100644 --- a/pkg/cluster/spec/grafana.go +++ b/pkg/cluster/spec/grafana.go @@ -374,13 +374,16 @@ func (i *GrafanaInstance) initDashboards(ctx context.Context, e ctxt.Executor, s } // Deal with the cluster name and datasource + // Replace literal dashboard placeholders first. The datasource name can contain + // "test-cluster" (for example, "test-cluster-vm"), so replacing these literals + // after datasource placeholders would rewrite the generated name a second time. for _, cmd := range []string{ + `find %s -type f -exec sed -i 's/test-cluster/%s/g' {} \;`, + `find %s -type f -exec sed -i 's/Test-Cluster/%s/g' {} \;`, `find %s -type f -exec sed -i 's/\${DS_.*-CLUSTER}/%s/g' {} \;`, `find %s -type f -exec sed -i 's/DS_.*-CLUSTER/%s/g' {} \;`, `find %s -type f -exec sed -i 's/\${DS_LIGHTNING}/%s/g' {} \;`, `find %s -type f -exec sed -i 's/DS_LIGHTNING/%s/g' {} \;`, - `find %s -type f -exec sed -i 's/test-cluster/%s/g' {} \;`, - `find %s -type f -exec sed -i 's/Test-Cluster/%s/g' {} \;`, } { cmd := fmt.Sprintf(cmd, dashboardsDir, datasourceName) _, stderr, err := e.Execute(ctx, cmd, false) diff --git a/pkg/cluster/spec/grafana_test.go b/pkg/cluster/spec/grafana_test.go index 901f7cefbd..1de58af724 100644 --- a/pkg/cluster/spec/grafana_test.go +++ b/pkg/cluster/spec/grafana_test.go @@ -282,6 +282,18 @@ func TestVictoriaMetricsDefaultDatasource(t *testing.T) { err = os.MkdirAll(binDir, 0755) require.NoError(t, err) + datasourceName := "test-cluster-vm" + dashboardReplacements := []struct { + commandPattern string + placeholder string + }{ + {`s/test-cluster/`, "test-cluster"}, + {`s/Test-Cluster/`, "Test-Cluster"}, + {`s/\${DS_.*-CLUSTER}/`, "${DS_TEST-CLUSTER}"}, + {`s/DS_.*-CLUSTER/`, "DS_TEST-CLUSTER"}, + {`s/\${DS_LIGHTNING}/`, "${DS_LIGHTNING}"}, + {`s/DS_LIGHTNING/`, "DS_LIGHTNING"}, + } // Create a mock for the execute function to handle the dashboard copy command origExecutor := &mockExecutor{ executeFunc: func(ctx context.Context, cmd string, sudo bool, timeouts ...time.Duration) ([]byte, []byte, error) { @@ -297,7 +309,7 @@ func TestVictoriaMetricsDefaultDatasource(t *testing.T) { return nil, nil, err } } else if strings.Contains(cmd, "sed") { - // Handle the sed command to replace datasource references + // Handle each sed command using the same replacement order as initDashboards. files, err := os.ReadDir(dashboardsDir) if err != nil { return nil, nil, err @@ -305,23 +317,21 @@ func TestVictoriaMetricsDefaultDatasource(t *testing.T) { for _, file := range files { if strings.HasSuffix(file.Name(), ".json") { - content, err := os.ReadFile(filepath.Join(dashboardsDir, file.Name())) + filePath := filepath.Join(dashboardsDir, file.Name()) + content, err := os.ReadFile(filePath) if err != nil { return nil, nil, err } - // Replace datasource references - simulating what sed would do - modifiedContent := strings.ReplaceAll(string(content), - `"DS_TEST-CLUSTER"`, - fmt.Sprintf(`"DS_%s-VM"`, strings.ToUpper("test-cluster"))) - modifiedContent = strings.ReplaceAll(modifiedContent, - `"text": "test-cluster"`, - fmt.Sprintf(`"text": "%s-vm"`, "test-cluster")) - modifiedContent = strings.ReplaceAll(modifiedContent, - `"value": "test-cluster"`, - fmt.Sprintf(`"value": "%s-vm"`, "test-cluster")) - - err = os.WriteFile(filepath.Join(dashboardsDir, file.Name()), []byte(modifiedContent), 0644) + modifiedContent := string(content) + for _, replacement := range dashboardReplacements { + if strings.Contains(cmd, replacement.commandPattern) { + modifiedContent = strings.ReplaceAll(modifiedContent, replacement.placeholder, datasourceName) + break + } + } + + err = os.WriteFile(filePath, []byte(modifiedContent), 0644) if err != nil { return nil, nil, err } @@ -335,7 +345,11 @@ func TestVictoriaMetricsDefaultDatasource(t *testing.T) { // Create a sample dashboard file with datasource references dashboardContent := `{ "annotations": { - "list": [] + "list": [ + { + "datasource": "${DS_TEST-CLUSTER}" + } + ] }, "editable": true, "fiscalYearStartMonth": 0, @@ -408,8 +422,15 @@ func TestVictoriaMetricsDefaultDatasource(t *testing.T) { content, err := os.ReadFile(dashboardFile) require.NoError(t, err) - // Verify VM datasource was used - assert.Contains(t, string(content), `"DS_TEST-CLUSTER-VM"`) + // Verify every dashboard reference uses the provisioned VM datasource name + // without rewriting it to test-cluster-vm-vm. + assert.Contains(t, string(content), `"datasource": "test-cluster-vm"`) + assert.Contains(t, string(content), `"name": "test-cluster-vm"`) assert.Contains(t, string(content), `"text": "test-cluster-vm"`) assert.Contains(t, string(content), `"value": "test-cluster-vm"`) + assert.NotContains(t, string(content), "test-cluster-vm-vm") + + dsContent, err := os.ReadFile(filepath.Join(deployDir, "provisioning", "datasources", "datasource.yml")) + require.NoError(t, err) + assert.Contains(t, string(dsContent), "name: test-cluster-vm") } From 956caae237e7b26d0335eb44db59505af5e48e85 Mon Sep 17 00:00:00 2001 From: mayjiang0203 Date: Thu, 3 Sep 2026 17:06:17 +0800 Subject: [PATCH 2/6] cluster: scrape node_exporter on dedicated tidb-dashboard hosts (#2734) (cherry picked from commit c1c95d735e9f0777b3a1ea4f4fdf36f389b11a59) --- pkg/cluster/spec/monitoring.go | 14 ++++++ pkg/cluster/spec/monitoring_test.go | 78 +++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/pkg/cluster/spec/monitoring.go b/pkg/cluster/spec/monitoring.go index 743166e04a..28ba7bd585 100644 --- a/pkg/cluster/spec/monitoring.go +++ b/pkg/cluster/spec/monitoring.go @@ -255,6 +255,17 @@ func (i *MonitorInstance) handleRemoteWrite(spec *PrometheusSpec, monitoring *Pr } } +func addDashboardScrapeHosts(uniqueHosts set.StringSet, topoHasField func(string) (reflect.Value, bool)) { + servers, found := topoHasField("DashboardServers") + if !found { + return + } + for idx := 0; idx < servers.Len(); idx++ { + dashboard := servers.Index(idx).Interface().(*DashboardSpec) + uniqueHosts.Insert(dashboard.Host) + } +} + // InitConfig implement Instance interface func (i *MonitorInstance) InitConfig( ctx context.Context, @@ -465,6 +476,9 @@ func (i *MonitorInstance) InitConfig( cfig.AddDMWorker(host, uint64(port)) } } + // Keep this out of the uniqueHosts loops above: another if+for would + // push InitConfig over revive's cognitive-complexity limit of 110. + addDashboardScrapeHosts(uniqueHosts, topoHasField) if monitoredOptions != nil { for host := range uniqueHosts { diff --git a/pkg/cluster/spec/monitoring_test.go b/pkg/cluster/spec/monitoring_test.go index ce8b34e355..cb3bd236f1 100644 --- a/pkg/cluster/spec/monitoring_test.go +++ b/pkg/cluster/spec/monitoring_test.go @@ -23,10 +23,13 @@ import ( "testing" "github.com/pingcap/tiup/pkg/checkpoint" + "github.com/pingcap/tiup/pkg/cluster/ctxt" "github.com/pingcap/tiup/pkg/cluster/executor" + logprinter "github.com/pingcap/tiup/pkg/logger/printer" "github.com/pingcap/tiup/pkg/meta" "github.com/pingcap/tiup/pkg/utils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" ) @@ -440,3 +443,78 @@ func TestHandleRemoteWriteDisabled(t *testing.T) { assert.Len(t, spec.RemoteConfig.RemoteWrite, 1) assert.Equal(t, vmURL, spec.RemoteConfig.RemoteWrite[0]["url"]) } + +func TestStandaloneDashboardHostInNodeExporterTargets(t *testing.T) { + topo := &Specification{ + GlobalOptions: GlobalOptions{ + User: "tidb", + SystemdMode: UserMode, + }, + MonitoredOptions: MonitoredOptions{ + NodeExporterPort: 9100, + BlackboxExporterPort: 9115, + }, + PDServers: []*PDSpec{ + {Host: "10.0.1.11", ClientPort: 2379}, + }, + Monitors: []*PrometheusSpec{ + {Host: "10.0.1.21", Port: 9090}, + }, + DashboardServers: []*DashboardSpec{ + {Host: "10.0.1.50", Port: 12333}, + }, + } + + deployDir := t.TempDir() + cacheDir := t.TempDir() + paths := meta.DirPaths{ + Deploy: deployDir, + Cache: cacheDir, + Data: []string{filepath.Join(deployDir, "data")}, + Log: filepath.Join(deployDir, "log"), + } + require.NoError(t, os.MkdirAll(filepath.Join(deployDir, "bin", "prometheus"), 0755)) + + comp := MonitorComponent{Topology: topo} + prom := comp.Instances()[0].(*MonitorInstance) + err := prom.InitConfig( + ctxt.New(context.Background(), 0, logprinter.NewLogger("")), + &mockExecutor{}, + "verify-dashboard", + "v8.5.0", + "tidb", + paths, + ) + require.NoError(t, err) + + body, err := os.ReadFile(filepath.Join(deployDir, "conf", "prometheus.yml")) + require.NoError(t, err) + + var parsed struct { + ScrapeConfigs []struct { + JobName string `yaml:"job_name"` + StaticConfigs []struct { + Targets []string `yaml:"targets"` + } `yaml:"static_configs"` + } `yaml:"scrape_configs"` + } + require.NoError(t, yaml.Unmarshal(body, &parsed)) + + targetsOf := func(job string) []string { + var out []string + for _, sc := range parsed.ScrapeConfigs { + if sc.JobName != job { + continue + } + for _, cfg := range sc.StaticConfigs { + out = append(out, cfg.Targets...) + } + } + return out + } + + assert.Contains(t, targetsOf("overwritten-nodes"), "10.0.1.50:9100") + assert.Contains(t, targetsOf("overwritten-nodes"), "10.0.1.11:9100") + assert.Contains(t, targetsOf("overwritten-nodes"), "10.0.1.21:9100") + assert.Contains(t, targetsOf("monitor_port_probe"), "10.0.1.50:9115") +} From 986e6b13f3e0ece7049726eceaf25f6cc27c28e1 Mon Sep 17 00:00:00 2001 From: Ziqian Qin Date: Tue, 8 Sep 2026 15:01:17 +0800 Subject: [PATCH 3/6] ci: migrate to Bookworm and fix concurrent TLS CA copying (#2738) * ci: migrate integration test nodes to Debian Bookworm Remove the unused JRE 11 dependency, declare the systemd and process-management tools needed by node tests, and use systemctl is-enabled instead of distro-specific status output. Signed-off-by: Ziqian Qin * ci: export cluster failure logs from the control container Copy collected node logs to the runner before detection and upload, and retain separate artifacts for each matrix case. This makes the runtime failures exposed by the Bookworm migration diagnosable. Signed-off-by: Ziqian Qin * ci: bump Bookworm scale tests off TiDB v4.0.12 TiFlash v4.0.12 cannot load on glibc 2.36 (GLIBC_PRIVATE in libpthread). The v4.0.12 TLS start path also hits empty-CA transfers more readily. Use versions already green on this PR: v6.2.0 for tools, v6.0.0 for core TLS. Signed-off-by: Ziqian Qin * cluster: copy TLS CA from a per-instance cache file Parallel TLSCert tasks shared cache/ca.crt and could SCP a truncated file (#2727). Native-SSH rename on this Bookworm PR hits that race. Give each instance its own CA cache path, matching key/cert files. Remote dest remains ca.crt. Signed-off-by: Ziqian Qin --------- Signed-off-by: Ziqian Qin (cherry picked from commit 6f9f32979a4a70e83da313b5c1ad5a95aa7d7add) --- .github/workflows/integrate-cluster-cmd.yaml | 4 +- .../workflows/integrate-cluster-scale.yaml | 4 +- docker/node/Dockerfile | 8 +- pkg/cluster/task/tls.go | 5 +- pkg/cluster/task/tls_test.go | 134 ++++++++++++++++++ tests/tiup-cluster/script/scale_core.sh | 2 +- tests/tiup-cluster/test_scale_core_tls.sh | 4 +- tests/tiup-cluster/test_scale_tools.sh | 4 +- 8 files changed, 152 insertions(+), 13 deletions(-) create mode 100644 pkg/cluster/task/tls_test.go diff --git a/.github/workflows/integrate-cluster-cmd.yaml b/.github/workflows/integrate-cluster-cmd.yaml index e084284042..2bc3c54565 100644 --- a/.github/workflows/integrate-cluster-cmd.yaml +++ b/.github/workflows/integrate-cluster-cmd.yaml @@ -90,6 +90,8 @@ jobs: # if: always() run: | docker exec tiup-cluster-control bash /tiup-cluster/tests/tiup-cluster/script/pull_log.sh /tiup-cluster/logs + mkdir -p ./logs + docker cp tiup-cluster-control:/tiup-cluster/logs/. ./logs/ - name: Detect error log if: ${{ failure() }} @@ -103,7 +105,7 @@ jobs: uses: actions/upload-artifact@v6 with: overwrite: true - name: component_logs + name: component_logs-${{ matrix.cases }} path: ./logs - name: Output cluster debug log diff --git a/.github/workflows/integrate-cluster-scale.yaml b/.github/workflows/integrate-cluster-scale.yaml index eb07f7ed2d..518580a378 100644 --- a/.github/workflows/integrate-cluster-scale.yaml +++ b/.github/workflows/integrate-cluster-scale.yaml @@ -91,6 +91,8 @@ jobs: # if: always() run: | docker exec tiup-cluster-control bash /tiup-cluster/tests/tiup-cluster/script/pull_log.sh /tiup-cluster/logs + mkdir -p ./logs + docker cp tiup-cluster-control:/tiup-cluster/logs/. ./logs/ - name: Detect error log if: ${{ failure() }} @@ -103,7 +105,7 @@ jobs: uses: actions/upload-artifact@v6 with: overwrite: true - name: cluster_logs + name: cluster_logs-${{ matrix.cases }} path: ./logs - name: Output cluster debug log diff --git a/docker/node/Dockerfile b/docker/node/Dockerfile index b96ef1d47e..bf898971ce 100644 --- a/docker/node/Dockerfile +++ b/docker/node/Dockerfile @@ -1,19 +1,17 @@ # Based on the deprecated `https://github.com/tutumcloud/tutum-debian` -FROM golang:1.24-bullseye +FROM golang:1.24-bookworm # Use mirrors for poor network... #RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list && \ # sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list # Install packages -# JRE 11 is installed for tispark testing, a tispark node could be started -# with Java 11, but is not going to work properly. However, we don't test -# for SQL in CI, so it's ok to use Java 11 instead of Java 8. RUN apt-get update && \ apt-get -y install \ dos2unix \ openssh-server \ - openjdk-11-jre-headless \ + systemd systemd-sysv \ + psmisc \ sudo vim \ iproute2 \ && \ diff --git a/pkg/cluster/task/tls.go b/pkg/cluster/task/tls.go index 892b64a6b7..2afdca9aef 100644 --- a/pkg/cluster/task/tls.go +++ b/pkg/cluster/task/tls.go @@ -71,6 +71,9 @@ func (c *TLSCert) Execute(ctx context.Context) error { // save cert to cache dir keyFileName := fmt.Sprintf("%s-%s-%d.pem", c.role, c.host, c.port) certFileName := fmt.Sprintf("%s-%s-%d.crt", c.role, c.host, c.port) + // Per-instance CA cache path. Parallel TLSCert tasks used to share + // cache/ca.crt and SCP a truncated file (#2727). + caFileName := fmt.Sprintf("%s-%s-%d-ca.crt", c.role, c.host, c.port) keyFile := filepath.Join( c.paths.Cache, keyFileName, @@ -79,7 +82,7 @@ func (c *TLSCert) Execute(ctx context.Context) error { c.paths.Cache, certFileName, ) - caFile := filepath.Join(c.paths.Cache, spec.TLSCACert) + caFile := filepath.Join(c.paths.Cache, caFileName) if err := utils.SaveFileWithBackup(keyFile, privKey.Pem(), ""); err != nil { return err } diff --git a/pkg/cluster/task/tls_test.go b/pkg/cluster/task/tls_test.go new file mode 100644 index 0000000000..81f6591fc7 --- /dev/null +++ b/pkg/cluster/task/tls_test.go @@ -0,0 +1,134 @@ +// Copyright 2026 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 task + +import ( + "context" + "encoding/pem" + "os" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/pingcap/tiup/pkg/cluster/ctxt" + "github.com/pingcap/tiup/pkg/cluster/spec" + "github.com/pingcap/tiup/pkg/crypto" + logprinter "github.com/pingcap/tiup/pkg/logger/printer" + "github.com/pingcap/tiup/pkg/meta" + "github.com/stretchr/testify/require" +) + +type overlappingCATransfer struct { + firstCA string + firstReady chan struct{} + readFirst chan struct{} + firstRead chan struct{} + mu sync.Mutex + transferred map[string][]byte +} + +func (*overlappingCATransfer) Execute(context.Context, string, bool, ...time.Duration) ([]byte, []byte, error) { + return nil, nil, nil +} + +func (e *overlappingCATransfer) Transfer(ctx context.Context, src, dst string, _ bool, _ int, _ bool) error { + if filepath.Base(dst) != spec.TLSCACert { + return nil + } + if dst == e.firstCA { + close(e.firstReady) + select { + case <-e.readFirst: + case <-ctx.Done(): + return ctx.Err() + } + data, err := os.ReadFile(src) + e.mu.Lock() + e.transferred[dst] = data + e.mu.Unlock() + close(e.firstRead) + return err + } + + data, err := os.ReadFile(src) + if err != nil { + return err + } + // Reproduce a competing writer's truncation window without relying on + // scheduler timing. It must not corrupt the first task's in-flight source. + if err := os.Truncate(src, 0); err != nil { + return err + } + close(e.readFirst) + select { + case <-e.firstRead: + case <-ctx.Done(): + return ctx.Err() + } + if err := os.WriteFile(src, data, 0600); err != nil { + return err + } + e.mu.Lock() + e.transferred[dst] = data + e.mu.Unlock() + return nil +} + +func TestTLSCertPreservesInFlightCA(t *testing.T) { + ca, err := crypto.NewCA("test") + require.NoError(t, err) + expected := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Cert.Raw}) + for _, tc := range []struct { + name string + host string + port int + }{ + {name: "different_hosts", host: "n2", port: 20160}, + {name: "same_host_different_ports", host: "n1", port: 20161}, + } { + t.Run(tc.name, func(t *testing.T) { + root := t.TempDir() + first := &TLSCert{comp: "tikv", role: "tikv", host: "n1", port: 20160, ca: ca, + paths: meta.DirPaths{Deploy: filepath.Join(root, "first"), Cache: filepath.Join(root, "cache")}} + second := &TLSCert{comp: "tikv", role: "tikv", host: tc.host, port: tc.port, ca: ca, + paths: meta.DirPaths{Deploy: filepath.Join(root, "second"), Cache: first.paths.Cache}} + firstCA := filepath.Join(first.paths.Deploy, spec.TLSCertKeyDir, spec.TLSCACert) + secondCA := filepath.Join(second.paths.Deploy, spec.TLSCertKeyDir, spec.TLSCACert) + e := &overlappingCATransfer{firstCA: firstCA, firstReady: make(chan struct{}), + readFirst: make(chan struct{}), firstRead: make(chan struct{}), transferred: make(map[string][]byte)} + base, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + ctx := ctxt.New(base, 2, logprinter.NewLogger("")) + ctxt.GetInner(ctx).SetExecutor(first.host, e) + ctxt.GetInner(ctx).SetExecutor(second.host, e) + done := make(chan error, 1) + go func() { done <- first.Execute(ctx) }() + select { + case <-e.firstReady: + case err := <-done: + require.NoError(t, err) + t.Fatal("first task did not reach CA transfer") + case <-ctx.Done(): + t.Fatal(ctx.Err()) + } + require.NoError(t, second.Execute(ctx)) + require.NoError(t, <-done) + e.mu.Lock() + defer e.mu.Unlock() + require.Equal(t, expected, e.transferred[firstCA]) + require.Equal(t, expected, e.transferred[secondCA]) + }) + } +} diff --git a/tests/tiup-cluster/script/scale_core.sh b/tests/tiup-cluster/script/scale_core.sh index 570ea43102..5fa4e09b41 100755 --- a/tests/tiup-cluster/script/scale_core.sh +++ b/tests/tiup-cluster/script/scale_core.sh @@ -61,7 +61,7 @@ function scale_core() { topo=./topo/full_scale_in_tidb.yaml tiup-cluster $client --yes scale-out $name $topo # after scale-out, ensure the service is enabled - tiup-cluster $client exec $name -N n1 --command "systemctl status tidb-4000 | grep Loaded |grep 'enabled; vendor'" + tiup-cluster $client exec $name -N n1 --command "systemctl is-enabled --quiet tidb-4000" tiup-cluster $client exec $name -N n1 --command "grep -q n1:10080 /home/tidb/deploy/prometheus-9090/conf/prometheus.yml" assert_prometheus_external_labels $name n1 production us-east-1 diff --git a/tests/tiup-cluster/test_scale_core_tls.sh b/tests/tiup-cluster/test_scale_core_tls.sh index 0b9efabda2..2e7aeb92a5 100755 --- a/tests/tiup-cluster/test_scale_core_tls.sh +++ b/tests/tiup-cluster/test_scale_core_tls.sh @@ -4,5 +4,5 @@ set -eu source script/scale_core.sh -echo "test scaling of core components in cluster for version v5.3.0 w/ TLS, via easy ssh" -scale_core v4.0.12 true false +echo "test scaling of core components in cluster for version v6.0.0 w/ TLS, via easy ssh" +scale_core v6.0.0 true false diff --git a/tests/tiup-cluster/test_scale_tools.sh b/tests/tiup-cluster/test_scale_tools.sh index 7aca52a80d..5332b11f9e 100755 --- a/tests/tiup-cluster/test_scale_tools.sh +++ b/tests/tiup-cluster/test_scale_tools.sh @@ -4,5 +4,5 @@ set -eu source script/scale_tools.sh -echo "test scaling of tools components in cluster for version v4.0.12, via easy ssh" -scale_tools v4.0.12 false false +echo "test scaling of tools components in cluster for version v6.2.0, via easy ssh" +scale_tools v6.2.0 false false From 65971e577d7f328439156d2fb7dff4704dc621d8 Mon Sep 17 00:00:00 2001 From: Ziqian Qin Date: Tue, 8 Sep 2026 15:38:23 +0800 Subject: [PATCH 4/6] cluster: honor ignore_exporter when destroying a cluster (#2737) * cluster: honor ignore_exporter when destroying a cluster Adapt the final fix from pingcap/tiup#2197 to current master. The original implementation and review update are in commits 4e06282dacfa73e7f0d7fd20f82c492c09c6003d and 8025e3ae50f03c1a99ef66733cdb0b1d09059ba3. Signed-off-by: Smityz Signed-off-by: Ziqian Qin * cluster: test exporter preservation during destroy Cover shared and managed exporters, multiple instances per host, and force destroy. Verify exporter directories, systemd units, and port checks while keeping cleanup of cluster-owned components intact. Signed-off-by: Ziqian Qin --------- Signed-off-by: Smityz Signed-off-by: Ziqian Qin Co-authored-by: Smityz (cherry picked from commit c00f987b0e3c5bffb7b2a0513766dd0ecf283f1c) --- pkg/cluster/operation/destroy.go | 2 +- pkg/cluster/operation/destroy_test.go | 128 ++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 pkg/cluster/operation/destroy_test.go diff --git a/pkg/cluster/operation/destroy.go b/pkg/cluster/operation/destroy.go index 0f541f387e..76c1c8f69b 100644 --- a/pkg/cluster/operation/destroy.go +++ b/pkg/cluster/operation/destroy.go @@ -58,7 +58,7 @@ func Destroy( for _, inst := range insts { instCount[inst.GetManageHost()]-- if instCount[inst.GetManageHost()] == 0 { - if cluster.GetMonitoredOptions() != nil { + if cluster.GetMonitoredOptions() != nil && !inst.IgnoreMonitorAgent() { if err := DestroyMonitored(ctx, inst, cluster.GetMonitoredOptions(), options.OptTimeout, cluster.BaseTopo().GlobalOptions.SystemdMode); err != nil && !options.Force { return err } diff --git a/pkg/cluster/operation/destroy_test.go b/pkg/cluster/operation/destroy_test.go new file mode 100644 index 0000000000..3bcb9b3f63 --- /dev/null +++ b/pkg/cluster/operation/destroy_test.go @@ -0,0 +1,128 @@ +// Copyright 2026 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" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/pingcap/tiup/pkg/cluster/ctxt" + "github.com/pingcap/tiup/pkg/cluster/spec" + logprinter "github.com/pingcap/tiup/pkg/logger/printer" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +type destroyExecutor struct { + commands []string +} + +func (e *destroyExecutor) Execute(_ context.Context, cmd string, _ bool, _ ...time.Duration) ([]byte, []byte, error) { + e.commands = append(e.commands, cmd) + // Empty ss output models stopped ports, so an unexpected exporter cleanup + // is caught by the assertions without waiting for a timeout. + return nil, nil, nil +} + +func (*destroyExecutor) Transfer(_ context.Context, _, _ string, _ bool, _ int, _ bool) error { + return nil +} + +func TestDestroyIgnoreExporter(t *testing.T) { + for _, tc := range []struct { + name string + ignoreExporter bool + multipleInstances bool + force bool + }{ + {name: "shared", ignoreExporter: true}, + {name: "shared_multiple_instances", ignoreExporter: true, multipleInstances: true}, + {name: "shared_force", ignoreExporter: true, force: true}, + {name: "managed"}, + {name: "managed_multiple_instances", multipleInstances: true}, + } { + t.Run(tc.name, func(t *testing.T) { + topology := fmt.Sprintf(` +global: + user: tidb + deploy_dir: /tidb-deploy + data_dir: /tidb-data +monitored: + deploy_dir: /tidb-deploy/monitor-9100 + data_dir: /tidb-data/monitor-9100 + log_dir: /tidb-log/monitor-9100 +pd_servers: + - host: 192.0.2.1 + ignore_exporter: %t +`, tc.ignoreExporter) + if tc.multipleInstances { + topology += fmt.Sprintf(` +tidb_servers: + - host: 192.0.2.1 + ignore_exporter: %t +`, tc.ignoreExporter) + } + var topo spec.Specification + require.NoError(t, yaml.Unmarshal([]byte(topology), &topo)) + + exec := &destroyExecutor{} + ctx := ctxt.New(context.Background(), 1, logprinter.NewLogger("")) + inner := ctxt.GetInner(ctx) + inner.SetExecutor("192.0.2.1", exec) + inner.PublicKeyPath = filepath.Join(t.TempDir(), "id_rsa.pub") + require.NoError(t, os.WriteFile(inner.PublicKeyPath, []byte("ssh-ed25519 test-key"), 0600)) + + require.NoError(t, Destroy(ctx, &topo, Options{Force: tc.force})) + + // Destroy must still remove the cluster's own components. + require.Contains(t, exec.commands, "rm -rf /etc/systemd/system/pd-2379.service;") + if tc.multipleInstances { + require.Contains(t, exec.commands, "rm -rf /etc/systemd/system/tidb-4000.service;") + } + + wantDeletes := 1 + if tc.ignoreExporter { + wantDeletes = 0 + } + for _, path := range []string{ + "/tidb-deploy/monitor-9100", + "/tidb-data/monitor-9100", + "/tidb-log/monitor-9100", + "/etc/systemd/system/node_exporter-9100.service", + "/etc/systemd/system/blackbox_exporter-9115.service", + } { + deletes := 0 + for _, cmd := range exec.commands { + if strings.HasPrefix(cmd, "rm -rf ") && strings.Contains(cmd, path) { + deletes++ + } + } + require.Equal(t, wantDeletes, deletes, "deletions of %s: %v", path, exec.commands) + } + + portChecks := 0 + for _, cmd := range exec.commands { + if cmd == "ss -ltn" { + portChecks++ + } + } + require.Equal(t, 2*wantDeletes, portChecks, "each managed exporter port is checked once") + }) + } +} From 87754bd7c99f12642ec4e96e05ebfae5798fefeb Mon Sep 17 00:00:00 2001 From: Ziqian Qin Date: Tue, 8 Sep 2026 16:13:05 +0800 Subject: [PATCH 5/6] release: bump version to v1.17.1 (cherry picked from commit f965165e5bcab4be73318578339a32284421e9a0) --- CHANGELOG.md | 9 +++++++++ pkg/version/version.go | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2446f0505f..451958b075 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ TiUP Changelog +## [1.17.1] 2026-09-08 + +### Fixes + +- `tiup-cluster` fix Grafana dashboard datasource replacement when using VictoriaMetrics (#2732, @Defined2014) +- `tiup-cluster` collect node_exporter metrics on dedicated TiDB Dashboard hosts (#2734, @mayjiang0203) +- `tiup-cluster` preserve shared exporter directories and systemd units when destroying a cluster with `ignore_exporter: true` (#2737, @Smityz, @ekexium) +- `tiup-cluster` prevent concurrent TLS certificate distribution from copying an empty CA certificate and blocking component startup (#2738, @ekexium) + ## [1.17.0] 2026-07-27 ### New Features diff --git a/pkg/version/version.go b/pkg/version/version.go index 6e4ce8a251..3daf1c74b1 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -23,7 +23,7 @@ var ( // TiUPVerMinor is the minor version of TiUP TiUPVerMinor = 17 // TiUPVerPatch is the patch version of TiUP - TiUPVerPatch = 0 + TiUPVerPatch = 1 // TiUPVerName is an alternative name of the version TiUPVerName = "tiup" // GitHash is the current git commit hash From 1fdf44631c5f493d9a310200832de157e8c2e79f Mon Sep 17 00:00:00 2001 From: Ziqian Qin Date: Tue, 8 Sep 2026 19:16:31 +0800 Subject: [PATCH 6/6] ci: rerun release checks after mirror publication