github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/master/openapi_controller_test.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 // MVC for dm-master's openapi server 15 // Model(data in etcd): source of truth 16 // View(openapi_view): do some inner work such as validate, filter, prepare parameters/response and call controller to update model. 17 // Controller(openapi_controller): call model func to update data. 18 19 package master 20 21 import ( 22 "context" 23 "fmt" 24 "testing" 25 26 "github.com/golang/mock/gomock" 27 "github.com/pingcap/failpoint" 28 "github.com/pingcap/tiflow/dm/checker" 29 "github.com/pingcap/tiflow/dm/config" 30 "github.com/pingcap/tiflow/dm/master/scheduler" 31 "github.com/pingcap/tiflow/dm/openapi" 32 "github.com/pingcap/tiflow/dm/openapi/fixtures" 33 "github.com/pingcap/tiflow/dm/pb" 34 "github.com/pingcap/tiflow/dm/pbmock" 35 "github.com/pingcap/tiflow/dm/pkg/ha" 36 "github.com/pingcap/tiflow/dm/pkg/log" 37 "github.com/pingcap/tiflow/dm/pkg/terror" 38 "github.com/stretchr/testify/suite" 39 ) 40 41 type OpenAPIControllerSuite struct { 42 suite.Suite 43 44 testSource *openapi.Source 45 testTask *openapi.Task 46 } 47 48 func (s *OpenAPIControllerSuite) SetupSuite() { 49 s.NoError(log.InitLogger(&log.Config{})) 50 51 dbCfg := config.GetDBConfigForTest() 52 s.testSource = &openapi.Source{ 53 SourceName: source1Name, 54 Enable: true, 55 EnableGtid: false, 56 Host: dbCfg.Host, 57 Password: &dbCfg.Password, 58 Port: dbCfg.Port, 59 User: dbCfg.User, 60 } 61 62 task, err := fixtures.GenNoShardOpenAPITaskForTest() 63 s.Nil(err) 64 s.testTask = &task 65 66 checker.CheckSyncConfigFunc = mockCheckSyncConfig 67 CheckAndAdjustSourceConfigFunc = checkAndNoAdjustSourceConfigMock 68 s.Nil(failpoint.Enable("github.com/pingcap/tiflow/dm/master/MockSkipAdjustTargetDB", `return(true)`)) 69 s.Nil(failpoint.Enable("github.com/pingcap/tiflow/dm/master/MockSkipRemoveMetaData", `return(true)`)) 70 } 71 72 func (s *OpenAPIControllerSuite) TearDownSuite() { 73 CheckAndAdjustSourceConfigFunc = checkAndAdjustSourceConfig 74 checker.CheckSyncConfigFunc = checker.CheckSyncConfig 75 s.Nil(failpoint.Disable("github.com/pingcap/tiflow/dm/master/MockSkipAdjustTargetDB")) 76 s.Nil(failpoint.Disable("github.com/pingcap/tiflow/dm/master/MockSkipRemoveMetaData")) 77 } 78 79 func (s *OpenAPIControllerSuite) TestSourceController() { 80 ctx, cancel := context.WithCancel(context.Background()) 81 server := setupTestServer(ctx, s.T()) 82 defer func() { 83 cancel() 84 server.Close() 85 }() 86 87 // reset omitted value after get 88 resetValue := func(source *openapi.Source) { 89 source.Purge = s.testSource.Purge 90 source.Password = s.testSource.Password 91 source.RelayConfig = s.testSource.RelayConfig 92 } 93 94 // create 95 { 96 createReq := openapi.CreateSourceRequest{ 97 Source: *s.testSource, 98 } 99 source, err := server.createSource(ctx, createReq) 100 s.NoError(err) 101 s.EqualValues(s.testSource, source) 102 } 103 104 // update 105 { 106 source := *s.testSource 107 source.EnableGtid = !source.EnableGtid 108 updateReq := openapi.UpdateSourceRequest{Source: source} 109 sourceAfterUpdated, err := server.updateSource(ctx, source.SourceName, updateReq) 110 s.NoError(err) 111 s.Equal(source.EnableGtid, sourceAfterUpdated.EnableGtid) 112 113 // update back to continue next text 114 updateReq.Source = *s.testSource 115 sourceAfterUpdated, err = server.updateSource(ctx, source.SourceName, updateReq) 116 s.NoError(err) 117 s.EqualValues(s.testSource, sourceAfterUpdated) 118 } 119 120 // get and with status 121 { 122 withStatus := false 123 params := openapi.DMAPIGetSourceParams{WithStatus: &withStatus} 124 sourceAfterGet, err := server.getSource(ctx, s.testSource.SourceName, params) 125 s.NoError(err) 126 127 resetValue(sourceAfterGet) 128 s.EqualValues(s.testSource, sourceAfterGet) 129 s.Nil(sourceAfterGet.StatusList) 130 131 // with status 132 withStatus = true 133 params.WithStatus = &withStatus 134 sourceAfterGet, err = server.getSource(ctx, s.testSource.SourceName, params) 135 s.NoError(err) 136 s.NotNil(sourceAfterGet.StatusList) 137 statusList := *sourceAfterGet.StatusList 138 s.Len(statusList, 1) 139 s.NotNil(statusList[0].ErrorMsg) // no worker, will get an error msg 140 s.Contains(*statusList[0].ErrorMsg, "code=38029") 141 } 142 143 // get status 144 { 145 statusList, err := server.getSourceStatus(ctx, s.testSource.SourceName) 146 s.NoError(err) 147 s.Len(statusList, 1) 148 s.NotNil(statusList[0].ErrorMsg) // no worker, will get an error msg 149 s.Contains(*statusList[0].ErrorMsg, "code=38029") 150 } 151 152 // list and with status 153 { 154 withStatus := false 155 params := openapi.DMAPIGetSourceListParams{WithStatus: &withStatus} 156 sourceList, err := server.listSource(ctx, params) 157 s.NoError(err) 158 s.Len(sourceList, 1) 159 resetValue(&sourceList[0]) 160 s.EqualValues(s.testSource, &sourceList[0]) 161 162 withStatus = true 163 sourceList, err = server.listSource(ctx, params) 164 s.NoError(err) 165 s.Len(sourceList, 1) 166 s.NotNil(sourceList[0].StatusList) 167 statusList := *sourceList[0].StatusList 168 s.Contains(*statusList[0].ErrorMsg, "code=38029") // no worker, will get an error msg 169 } 170 171 // test with fake worker enable/disable/transfer/enable/disable/purge-relay 172 { 173 // enable on a free worker 174 s.True(terror.ErrWorkerNoStart.Equal(server.enableSource(ctx, s.testSource.SourceName))) // no free worker now 175 176 worker1Name := "worker1" 177 worker1Addr := "172.16.10.72:8262" 178 s.Nil(server.scheduler.AddWorker(worker1Name, worker1Addr)) 179 worker1 := server.scheduler.GetWorkerByName(worker1Name) 180 worker1.ToFree() 181 s.NoError(server.transferSource(ctx, s.testSource.SourceName, worker1Name)) 182 s.Equal(worker1.Stage(), scheduler.WorkerBound) 183 184 // disable no task now, so no error 185 s.NoError(server.disableSource(ctx, s.testSource.SourceName)) 186 187 // add worker 2 and transfer 188 worker2Name := "worker2" 189 worker2Addr := "172.16.10.72:8263" 190 s.Nil(server.scheduler.AddWorker(worker2Name, worker2Addr)) 191 worker2 := server.scheduler.GetWorkerByName(worker2Name) 192 worker2.ToFree() 193 s.NoError(server.transferSource(ctx, s.testSource.SourceName, worker2Name)) 194 s.Equal(worker1.Stage(), scheduler.WorkerFree) 195 s.Equal(worker2.Stage(), scheduler.WorkerBound) 196 197 // enable relay with binlog name will trigger a update source 198 relayBinLogName := "mysql-bin.000001" 199 enableRelayReq := openapi.EnableRelayRequest{RelayBinlogName: &relayBinLogName} 200 s.NoError(server.enableRelay(ctx, s.testSource.SourceName, enableRelayReq)) 201 202 updatedSource, err := server.getSource(ctx, s.testSource.SourceName, openapi.DMAPIGetSourceParams{}) 203 s.NoError(err) 204 s.Equal(*updatedSource.RelayConfig.RelayBinlogName, relayBinLogName) 205 206 // disable relay 207 disableRelayReq := openapi.DisableRelayRequest{} 208 err = server.disableRelay(ctx, s.testSource.SourceName, disableRelayReq) 209 s.NoError(err) 210 updatedSource, err = server.getSource(ctx, s.testSource.SourceName, openapi.DMAPIGetSourceParams{}) 211 s.NoError(err) 212 s.False(*updatedSource.RelayConfig.EnableRelay) 213 214 // purge 215 purgeRelayReq := openapi.PurgeRelayRequest{} 216 err = server.purgeRelay(ctx, s.testSource.SourceName, purgeRelayReq) 217 s.Error(err) 218 s.Regexp(".*please `enable-relay` first", err.Error()) 219 } 220 221 // delete 222 { 223 s.Nil(server.deleteSource(ctx, s.testSource.SourceName, false)) 224 sourceList, err := server.listSource(ctx, openapi.DMAPIGetSourceListParams{}) 225 s.Nil(err) 226 s.Len(sourceList, 0) 227 } 228 229 // create and update no password source 230 { 231 // no password will use "" as password 232 source := *s.testSource 233 source.Password = nil 234 createReq := openapi.CreateSourceRequest{Source: source} 235 resp, err := server.createSource(ctx, createReq) 236 s.NoError(err) 237 s.EqualValues(source, *resp) 238 config := server.scheduler.GetSourceCfgByID(source.SourceName) 239 s.NotNil(config) 240 s.Equal("", config.From.Password) 241 242 // update to have password 243 updateReq := openapi.UpdateSourceRequest{Source: *s.testSource} 244 sourceAfterUpdated, err := server.updateSource(ctx, source.SourceName, updateReq) 245 s.NoError(err) 246 s.EqualValues(s.testSource, sourceAfterUpdated) 247 248 // update without password will use old password 249 source = *s.testSource 250 source.Password = nil 251 updateReq = openapi.UpdateSourceRequest{Source: source} 252 sourceAfterUpdated, err = server.updateSource(ctx, source.SourceName, updateReq) 253 s.NoError(err) 254 s.Equal(source, *sourceAfterUpdated) 255 // password is old 256 config = server.scheduler.GetSourceCfgByID(source.SourceName) 257 s.NotNil(config) 258 s.Equal(*s.testSource.Password, config.From.Password) 259 260 s.Nil(server.deleteSource(ctx, s.testSource.SourceName, false)) 261 } 262 } 263 264 func (s *OpenAPIControllerSuite) TestTaskController() { 265 ctx, cancel := context.WithCancel(context.Background()) 266 server := setupTestServer(ctx, s.T()) 267 defer func() { 268 cancel() 269 server.Close() 270 }() 271 272 // create source for task 273 { 274 // add a mock worker 275 worker1Name := "worker1" 276 worker1Addr := "172.16.10.72:8262" 277 s.Nil(server.scheduler.AddWorker(worker1Name, worker1Addr)) 278 worker1 := server.scheduler.GetWorkerByName(worker1Name) 279 worker1.ToFree() 280 281 _, err := server.createSource(ctx, openapi.CreateSourceRequest{Source: *s.testSource, WorkerName: &worker1Name}) 282 s.Nil(err) 283 s.Equal(worker1.Stage(), scheduler.WorkerBound) 284 sourceList, err := server.listSource(ctx, openapi.DMAPIGetSourceListParams{}) 285 s.Nil(err) 286 s.Len(sourceList, 1) 287 } 288 289 // create 290 { 291 createTaskReq := openapi.CreateTaskRequest{Task: *s.testTask} 292 res, err := server.createTask(ctx, createTaskReq) 293 s.Nil(err) 294 s.EqualValues(*s.testTask, res.Task) 295 } 296 297 // update 298 { 299 task := *s.testTask 300 batch := 1000 301 task.SourceConfig.IncrMigrateConf.ReplBatch = &batch 302 updateReq := openapi.UpdateTaskRequest{Task: task} 303 s.NoError(failpoint.Enable("github.com/pingcap/tiflow/dm/master/scheduler/operateCheckSubtasksCanUpdate", `return("success")`)) 304 res, err := server.updateTask(ctx, updateReq) 305 s.NoError(err) 306 s.EqualValues(task.SourceConfig.IncrMigrateConf, res.Task.SourceConfig.IncrMigrateConf) 307 308 // update back to continue next text 309 updateReq.Task = *s.testTask 310 res, err = server.updateTask(ctx, updateReq) 311 s.NoError(err) 312 s.EqualValues(*s.testTask, res.Task) 313 s.NoError(failpoint.Disable("github.com/pingcap/tiflow/dm/master/scheduler/operateCheckSubtasksCanUpdate")) 314 } 315 316 // get and with status 317 { 318 withStatus := false 319 params := openapi.DMAPIGetTaskParams{WithStatus: &withStatus} 320 taskAfterGet, err := server.getTask(ctx, s.testTask.Name, params) 321 s.NoError(err) 322 s.EqualValues(s.testTask, taskAfterGet) 323 s.Nil(taskAfterGet.StatusList) 324 325 // with status 326 withStatus = true 327 params.WithStatus = &withStatus 328 taskAfterGet, err = server.getTask(ctx, s.testTask.Name, params) 329 s.NoError(err) 330 s.NotNil(taskAfterGet.StatusList) 331 statusList := *taskAfterGet.StatusList 332 s.Len(statusList, 1) 333 s.NotNil(statusList[0].ErrorMsg) // no true worker, will get an error msg 334 s.Contains(*statusList[0].ErrorMsg, "code=38008") 335 } 336 337 // get status 338 { 339 params := openapi.DMAPIGetTaskStatusParams{} 340 statusList, err := server.getTaskStatus(ctx, s.testTask.Name, params) 341 s.NoError(err) 342 s.Len(statusList, 1) 343 s.NotNil(statusList[0].ErrorMsg) // no worker, will get an error msg 344 s.Contains(*statusList[0].ErrorMsg, "code=38008") 345 } 346 347 // list and with status 348 { 349 withStatus := false 350 params := openapi.DMAPIGetTaskListParams{WithStatus: &withStatus} 351 taskList, err := server.listTask(ctx, params) 352 s.NoError(err) 353 s.Len(taskList, 1) 354 s.EqualValues(s.testTask, &taskList[0]) 355 356 withStatus = true 357 taskList, err = server.listTask(ctx, params) 358 s.NoError(err) 359 s.Len(taskList, 1) 360 s.NotNil(taskList[0].StatusList) 361 statusList := *taskList[0].StatusList 362 s.Contains(*statusList[0].ErrorMsg, "code=38008") 363 } 364 365 // start and stop 366 { 367 // can't start when source not enabled 368 req := openapi.StartTaskRequest{} 369 s.Nil(server.disableSource(ctx, s.testSource.SourceName)) 370 s.True(terror.ErrMasterStartTask.Equal(server.startTask(ctx, s.testTask.Name, req))) 371 // start success 372 s.Nil(server.enableSource(ctx, s.testSource.SourceName)) 373 s.Nil(server.startTask(ctx, s.testTask.Name, req)) 374 s.Equal(server.scheduler.GetExpectSubTaskStage(s.testTask.Name, s.testSource.SourceName).Expect, pb.Stage_Running) 375 376 // stop success 377 s.Nil(server.stopTask(ctx, s.testTask.Name, openapi.StopTaskRequest{})) 378 s.Equal(server.scheduler.GetExpectSubTaskStage(s.testTask.Name, s.testSource.SourceName).Expect, pb.Stage_Stopped) 379 380 // start with cli args 381 startTime := "2022-05-05 12:12:12" 382 safeModeTimeDuration := "10s" 383 req = openapi.StartTaskRequest{ 384 StartTime: &startTime, 385 SafeModeTimeDuration: &safeModeTimeDuration, 386 } 387 s.Nil(server.startTask(ctx, s.testTask.Name, req)) 388 taskCliConf, err := ha.GetTaskCliArgs(server.etcdClient, s.testTask.Name, s.testSource.SourceName) 389 s.Nil(err) 390 s.NotNil(taskCliConf) 391 s.Equal(startTime, taskCliConf.StartTime) 392 s.Equal(safeModeTimeDuration, taskCliConf.SafeModeDuration) 393 394 // stop success 395 s.Nil(server.stopTask(ctx, s.testTask.Name, openapi.StopTaskRequest{})) 396 s.Equal(server.scheduler.GetExpectSubTaskStage(s.testTask.Name, s.testSource.SourceName).Expect, pb.Stage_Stopped) 397 } 398 399 // delete 400 { 401 s.Nil(server.deleteTask(ctx, s.testTask.Name, true)) // delete with fore 402 taskList, err := server.listTask(ctx, openapi.DMAPIGetTaskListParams{}) 403 s.Nil(err) 404 s.Len(taskList, 0) 405 } 406 407 // convert between task and taskConfig 408 { 409 // from task to taskConfig 410 req := openapi.ConverterTaskRequest{Task: s.testTask} 411 task, taskCfg, err := server.convertTaskConfig(ctx, req) 412 s.NoError(err) 413 s.NotNil(task) 414 s.NotNil(taskCfg) 415 s.EqualValues(s.testTask, task) 416 417 // from taskCfg to task 418 req.Task = nil 419 taskCfgStr := taskCfg.String() 420 req.TaskConfigFile = &taskCfgStr 421 task2, taskCfg2, err := server.convertTaskConfig(ctx, req) 422 s.NoError(err) 423 s.NotNil(task2) 424 s.NotNil(taskCfg2) 425 s.EqualValues(task2, task) 426 s.Equal(taskCfg2.String(), taskCfg.String()) 427 428 // incremental task without source meta 429 taskTest := *s.testTask 430 taskTest.TaskMode = config.ModeIncrement 431 req = openapi.ConverterTaskRequest{Task: &taskTest} 432 task3, taskCfg3, err := server.convertTaskConfig(ctx, req) 433 s.NoError(err) 434 s.NotNil(task3) 435 s.NotNil(taskCfg3) 436 s.EqualValues(&taskTest, task3) 437 438 req.Task = nil 439 taskCfgStr = taskCfg3.String() 440 req.TaskConfigFile = &taskCfgStr 441 task4, taskCfg4, err := server.convertTaskConfig(ctx, req) 442 s.NoError(err) 443 s.NotNil(task4) 444 s.NotNil(taskCfg4) 445 s.EqualValues(task4, task3) 446 s.Equal(taskCfg4.String(), taskCfg3.String()) 447 } 448 } 449 450 func (s *OpenAPIControllerSuite) TestTaskControllerWithInvalidTask() { 451 ctx, cancel := context.WithCancel(context.Background()) 452 server := setupTestServer(ctx, s.T()) 453 defer func() { 454 cancel() 455 server.Close() 456 }() 457 458 // create an invalid task 459 task, err := fixtures.GenNoShardErrNameOpenAPITaskForTest() 460 s.NoError(err) 461 462 // create source for task 463 { 464 // add a mock worker 465 worker1Name := "worker1" 466 worker1Addr := "172.16.10.72:8262" 467 s.NoError(server.scheduler.AddWorker(worker1Name, worker1Addr)) 468 worker1 := server.scheduler.GetWorkerByName(worker1Name) 469 worker1.ToFree() 470 471 // create the corresponding mock worker 472 ctrl := gomock.NewController(s.T()) 473 defer ctrl.Finish() 474 mockWorkerClient := pbmock.NewMockWorkerClient(ctrl) 475 queryResp := &pb.QueryStatusResponse{ 476 Result: true, 477 Msg: "", 478 SourceStatus: &pb.SourceStatus{ 479 Source: s.testSource.SourceName, 480 Worker: worker1Name, 481 }, 482 SubTaskStatus: [](*pb.SubTaskStatus){&pb.SubTaskStatus{ 483 Result: &pb.ProcessResult{ 484 Errors: [](*pb.ProcessError){&pb.ProcessError{ 485 ErrCode: 10006, 486 ErrClass: "database", 487 ErrScope: "downstream", 488 ErrLevel: "high", 489 Message: fmt.Sprintf("fail to initialize unit Load of %s : execute statement failed: CREATE TABLE IF NOT EXISTS `dm_meta`.`%s` (\n\t\ttask_name varchar(255) NOT NULL,\n\t\tsource_name varchar(255) NOT NULL,\n\t\tstatus varchar(10) NOT NULL DEFAULT 'init' COMMENT 'init,running,finished',\n\t\tPRIMARY KEY (task_name, source_name)\n\t);\n", task.Name, task.Name), 490 RawCause: fmt.Sprintf("Error 1059: Identifier name '%s' is too long.", task.Name), 491 }}, 492 }, 493 }}, 494 } 495 mockWorkerClient.EXPECT().QueryStatus( 496 gomock.Any(), 497 gomock.Any(), 498 ).Return(queryResp, nil).MaxTimes(maxRetryNum) 499 server.scheduler.SetWorkerClientForTest(worker1Name, newMockRPCClient(mockWorkerClient)) 500 501 _, err := server.createSource(ctx, openapi.CreateSourceRequest{Source: *s.testSource, WorkerName: &worker1Name}) 502 s.NoError(err) 503 s.Equal(worker1.Stage(), scheduler.WorkerBound) 504 sourceList, err := server.listSource(ctx, openapi.DMAPIGetSourceListParams{}) 505 s.NoError(err) 506 s.Len(sourceList, 1) 507 } 508 509 // create 510 { 511 createTaskReq := openapi.CreateTaskRequest{Task: task} 512 res, err := server.createTask(ctx, createTaskReq) 513 s.NoError(err) 514 s.EqualValues(task, res.Task) 515 } 516 517 // start and stop 518 { 519 // start success 520 req := openapi.StartTaskRequest{} 521 s.NoError(server.enableSource(ctx, s.testSource.SourceName)) 522 s.NoError(server.startTask(ctx, task.Name, req)) 523 s.Equal(server.scheduler.GetExpectSubTaskStage(task.Name, s.testSource.SourceName).Expect, pb.Stage_Running) 524 525 // get status 526 { 527 params := openapi.DMAPIGetTaskStatusParams{} 528 statusList, err := server.getTaskStatus(ctx, task.Name, params) 529 s.NoError(err) 530 s.Len(statusList, 1) 531 s.NotNil(statusList[0].ErrorMsg) 532 s.Contains(*statusList[0].ErrorMsg, "Error 1059: ") // database error, will return an error message 533 } 534 535 // stop success 536 s.NoError(server.stopTask(ctx, task.Name, openapi.StopTaskRequest{})) 537 s.Equal(server.scheduler.GetExpectSubTaskStage(task.Name, s.testSource.SourceName).Expect, pb.Stage_Stopped) 538 } 539 540 // delete 541 { 542 s.NoError(server.deleteTask(ctx, task.Name, true)) // delete with fore 543 taskList, err := server.listTask(ctx, openapi.DMAPIGetTaskListParams{}) 544 s.NoError(err) 545 s.Len(taskList, 0) 546 } 547 } 548 549 func TestOpenAPIControllerSuite(t *testing.T) { 550 suite.Run(t, new(OpenAPIControllerSuite)) 551 }