Repository: tikv/pd
Pull request #7606 (2023-12-21 10:08:01)

mcs: support status api
<!--

Thank you for working on PD! Please read PD's [CONTRIBUTING](https://github.com/tikv/pd/blob/master/CONTRIBUTING.md) document **BEFORE** filing this PR.

PR Title Format:
1. pkg [, pkg2, pkg3]: what's changed
2. *: what's changed

-->

### What problem does this PR solve?

<!--

Please create an issue first to describe the problem.
There MUST be one line starting with "Issue Number:  " and 
linking the relevant issues via the "close" or "ref".
For more info, check https://github.com/tikv/pd/blob/master/CONTRIBUTING.md#linking-issues.

-->
Issue Number: Ref #5839.

### What is changed and how does it work?

Support /status HTTP API.

<!--

You could use the "commit message" code block to add more description to the final commit message.
For more info, check https://github.com/tikv/pd/blob/master/CONTRIBUTING.md#format-of-the-commit-message.

-->

```commit-message
```

### Check List

<!-- Remove the items that are not applicable. -->

Tests

<!-- At least one of these tests must be included. -->

- Unit test

### Release note

<!--

A bugfix or a new feature needs a release note. If there is no need to give a release note, just leave it with the `None`.

Please refer to [Release Notes Language Style Guide](https://pingcap.github.io/tidb-dev-guide/contribute-to-tidb/release-notes-style-guide.html) to write a quality release note.

-->

```release-note
None.
```


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/tikv/pd/pull/7606?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tikv) Report
> Merging [#7606](https://app.codecov.io/gh/tikv/pd/pull/7606?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tikv) (576856d) into [master](https://app.codecov.io/gh/tikv/pd/commit/cfbc9b96cdc23d4f7c723b0b4130f6612f9ed00a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tikv) (cfbc9b9) will **decrease** coverage by `0.04%`.
> Report is 13 commits behind head on master.
> The diff coverage is `100.00%`.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #7606      +/-   ##
==========================================
- Coverage   74.72%   74.68%   -0.04%     
==========================================
  Files         457      459       +2     
  Lines       50388    50552     +164     
==========================================
+ Hits        37650    37754     +104     
- Misses       9405     9437      +32     
- Partials     3333     3361      +28     
```

| [Flag](https://app.codecov.io/gh/tikv/pd/pull/7606/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tikv) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/tikv/pd/pull/7606/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tikv) | `74.68% <100.00%> (-0.04%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tikv#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>
JmPotato: /merge
ti-chi-bot[bot]: @JmPotato: It seems you want to merge this PR, I will help you trigger all the tests:

/run-all-tests

You only need to trigger `/merge` once, and if the CI test fails, you just re-trigger the test that failed and the bot will merge the PR for you after the CI passes.

> If you have any questions about the PR merge process, please refer to [pr process](https://book.prow.tidb.net/#/en/workflows/pr).


<details>

Instructions for interacting with me using PR comments are available [here](https://prow.tidb.net/command-help).  If you have questions or suggestions related to my behavior, please file an issue against the [ti-community-infra/tichi](https://github.com/ti-community-infra/tichi/issues/new?title=Prow%20issue:) repository.
</details>
ti-chi-bot[bot]: This pull request has been accepted and is ready to merge. <details>Commit hash: 576856da25aae5c47c8a9587aa0a3c0f76380d3d</details>
ti-chi-bot[bot]: @rleungx: Your PR was out of date, I have automatically updated it for you.

If the CI test fails, you just re-trigger the test that failed and the bot will merge the PR for you after the CI passes.


<details>

Instructions for interacting with me using PR comments are available [here](https://prow.tidb.net/command-help).  If you have questions or suggestions related to my behavior, please file an issue against the [ti-community-infra/tichi](https://github.com/ti-community-infra/tichi/issues/new?title=Prow%20issue:) repository.
</details>
JmPotato: Need to fix the `Check PD`.

/unmerge
ti-chi-bot[bot]: Merge canceled because a new commit is pushed.
rleungx: /merge
ti-chi-bot[bot]: @rleungx: It seems you want to merge this PR, I will help you trigger all the tests:

/run-all-tests

You only need to trigger `/merge` once, and if the CI test fails, you just re-trigger the test that failed and the bot will merge the PR for you after the CI passes.

> If you have any questions about the PR merge process, please refer to [pr process](https://book.prow.tidb.net/#/en/workflows/pr).


<details>

Instructions for interacting with me using PR comments are available [here](https://prow.tidb.net/command-help).  If you have questions or suggestions related to my behavior, please file an issue against the [ti-community-infra/tichi](https://github.com/ti-community-infra/tichi/issues/new?title=Prow%20issue:) repository.
</details>
ti-chi-bot[bot]: This pull request has been accepted and is ready to merge. <details>Commit hash: c46aadf26860f888f6e244a55bad2b05fe291198</details>
ti-chi-bot[bot]: @rleungx: Your PR was out of date, I have automatically updated it for you.

If the CI test fails, you just re-trigger the test that failed and the bot will merge the PR for you after the CI passes.


<details>

Instructions for interacting with me using PR comments are available [here](https://prow.tidb.net/command-help).  If you have questions or suggestions related to my behavior, please file an issue against the [ti-community-infra/tichi](https://github.com/ti-community-infra/tichi/issues/new?title=Prow%20issue:) repository.
</details>

Patch:
diff --git a/pkg/mcs/resourcemanager/server/apis/v1/api.go b/pkg/mcs/resourcemanager/server/apis/v1/api.go
index 7b5f29034..937386643 100644
--- a/pkg/mcs/resourcemanager/server/apis/v1/api.go
+++ b/pkg/mcs/resourcemanager/server/apis/v1/api.go
@@ -82,6 +82,7 @@ func NewService(srv *rmserver.Service) *Service {
 		c.Next()
 	})
 	apiHandlerEngine.GET("metrics", utils.PromHandler())
+	apiHandlerEngine.GET("status", utils.StatusHandler)
 	pprof.Register(apiHandlerEngine)
 	endpoint := apiHandlerEngine.Group(APIPathPrefix)
 	endpoint.Use(multiservicesapi.ServiceRedirector())
diff --git a/pkg/mcs/scheduling/server/apis/v1/api.go b/pkg/mcs/scheduling/server/apis/v1/api.go
index e6881f2f8..8c4ed015a 100644
--- a/pkg/mcs/scheduling/server/apis/v1/api.go
+++ b/pkg/mcs/scheduling/server/apis/v1/api.go
@@ -107,6 +107,7 @@ func NewService(srv *scheserver.Service) *Service {
 		c.Next()
 	})
 	apiHandlerEngine.GET("metrics", mcsutils.PromHandler())
+	apiHandlerEngine.GET("status", mcsutils.StatusHandler)
 	pprof.Register(apiHandlerEngine)
 	root := apiHandlerEngine.Group(APIPathPrefix)
 	root.Use(multiservicesapi.ServiceRedirector())
diff --git a/pkg/mcs/server/server.go b/pkg/mcs/server/server.go
index 95b806e20..a8dedd8ad 100644
--- a/pkg/mcs/server/server.go
+++ b/pkg/mcs/server/server.go
@@ -162,3 +162,8 @@ func (bs *BaseServer) GetListener() net.Listener {
 func (bs *BaseServer) IsSecure() bool {
 	return bs.secure
 }
+
+// StartTimestamp returns the start timestamp of this server
+func (bs *BaseServer) StartTimestamp() int64 {
+	return bs.startTimestamp
+}
diff --git a/pkg/mcs/tso/server/apis/v1/api.go b/pkg/mcs/tso/server/apis/v1/api.go
index e5f0dfb54..942825921 100644
--- a/pkg/mcs/tso/server/apis/v1/api.go
+++ b/pkg/mcs/tso/server/apis/v1/api.go
@@ -91,6 +91,7 @@ func NewService(srv *tsoserver.Service) *Service {
 		c.Next()
 	})
 	apiHandlerEngine.GET("metrics", utils.PromHandler())
+	apiHandlerEngine.GET("status", utils.StatusHandler)
 	pprof.Register(apiHandlerEngine)
 	root := apiHandlerEngine.Group(APIPathPrefix)
 	root.Use(multiservicesapi.ServiceRedirector())
diff --git a/pkg/mcs/utils/util.go b/pkg/mcs/utils/util.go
index a0708f9bf..6fa6c2cb0 100644
--- a/pkg/mcs/utils/util.go
+++ b/pkg/mcs/utils/util.go
@@ -33,9 +33,11 @@ import (
 	"github.com/soheilhy/cmux"
 	"github.com/tikv/pd/pkg/errs"
 	"github.com/tikv/pd/pkg/utils/apiutil"
+	"github.com/tikv/pd/pkg/utils/apiutil/multiservicesapi"
 	"github.com/tikv/pd/pkg/utils/etcdutil"
 	"github.com/tikv/pd/pkg/utils/grpcutil"
 	"github.com/tikv/pd/pkg/utils/logutil"
+	"github.com/tikv/pd/pkg/versioninfo"
 	"go.etcd.io/etcd/clientv3"
 	"go.etcd.io/etcd/pkg/types"
 	"go.uber.org/zap"
@@ -78,6 +80,19 @@ func PromHandler() gin.HandlerFunc {
 	}
 }
 
+// StatusHandler is a handler to get status info.
+func StatusHandler(c *gin.Context) {
+	svr := c.MustGet(multiservicesapi.ServiceContextKey).(server)
+	version := versioninfo.Status{
+		BuildTS:        versioninfo.PDBuildTS,
+		GitHash:        versioninfo.PDGitHash,
+		Version:        versioninfo.PDReleaseVersion,
+		StartTimestamp: svr.StartTimestamp(),
+	}
+
+	c.IndentedJSON(http.StatusOK, version)
+}
+
 type server interface {
 	GetBackendEndpoints() string
 	Context() context.Context
@@ -97,6 +112,7 @@ type server interface {
 	RegisterGRPCService(*grpc.Server)
 	SetUpRestHandler() (http.Handler, apiutil.APIServiceGroup)
 	diagnosticspb.DiagnosticsServer
+	StartTimestamp() int64
 }
 
 // WaitAPIServiceReady waits for the api service ready.
diff --git a/pkg/versioninfo/versioninfo.go b/pkg/versioninfo/versioninfo.go
index 7b6663340..d6f4738d7 100644
--- a/pkg/versioninfo/versioninfo.go
+++ b/pkg/versioninfo/versioninfo.go
@@ -24,6 +24,15 @@ import (
 	"go.uber.org/zap"
 )
 
+// Status is the status of PD server.
+// NOTE: This type is exported by HTTP API. Please pay more attention when modifying it.
+type Status struct {
+	BuildTS        string `json:"build_ts"`
+	Version        string `json:"version"`
+	GitHash        string `json:"git_hash"`
+	StartTimestamp int64  `json:"start_timestamp"`
+}
+
 const (
 	// CommunityEdition is the default edition for building.
 	CommunityEdition = "Community"
diff --git a/server/api/status.go b/server/api/status.go
index 8f837ec01..ba8f3b484 100644
--- a/server/api/status.go
+++ b/server/api/status.go
@@ -27,14 +27,6 @@ type statusHandler struct {
 	rd  *render.Render
 }
 
-// NOTE: This type is exported by HTTP API. Please pay more attention when modifying it.
-type status struct {
-	BuildTS        string `json:"build_ts"`
-	Version        string `json:"version"`
-	GitHash        string `json:"git_hash"`
-	StartTimestamp int64  `json:"start_timestamp"`
-}
-
 func newStatusHandler(svr *server.Server, rd *render.Render) *statusHandler {
 	return &statusHandler{
 		svr: svr,
@@ -44,10 +36,10 @@ func newStatusHandler(svr *server.Server, rd *render.Render) *statusHandler {
 
 // @Summary  Get the build info of PD server.
 // @Produce  json
-// @Success  200  {object}  status
+// @Success  200  {object}  versioninfo.Status
 // @Router   /status [get]
 func (h *statusHandler) GetPDStatus(w http.ResponseWriter, r *http.Request) {
-	version := status{
+	version := versioninfo.Status{
 		BuildTS:        versioninfo.PDBuildTS,
 		GitHash:        versioninfo.PDGitHash,
 		Version:        versioninfo.PDReleaseVersion,


Test patch:
diff --git a/server/api/status_test.go b/server/api/status_test.go
index 7017c4271..065618efb 100644
--- a/server/api/status_test.go
+++ b/server/api/status_test.go
@@ -24,7 +24,7 @@ import (
 )
 
 func checkStatusResponse(re *require.Assertions, body []byte) {
-	got := status{}
+	got := versioninfo.Status{}
 	re.NoError(json.Unmarshal(body, &got))
 	re.Equal(versioninfo.PDBuildTS, got.BuildTS)
 	re.Equal(versioninfo.PDGitHash, got.GitHash)
diff --git a/tests/integrations/mcs/resourcemanager/server_test.go b/tests/integrations/mcs/resourcemanager/server_test.go
index b7a9b0be2..d3837ad15 100644
--- a/tests/integrations/mcs/resourcemanager/server_test.go
+++ b/tests/integrations/mcs/resourcemanager/server_test.go
@@ -26,6 +26,7 @@ import (
 	"github.com/stretchr/testify/require"
 	"github.com/tikv/pd/client/grpcutil"
 	"github.com/tikv/pd/pkg/utils/tempurl"
+	"github.com/tikv/pd/pkg/versioninfo"
 	"github.com/tikv/pd/tests"
 )
 
@@ -102,4 +103,19 @@ func TestResourceManagerServer(t *testing.T) {
 		re.NoError(err)
 		re.Contains(string(respBytes), "resource_manager_server_info")
 	}
+
+	// Test status handler
+	{
+		resp, err := http.Get(addr + "/status")
+		re.NoError(err)
+		defer resp.Body.Close()
+		re.Equal(http.StatusOK, resp.StatusCode)
+		respBytes, err := io.ReadAll(resp.Body)
+		re.NoError(err)
+		var s versioninfo.Status
+		re.NoError(json.Unmarshal(respBytes, &s))
+		re.Equal(versioninfo.PDBuildTS, s.BuildTS)
+		re.Equal(versioninfo.PDGitHash, s.GitHash)
+		re.Equal(versioninfo.PDReleaseVersion, s.Version)
+	}
 }
diff --git a/tests/integrations/mcs/scheduling/api_test.go b/tests/integrations/mcs/scheduling/api_test.go
index 867a7bfb5..177f959b9 100644
--- a/tests/integrations/mcs/scheduling/api_test.go
+++ b/tests/integrations/mcs/scheduling/api_test.go
@@ -5,6 +5,7 @@ import (
 	"encoding/hex"
 	"encoding/json"
 	"fmt"
+	"io"
 	"net/http"
 	"testing"
 	"time"
@@ -22,6 +23,7 @@ import (
 	"github.com/tikv/pd/pkg/storage"
 	"github.com/tikv/pd/pkg/utils/apiutil"
 	"github.com/tikv/pd/pkg/utils/testutil"
+	"github.com/tikv/pd/pkg/versioninfo"
 	"github.com/tikv/pd/tests"
 )
 
@@ -547,3 +549,45 @@ func (suite *apiTestSuite) checkFollowerForward(cluster *tests.TestCluster) {
 	)
 	re.NoError(err)
 }
+
+func (suite *apiTestSuite) TestMetrics() {
+	suite.env.RunTestInAPIMode(suite.checkMetrics)
+}
+
+func (suite *apiTestSuite) checkMetrics(cluster *tests.TestCluster) {
+	re := suite.Require()
+	s := cluster.GetSchedulingPrimaryServer()
+	testutil.Eventually(re, func() bool {
+		return s.IsServing()
+	}, testutil.WithWaitFor(5*time.Second), testutil.WithTickInterval(50*time.Millisecond))
+	resp, err := http.Get(s.GetConfig().GetAdvertiseListenAddr() + "/metrics")
+	re.NoError(err)
+	defer resp.Body.Close()
+	re.Equal(http.StatusOK, resp.StatusCode)
+	respBytes, err := io.ReadAll(resp.Body)
+	re.NoError(err)
+	re.Contains(string(respBytes), "scheduling_server_info")
+}
+
+func (suite *apiTestSuite) TestStatus() {
+	suite.env.RunTestInAPIMode(suite.checkStatus)
+}
+
+func (suite *apiTestSuite) checkStatus(cluster *tests.TestCluster) {
+	re := suite.Require()
+	s := cluster.GetSchedulingPrimaryServer()
+	testutil.Eventually(re, func() bool {
+		return s.IsServing()
+	}, testutil.WithWaitFor(5*time.Second), testutil.WithTickInterval(50*time.Millisecond))
+	resp, err := http.Get(s.GetConfig().GetAdvertiseListenAddr() + "/status")
+	re.NoError(err)
+	defer resp.Body.Close()
+	re.Equal(http.StatusOK, resp.StatusCode)
+	respBytes, err := io.ReadAll(resp.Body)
+	re.NoError(err)
+	var status versioninfo.Status
+	re.NoError(json.Unmarshal(respBytes, &status))
+	re.Equal(versioninfo.PDBuildTS, status.BuildTS)
+	re.Equal(versioninfo.PDGitHash, status.GitHash)
+	re.Equal(versioninfo.PDReleaseVersion, status.Version)
+}
diff --git a/tests/integrations/mcs/tso/api_test.go b/tests/integrations/mcs/tso/api_test.go
index 59073aca1..a5d68cfa9 100644
--- a/tests/integrations/mcs/tso/api_test.go
+++ b/tests/integrations/mcs/tso/api_test.go
@@ -33,6 +33,7 @@ import (
 	"github.com/tikv/pd/pkg/storage/endpoint"
 	"github.com/tikv/pd/pkg/utils/apiutil"
 	"github.com/tikv/pd/pkg/utils/testutil"
+	"github.com/tikv/pd/pkg/versioninfo"
 	"github.com/tikv/pd/server/config"
 	"github.com/tikv/pd/tests"
 )
@@ -244,3 +245,33 @@ func TestForwardOnlyTSONoScheduling(t *testing.T) {
 		testutil.Status(re, http.StatusInternalServerError), testutil.StringContain(re, "[PD:apiutil:ErrRedirect]redirect failed"))
 	re.NoError(err)
 }
+
+func (suite *tsoAPITestSuite) TestMetrics() {
+	re := suite.Require()
+
+	primary := suite.tsoCluster.WaitForDefaultPrimaryServing(re)
+	resp, err := http.Get(primary.GetConfig().GetAdvertiseListenAddr() + "/metrics")
+	re.NoError(err)
+	defer resp.Body.Close()
+	re.Equal(http.StatusOK, resp.StatusCode)
+	respBytes, err := io.ReadAll(resp.Body)
+	re.NoError(err)
+	re.Contains(string(respBytes), "tso_server_info")
+}
+
+func (suite *tsoAPITestSuite) TestStatus() {
+	re := suite.Require()
+
+	primary := suite.tsoCluster.WaitForDefaultPrimaryServing(re)
+	resp, err := http.Get(primary.GetConfig().GetAdvertiseListenAddr() + "/status")
+	re.NoError(err)
+	defer resp.Body.Close()
+	re.Equal(http.StatusOK, resp.StatusCode)
+	respBytes, err := io.ReadAll(resp.Body)
+	re.NoError(err)
+	var s versioninfo.Status
+	re.NoError(json.Unmarshal(respBytes, &s))
+	re.Equal(versioninfo.PDBuildTS, s.BuildTS)
+	re.Equal(versioninfo.PDGitHash, s.GitHash)
+	re.Equal(versioninfo.PDReleaseVersion, s.Version)
+}
diff --git a/tests/integrations/mcs/tso/server_test.go b/tests/integrations/mcs/tso/server_test.go
index 9dc9aa323..a6a2c42ac 100644
--- a/tests/integrations/mcs/tso/server_test.go
+++ b/tests/integrations/mcs/tso/server_test.go
@@ -576,18 +576,6 @@ func (suite *CommonTestSuite) TestAdvertiseAddr() {
 	re.Equal(conf.GetListenAddr(), conf.GetAdvertiseListenAddr())
 }
 
-func (suite *CommonTestSuite) TestMetrics() {
-	re := suite.Require()
-
-	resp, err := http.Get(suite.tsoDefaultPrimaryServer.GetConfig().GetAdvertiseListenAddr() + "/metrics")
-	re.NoError(err)
-	defer resp.Body.Close()
-	re.Equal(http.StatusOK, resp.StatusCode)
-	respBytes, err := io.ReadAll(resp.Body)
-	re.NoError(err)
-	re.Contains(string(respBytes), "tso_server_info")
-}
-
 func (suite *CommonTestSuite) TestBootstrapDefaultKeyspaceGroup() {
 	re := suite.Require()