Skip to content

Commit

Permalink
feature: Query Editor (#713)
Browse files Browse the repository at this point in the history
Signed-off-by: Breezewish <[email protected]>
(cherry picked from commit 2a372d4)
  • Loading branch information
breezewish committed Sep 8, 2020
1 parent 01f0abe commit ac6e04a
Show file tree
Hide file tree
Showing 40 changed files with 1,413 additions and 436 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ endif
go build -o bin/tidb-dashboard -ldflags '$(LDFLAGS)' -tags "${BUILD_TAGS}" cmd/tidb-dashboard/main.go

run:
bin/tidb-dashboard --debug
bin/tidb-dashboard --debug --experimental
3 changes: 2 additions & 1 deletion cmd/tidb-dashboard/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ func NewCLIConfig() *DashboardCLIConfig {
flag.StringVar(&cfg.CoreConfig.DataDir, "data-dir", "/tmp/dashboard-data", "path to the Dashboard Server data directory")
flag.StringVar(&cfg.CoreConfig.PublicPathPrefix, "path-prefix", config.DefaultPublicPathPrefix, "public URL path prefix for reverse proxies")
flag.StringVar(&cfg.CoreConfig.PDEndPoint, "pd", "http://127.0.0.1:2379", "PD endpoint address that Dashboard Server connects to")
flag.BoolVar(&cfg.CoreConfig.EnableTelemetry, "enable-telemetry", true, "enable client to report data for analysis")
flag.BoolVar(&cfg.CoreConfig.EnableTelemetry, "telemetry", true, "allow telemetry")
flag.BoolVar(&cfg.CoreConfig.EnableExperimental, "experimental", false, "allow experimental features")

showVersion := flag.BoolP("version", "v", false, "print version information and exit")

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/pingcap-incubator/tidb-dashboard
go 1.13

require (
github.com/VividCortex/mysqlerr v0.0.0-20200629151747-c28746d985dd
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
github.com/appleboy/gin-jwt/v2 v2.6.3
github.com/cenkalti/backoff/v4 v4.0.2
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/VividCortex/mysqlerr v0.0.0-20200629151747-c28746d985dd h1:59Whn6shj5MTVjTf2OX6+7iMcmY6h5CK0kTWwRaplL4=
github.com/VividCortex/mysqlerr v0.0.0-20200629151747-c28746d985dd/go.mod h1:f3HiCrHjHBdcm6E83vGaXh1KomZMA2P6aeo3hKx/wg0=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
Expand Down
6 changes: 3 additions & 3 deletions pkg/apiserver/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ import (

"github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/clusterinfo"
"github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/diagnose"
"github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/foo"
"github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/info"
"github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/logsearch"
"github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/metrics"
"github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/profiling"
"github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/queryeditor"
"github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/slowquery"
"github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/statement"
"github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/user"
Expand Down Expand Up @@ -110,7 +110,6 @@ func (s *Service) Start(ctx context.Context) error {
tidb.NewForwarder,
pkghttp.NewHTTPClientWithConf,
user.NewAuthService,
foo.NewService,
info.NewService,
clusterinfo.NewService,
profiling.NewService,
Expand All @@ -120,11 +119,11 @@ func (s *Service) Start(ctx context.Context) error {
diagnose.NewService,
keyvisual.NewService,
metrics.NewService,
queryeditor.NewService,
),
fx.Populate(&s.apiHandlerEngine),
fx.Invoke(
user.Register,
foo.Register,
info.Register,
clusterinfo.Register,
profiling.Register,
Expand All @@ -134,6 +133,7 @@ func (s *Service) Start(ctx context.Context) error {
diagnose.Register,
keyvisual.Register,
metrics.Register,
queryeditor.Register,
// Must be at the end
s.status.Register,
),
Expand Down
53 changes: 0 additions & 53 deletions pkg/apiserver/foo/foo.go

This file was deleted.

10 changes: 6 additions & 4 deletions pkg/apiserver/info/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
}

type InfoResponse struct { //nolint:golint
Version *version.Info `json:"version"`
EnableTelemetry bool `json:"enable_telemetry"`
Version *version.Info `json:"version"`
EnableTelemetry bool `json:"enable_telemetry"`
EnableExperimental bool `json:"enable_experimental"`
}

// @Summary Dashboard info
Expand All @@ -61,8 +62,9 @@ type InfoResponse struct { //nolint:golint
// @Failure 401 {object} utils.APIError "Unauthorized failure"
func (s *Service) infoHandler(c *gin.Context) {
resp := InfoResponse{
Version: version.GetInfo(),
EnableTelemetry: s.config.EnableTelemetry,
Version: version.GetInfo(),
EnableTelemetry: s.config.EnableTelemetry,
EnableExperimental: s.config.EnableExperimental,
}
c.JSON(http.StatusOK, resp)
}
Expand Down
174 changes: 174 additions & 0 deletions pkg/apiserver/queryeditor/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Copyright 2020 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 queryeditor

import (
"context"
"database/sql"
"net/http"
"time"

"github.com/gin-gonic/gin"
"github.com/pingcap/log"
"go.uber.org/fx"
"go.uber.org/zap"

"github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/user"
"github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/utils"
"github.com/pingcap-incubator/tidb-dashboard/pkg/config"
"github.com/pingcap-incubator/tidb-dashboard/pkg/tidb"
)

type Service struct {
lifecycleCtx context.Context

config *config.Config
tidbForwarder *tidb.Forwarder
}

func NewService(lc fx.Lifecycle, config *config.Config, tidbForwarder *tidb.Forwarder) *Service {
service := &Service{config: config, tidbForwarder: tidbForwarder}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
service.lifecycleCtx = ctx
return nil
},
})

return service
}

func Register(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
endpoint := r.Group("/query_editor")
endpoint.Use(auth.MWAuthRequired())
endpoint.Use(utils.MWConnectTiDB(s.tidbForwarder))
endpoint.POST("/run", s.runHandler)
}

type RunRequest struct {
Statements string `json:"statements" example:"show databases;"`
MaxRows int `json:"max_rows" example:"1000"`
}

type RunResponse struct {
ErrorMsg string `json:"error_msg"`
ColumnNames []string `json:"column_names"`
Rows [][]interface{} `json:"rows"`
ExecutionMs int64 `json:"execution_ms"`
ActualRows int `json:"actual_rows"`
}

func executeStatements(context context.Context, db *sql.DB, statements string) ([]string, [][]interface{}, error) {
rows, err := db.QueryContext(context, statements)
if err != nil {
return nil, nil, err
}

defer rows.Close()

colNames, err := rows.Columns()
if err != nil {
return nil, nil, err
}

retRows := make([][]interface{}, 0)

values := make([]sql.RawBytes, len(colNames))
scanArgs := make([]interface{}, len(values))
for i := range values {
scanArgs[i] = &values[i]
}

for rows.Next() {
err = rows.Scan(scanArgs...)
if err != nil {
return nil, nil, err
}

retRow := make([]interface{}, 0, len(values))
var value interface{}
for _, col := range values {
if col == nil {
value = nil
} else {
value = string(col)
}
retRow = append(retRow, value)
}
retRows = append(retRows, retRow)
}

if err = rows.Err(); err != nil {
return nil, nil, err
}

return colNames, retRows, nil
}

// @ID queryEditorRun
// @Summary Run
// @Description Run statements
// @Produce json
// @Param request body RunRequest true "Request body"
// @Success 200 {object} RunResponse
// @Router /query_editor/run [post]
// @Security JwtAuth
// @Failure 400 {object} utils.APIError "Bad request"
// @Failure 401 {object} utils.APIError "Unauthorized failure"
// @Failure 403 {object} utils.APIError "Experimental feature not enabled"
func (s *Service) runHandler(c *gin.Context) {
if !s.config.EnableExperimental {
c.Status(http.StatusForbidden)
_ = c.Error(utils.ErrExpNotEnabled.NewWithNoMessage())
return
}

var req RunRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.Status(http.StatusBadRequest)
_ = c.Error(utils.ErrInvalidRequest.WrapWithNoMessage(err))
return
}

ctx, cancel := context.WithTimeout(s.lifecycleCtx, time.Minute*5)
defer cancel()

startTime := time.Now()
colNames, rows, err := executeStatements(ctx, utils.GetTiDBConnection(c).DB(), req.Statements)
elapsedTime := time.Since(startTime)

if err != nil {
log.Warn("Failed to execute user input statements", zap.String("statements", req.Statements), zap.Error(err))
c.JSON(http.StatusOK, RunResponse{
ErrorMsg: err.Error(),
ColumnNames: nil,
Rows: nil,
ExecutionMs: elapsedTime.Milliseconds(),
ActualRows: 0,
})
return
}

truncatedRows := rows
if len(truncatedRows) > req.MaxRows {
truncatedRows = truncatedRows[:req.MaxRows]
}

c.JSON(http.StatusOK, RunResponse{
ColumnNames: colNames,
Rows: truncatedRows,
ExecutionMs: elapsedTime.Milliseconds(),
ActualRows: len(rows),
})
}
2 changes: 1 addition & 1 deletion pkg/apiserver/statement/statement.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (s *Service) configHandler(c *gin.Context) {
c.JSON(http.StatusOK, cfg)
}

// @Summary Statement configurationt
// @Summary Statement configuration
// @Description Modify configuration of statements
// @Param request body statement.Config true "Request body"
// @Success 204 {object} string
Expand Down
6 changes: 3 additions & 3 deletions pkg/apiserver/user/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ type AuthService struct {
}

type authenticateForm struct {
IsTiDBAuth bool `json:"is_tidb_auth" binding:"required"`
Username string `json:"username" binding:"required"`
Password string `json:"password"`
IsTiDBAuth bool `json:"is_tidb_auth" binding:"required" example:"true"`
Username string `json:"username" binding:"required" example:"root"`
Password string `json:"password" example:""`
}

type TokenResponse struct {
Expand Down
1 change: 1 addition & 0 deletions pkg/apiserver/utils/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var (
ErrUnauthorized = ErrNS.NewType("unauthorized")
ErrInsufficientPrivilege = ErrNS.NewType("insufficient_privilege")
ErrInvalidRequest = ErrNS.NewType("invalid_request")
ErrExpNotEnabled = ErrNS.NewType("experimental_feature_not_enabled")
)

type APIError struct {
Expand Down
11 changes: 4 additions & 7 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,11 @@ type Config struct {
PDEndPoint string
PublicPathPrefix string

// TLS config for mTLS authentication between TiDB components.
ClusterTLSConfig *tls.Config
ClusterTLSConfig *tls.Config // TLS config for mTLS authentication between TiDB components.
TiDBTLSConfig *tls.Config // TLS config for mTLS authentication between TiDB and MySQL client.

// TLS config for mTLS authentication between TiDB and MySQL client.
TiDBTLSConfig *tls.Config

// Enable client to report data for analysis
EnableTelemetry bool
EnableTelemetry bool
EnableExperimental bool
}

func (c *Config) NormalizePDEndPoint() error {
Expand Down
Loading

0 comments on commit ac6e04a

Please sign in to comment.