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 }