github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/engine/jobmaster/dm/openapi.go (about)

     1  // Copyright 2022 PingCAP, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package dm
    15  
    16  import (
    17  	"net/http"
    18  
    19  	"github.com/gin-gonic/gin"
    20  	"github.com/pingcap/tiflow/dm/pb"
    21  	"github.com/pingcap/tiflow/dm/pkg/terror"
    22  	"github.com/pingcap/tiflow/engine/jobmaster/dm/config"
    23  	"github.com/pingcap/tiflow/engine/jobmaster/dm/openapi"
    24  	dmpkg "github.com/pingcap/tiflow/engine/pkg/dm"
    25  	engineOpenAPI "github.com/pingcap/tiflow/engine/pkg/openapi"
    26  	"github.com/pingcap/tiflow/pkg/errors"
    27  )
    28  
    29  // errCodePrefix is the prefix that attach to terror's error code string.
    30  // e.g. codeDBBadConn -> DM:ErrDBBadConn, codeDBInvalidConn -> DM:ErrDBInvalidConn
    31  // This is used to distinguish with other components' error code, such as DFLOW and CDC.
    32  // TODO: replace all terror usage with pkg/errors, need further discussion.
    33  const errCodePrefix = "DM:Err"
    34  
    35  func (jm *JobMaster) initOpenAPI(router *gin.RouterGroup) {
    36  	router.Use(httpErrorHandler())
    37  
    38  	router.Use(func(c *gin.Context) {
    39  		if !jm.initialized.Load() {
    40  			_ = c.Error(errors.ErrJobNotRunning.GenWithStackByArgs(jm.ID()))
    41  			c.Abort()
    42  			return
    43  		}
    44  	})
    45  
    46  	wrapper := openapi.ServerInterfaceWrapper{
    47  		Handler: jm,
    48  	}
    49  	// copy from openapi.RegisterHandlersWithOptions
    50  	router.DELETE("/binlog/tasks/:task-name", wrapper.DMAPIDeleteBinlogOperator)
    51  
    52  	router.GET("/binlog/tasks/:task-name", wrapper.DMAPIGetBinlogOperator)
    53  
    54  	router.POST("/binlog/tasks/:task-name", wrapper.DMAPISetBinlogOperator)
    55  
    56  	router.GET("/config", wrapper.DMAPIGetJobConfig)
    57  
    58  	router.PUT("/config", wrapper.DMAPIUpdateJobConfig)
    59  
    60  	router.GET("/schema/tasks/:task-name", wrapper.DMAPIGetSchema)
    61  
    62  	router.PUT("/schema/tasks/:task-name", wrapper.DMAPISetSchema)
    63  
    64  	router.GET("/status", wrapper.DMAPIGetJobStatus)
    65  
    66  	router.PUT("/status", wrapper.DMAPIOperateJob)
    67  }
    68  
    69  // DMAPIGetJobStatus implements the api of get job status.
    70  func (jm *JobMaster) DMAPIGetJobStatus(c *gin.Context, params openapi.DMAPIGetJobStatusParams) {
    71  	var tasks []string
    72  	if params.Tasks != nil {
    73  		tasks = *params.Tasks
    74  	}
    75  	resp, err := jm.QueryJobStatus(c.Request.Context(), tasks)
    76  	if err != nil {
    77  		// nolint:errcheck
    78  		_ = c.Error(err)
    79  		return
    80  	}
    81  	c.IndentedJSON(http.StatusOK, resp)
    82  }
    83  
    84  // TODO: extract terror from worker response
    85  func httpErrorHandler() gin.HandlerFunc {
    86  	return func(c *gin.Context) {
    87  		c.Next()
    88  		gErr := c.Errors.Last()
    89  		if gErr == nil {
    90  			return
    91  		}
    92  
    93  		// Adapt to the error format of engine openapi.
    94  		var httpErr *engineOpenAPI.HTTPError
    95  		var tErr *terror.Error
    96  		if errors.As(gErr.Err, &tErr) {
    97  			code := errCodePrefix + tErr.Code().String()
    98  			message := tErr.Error()
    99  			httpErr = &engineOpenAPI.HTTPError{
   100  				Code:    code,
   101  				Message: message,
   102  			}
   103  		} else {
   104  			httpErr = engineOpenAPI.NewHTTPError(gErr.Err)
   105  		}
   106  		c.JSON(errors.HTTPStatusCode(gErr.Err), httpErr)
   107  	}
   108  }
   109  
   110  // DMAPIGetJobConfig implements the api of get job config.
   111  func (jm *JobMaster) DMAPIGetJobConfig(c *gin.Context) {
   112  	cfg, err := jm.GetJobCfg(c.Request.Context())
   113  	if err != nil {
   114  		// nolint:errcheck
   115  		_ = c.Error(err)
   116  		return
   117  	}
   118  	bs, err := cfg.Yaml()
   119  	if err != nil {
   120  		// nolint:errcheck
   121  		_ = c.Error(err)
   122  		return
   123  	}
   124  	c.IndentedJSON(http.StatusOK, string(bs))
   125  }
   126  
   127  // DMAPIUpdateJobConfig implements the api of update job config.
   128  func (jm *JobMaster) DMAPIUpdateJobConfig(c *gin.Context) {
   129  	var req openapi.UpdateJobConfigRequest
   130  	if err := c.Bind(&req); err != nil {
   131  		// nolint:errcheck
   132  		_ = c.Error(err)
   133  		return
   134  	}
   135  
   136  	var jobCfg config.JobCfg
   137  	if err := jobCfg.Decode([]byte(req.Config)); err != nil {
   138  		// nolint:errcheck
   139  		_ = c.Error(err)
   140  		return
   141  	}
   142  
   143  	if err := jm.UpdateJobCfg(c.Request.Context(), &jobCfg); err != nil {
   144  		// nolint:errcheck
   145  		_ = c.Error(err)
   146  		return
   147  	}
   148  	c.Status(http.StatusOK)
   149  }
   150  
   151  // DMAPIOperateJob implements the api of operate job.
   152  func (jm *JobMaster) DMAPIOperateJob(c *gin.Context) {
   153  	var req openapi.OperateJobRequest
   154  	if err := c.Bind(&req); err != nil {
   155  		// nolint:errcheck
   156  		_ = c.Error(err)
   157  		return
   158  	}
   159  
   160  	var op dmpkg.OperateType
   161  	switch req.Op {
   162  	case openapi.OperateJobRequestOpPause:
   163  		op = dmpkg.Pause
   164  	case openapi.OperateJobRequestOpResume:
   165  		op = dmpkg.Resume
   166  	default:
   167  		// nolint:errcheck
   168  		_ = c.Error(errors.Errorf("unsupported op type '%s' for operate task", req.Op))
   169  		return
   170  	}
   171  
   172  	var tasks []string
   173  	if req.Tasks != nil {
   174  		tasks = *req.Tasks
   175  	}
   176  	err := jm.operateTask(c.Request.Context(), op, nil, tasks)
   177  	if err != nil {
   178  		// nolint:errcheck
   179  		_ = c.Error(err)
   180  		return
   181  	}
   182  	c.Status(http.StatusOK)
   183  }
   184  
   185  // DMAPIGetBinlogOperator implements the api of get binlog operator.
   186  // TODO: pagination support if needed
   187  func (jm *JobMaster) DMAPIGetBinlogOperator(c *gin.Context, taskName string, params openapi.DMAPIGetBinlogOperatorParams) {
   188  	req := &dmpkg.BinlogRequest{
   189  		Op:      pb.ErrorOp_List,
   190  		Sources: []string{taskName},
   191  	}
   192  	if params.BinlogPos != nil {
   193  		req.BinlogPos = *params.BinlogPos
   194  	}
   195  	resp, err := jm.Binlog(c.Request.Context(), req)
   196  	if err != nil {
   197  		// nolint:errcheck
   198  		_ = c.Error(err)
   199  		return
   200  	}
   201  	c.IndentedJSON(http.StatusOK, resp)
   202  }
   203  
   204  // DMAPISetBinlogOperator implements the api of set binlog operator.
   205  func (jm *JobMaster) DMAPISetBinlogOperator(c *gin.Context, taskName string) {
   206  	var req openapi.SetBinlogOperatorRequest
   207  	if err := c.Bind(&req); err != nil {
   208  		// nolint:errcheck
   209  		_ = c.Error(err)
   210  		return
   211  	}
   212  
   213  	r := &dmpkg.BinlogRequest{
   214  		Sources: []string{taskName},
   215  	}
   216  	if req.BinlogPos != nil {
   217  		r.BinlogPos = *req.BinlogPos
   218  	}
   219  	if req.Sqls != nil {
   220  		r.Sqls = *req.Sqls
   221  	}
   222  	switch req.Op {
   223  	case openapi.SetBinlogOperatorRequestOpInject:
   224  		r.Op = pb.ErrorOp_Inject
   225  	case openapi.SetBinlogOperatorRequestOpSkip:
   226  		r.Op = pb.ErrorOp_Skip
   227  	case openapi.SetBinlogOperatorRequestOpReplace:
   228  		r.Op = pb.ErrorOp_Replace
   229  	default:
   230  		// nolint:errcheck
   231  		_ = c.Error(errors.Errorf("unsupported op type '%s' for set binlog operator", req.Op))
   232  		return
   233  	}
   234  	resp, err := jm.Binlog(c.Request.Context(), r)
   235  	if err != nil {
   236  		// nolint:errcheck
   237  		_ = c.Error(err)
   238  		return
   239  	}
   240  	c.IndentedJSON(http.StatusCreated, resp)
   241  }
   242  
   243  // DMAPIDeleteBinlogOperator implements the api of delete binlog operator.
   244  func (jm *JobMaster) DMAPIDeleteBinlogOperator(c *gin.Context, taskName string, params openapi.DMAPIDeleteBinlogOperatorParams) {
   245  	req := &dmpkg.BinlogRequest{
   246  		Op:      pb.ErrorOp_Revert,
   247  		Sources: []string{taskName},
   248  	}
   249  	if params.BinlogPos != nil {
   250  		req.BinlogPos = *params.BinlogPos
   251  	}
   252  	resp, err := jm.Binlog(c.Request.Context(), req)
   253  	if err != nil {
   254  		// nolint:errcheck
   255  		_ = c.Error(err)
   256  		return
   257  	}
   258  
   259  	if resp.ErrorMsg != "" {
   260  		c.IndentedJSON(http.StatusOK, resp)
   261  		return
   262  	}
   263  	for _, r := range resp.Results {
   264  		if r.ErrorMsg != "" {
   265  			c.IndentedJSON(http.StatusOK, resp)
   266  			return
   267  		}
   268  	}
   269  	c.Status(http.StatusNoContent)
   270  }
   271  
   272  // DMAPIGetSchema implements the api of get schema.
   273  func (jm *JobMaster) DMAPIGetSchema(c *gin.Context, taskname string, params openapi.DMAPIGetSchemaParams) {
   274  	var (
   275  		op       pb.SchemaOp
   276  		database string
   277  		table    string
   278  	)
   279  	if params.Database != nil {
   280  		database = *params.Database
   281  	}
   282  	if params.Table != nil {
   283  		table = *params.Table
   284  	}
   285  	switch {
   286  	case params.Target != nil && *params.Target:
   287  		op = pb.SchemaOp_ListMigrateTargets
   288  	case database != "" && table != "":
   289  		op = pb.SchemaOp_GetSchema
   290  	case database != "" && table == "":
   291  		op = pb.SchemaOp_ListTable
   292  	case database == "" && table == "":
   293  		op = pb.SchemaOp_ListSchema
   294  	default:
   295  		// nolint:errcheck
   296  		_ = c.Error(errors.New("invalid query params for get schema"))
   297  		return
   298  	}
   299  
   300  	r := &dmpkg.BinlogSchemaRequest{
   301  		Op:       op,
   302  		Sources:  []string{taskname},
   303  		Database: database,
   304  		Table:    table,
   305  	}
   306  	resp := jm.BinlogSchema(c.Request.Context(), r)
   307  	c.IndentedJSON(http.StatusOK, resp)
   308  }
   309  
   310  // DMAPISetSchema implements the api of set schema.
   311  func (jm *JobMaster) DMAPISetSchema(c *gin.Context, taskName string) {
   312  	var (
   313  		req        openapi.SetBinlogSchemaRequest
   314  		fromSource bool
   315  		fromTarget bool
   316  	)
   317  	if err := c.Bind(&req); err != nil {
   318  		// nolint:errcheck
   319  		_ = c.Error(err)
   320  		return
   321  	}
   322  	if req.FromSource != nil {
   323  		fromSource = *req.FromSource
   324  	}
   325  	if req.FromTarget != nil {
   326  		fromTarget = *req.FromTarget
   327  	}
   328  	r := &dmpkg.BinlogSchemaRequest{
   329  		Op:         pb.SchemaOp_SetSchema,
   330  		Sources:    []string{taskName},
   331  		Database:   req.Database,
   332  		Table:      req.Table,
   333  		Schema:     req.Sql,
   334  		FromSource: fromSource,
   335  		FromTarget: fromTarget,
   336  	}
   337  	resp := jm.BinlogSchema(c.Request.Context(), r)
   338  	c.IndentedJSON(http.StatusOK, resp)
   339  }