diff --git a/conf/config.toml b/conf/config.toml index ea863746642..9b7d45c6d0a 100644 --- a/conf/config.toml +++ b/conf/config.toml @@ -123,3 +123,7 @@ location-labels = [] ## is running behind a reverse proxy. Do not configure it if you access ## Dashboard directly. # public-path-prefix = "/dashboard" + +## When enabled, request will be proxied to the instance running Dashboard +## internally instead of result in a 307 redirection. +# internal-proxy = false diff --git a/pkg/dashboard/adapter/redirector.go b/pkg/dashboard/adapter/redirector.go index 9c710d3654a..20d0350e21d 100644 --- a/pkg/dashboard/adapter/redirector.go +++ b/pkg/dashboard/adapter/redirector.go @@ -21,6 +21,7 @@ import ( "sync" "github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver" + "github.com/pingcap-incubator/tidb-dashboard/pkg/utils" ) const ( @@ -36,6 +37,9 @@ type Redirector struct { address string proxy *httputil.ReverseProxy + // The status of the dashboard in the cluster. + // It is not equal to `apiserver.Service.status`. + status *utils.ServiceStatus } // NewRedirector creates a new Redirector. @@ -43,6 +47,7 @@ func NewRedirector(name string, tlsConfig *tls.Config) *Redirector { return &Redirector{ name: name, tlsConfig: tlsConfig, + status: utils.NewServiceStatus(), } } @@ -56,11 +61,13 @@ func (h *Redirector) SetAddress(addr string) { } if addr == "" { + h.status.Stop() h.address = "" h.proxy = nil return } + h.status.Start() h.address = addr target, _ := url.Parse(addr) // error has been handled in checkAddress h.proxy = httputil.NewSingleHostReverseProxy(target) @@ -117,3 +124,8 @@ func (h *Redirector) ReverseProxy(w http.ResponseWriter, r *http.Request) { proxy.ServeHTTP(w, r) } + +// NewStatusAwareHandler returns a Handler that switches between different Handlers based on status. +func (h *Redirector) NewStatusAwareHandler(handler http.Handler) http.Handler { + return h.status.NewStatusAwareHandler(handler, apiserver.StoppedHandler) +} diff --git a/pkg/dashboard/dashboard.go b/pkg/dashboard/dashboard.go index fd7e3774e7a..9082e6b2ee7 100644 --- a/pkg/dashboard/dashboard.go +++ b/pkg/dashboard/dashboard.go @@ -53,24 +53,34 @@ func SetCheckInterval(d time.Duration) { // GetServiceBuilders returns all ServiceBuilders required by Dashboard func GetServiceBuilders() []server.HandlerBuilder { var ( - err error - cfg *config.Config - redirector *adapter.Redirector - assets http.FileSystem - s *apiserver.Service + err error + cfg *config.Config + internalProxy bool + redirector *adapter.Redirector + assets http.FileSystem + s *apiserver.Service ) + // The order of execution must be sequential. return []server.HandlerBuilder{ // Dashboard API Service func(ctx context.Context, srv *server.Server) (http.Handler, server.ServiceGroup, error) { if cfg, err = adapter.GenDashboardConfig(srv); err != nil { return nil, apiServiceGroup, err } + internalProxy = srv.GetConfig().Dashboard.InternalProxy redirector = adapter.NewRedirector(srv.Name(), cfg.ClusterTLSConfig) assets = ui.Assets(cfg) + + var stoppedHandler http.Handler + if internalProxy { + stoppedHandler = http.HandlerFunc(redirector.ReverseProxy) + } else { + stoppedHandler = http.HandlerFunc(redirector.TemporaryRedirect) + } s = apiserver.NewService( cfg, - http.HandlerFunc(redirector.ReverseProxy), + stoppedHandler, assets, adapter.GenPDDataProviderConstructor(srv), ) @@ -87,10 +97,14 @@ func GetServiceBuilders() []server.HandlerBuilder { return nil, uiServiceGroup, err } - handler := s.NewStatusAwareHandler( - http.StripPrefix(uiServiceGroup.PathPrefix, uiserver.Handler(assets)), - http.HandlerFunc(redirector.TemporaryRedirect), - ) + var handler http.Handler + uiHandler := http.StripPrefix(uiServiceGroup.PathPrefix, uiserver.Handler(assets)) + if internalProxy { + handler = redirector.NewStatusAwareHandler(uiHandler) + } else { + handler = s.NewStatusAwareHandler(uiHandler, http.HandlerFunc(redirector.TemporaryRedirect)) + } + return handler, uiServiceGroup, nil }, } diff --git a/server/config/config.go b/server/config/config.go index a4c12b69258..6bc8f8a3b73 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -1122,6 +1122,7 @@ type DashboardConfig struct { TiDBCertPath string `toml:"tidb-cert-path" json:"tidb_cert_path"` TiDBKeyPath string `toml:"tidb-key-path" json:"tidb_key_path"` PublicPathPrefix string `toml:"public-path-prefix" json:"public_path_prefix"` + InternalProxy bool `toml:"internal-proxy" json:"internal_proxy"` } // ToTiDBTLSConfig generates tls config for connecting to TiDB, used by tidb-dashboard. diff --git a/tests/dashboard/service_test.go b/tests/dashboard/service_test.go index 705291d3795..b9e7320a015 100644 --- a/tests/dashboard/service_test.go +++ b/tests/dashboard/service_test.go @@ -27,6 +27,7 @@ import ( "github.com/pingcap/pd/v4/pkg/dashboard" "github.com/pingcap/pd/v4/pkg/testutil" "github.com/pingcap/pd/v4/server" + "github.com/pingcap/pd/v4/server/config" "github.com/pingcap/pd/v4/tests" "github.com/pingcap/pd/v4/tests/pdctl" @@ -72,6 +73,14 @@ func (s *serverTestSuite) TearDownSuite(c *C) { dashboard.SetCheckInterval(time.Second) } +func (s *serverTestSuite) TestDashboardRedirect(c *C) { + s.testDashboard(c, false) +} + +func (s *serverTestSuite) TestDashboardProxy(c *C) { + s.testDashboard(c, true) +} + func (s *serverTestSuite) checkRespCode(c *C, url string, code int) { resp, err := s.httpClient.Get(url) //nolint:gosec c.Assert(err, IsNil) @@ -85,20 +94,22 @@ func (s *serverTestSuite) waitForConfigSync() { time.Sleep(time.Second) } -func (s *serverTestSuite) checkServiceIsStarted(c *C, servers map[string]*tests.TestServer, leader *tests.TestServer) string { +func (s *serverTestSuite) checkServiceIsStarted(c *C, internalProxy bool, servers map[string]*tests.TestServer, leader *tests.TestServer) string { s.waitForConfigSync() dashboardAddress := leader.GetServer().GetPersistOptions().GetDashboardAddress() hasServiceNode := false for _, srv := range servers { c.Assert(srv.GetPersistOptions().GetDashboardAddress(), Equals, dashboardAddress) addr := srv.GetAddr() - if addr == dashboardAddress { + if addr == dashboardAddress || internalProxy { s.checkRespCode(c, fmt.Sprintf("%s/dashboard/", addr), http.StatusOK) s.checkRespCode(c, fmt.Sprintf("%s/dashboard/api/keyvisual/heatmaps", addr), http.StatusUnauthorized) - hasServiceNode = true + if addr == dashboardAddress { + hasServiceNode = true + } } else { s.checkRespCode(c, fmt.Sprintf("%s/dashboard/", addr), http.StatusTemporaryRedirect) - s.checkRespCode(c, fmt.Sprintf("%s/dashboard/api/keyvisual/heatmaps", addr), http.StatusUnauthorized) + s.checkRespCode(c, fmt.Sprintf("%s/dashboard/api/keyvisual/heatmaps", addr), http.StatusTemporaryRedirect) } } c.Assert(hasServiceNode, IsTrue) @@ -115,8 +126,10 @@ func (s *serverTestSuite) checkServiceIsStopped(c *C, servers map[string]*tests. } } -func (s *serverTestSuite) TestDashboard(c *C) { - cluster, err := tests.NewTestCluster(s.ctx, 3) +func (s *serverTestSuite) testDashboard(c *C, internalProxy bool) { + cluster, err := tests.NewTestCluster(s.ctx, 3, func(conf *config.Config) { + conf.Dashboard.InternalProxy = internalProxy + }) c.Assert(err, IsNil) defer cluster.Destroy() err = cluster.RunInitialServers() @@ -130,7 +143,7 @@ func (s *serverTestSuite) TestDashboard(c *C) { leaderAddr := leader.GetAddr() // auto select node - dashboardAddress1 := s.checkServiceIsStarted(c, servers, leader) + dashboardAddress1 := s.checkServiceIsStarted(c, internalProxy, servers, leader) // pd-ctl set another addr var dashboardAddress2 string @@ -143,7 +156,7 @@ func (s *serverTestSuite) TestDashboard(c *C) { args := []string{"-u", leaderAddr, "config", "set", "dashboard-address", dashboardAddress2} _, _, err = pdctl.ExecuteCommandC(cmd, args...) c.Assert(err, IsNil) - s.checkServiceIsStarted(c, servers, leader) + s.checkServiceIsStarted(c, internalProxy, servers, leader) c.Assert(leader.GetServer().GetPersistOptions().GetDashboardAddress(), Equals, dashboardAddress2) // pd-ctl set stop