diff --git a/pkg/mcs/resourcemanager/server/apis/v1/api.go b/pkg/mcs/resourcemanager/server/apis/v1/api.go index ec4c40f2d3f..55cfc57b178 100644 --- a/pkg/mcs/resourcemanager/server/apis/v1/api.go +++ b/pkg/mcs/resourcemanager/server/apis/v1/api.go @@ -80,7 +80,6 @@ func NewService(srv *rmserver.Service) *Service { apiHandlerEngine.GET("status", utils.StatusHandler) pprof.Register(apiHandlerEngine) endpoint := apiHandlerEngine.Group(APIPathPrefix) - endpoint.Use(multiservicesapi.ServiceRedirector()) s := &Service{ manager: manager, apiHandlerEngine: apiHandlerEngine, @@ -100,6 +99,8 @@ func (s *Service) RegisterAdminRouter() { // RegisterRouter registers the router of the service. func (s *Service) RegisterRouter() { configEndpoint := s.root.Group("/config") + configEndpoint.GET("", getConfig) + configEndpoint.Use(multiservicesapi.ServiceRedirector()) configEndpoint.POST("/group", s.postResourceGroup) configEndpoint.PUT("/group", s.putResourceGroup) configEndpoint.GET("/group/:name", s.getResourceGroup) @@ -122,7 +123,7 @@ func (s *Service) handler() http.Handler { } func changeLogLevel(c *gin.Context) { - svr := c.MustGet(multiservicesapi.ServiceContextKey).(*rmserver.Service) + svr := c.MustGet(multiservicesapi.ServiceContextKey).(*rmserver.Server) var level string if err := c.Bind(&level); err != nil { c.String(http.StatusBadRequest, err.Error()) @@ -376,3 +377,14 @@ func (s *Service) getKeyspaceServiceLimit(c *gin.Context) { } c.IndentedJSON(http.StatusOK, limiter) } + +// GetConfig +// +// @Tags ResourceManager +// @Summary Get the resource manager config. +// @Success 200 {string} json format of rmserver.Config +func getConfig(c *gin.Context) { + svr := c.MustGet(multiservicesapi.ServiceContextKey).(*rmserver.Server) + config := svr.GetConfig() + c.IndentedJSON(http.StatusOK, config) +} diff --git a/pkg/mcs/resourcemanager/server/server.go b/pkg/mcs/resourcemanager/server/server.go index 2c625172eba..06c5163e1a6 100644 --- a/pkg/mcs/resourcemanager/server/server.go +++ b/pkg/mcs/resourcemanager/server/server.go @@ -254,6 +254,11 @@ func (s *Server) Close() { log.Info("resource manager server is closed") } +// GetConfig returns the config. +func (s *Server) GetConfig() *Config { + return s.cfg +} + // GetControllerConfig returns the controller config. func (s *Server) GetControllerConfig() *ControllerConfig { return &s.cfg.Controller diff --git a/tests/integrations/mcs/resourcemanager/api_test.go b/tests/integrations/mcs/resourcemanager/api_test.go index 7b1d9d94777..297a98c3d14 100644 --- a/tests/integrations/mcs/resourcemanager/api_test.go +++ b/tests/integrations/mcs/resourcemanager/api_test.go @@ -33,6 +33,7 @@ import ( "github.com/tikv/pd/pkg/keyspace" "github.com/tikv/pd/pkg/mcs/resourcemanager/server" "github.com/tikv/pd/pkg/mcs/resourcemanager/server/apis/v1" + "github.com/tikv/pd/pkg/utils/testutil" "github.com/tikv/pd/tests" ) @@ -397,3 +398,92 @@ func tryToSetKeyspaceServiceLimit(re *require.Assertions, leaderAddr, keyspaceNa ) return string(bodyBytes), statusCode } + +// resourceManagerForwardingTestSuite is a test suite for testing the forwarding behavior of Resource Manager APIs. +type resourceManagerForwardingTestSuite struct { + suite.Suite + ctx context.Context + cancel context.CancelFunc + pdCluster *tests.TestCluster + rmCluster *tests.TestResourceManagerCluster + backendEndpoints string + primary *server.Server + follower *server.Server +} + +func TestResourceManagerForwarding(t *testing.T) { + suite.Run(t, new(resourceManagerForwardingTestSuite)) +} + +func (suite *resourceManagerForwardingTestSuite) SetupTest() { + re := suite.Require() + var err error + suite.ctx, suite.cancel = context.WithCancel(context.Background()) + + suite.pdCluster, err = tests.NewTestCluster(suite.ctx, 1) + re.NoError(err) + err = suite.pdCluster.RunInitialServers() + re.NoError(err) + leaderName := suite.pdCluster.WaitLeader() + re.NotEmpty(leaderName) + pdLeaderServer := suite.pdCluster.GetServer(leaderName) + re.NoError(pdLeaderServer.BootstrapCluster()) + suite.backendEndpoints = pdLeaderServer.GetAddr() + + suite.rmCluster, err = tests.NewTestResourceManagerCluster(suite.ctx, 2, suite.backendEndpoints) + re.NoError(err) + + suite.primary = suite.rmCluster.WaitForPrimaryServing(re) + re.NotNil(suite.primary) + for _, srv := range suite.rmCluster.GetServers() { + if srv.GetAddr() != suite.primary.GetAddr() { + suite.follower = srv + break + } + } + re.NotNil(suite.follower, "follower should not be nil") + re.False(suite.follower.IsServing(), "follower should not be serving") +} + +func (suite *resourceManagerForwardingTestSuite) TearDownTest() { + suite.cancel() + suite.rmCluster.Destroy() + suite.pdCluster.Destroy() +} + +// TestResourceManagerForwardingBehavior checks that requests are correctly forwarded or handled locally. +func (suite *resourceManagerForwardingTestSuite) TestResourceManagerForwardingBehavior() { + re := suite.Require() + followerAddr := suite.follower.GetAddr() + followerURL := func(path string) string { + return fmt.Sprintf("%s%s%s", followerAddr, apis.APIPathPrefix, path) + } + + // Case 1: PUT /admin/log should be handled by the follower locally. + logURL := followerURL("admin/log") + level := "debug" + logPayload, err := json.Marshal(level) + re.NoError(err) + req, err := http.NewRequest(http.MethodPut, logURL, bytes.NewBuffer(logPayload)) + re.NoError(err) + req.Header.Set("Content-Type", "application/json") + resp, err := tests.TestDialClient.Do(req) + re.NoError(err) + defer resp.Body.Close() + re.Equal(http.StatusOK, resp.StatusCode) + + // Case 2: GET /config should be handled by the follower locally. + configURL := followerURL("config") + var followerCfg server.Config + err = testutil.ReadGetJSON(re, tests.TestDialClient, configURL, &followerCfg, testutil.StatusOK(re)) + re.NoError(err) + re.Equal(suite.follower.GetConfig().GetListenAddr(), followerCfg.GetListenAddr()) + re.NotEqual(suite.primary.GetConfig().GetListenAddr(), followerCfg.GetListenAddr()) + re.Equal(level, followerCfg.Log.Level) + + // Case 3: GET /config/groups should be handled by the follower forwarded to the primary. + controllerURL := followerURL("config/groups") + groups := make([]*server.ResourceGroup, 0) + err = testutil.ReadGetJSON(re, tests.TestDialClient, controllerURL, &groups, testutil.StatusOK(re)) + re.NoError(err) +} diff --git a/tests/resource_manager_cluster.go b/tests/resource_manager_cluster.go new file mode 100644 index 00000000000..e9fc638f006 --- /dev/null +++ b/tests/resource_manager_cluster.go @@ -0,0 +1,107 @@ +// Copyright 2025 TiKV Project Authors. +// +// 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, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// in tests/resource_manager_cluster.go + +package tests + +import ( + "context" + "time" + + "github.com/stretchr/testify/require" + + rmserver "github.com/tikv/pd/pkg/mcs/resourcemanager/server" + "github.com/tikv/pd/pkg/utils/tempurl" + "github.com/tikv/pd/pkg/utils/testutil" +) + +// TestResourceManagerCluster is a test cluster for Resource Manager. +type TestResourceManagerCluster struct { + ctx context.Context + + backendEndpoints string + servers map[string]*rmserver.Server + cleanupFuncs map[string]testutil.CleanupFunc +} + +// NewTestResourceManagerCluster creates a new Resource Manager test cluster. +func NewTestResourceManagerCluster(ctx context.Context, initialServerCount int, backendEndpoints string) (tc *TestResourceManagerCluster, err error) { + tc = &TestResourceManagerCluster{ + ctx: ctx, + backendEndpoints: backendEndpoints, + servers: make(map[string]*rmserver.Server, initialServerCount), + cleanupFuncs: make(map[string]testutil.CleanupFunc, initialServerCount), + } + for range initialServerCount { + err = tc.AddServer(tempurl.Alloc()) + if err != nil { + return nil, err + } + } + return tc, nil +} + +// AddServer adds a new Resource Manager server to the test cluster. +func (tc *TestResourceManagerCluster) AddServer(addr string) error { + cfg := rmserver.NewConfig() + cfg.BackendEndpoints = tc.backendEndpoints + cfg.ListenAddr = addr + cfg.Name = cfg.ListenAddr + generatedCfg, err := rmserver.GenerateConfig(cfg) + if err != nil { + return err + } + err = InitLogger(generatedCfg.Log, generatedCfg.Logger, generatedCfg.LogProps, generatedCfg.Security.RedactInfoLog) + if err != nil { + return err + } + server, cleanup, err := NewResourceManagerTestServer(tc.ctx, generatedCfg) + if err != nil { + return err + } + tc.servers[generatedCfg.GetListenAddr()] = server + tc.cleanupFuncs[generatedCfg.GetListenAddr()] = cleanup + return nil +} + +// Destroy stops and destroy the test cluster. +func (tc *TestResourceManagerCluster) Destroy() { + for _, cleanup := range tc.cleanupFuncs { + cleanup() + } + tc.cleanupFuncs = nil + tc.servers = nil +} + +// GetServers returns all Resource Manager servers. +func (tc *TestResourceManagerCluster) GetServers() map[string]*rmserver.Server { + return tc.servers +} + +// WaitForPrimaryServing waits for one of servers being elected to be the primary. +func (tc *TestResourceManagerCluster) WaitForPrimaryServing(re *require.Assertions) *rmserver.Server { + var primary *rmserver.Server + testutil.Eventually(re, func() bool { + for _, server := range tc.servers { + if server.IsServing() { + primary = server + return true + } + } + return false + }, testutil.WithWaitFor(30*time.Second), testutil.WithTickInterval(100*time.Millisecond)) + + return primary +} diff --git a/tests/testutil.go b/tests/testutil.go index c9fec3fc35e..0af8cef1ede 100644 --- a/tests/testutil.go +++ b/tests/testutil.go @@ -877,3 +877,15 @@ func MustCallSchedulerConfigAPI( re.NoError(err) re.Equal(http.StatusOK, resp.StatusCode, string(data)) } + +// NewResourceManagerTestServer creates a resource manager server with given config for testing. +func NewResourceManagerTestServer(ctx context.Context, cfg *rm.Config) (*rm.Server, testutil.CleanupFunc, error) { + s := rm.CreateServer(ctx, cfg) + if err := s.Run(); err != nil { + return nil, nil, err + } + cleanup := func() { + s.Close() + } + return s, cleanup, nil +}