github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/cdc/api/v1/api.go (about) 1 // Copyright 2021 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 v1 15 16 import ( 17 "fmt" 18 "net/http" 19 "os" 20 "sort" 21 "strings" 22 23 "github.com/gin-gonic/gin" 24 "github.com/pingcap/log" 25 "github.com/pingcap/tiflow/cdc/api" 26 "github.com/pingcap/tiflow/cdc/api/middleware" 27 "github.com/pingcap/tiflow/cdc/capture" 28 "github.com/pingcap/tiflow/cdc/model" 29 "github.com/pingcap/tiflow/cdc/owner" 30 cerror "github.com/pingcap/tiflow/pkg/errors" 31 "github.com/pingcap/tiflow/pkg/logutil" 32 "github.com/pingcap/tiflow/pkg/retry" 33 "github.com/pingcap/tiflow/pkg/util" 34 "github.com/pingcap/tiflow/pkg/version" 35 "github.com/tikv/client-go/v2/oracle" 36 "go.uber.org/zap" 37 ) 38 39 // OpenAPI provides capture APIs. 40 type OpenAPI struct { 41 capture capture.Capture 42 // use for unit test only 43 testStatusProvider owner.StatusProvider 44 } 45 46 // NewOpenAPI creates a new OpenAPI. 47 func NewOpenAPI(c capture.Capture) OpenAPI { 48 return OpenAPI{capture: c} 49 } 50 51 // NewOpenAPI4Test return a OpenAPI for test 52 func NewOpenAPI4Test(c capture.Capture, p owner.StatusProvider) OpenAPI { 53 return OpenAPI{capture: c, testStatusProvider: p} 54 } 55 56 func (h *OpenAPI) statusProvider() owner.StatusProvider { 57 if h.testStatusProvider != nil { 58 return h.testStatusProvider 59 } 60 return h.capture.StatusProvider() 61 } 62 63 // RegisterOpenAPIRoutes registers routes for OpenAPI 64 func RegisterOpenAPIRoutes(router *gin.Engine, api OpenAPI) { 65 v1 := router.Group("/api/v1") 66 67 v1.Use(middleware.CheckServerReadyMiddleware(api.capture)) 68 v1.Use(middleware.LogMiddleware()) 69 v1.Use(middleware.ErrorHandleMiddleware()) 70 71 // common API 72 v1.GET("/status", api.ServerStatus) 73 v1.GET("/health", api.Health) 74 v1.POST("/log", SetLogLevel) 75 76 controllerMiddleware := middleware.ForwardToControllerMiddleware(api.capture) 77 changefeedOwnerMiddleware := middleware. 78 ForwardToChangefeedOwnerMiddleware(api.capture, getChangefeedFromRequest) 79 authenticateMiddleware := middleware.AuthenticateMiddleware(api.capture) 80 81 // changefeed API 82 changefeedGroup := v1.Group("/changefeeds") 83 changefeedGroup.GET("", controllerMiddleware, api.ListChangefeed) 84 changefeedGroup.GET("/:changefeed_id", changefeedOwnerMiddleware, api.GetChangefeed) 85 changefeedGroup.POST("", controllerMiddleware, authenticateMiddleware, api.CreateChangefeed) 86 changefeedGroup.PUT("/:changefeed_id", changefeedOwnerMiddleware, authenticateMiddleware, api.UpdateChangefeed) 87 changefeedGroup.POST("/:changefeed_id/pause", changefeedOwnerMiddleware, authenticateMiddleware, api.PauseChangefeed) 88 changefeedGroup.POST("/:changefeed_id/resume", changefeedOwnerMiddleware, authenticateMiddleware, api.ResumeChangefeed) 89 changefeedGroup.DELETE("/:changefeed_id", controllerMiddleware, authenticateMiddleware, api.RemoveChangefeed) 90 changefeedGroup.POST("/:changefeed_id/tables/rebalance_table", changefeedOwnerMiddleware, authenticateMiddleware, api.RebalanceTables) 91 changefeedGroup.POST("/:changefeed_id/tables/move_table", changefeedOwnerMiddleware, authenticateMiddleware, api.MoveTable) 92 93 // owner API 94 ownerGroup := v1.Group("/owner") 95 ownerGroup.POST("/resign", controllerMiddleware, api.ResignController) 96 97 // processor API 98 processorGroup := v1.Group("/processors") 99 processorGroup.GET("", controllerMiddleware, api.ListProcessor) 100 processorGroup.GET("/:changefeed_id/:capture_id", 101 changefeedOwnerMiddleware, api.GetProcessor) 102 103 // capture API 104 captureGroup := v1.Group("/captures") 105 captureGroup.Use(controllerMiddleware) 106 captureGroup.GET("", api.ListCapture) 107 captureGroup.PUT("/drain", api.DrainCapture) 108 } 109 110 // ListChangefeed lists all changgefeeds in cdc cluster 111 // @Summary List changefeed 112 // @Description list all changefeeds in cdc cluster 113 // @Tags changefeed 114 // @Accept json 115 // @Produce json 116 // @Param state query string false "state" 117 // @Success 200 {array} model.ChangefeedCommonInfo 118 // @Failure 500 {object} model.HTTPError 119 // @Router /api/v1/changefeeds [get] 120 func (h *OpenAPI) ListChangefeed(c *gin.Context) { 121 ctx := c.Request.Context() 122 state := c.Query(api.APIOpVarChangefeedState) 123 controller, err := h.capture.GetController() 124 if err != nil { 125 _ = c.Error(err) 126 return 127 } 128 // get all changefeed status 129 statuses, err := controller.GetAllChangeFeedCheckpointTs(ctx) 130 if err != nil { 131 _ = c.Error(err) 132 return 133 } 134 // get all changefeed infos 135 infos, err := controller.GetAllChangeFeedInfo(ctx) 136 if err != nil { 137 // this call will return a parsedError generated by the error we passed in 138 // so it is no need to check the parsedError 139 _ = c.Error(err) 140 return 141 } 142 143 resps := make([]*model.ChangefeedCommonInfo, 0) 144 changefeeds := make([]model.ChangeFeedID, 0) 145 146 for cfID := range infos { 147 changefeeds = append(changefeeds, cfID) 148 } 149 sort.Slice(changefeeds, func(i, j int) bool { 150 if changefeeds[i].Namespace == changefeeds[j].Namespace { 151 return changefeeds[i].ID < changefeeds[j].ID 152 } 153 154 return changefeeds[i].Namespace < changefeeds[j].Namespace 155 }) 156 157 for _, cfID := range changefeeds { 158 cfInfo, exist := infos[cfID] 159 if !exist { 160 // If a changefeed info does not exist, skip it 161 continue 162 } 163 164 if !cfInfo.State.IsNeeded(state) { 165 continue 166 } 167 168 resp := &model.ChangefeedCommonInfo{ 169 UpstreamID: cfInfo.UpstreamID, 170 Namespace: cfID.Namespace, 171 ID: cfID.ID, 172 } 173 174 resp.FeedState = cfInfo.State 175 resp.RunningError = cfInfo.Error 176 177 resp.CheckpointTSO = statuses[cfID] 178 tm := oracle.GetTimeFromTS(resp.CheckpointTSO) 179 resp.CheckpointTime = model.JSONTime(tm) 180 181 resps = append(resps, resp) 182 } 183 c.IndentedJSON(http.StatusOK, resps) 184 } 185 186 // GetChangefeed get detailed info of a changefeed 187 // @Summary Get changefeed 188 // @Description get detail information of a changefeed 189 // @Tags changefeed 190 // @Accept json 191 // @Produce json 192 // @Param changefeed_id path string true "changefeed_id" 193 // @Success 200 {object} model.ChangefeedDetail 194 // @Failure 500,400 {object} model.HTTPError 195 // @Router /api/v1/changefeeds/{changefeed_id} [get] 196 func (h *OpenAPI) GetChangefeed(c *gin.Context) { 197 ctx := c.Request.Context() 198 changefeedID := model.DefaultChangeFeedID(c.Param(api.APIOpVarChangefeedID)) 199 if err := model.ValidateChangefeedID(changefeedID.ID); err != nil { 200 _ = c.Error(cerror.ErrAPIInvalidParam.GenWithStack("invalid changefeed_id: %s", 201 changefeedID.ID)) 202 return 203 } 204 205 info, err := h.statusProvider().GetChangeFeedInfo(ctx, changefeedID) 206 if err != nil { 207 _ = c.Error(err) 208 return 209 } 210 211 status, err := h.statusProvider().GetChangeFeedStatus(ctx, changefeedID) 212 if err != nil { 213 _ = c.Error(err) 214 return 215 } 216 217 taskStatus := make([]model.CaptureTaskStatus, 0) 218 if info.State == model.StateNormal { 219 processorInfos, err := h.statusProvider().GetAllTaskStatuses(ctx, changefeedID) 220 if err != nil { 221 _ = c.Error(err) 222 return 223 } 224 for captureID, status := range processorInfos { 225 tables := make([]int64, 0) 226 for tableID := range status.Tables { 227 tables = append(tables, tableID) 228 } 229 taskStatus = append(taskStatus, 230 model.CaptureTaskStatus{ 231 CaptureID: captureID, Tables: tables, 232 Operation: status.Operation, 233 }) 234 } 235 } 236 sinkURI, err := util.MaskSinkURI(info.SinkURI) 237 if err != nil { 238 log.Error("failed to mask sink URI", zap.Error(err)) 239 } 240 241 changefeedDetail := &model.ChangefeedDetail{ 242 UpstreamID: info.UpstreamID, 243 Namespace: changefeedID.Namespace, 244 ID: changefeedID.ID, 245 SinkURI: sinkURI, 246 CreateTime: model.JSONTime(info.CreateTime), 247 StartTs: info.StartTs, 248 TargetTs: info.TargetTs, 249 CheckpointTSO: status.CheckpointTs, 250 CheckpointTime: model.JSONTime(oracle.GetTimeFromTS(status.CheckpointTs)), 251 ResolvedTs: status.ResolvedTs, 252 Engine: info.Engine, 253 FeedState: info.State, 254 TaskStatus: taskStatus, 255 CreatorVersion: info.CreatorVersion, 256 } 257 258 c.IndentedJSON(http.StatusOK, changefeedDetail) 259 } 260 261 // CreateChangefeed creates a changefeed 262 // @Summary Create changefeed 263 // @Description create a new changefeed 264 // @Tags changefeed 265 // @Accept json 266 // @Produce json 267 // @Param changefeed body model.ChangefeedConfig true "changefeed config" 268 // @Success 202 269 // @Failure 500,400 {object} model.HTTPError 270 // @Router /api/v1/changefeeds [post] 271 func (h *OpenAPI) CreateChangefeed(c *gin.Context) { 272 // c does not have a cancel() func and its Done() method always return nil, 273 // so we should not use c as a context. 274 // Ref:https://github.com/gin-gonic/gin/blob/92eeaa4ebbadec2376e2ca5f5749888da1a42e24/context.go#L1157 275 ctx := c.Request.Context() 276 var changefeedConfig model.ChangefeedConfig 277 if err := c.BindJSON(&changefeedConfig); err != nil { 278 _ = c.Error(cerror.ErrAPIInvalidParam.Wrap(err)) 279 return 280 } 281 282 upManager, err := h.capture.GetUpstreamManager() 283 if err != nil { 284 _ = c.Error(err) 285 return 286 } 287 up, err := upManager.GetDefaultUpstream() 288 if err != nil { 289 _ = c.Error(err) 290 return 291 } 292 293 ctrl, err := h.capture.GetController() 294 if err != nil { 295 _ = c.Error(err) 296 return 297 } 298 info, err := verifyCreateChangefeedConfig(ctx, changefeedConfig, 299 up, ctrl, h.capture.GetEtcdClient()) 300 if err != nil { 301 _ = c.Error(err) 302 return 303 } 304 upstreamInfo := &model.UpstreamInfo{ 305 ID: up.ID, 306 PDEndpoints: strings.Join(up.PdEndpoints, ","), 307 KeyPath: up.SecurityConfig.KeyPath, 308 CertPath: up.SecurityConfig.CertPath, 309 CAPath: up.SecurityConfig.CAPath, 310 CertAllowedCN: up.SecurityConfig.CertAllowedCN, 311 } 312 err = ctrl.CreateChangefeed( 313 ctx, upstreamInfo, 314 info) 315 if err != nil { 316 _ = c.Error(err) 317 return 318 } 319 320 log.Info("Create changefeed successfully!", 321 zap.String("id", changefeedConfig.ID), 322 zap.String("changefeed", info.String())) 323 c.Status(http.StatusAccepted) 324 } 325 326 // PauseChangefeed pauses a changefeed 327 // @Summary Pause a changefeed 328 // @Description Pause a changefeed 329 // @Tags changefeed 330 // @Accept json 331 // @Produce json 332 // @Param changefeed_id path string true "changefeed_id" 333 // @Success 202 334 // @Failure 500,400 {object} model.HTTPError 335 // @Router /api/v1/changefeeds/{changefeed_id}/pause [post] 336 func (h *OpenAPI) PauseChangefeed(c *gin.Context) { 337 ctx := c.Request.Context() 338 339 changefeedID := model.DefaultChangeFeedID(c.Param(api.APIOpVarChangefeedID)) 340 if err := model.ValidateChangefeedID(changefeedID.ID); err != nil { 341 _ = c.Error(cerror.ErrAPIInvalidParam.GenWithStack("invalid changefeed_id: %s", 342 changefeedID.ID)) 343 return 344 } 345 // check if the changefeed exists 346 _, err := h.statusProvider().GetChangeFeedStatus(ctx, changefeedID) 347 if err != nil { 348 _ = c.Error(err) 349 return 350 } 351 352 job := model.AdminJob{ 353 CfID: changefeedID, 354 Type: model.AdminStop, 355 } 356 357 if err := api.HandleOwnerJob(ctx, h.capture, job); err != nil { 358 _ = c.Error(err) 359 return 360 } 361 c.Status(http.StatusAccepted) 362 } 363 364 // ResumeChangefeed resumes a changefeed 365 // @Summary Resume a changefeed 366 // @Description Resume a changefeed 367 // @Tags changefeed 368 // @Accept json 369 // @Produce json 370 // @Param changefeed_id path string true "changefeed_id" 371 // @Success 202 372 // @Failure 500,400 {object} model.HTTPError 373 // @Router /api/v1/changefeeds/{changefeed_id}/resume [post] 374 func (h *OpenAPI) ResumeChangefeed(c *gin.Context) { 375 ctx := c.Request.Context() 376 changefeedID := model.DefaultChangeFeedID(c.Param(api.APIOpVarChangefeedID)) 377 if err := model.ValidateChangefeedID(changefeedID.ID); err != nil { 378 _ = c.Error(cerror.ErrAPIInvalidParam.GenWithStack("invalid changefeed_id: %s", 379 changefeedID.ID)) 380 return 381 } 382 // check if the changefeed exists 383 _, err := h.capture.StatusProvider().GetChangeFeedStatus(ctx, changefeedID) 384 if err != nil { 385 _ = c.Error(err) 386 return 387 } 388 389 job := model.AdminJob{ 390 CfID: changefeedID, 391 Type: model.AdminResume, 392 } 393 394 if err := api.HandleOwnerJob(ctx, h.capture, job); err != nil { 395 _ = c.Error(err) 396 return 397 } 398 c.Status(http.StatusAccepted) 399 } 400 401 // UpdateChangefeed updates a changefeed 402 // @Summary Update a changefeed 403 // @Description Update a changefeed 404 // @Tags changefeed 405 // @Accept json 406 // @Produce json 407 // @Param changefeed_id path string true "changefeed_id" 408 // @Param changefeedConfig body model.ChangefeedConfig true "changefeed config" 409 // @Success 202 410 // @Failure 500,400 {object} model.HTTPError 411 // @Router /api/v1/changefeeds/{changefeed_id} [put] 412 func (h *OpenAPI) UpdateChangefeed(c *gin.Context) { 413 ctx := c.Request.Context() 414 changefeedID := model.DefaultChangeFeedID(c.Param(api.APIOpVarChangefeedID)) 415 416 if err := model.ValidateChangefeedID(changefeedID.ID); err != nil { 417 _ = c.Error(cerror.ErrAPIInvalidParam.GenWithStack("invalid changefeed_id: %s", 418 changefeedID.ID)) 419 return 420 } 421 422 owner, err := h.capture.GetOwner() 423 if err != nil { 424 _ = c.Error(err) 425 return 426 } 427 428 info, err := h.statusProvider().GetChangeFeedInfo(ctx, changefeedID) 429 if err != nil { 430 _ = c.Error(err) 431 return 432 } 433 434 switch info.State { 435 case model.StateFailed, model.StateStopped: 436 default: 437 _ = c.Error( 438 cerror.ErrChangefeedUpdateRefused.GenWithStackByArgs( 439 "can only update changefeed config when it is stopped or failed", 440 ), 441 ) 442 return 443 } 444 445 info.ID = changefeedID.ID 446 info.Namespace = changefeedID.Namespace 447 448 // can only update target-ts, sink-uri 449 // filter_rules, ignore_txn_start_ts, mounter_worker_num, sink_config 450 var changefeedConfig model.ChangefeedConfig 451 if err = c.BindJSON(&changefeedConfig); err != nil { 452 _ = c.Error(err) 453 return 454 } 455 456 newInfo, err := VerifyUpdateChangefeedConfig(ctx, changefeedConfig, info) 457 if err != nil { 458 _ = c.Error(err) 459 return 460 } 461 462 err = owner.UpdateChangefeed(ctx, newInfo) 463 if err != nil { 464 _ = c.Error(err) 465 return 466 } 467 468 c.Status(http.StatusAccepted) 469 } 470 471 // RemoveChangefeed removes a changefeed 472 // @Summary Remove a changefeed 473 // @Description Remove a changefeed 474 // @Tags changefeed 475 // @Accept json 476 // @Produce json 477 // @Param changefeed_id path string true "changefeed_id" 478 // @Success 202 479 // @Failure 500,400 {object} model.HTTPError 480 // @Router /api/v1/changefeeds/{changefeed_id} [delete] 481 func (h *OpenAPI) RemoveChangefeed(c *gin.Context) { 482 ctx := c.Request.Context() 483 changefeedID := model.DefaultChangeFeedID(c.Param(api.APIOpVarChangefeedID)) 484 if err := model.ValidateChangefeedID(changefeedID.ID); err != nil { 485 _ = c.Error(cerror.ErrAPIInvalidParam.GenWithStack("invalid changefeed_id: %s", 486 changefeedID.ID)) 487 return 488 } 489 // check if the changefeed exists 490 ctrl, err := h.capture.GetController() 491 if err != nil { 492 _ = c.Error(err) 493 return 494 } 495 exist, err := ctrl.IsChangefeedExists(ctx, changefeedID) 496 if err != nil { 497 _ = c.Error(err) 498 return 499 } 500 if !exist { 501 _ = c.Error(cerror.ErrChangeFeedNotExists.GenWithStackByArgs(changefeedID)) 502 return 503 } 504 505 job := model.AdminJob{ 506 CfID: changefeedID, 507 Type: model.AdminRemove, 508 } 509 510 if err := api.HandleOwnerJob(ctx, h.capture, job); err != nil { 511 _ = c.Error(err) 512 return 513 } 514 515 // Owner needs at least tow ticks to remove a changefeed, 516 // we need to wait for it. 517 err = retry.Do(ctx, func() error { 518 exist, err = ctrl.IsChangefeedExists(ctx, changefeedID) 519 if err != nil { 520 if strings.Contains(err.Error(), "ErrChangeFeedNotExists") { 521 return nil 522 } 523 return err 524 } 525 if !exist { 526 return nil 527 } 528 return cerror.ErrChangeFeedDeletionUnfinished.GenWithStackByArgs(changefeedID) 529 }, 530 retry.WithMaxTries(100), // max retry duration is 1 minute 531 retry.WithBackoffBaseDelay(600), // default owner tick interval is 200ms 532 retry.WithIsRetryableErr(cerror.IsRetryableError)) 533 534 if err != nil { 535 _ = c.Error(err) 536 return 537 } 538 539 c.Status(http.StatusAccepted) 540 } 541 542 // RebalanceTables rebalances tables 543 // @Summary rebalance tables 544 // @Description rebalance all tables of a changefeed 545 // @Tags changefeed 546 // @Accept json 547 // @Produce json 548 // @Param changefeed_id path string true "changefeed_id" 549 // @Success 202 550 // @Failure 500,400 {object} model.HTTPError 551 // @Router /api/v1/changefeeds/{changefeed_id}/tables/rebalance_table [post] 552 func (h *OpenAPI) RebalanceTables(c *gin.Context) { 553 ctx := c.Request.Context() 554 changefeedID := model.DefaultChangeFeedID(c.Param(api.APIOpVarChangefeedID)) 555 556 if err := model.ValidateChangefeedID(changefeedID.ID); err != nil { 557 _ = c.Error(cerror.ErrAPIInvalidParam.GenWithStack("invalid changefeed_id: %s", 558 changefeedID.ID)) 559 return 560 } 561 // check if the changefeed exists 562 _, err := h.statusProvider().GetChangeFeedStatus(ctx, changefeedID) 563 if err != nil { 564 _ = c.Error(err) 565 return 566 } 567 568 if err := api.HandleOwnerBalance(ctx, h.capture, changefeedID); err != nil { 569 _ = c.Error(err) 570 return 571 } 572 c.Status(http.StatusAccepted) 573 } 574 575 // MoveTable moves a table to target capture 576 // @Summary move table 577 // @Description move one table to the target capture 578 // @Tags changefeed 579 // @Accept json 580 // @Produce json 581 // @Param changefeed_id path string true "changefeed_id" 582 // @Param MoveTable body model.MoveTableReq true "move table request" 583 // @Success 202 584 // @Failure 500,400 {object} model.HTTPError 585 // @Router /api/v1/changefeeds/{changefeed_id}/tables/move_table [post] 586 func (h *OpenAPI) MoveTable(c *gin.Context) { 587 ctx := c.Request.Context() 588 changefeedID := model.DefaultChangeFeedID(c.Param(api.APIOpVarChangefeedID)) 589 if err := model.ValidateChangefeedID(changefeedID.ID); err != nil { 590 _ = c.Error(cerror.ErrAPIInvalidParam.GenWithStack("invalid changefeed_id: %s", 591 changefeedID.ID)) 592 return 593 } 594 // check if the changefeed exists 595 _, err := h.statusProvider().GetChangeFeedStatus(ctx, changefeedID) 596 if err != nil { 597 _ = c.Error(err) 598 return 599 } 600 601 data := model.MoveTableReq{} 602 err = c.BindJSON(&data) 603 if err != nil { 604 _ = c.Error(cerror.ErrAPIInvalidParam.Wrap(err)) 605 return 606 } 607 608 if err := model.ValidateChangefeedID(data.CaptureID); err != nil { 609 _ = c.Error(cerror.ErrAPIInvalidParam.GenWithStack("invalid capture_id: %s", data.CaptureID)) 610 return 611 } 612 613 err = api.HandleOwnerScheduleTable( 614 ctx, h.capture, changefeedID, data.CaptureID, data.TableID) 615 if err != nil { 616 _ = c.Error(err) 617 return 618 } 619 c.Status(http.StatusAccepted) 620 } 621 622 // ResignController makes the current controller resign 623 // @Summary notify the ticdc cluster controller to resign 624 // @Description notify the current controller to resign 625 // @Tags owner 626 // @Accept json 627 // @Produce json 628 // @Success 202 629 // @Failure 500,400 {object} model.HTTPError 630 // @Router /api/v1/owner/resign [post] 631 func (h *OpenAPI) ResignController(c *gin.Context) { 632 o, _ := h.capture.GetController() 633 if o != nil { 634 o.AsyncStop() 635 } 636 637 c.Status(http.StatusAccepted) 638 } 639 640 // GetProcessor gets the detailed info of a processor 641 // @Summary Get processor detail information 642 // @Description get the detail information of a processor 643 // @Tags processor 644 // @Accept json 645 // @Produce json 646 // @Param changefeed_id path string true "changefeed ID" 647 // @Param capture_id path string true "capture ID" 648 // @Success 200 {object} model.ProcessorDetail 649 // @Failure 500,400 {object} model.HTTPError 650 // @Router /api/v1/processors/{changefeed_id}/{capture_id} [get] 651 func (h *OpenAPI) GetProcessor(c *gin.Context) { 652 ctx := c.Request.Context() 653 654 changefeedID := model.DefaultChangeFeedID(c.Param(api.APIOpVarChangefeedID)) 655 if err := model.ValidateChangefeedID(changefeedID.ID); err != nil { 656 _ = c.Error(cerror.ErrAPIInvalidParam.GenWithStack("invalid changefeed_id: %s", 657 changefeedID.ID)) 658 return 659 } 660 661 captureID := c.Param(api.APIOpVarCaptureID) 662 if err := model.ValidateChangefeedID(captureID); err != nil { 663 _ = c.Error(cerror.ErrAPIInvalidParam.GenWithStack("invalid capture_id: %s", captureID)) 664 return 665 } 666 667 info, err := h.capture.StatusProvider().GetChangeFeedInfo(ctx, changefeedID) 668 if err != nil { 669 _ = c.Error(err) 670 return 671 } 672 if info.State != model.StateNormal { 673 _ = c.Error(cerror.WrapError(cerror.ErrAPIInvalidParam, 674 fmt.Errorf("changefeed in abnormal state: %s, "+ 675 "can't get processors of an abnormal changefeed", 676 string(info.State)))) 677 } 678 // check if this captureID exist 679 procInfos, err := h.capture.StatusProvider().GetProcessors(ctx) 680 if err != nil { 681 _ = c.Error(err) 682 return 683 } 684 var found bool 685 for _, info := range procInfos { 686 if info.CaptureID == captureID { 687 found = true 688 break 689 } 690 } 691 if !found { 692 _ = c.Error(cerror.ErrCaptureNotExist.GenWithStackByArgs(captureID)) 693 return 694 } 695 696 statuses, err := h.capture.StatusProvider().GetAllTaskStatuses(ctx, changefeedID) 697 if err != nil { 698 _ = c.Error(err) 699 return 700 } 701 status, captureExist := statuses[captureID] 702 703 // Note: for the case that no tables are attached to a newly created changefeed, 704 // we just do not report an error. 705 var processorDetail model.ProcessorDetail 706 if captureExist { 707 tables := make([]int64, 0) 708 for tableID := range status.Tables { 709 tables = append(tables, tableID) 710 } 711 processorDetail.Tables = tables 712 } 713 c.IndentedJSON(http.StatusOK, &processorDetail) 714 } 715 716 // ListProcessor lists all processors in the TiCDC cluster 717 // @Summary List processors 718 // @Description list all processors in the TiCDC cluster 719 // @Tags processor 720 // @Accept json 721 // @Produce json 722 // @Success 200 {array} model.ProcessorCommonInfo 723 // @Failure 500,400 {object} model.HTTPError 724 // @Router /api/v1/processors [get] 725 func (h *OpenAPI) ListProcessor(c *gin.Context) { 726 ctx := c.Request.Context() 727 controller, err := h.capture.GetController() 728 if err != nil { 729 _ = c.Error(err) 730 return 731 } 732 infos, err := controller.GetProcessors(ctx) 733 if err != nil { 734 _ = c.Error(err) 735 return 736 } 737 resps := make([]*model.ProcessorCommonInfo, len(infos)) 738 for i, info := range infos { 739 resp := &model.ProcessorCommonInfo{ 740 Namespace: info.CfID.Namespace, 741 CfID: info.CfID.ID, 742 CaptureID: info.CaptureID, 743 } 744 resps[i] = resp 745 } 746 c.IndentedJSON(http.StatusOK, resps) 747 } 748 749 // ListCapture lists all captures 750 // @Summary List captures 751 // @Description list all captures in cdc cluster 752 // @Tags capture 753 // @Accept json 754 // @Produce json 755 // @Success 200 {array} model.Capture 756 // @Failure 500,400 {object} model.HTTPError 757 // @Router /api/v1/captures [get] 758 func (h *OpenAPI) ListCapture(c *gin.Context) { 759 ctx := c.Request.Context() 760 controller, err := h.capture.GetController() 761 if err != nil { 762 _ = c.Error(err) 763 return 764 } 765 captureInfos, err := controller.GetCaptures(ctx) 766 if err != nil { 767 _ = c.Error(err) 768 return 769 } 770 info, err := h.capture.Info() 771 if err != nil { 772 _ = c.Error(err) 773 return 774 } 775 ownerID := info.ID 776 777 captures := make([]*model.Capture, 0, len(captureInfos)) 778 for _, c := range captureInfos { 779 isOwner := c.ID == ownerID 780 captures = append(captures, 781 &model.Capture{ 782 ID: c.ID, 783 IsOwner: isOwner, 784 AdvertiseAddr: c.AdvertiseAddr, 785 ClusterID: h.capture.GetEtcdClient().GetClusterID(), 786 }) 787 } 788 789 c.IndentedJSON(http.StatusOK, captures) 790 } 791 792 // DrainCapture remove all tables at the given capture. 793 // @Summary Drain captures 794 // @Description Drain all tables at the target captures in cdc cluster 795 // @Tags capture 796 // @Accept json 797 // @Produce json 798 // @Success 200,202 799 // @Failure 503,500,400 {object} model.HTTPError 800 // @Router /api/v1/captures/drain [put] 801 func (h *OpenAPI) DrainCapture(c *gin.Context) { 802 var req model.DrainCaptureRequest 803 if err := c.ShouldBindJSON(&req); err != nil { 804 _ = c.Error(cerror.ErrAPIInvalidParam.Wrap(err)) 805 return 806 } 807 808 ctx := c.Request.Context() 809 captures, err := h.statusProvider().GetCaptures(ctx) 810 if err != nil { 811 _ = c.Error(err) 812 return 813 } 814 815 // drain capture only work if there is at least two alive captures, 816 // it cannot work properly if it has only one capture. 817 if len(captures) <= 1 { 818 _ = c.Error(cerror.ErrSchedulerRequestFailed. 819 GenWithStackByArgs("only one capture alive")) 820 return 821 } 822 823 target := req.CaptureID 824 checkCaptureFound := func() bool { 825 // make sure the target capture exist 826 for _, capture := range captures { 827 if capture.ID == target { 828 return true 829 } 830 } 831 return false 832 } 833 834 if !checkCaptureFound() { 835 _ = c.Error(cerror.ErrCaptureNotExist.GenWithStackByArgs(target)) 836 return 837 } 838 839 // only owner handle api request, so this must be the owner. 840 ownerInfo, err := h.capture.Info() 841 if err != nil { 842 _ = c.Error(err) 843 return 844 } 845 846 if ownerInfo.ID == target { 847 _ = c.Error(cerror.ErrSchedulerRequestFailed. 848 GenWithStackByArgs("cannot drain the owner")) 849 return 850 } 851 852 resp, err := api.HandleOwnerDrainCapture(ctx, h.capture, target) 853 if err != nil { 854 _ = c.AbortWithError(http.StatusServiceUnavailable, err) 855 return 856 } 857 858 c.JSON(http.StatusAccepted, resp) 859 } 860 861 // ServerStatus gets the status of server(capture) 862 // @Summary Get server status 863 // @Description get the status of a server(capture) 864 // @Tags common 865 // @Accept json 866 // @Produce json 867 // @Success 200 {object} model.ServerStatus 868 // @Failure 500,400 {object} model.HTTPError 869 // @Router /api/v1/status [get] 870 func (h *OpenAPI) ServerStatus(c *gin.Context) { 871 info, err := h.capture.Info() 872 if err != nil { 873 _ = c.Error(err) 874 return 875 } 876 status := model.ServerStatus{ 877 Version: version.ReleaseVersion, 878 GitHash: version.GitHash, 879 Pid: os.Getpid(), 880 ID: info.ID, 881 ClusterID: h.capture.GetEtcdClient().GetClusterID(), 882 IsOwner: h.capture.IsController(), 883 Liveness: h.capture.Liveness(), 884 } 885 c.IndentedJSON(http.StatusOK, status) 886 } 887 888 // Health check if cdc cluster is health 889 // @Summary Check if CDC cluster is health 890 // @Description check if CDC cluster is health 891 // @Tags common 892 // @Accept json 893 // @Produce json 894 // @Success 200 895 // @Failure 500 {object} model.HTTPError 896 // @Router /api/v1/health [get] 897 func (h *OpenAPI) Health(c *gin.Context) { 898 if !h.capture.IsController() { 899 middleware.ForwardToControllerMiddleware(h.capture)(c) 900 return 901 } 902 903 ctx := c.Request.Context() 904 health, err := h.statusProvider().IsHealthy(ctx) 905 if err != nil { 906 c.IndentedJSON(http.StatusInternalServerError, model.NewHTTPError(err)) 907 return 908 } 909 if !health { 910 err = cerror.ErrClusterIsUnhealthy.FastGenByArgs() 911 c.IndentedJSON(http.StatusInternalServerError, model.NewHTTPError(err)) 912 return 913 } 914 c.Status(http.StatusOK) 915 } 916 917 // SetLogLevel changes TiCDC log level dynamically. 918 // @Summary Change TiCDC log level 919 // @Description change TiCDC log level dynamically 920 // @Tags common 921 // @Accept json 922 // @Produce json 923 // @Param log_level body string true "log level" 924 // @Success 200 925 // @Failure 400 {object} model.HTTPError 926 // @Router /api/v1/log [post] 927 func SetLogLevel(c *gin.Context) { 928 // get json data from request body 929 data := struct { 930 Level string `json:"log_level"` 931 }{} 932 err := c.BindJSON(&data) 933 if err != nil { 934 _ = c.Error(cerror.ErrAPIInvalidParam.GenWithStack("invalid log level: %s", err.Error())) 935 return 936 } 937 938 err = logutil.SetLogLevel(data.Level) 939 if err != nil { 940 _ = c.Error(cerror.ErrAPIInvalidParam.GenWithStack("fail to change log level: %s", data.Level)) 941 return 942 } 943 log.Warn("log level changed", zap.String("level", data.Level)) 944 c.Status(http.StatusOK) 945 } 946 947 // getChangefeedFromRequest returns the changefeed that parse from request 948 func getChangefeedFromRequest(ctx *gin.Context) model.ChangeFeedID { 949 return model.ChangeFeedID{ 950 Namespace: model.DefaultNamespace, 951 ID: ctx.Param(api.APIOpVarChangefeedID), 952 } 953 }