github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/master/openapi_view_test.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 // this file implement all of the APIs of the DataMigration service. 15 16 package master 17 18 import ( 19 "bytes" 20 "context" 21 "fmt" 22 "io" 23 "net/http" 24 "net/http/httptest" 25 "os" 26 "testing" 27 "time" 28 29 "github.com/DATA-DOG/go-sqlmock" 30 "github.com/deepmap/oapi-codegen/pkg/testutil" 31 "github.com/golang/mock/gomock" 32 "github.com/pingcap/failpoint" 33 "github.com/pingcap/tiflow/dm/checker" 34 "github.com/pingcap/tiflow/dm/config" 35 "github.com/pingcap/tiflow/dm/openapi" 36 "github.com/pingcap/tiflow/dm/openapi/fixtures" 37 "github.com/pingcap/tiflow/dm/pb" 38 "github.com/pingcap/tiflow/dm/pbmock" 39 "github.com/pingcap/tiflow/dm/pkg/conn" 40 "github.com/pingcap/tiflow/dm/pkg/ha" 41 "github.com/pingcap/tiflow/dm/pkg/log" 42 "github.com/pingcap/tiflow/dm/pkg/terror" 43 "github.com/pingcap/tiflow/dm/pkg/utils" 44 "github.com/stretchr/testify/require" 45 "github.com/stretchr/testify/suite" 46 "github.com/tikv/pd/pkg/utils/tempurl" 47 ) 48 49 // some data for test. 50 var ( 51 source1Name = "mysql-replica-01" 52 ) 53 54 func setupTestServer(ctx context.Context, t *testing.T) *Server { 55 t.Helper() 56 // create a new cluster 57 cfg1 := NewConfig() 58 require.NoError(t, cfg1.FromContent(SampleConfig)) 59 cfg1.Name = "dm-master-1" 60 cfg1.DataDir = t.TempDir() 61 cfg1.MasterAddr = tempurl.Alloc()[len("http://"):] 62 cfg1.PeerUrls = tempurl.Alloc() 63 cfg1.AdvertisePeerUrls = cfg1.PeerUrls 64 cfg1.AdvertiseAddr = cfg1.MasterAddr 65 cfg1.InitialCluster = fmt.Sprintf("%s=%s", cfg1.Name, cfg1.AdvertisePeerUrls) 66 cfg1.OpenAPI = true 67 68 s1 := NewServer(cfg1) 69 require.NoError(t, s1.Start(ctx)) 70 // wait the first one become the leader 71 require.True(t, utils.WaitSomething(30, 100*time.Millisecond, func() bool { 72 return s1.election.IsLeader() && s1.scheduler.Started() 73 })) 74 return s1 75 } 76 77 // nolint:unparam 78 func mockRelayQueryStatus( 79 mockWorkerClient *pbmock.MockWorkerClient, sourceName, workerName string, stage pb.Stage, 80 ) { 81 queryResp := &pb.QueryStatusResponse{ 82 Result: true, 83 SourceStatus: &pb.SourceStatus{ 84 Worker: workerName, 85 Source: sourceName, 86 }, 87 } 88 if stage == pb.Stage_Running { 89 queryResp.SourceStatus.RelayStatus = &pb.RelayStatus{Stage: stage} 90 } 91 if stage == pb.Stage_Paused { 92 queryResp.Result = false 93 queryResp.Msg = "some error happened" 94 } 95 mockWorkerClient.EXPECT().QueryStatus( 96 gomock.Any(), 97 &pb.QueryStatusRequest{Name: ""}, 98 ).Return(queryResp, nil).MaxTimes(maxRetryNum) 99 } 100 101 // nolint:unparam 102 func mockPurgeRelay(mockWorkerClient *pbmock.MockWorkerClient) { 103 resp := &pb.CommonWorkerResponse{Result: true} 104 mockWorkerClient.EXPECT().PurgeRelay(gomock.Any(), gomock.Any()).Return(resp, nil).MaxTimes(maxRetryNum) 105 } 106 107 func mockTaskQueryStatus( 108 mockWorkerClient *pbmock.MockWorkerClient, taskName, sourceName, workerName string, needError bool, 109 ) { 110 var queryResp *pb.QueryStatusResponse 111 if needError { 112 queryResp = &pb.QueryStatusResponse{ 113 Result: false, 114 Msg: "some error happened", 115 SourceStatus: &pb.SourceStatus{ 116 Worker: workerName, 117 Source: sourceName, 118 }, 119 } 120 } else { 121 queryResp = &pb.QueryStatusResponse{ 122 Result: true, 123 SourceStatus: &pb.SourceStatus{ 124 Worker: workerName, 125 Source: sourceName, 126 }, 127 SubTaskStatus: []*pb.SubTaskStatus{ 128 { 129 Stage: pb.Stage_Running, 130 Name: taskName, 131 Status: &pb.SubTaskStatus_Dump{ 132 Dump: &pb.DumpStatus{ 133 CompletedTables: 0.0, 134 EstimateTotalRows: 10.0, 135 FinishedBytes: 0.0, 136 FinishedRows: 5.0, 137 TotalTables: 1, 138 }, 139 }, 140 }, 141 }, 142 } 143 } 144 145 mockWorkerClient.EXPECT().QueryStatus( 146 gomock.Any(), 147 gomock.Any(), 148 ).Return(queryResp, nil).MaxTimes(maxRetryNum) 149 } 150 151 func mockCheckSyncConfig(ctx context.Context, cfgs []*config.SubTaskConfig, errCnt, warnCnt int64) (string, error) { 152 return "", nil 153 } 154 155 type OpenAPIViewSuite struct { 156 suite.Suite 157 } 158 159 func (s *OpenAPIViewSuite) SetupSuite() { 160 s.NoError(log.InitLogger(&log.Config{})) 161 } 162 163 func (s *OpenAPIViewSuite) SetupTest() { 164 checker.CheckSyncConfigFunc = mockCheckSyncConfig 165 CheckAndAdjustSourceConfigFunc = checkAndNoAdjustSourceConfigMock 166 s.NoError(failpoint.Enable("github.com/pingcap/tiflow/dm/master/MockSkipAdjustTargetDB", `return(true)`)) 167 s.NoError(failpoint.Enable("github.com/pingcap/tiflow/dm/master/MockSkipRemoveMetaData", `return(true)`)) 168 } 169 170 func (s *OpenAPIViewSuite) TearDownTest() { 171 checker.CheckSyncConfigFunc = checker.CheckSyncConfig 172 CheckAndAdjustSourceConfigFunc = checkAndAdjustSourceConfig 173 s.NoError(failpoint.Disable("github.com/pingcap/tiflow/dm/master/MockSkipAdjustTargetDB")) 174 s.NoError(failpoint.Disable("github.com/pingcap/tiflow/dm/master/MockSkipRemoveMetaData")) 175 } 176 177 func (s *OpenAPIViewSuite) TestClusterAPI() { 178 ctx1, cancel1 := context.WithCancel(context.Background()) 179 s1 := setupTestServer(ctx1, s.T()) 180 defer func() { 181 cancel1() 182 s1.Close() 183 }() 184 185 // join a new master node to an existing cluster 186 cfg2 := NewConfig() 187 s.Nil(cfg2.FromContent(SampleConfig)) 188 cfg2.Name = "dm-master-2" 189 cfg2.DataDir = s.T().TempDir() 190 cfg2.MasterAddr = tempurl.Alloc()[len("http://"):] 191 cfg2.PeerUrls = tempurl.Alloc() 192 cfg2.AdvertisePeerUrls = cfg2.PeerUrls 193 cfg2.Join = s1.cfg.MasterAddr // join to an existing cluster 194 cfg2.AdvertiseAddr = cfg2.MasterAddr 195 s2 := NewServer(cfg2) 196 ctx2, cancel2 := context.WithCancel(context.Background()) 197 require.NoError(s.T(), s2.Start(ctx2)) 198 199 defer func() { 200 cancel2() 201 s2.Close() 202 }() 203 204 baseURL := "/api/v1/cluster/" 205 masterURL := baseURL + "masters" 206 207 result := testutil.NewRequest().Get(masterURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 208 s.Equal(http.StatusOK, result.Code()) 209 var resultMasters openapi.GetClusterMasterListResponse 210 err := result.UnmarshalBodyToObject(&resultMasters) 211 s.NoError(err) 212 s.Equal(2, resultMasters.Total) 213 s.Equal(s1.cfg.Name, resultMasters.Data[0].Name) 214 s.Equal(s1.cfg.PeerUrls, resultMasters.Data[0].Addr) 215 s.True(resultMasters.Data[0].Leader) 216 s.True(resultMasters.Data[0].Alive) 217 218 // check cluster id 219 clusterInfoURL := baseURL + "info" 220 result = testutil.NewRequest().Get(clusterInfoURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 221 s.Equal(http.StatusOK, result.Code()) 222 var info openapi.GetClusterInfoResponse 223 s.NoError(result.UnmarshalBodyToObject(&info)) 224 s.Greater(info.ClusterId, uint64(0)) 225 s.Nil(info.Topology) 226 227 // update topo info 228 fakeHost := "1.1.1.1" 229 fakePort := 8261 230 masterTopo := []openapi.MasterTopology{{Host: fakeHost, Port: fakePort}} 231 workerTopo := []openapi.WorkerTopology{{Host: fakeHost, Port: fakePort}} 232 grafanaTopo := openapi.GrafanaTopology{Host: fakeHost, Port: fakePort} 233 prometheusTopo := openapi.PrometheusTopology{Host: fakeHost, Port: fakePort} 234 alertMangerTopo := openapi.AlertManagerTopology{Host: fakeHost, Port: fakePort} 235 topo := openapi.ClusterTopology{ 236 MasterTopologyList: &masterTopo, 237 WorkerTopologyList: &workerTopo, 238 GrafanaTopology: &grafanaTopo, 239 AlertManagerTopology: &alertMangerTopo, 240 PrometheusTopology: &prometheusTopo, 241 } 242 result = testutil.NewRequest().Put(clusterInfoURL).WithJsonBody(topo).GoWithHTTPHandler(s.T(), s1.openapiHandles) 243 s.Equal(http.StatusOK, result.Code()) 244 info = openapi.GetClusterInfoResponse{} 245 s.NoError(result.UnmarshalBodyToObject(&info)) 246 s.EqualValues(&topo, info.Topology) 247 // get again 248 result = testutil.NewRequest().Get(clusterInfoURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 249 s.Equal(http.StatusOK, result.Code()) 250 s.NoError(result.UnmarshalBodyToObject(&info)) 251 s.EqualValues(&topo, info.Topology) 252 253 // offline master-2 with retry 254 // operate etcd cluster may met `etcdserver: unhealthy cluster`, add some retry 255 for i := 0; i < 20; i++ { 256 result = testutil.NewRequest().Delete(fmt.Sprintf("%s/%s", masterURL, s2.cfg.Name)).GoWithHTTPHandler(s.T(), s1.openapiHandles) 257 if result.Code() == http.StatusBadRequest { 258 s.Equal(http.StatusBadRequest, result.Code()) 259 errResp := &openapi.ErrorWithMessage{} 260 err = result.UnmarshalBodyToObject(errResp) 261 s.Nil(err) 262 s.Regexp("etcdserver: unhealthy cluster", errResp.ErrorMsg) 263 time.Sleep(time.Second) 264 } else { 265 s.Equal(http.StatusNoContent, result.Code()) 266 break 267 } 268 } 269 cancel2() // stop dm-master-2 270 271 // list master again get one node 272 result = testutil.NewRequest().Get(masterURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 273 s.Equal(http.StatusOK, result.Code()) 274 s.NoError(result.UnmarshalBodyToObject(&resultMasters)) 275 s.Equal(1, resultMasters.Total) 276 277 workerName1 := "worker1" 278 s.NoError(s1.scheduler.AddWorker(workerName1, "172.16.10.72:8262")) 279 // list worker node 280 workerURL := baseURL + "workers" 281 result = testutil.NewRequest().Get(workerURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 282 var resultWorkers openapi.GetClusterWorkerListResponse 283 s.NoError(result.UnmarshalBodyToObject(&resultWorkers)) 284 s.Equal(1, resultWorkers.Total) 285 286 // offline worker-1 287 result = testutil.NewRequest().Delete(fmt.Sprintf("%s/%s", workerURL, workerName1)).GoWithHTTPHandler(s.T(), s1.openapiHandles) 288 s.Equal(http.StatusNoContent, result.Code()) 289 // after offline, no worker node 290 result = testutil.NewRequest().Get(workerURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 291 err = result.UnmarshalBodyToObject(&resultWorkers) 292 s.Nil(err) 293 s.Equal(0, resultWorkers.Total) 294 295 cancel1() 296 } 297 298 func (s *OpenAPIViewSuite) TestReverseRequestToLeader() { 299 ctx1, cancel1 := context.WithCancel(context.Background()) 300 s1 := setupTestServer(ctx1, s.T()) 301 defer func() { 302 cancel1() 303 s1.Close() 304 }() 305 306 // join a new master node to an existing cluster 307 cfg2 := NewConfig() 308 s.Nil(cfg2.FromContent(SampleConfig)) 309 cfg2.Name = "dm-master-2" 310 cfg2.DataDir = s.T().TempDir() 311 cfg2.MasterAddr = tempurl.Alloc()[len("http://"):] 312 cfg2.PeerUrls = tempurl.Alloc() 313 cfg2.AdvertisePeerUrls = cfg2.PeerUrls 314 cfg2.Join = s1.cfg.MasterAddr // join to an existing cluster 315 cfg2.AdvertiseAddr = cfg2.MasterAddr 316 cfg2.OpenAPI = true 317 s2 := NewServer(cfg2) 318 ctx2, cancel2 := context.WithCancel(context.Background()) 319 require.NoError(s.T(), s2.Start(ctx2)) 320 321 defer func() { 322 cancel2() 323 s2.Close() 324 }() 325 326 // wait the second master ready 327 s.False(utils.WaitSomething(30, 100*time.Millisecond, func() bool { 328 return s2.election.IsLeader() 329 })) 330 331 baseURL := "/api/v1/sources" 332 // list source from leader 333 result := testutil.NewRequest().Get(baseURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 334 s.Equal(http.StatusOK, result.Code()) 335 var resultListSource openapi.GetSourceListResponse 336 s.NoError(result.UnmarshalBodyToObject(&resultListSource)) 337 s.Len(resultListSource.Data, 0) 338 s.Equal(0, resultListSource.Total) 339 340 // list source from non-leader will get result too 341 result, err := HTTPTestWithTestResponseRecorder(testutil.NewRequest().Get(baseURL), s2.openapiHandles) 342 s.NoError(err) 343 s.Equal(http.StatusOK, result.Code()) 344 var resultListSource2 openapi.GetSourceListResponse 345 s.NoError(result.UnmarshalBodyToObject(&resultListSource2)) 346 s.Len(resultListSource2.Data, 0) 347 s.Equal(0, resultListSource2.Total) 348 } 349 350 func (s *OpenAPIViewSuite) TestReverseRequestToHttpsLeader() { 351 pwd2, err := os.Getwd() 352 require.NoError(s.T(), err) 353 caPath := pwd2 + "/tls_for_test/ca.pem" 354 certPath := pwd2 + "/tls_for_test/dm.pem" 355 keyPath := pwd2 + "/tls_for_test/dm.key" 356 357 // master1 358 masterAddr1 := tempurl.Alloc()[len("http://"):] 359 peerAddr1 := tempurl.Alloc()[len("http://"):] 360 cfg1 := NewConfig() 361 require.NoError(s.T(), cfg1.Parse([]string{ 362 "--name=dm-master-tls-1", 363 fmt.Sprintf("--data-dir=%s", s.T().TempDir()), 364 fmt.Sprintf("--master-addr=https://%s", masterAddr1), 365 fmt.Sprintf("--advertise-addr=https://%s", masterAddr1), 366 fmt.Sprintf("--peer-urls=https://%s", peerAddr1), 367 fmt.Sprintf("--advertise-peer-urls=https://%s", peerAddr1), 368 fmt.Sprintf("--initial-cluster=dm-master-tls-1=https://%s", peerAddr1), 369 "--ssl-ca=" + caPath, 370 "--ssl-cert=" + certPath, 371 "--ssl-key=" + keyPath, 372 })) 373 cfg1.OpenAPI = true 374 s1 := NewServer(cfg1) 375 ctx1, cancel1 := context.WithCancel(context.Background()) 376 require.NoError(s.T(), s1.Start(ctx1)) 377 defer func() { 378 cancel1() 379 s1.Close() 380 }() 381 // wait the first one become the leader 382 require.True(s.T(), utils.WaitSomething(30, 100*time.Millisecond, func() bool { 383 return s1.election.IsLeader() && s1.scheduler.Started() 384 })) 385 386 // master2 387 masterAddr2 := tempurl.Alloc()[len("http://"):] 388 peerAddr2 := tempurl.Alloc()[len("http://"):] 389 cfg2 := NewConfig() 390 require.NoError(s.T(), cfg2.Parse([]string{ 391 "--name=dm-master-tls-2", 392 fmt.Sprintf("--data-dir=%s", s.T().TempDir()), 393 fmt.Sprintf("--master-addr=https://%s", masterAddr2), 394 fmt.Sprintf("--advertise-addr=https://%s", masterAddr2), 395 fmt.Sprintf("--peer-urls=https://%s", peerAddr2), 396 fmt.Sprintf("--advertise-peer-urls=https://%s", peerAddr2), 397 "--ssl-ca=" + caPath, 398 "--ssl-cert=" + certPath, 399 "--ssl-key=" + keyPath, 400 })) 401 cfg2.OpenAPI = true 402 cfg2.Join = s1.cfg.MasterAddr // join to an existing cluster 403 s2 := NewServer(cfg2) 404 ctx2, cancel2 := context.WithCancel(context.Background()) 405 require.NoError(s.T(), s2.Start(ctx2)) 406 defer func() { 407 cancel2() 408 s2.Close() 409 }() 410 // wait the second master ready 411 require.False(s.T(), utils.WaitSomething(30, 100*time.Millisecond, func() bool { 412 return s2.election.IsLeader() 413 })) 414 415 baseURL := "/api/v1/sources" 416 // list source from leader 417 result := testutil.NewRequest().Get(baseURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 418 s.Equal(http.StatusOK, result.Code()) 419 var resultListSource openapi.GetSourceListResponse 420 s.NoError(result.UnmarshalBodyToObject(&resultListSource)) 421 s.Len(resultListSource.Data, 0) 422 s.Equal(0, resultListSource.Total) 423 424 // with tls, list source not from leader will get result too 425 result, err = HTTPTestWithTestResponseRecorder(testutil.NewRequest().Get(baseURL), s2.openapiHandles) 426 s.NoError(err) 427 s.Equal(http.StatusOK, result.Code()) 428 var resultListSource2 openapi.GetSourceListResponse 429 s.NoError(result.UnmarshalBodyToObject(&resultListSource2)) 430 s.Len(resultListSource2.Data, 0) 431 s.Equal(0, resultListSource2.Total) 432 433 // without tls, list source not from leader will be 502 434 s.NoError(failpoint.Enable("github.com/pingcap/tiflow/dm/master/MockNotSetTls", `return()`)) 435 result, err = HTTPTestWithTestResponseRecorder(testutil.NewRequest().Get(baseURL), s2.openapiHandles) 436 s.NoError(err) 437 s.Equal(http.StatusBadGateway, result.Code()) 438 s.NoError(failpoint.Disable("github.com/pingcap/tiflow/dm/master/MockNotSetTls")) 439 } 440 441 // httptest.ResponseRecorder is not http.CloseNotifier, will panic when test reverse proxy. 442 // We need to implement the interface ourselves. 443 // ref: https://github.com/gin-gonic/gin/blob/ce20f107f5dc498ec7489d7739541a25dcd48463/context_test.go#L1747-L1765 444 type TestResponseRecorder struct { 445 *httptest.ResponseRecorder 446 closeChannel chan bool 447 } 448 449 func (r *TestResponseRecorder) CloseNotify() <-chan bool { 450 return r.closeChannel 451 } 452 453 func CreateTestResponseRecorder() *TestResponseRecorder { 454 return &TestResponseRecorder{ 455 httptest.NewRecorder(), 456 make(chan bool, 1), 457 } 458 } 459 460 func HTTPTestWithTestResponseRecorder(r *testutil.RequestBuilder, handler http.Handler) (*testutil.CompletedRequest, error) { 461 if r == nil { 462 return nil, nil 463 } 464 if r.Error != nil { 465 return nil, r.Error 466 } 467 var bodyReader io.Reader 468 if r.Body != nil { 469 bodyReader = bytes.NewReader(r.Body) 470 } 471 472 req := httptest.NewRequest(r.Method, r.Path, bodyReader) 473 for h, v := range r.Headers { 474 req.Header.Add(h, v) 475 } 476 if host, ok := r.Headers["Host"]; ok { 477 req.Host = host 478 } 479 for _, c := range r.Cookies { 480 req.AddCookie(c) 481 } 482 483 rec := CreateTestResponseRecorder() 484 handler.ServeHTTP(rec, req) 485 486 return &testutil.CompletedRequest{ 487 Recorder: rec.ResponseRecorder, 488 }, nil 489 } 490 491 func (s *OpenAPIViewSuite) TestOpenAPIWillNotStartInDefaultConfig() { 492 // create a new cluster 493 cfg1 := NewConfig() 494 s.NoError(cfg1.FromContent(SampleConfig)) 495 cfg1.Name = "dm-master-1" 496 cfg1.DataDir = s.T().TempDir() 497 cfg1.MasterAddr = tempurl.Alloc()[len("http://"):] 498 cfg1.AdvertiseAddr = cfg1.MasterAddr 499 cfg1.PeerUrls = tempurl.Alloc() 500 cfg1.AdvertisePeerUrls = cfg1.PeerUrls 501 cfg1.InitialCluster = fmt.Sprintf("%s=%s", cfg1.Name, cfg1.AdvertisePeerUrls) 502 503 s1 := NewServer(cfg1) 504 ctx, cancel := context.WithCancel(context.Background()) 505 s.NoError(s1.Start(ctx)) 506 s.Nil(s1.openapiHandles) 507 cancel() 508 s1.Close() 509 } 510 511 func (s *OpenAPIViewSuite) TestTaskTemplatesAPI() { 512 ctx, cancel := context.WithCancel(context.Background()) 513 s1 := setupTestServer(ctx, s.T()) 514 defer func() { 515 cancel() 516 s1.Close() 517 }() 518 519 dbCfg := config.GetDBConfigForTest() 520 source1 := openapi.Source{ 521 SourceName: source1Name, 522 EnableGtid: false, 523 Host: dbCfg.Host, 524 Password: &dbCfg.Password, 525 Port: dbCfg.Port, 526 User: dbCfg.User, 527 } 528 createReq := openapi.CreateSourceRequest{Source: source1} 529 // create source 530 sourceURL := "/api/v1/sources" 531 result := testutil.NewRequest().Post(sourceURL).WithJsonBody(createReq).GoWithHTTPHandler(s.T(), s1.openapiHandles) 532 // check http status code 533 s.Equal(http.StatusCreated, result.Code()) 534 535 // create task config template 536 url := "/api/v1/tasks/templates" 537 538 task, err := fixtures.GenNoShardOpenAPITaskForTest() 539 s.NoError(err) 540 // use a valid target db 541 task.TargetConfig.Host = dbCfg.Host 542 task.TargetConfig.Port = dbCfg.Port 543 task.TargetConfig.User = dbCfg.User 544 task.TargetConfig.Password = dbCfg.Password 545 546 // create one 547 result = testutil.NewRequest().Post(url).WithJsonBody(task).GoWithHTTPHandler(s.T(), s1.openapiHandles) 548 s.Equal(http.StatusCreated, result.Code()) 549 var createTaskResp openapi.Task 550 s.NoError(result.UnmarshalBodyToObject(&createTaskResp)) 551 s.Equal(createTaskResp.Name, task.Name) 552 553 // create again will fail 554 result = testutil.NewRequest().Post(url).WithJsonBody(task).GoWithHTTPHandler(s.T(), s1.openapiHandles) 555 s.Equal(http.StatusBadRequest, result.Code()) 556 var errResp openapi.ErrorWithMessage 557 s.NoError(result.UnmarshalBodyToObject(&errResp)) 558 s.Equal(int(terror.ErrOpenAPITaskConfigExist.Code()), errResp.ErrorCode) 559 560 // list templates 561 result = testutil.NewRequest().Get(url).GoWithHTTPHandler(s.T(), s1.openapiHandles) 562 s.Equal(http.StatusOK, result.Code()) 563 var resultTaskList openapi.GetTaskListResponse 564 s.NoError(result.UnmarshalBodyToObject(&resultTaskList)) 565 s.Equal(1, resultTaskList.Total) 566 s.Equal(task.Name, resultTaskList.Data[0].Name) 567 568 // get detail 569 oneURL := fmt.Sprintf("%s/%s", url, task.Name) 570 result = testutil.NewRequest().Get(oneURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 571 s.Equal(http.StatusOK, result.Code()) 572 var respTask openapi.Task 573 s.NoError(result.UnmarshalBodyToObject(&respTask)) 574 s.Equal(task.Name, respTask.Name) 575 576 // get not exist 577 notExistURL := fmt.Sprintf("%s/%s", url, "notexist") 578 result = testutil.NewRequest().Get(notExistURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 579 s.Equal(http.StatusBadRequest, result.Code()) 580 s.NoError(result.UnmarshalBodyToObject(&errResp)) 581 s.Equal(int(terror.ErrOpenAPITaskConfigNotExist.Code()), errResp.ErrorCode) 582 583 // update 584 task.TaskMode = openapi.TaskTaskModeAll 585 result = testutil.NewRequest().Put(oneURL).WithJsonBody(task).GoWithHTTPHandler(s.T(), s1.openapiHandles) 586 s.Equal(http.StatusOK, result.Code()) 587 s.NoError(result.UnmarshalBodyToObject(&respTask)) 588 s.Equal(task.Name, respTask.Name) 589 590 // update not exist will fail 591 task.Name = "notexist" 592 result = testutil.NewRequest().Put(notExistURL).WithJsonBody(task).GoWithHTTPHandler(s.T(), s1.openapiHandles) 593 s.Equal(http.StatusBadRequest, result.Code()) 594 s.NoError(result.UnmarshalBodyToObject(&errResp)) 595 s.Equal(int(terror.ErrOpenAPITaskConfigNotExist.Code()), errResp.ErrorCode) 596 597 // delete task config template 598 result = testutil.NewRequest().Delete(oneURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 599 s.Equal(http.StatusNoContent, result.Code()) 600 result = testutil.NewRequest().Get(url).GoWithHTTPHandler(s.T(), s1.openapiHandles) 601 s.Equal(http.StatusOK, result.Code()) 602 s.NoError(result.UnmarshalBodyToObject(&resultTaskList)) 603 s.Equal(0, resultTaskList.Total) 604 } 605 606 func (s *OpenAPIViewSuite) TestSourceAPI() { 607 ctx, cancel := context.WithCancel(context.Background()) 608 s1 := setupTestServer(ctx, s.T()) 609 defer func() { 610 cancel() 611 s1.Close() 612 }() 613 614 baseURL := "/api/v1/sources" 615 616 dbCfg := config.GetDBConfigForTest() 617 purgeInterVal := int64(10) 618 source1 := openapi.Source{ 619 SourceName: source1Name, 620 Enable: true, 621 EnableGtid: false, 622 Host: dbCfg.Host, 623 Password: &dbCfg.Password, 624 Port: dbCfg.Port, 625 User: dbCfg.User, 626 Purge: &openapi.Purge{Interval: &purgeInterVal}, 627 } 628 createReq := openapi.CreateSourceRequest{Source: source1} 629 result := testutil.NewRequest().Post(baseURL).WithJsonBody(createReq).GoWithHTTPHandler(s.T(), s1.openapiHandles) 630 // check http status code 631 s.Equal(http.StatusCreated, result.Code()) 632 var resultSource openapi.Source 633 s.NoError(result.UnmarshalBodyToObject(&resultSource)) 634 s.Equal(source1.User, resultSource.User) 635 s.Equal(source1.Host, resultSource.Host) 636 s.Equal(source1.Port, resultSource.Port) 637 s.Equal(source1.Password, resultSource.Password) 638 s.Equal(source1.EnableGtid, resultSource.EnableGtid) 639 s.Equal(source1.SourceName, resultSource.SourceName) 640 s.EqualValues(source1.Purge.Interval, resultSource.Purge.Interval) 641 642 // create source with same name will failed 643 result = testutil.NewRequest().Post(baseURL).WithJsonBody(createReq).GoWithHTTPHandler(s.T(), s1.openapiHandles) 644 // check http status code 645 s.Equal(http.StatusBadRequest, result.Code()) 646 var errResp openapi.ErrorWithMessage 647 s.NoError(result.UnmarshalBodyToObject(&errResp)) 648 s.Equal(int(terror.ErrSchedulerSourceCfgExist.Code()), errResp.ErrorCode) 649 650 // get source 651 source1URL := fmt.Sprintf("%s/%s", baseURL, source1Name) 652 var source1FromHTTP openapi.Source 653 result = testutil.NewRequest().Get(source1URL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 654 s.Equal(http.StatusOK, result.Code()) 655 s.NoError(result.UnmarshalBodyToObject(&source1FromHTTP)) 656 s.Equal(source1FromHTTP.SourceName, source1.SourceName) 657 // update a source 658 clone := source1 659 clone.EnableGtid = true 660 updateReq := openapi.UpdateSourceRequest{Source: clone} 661 result = testutil.NewRequest().Put(source1URL).WithJsonBody(updateReq).GoWithHTTPHandler(s.T(), s1.openapiHandles) 662 s.Equal(http.StatusOK, result.Code()) 663 s.NoError(result.UnmarshalBodyToObject(&source1FromHTTP)) 664 s.Equal(source1FromHTTP.EnableGtid, clone.EnableGtid) 665 666 // get source not existed 667 sourceNotExistedURL := fmt.Sprintf("%s/not_existed", baseURL) 668 result = testutil.NewRequest().Get(sourceNotExistedURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 669 s.Equal(http.StatusNotFound, result.Code()) 670 // get source status 671 var source1Status openapi.GetSourceStatusResponse 672 source1StatusURL := fmt.Sprintf("%s/%s/status", baseURL, source1Name) 673 result = testutil.NewRequest().Get(source1StatusURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 674 s.Equal(http.StatusOK, result.Code()) 675 s.NoError(result.UnmarshalBodyToObject(&source1Status)) 676 s.Len(source1Status.Data, 1) 677 s.Equal(source1.SourceName, source1Status.Data[0].SourceName) 678 s.Equal("", source1Status.Data[0].WorkerName) // no worker now 679 680 // list source 681 result = testutil.NewRequest().Get(baseURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 682 // check http status code 683 s.Equal(http.StatusOK, result.Code()) 684 var resultListSource openapi.GetSourceListResponse 685 s.NoError(result.UnmarshalBodyToObject(&resultListSource)) 686 s.Len(resultListSource.Data, 1) 687 s.Equal(1, resultListSource.Total) 688 s.Equal(source1.SourceName, resultListSource.Data[0].SourceName) 689 690 // test get source schema and table 691 _, mockDB, err := conn.InitMockDBFull() 692 s.NoError(err) 693 schemaName := "information_schema" 694 mockDB.ExpectQuery("SHOW DATABASES").WillReturnRows(sqlmock.NewRows([]string{"Database"}).AddRow(schemaName)) 695 696 schemaURL := fmt.Sprintf("%s/%s/schemas", baseURL, source1.SourceName) 697 result = testutil.NewRequest().Get(schemaURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 698 s.Equal(http.StatusOK, result.Code()) 699 var schemaNameList openapi.SchemaNameList 700 s.NoError(result.UnmarshalBodyToObject(&schemaNameList)) 701 s.Len(schemaNameList, 1) 702 s.Equal(schemaName, schemaNameList[0]) 703 s.NoError(mockDB.ExpectationsWereMet()) 704 705 _, mockDB, err = conn.InitMockDBFull() 706 s.NoError(err) 707 tableName := "CHARACTER_SETS" 708 mockDB.ExpectQuery("SHOW FULL TABLES IN `information_schema` WHERE Table_Type != 'VIEW';").WillReturnRows( 709 sqlmock.NewRows([]string{"Tables_in_information_schema", "Table_type"}).AddRow(tableName, "BASE TABLE")) 710 tableURL := fmt.Sprintf("%s/%s/schemas/%s", baseURL, source1.SourceName, schemaName) 711 result = testutil.NewRequest().Get(tableURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 712 s.Equal(http.StatusOK, result.Code()) 713 var tableNameList openapi.TableNameList 714 s.NoError(result.UnmarshalBodyToObject(&tableNameList)) 715 s.Len(tableNameList, 1) 716 s.Equal(tableName, tableNameList[0]) 717 s.NoError(mockDB.ExpectationsWereMet()) 718 719 ctrl := gomock.NewController(s.T()) 720 defer ctrl.Finish() 721 // add mock worker to which the unbound sources should be bound 722 ctx1, cancel1 := context.WithCancel(ctx) 723 defer cancel1() 724 workerName1 := "worker1" 725 s.NoError(s1.scheduler.AddWorker(workerName1, "172.16.10.72:8262")) 726 go func(ctx context.Context, workerName string) { 727 s.NoError(ha.KeepAlive(ctx, s1.etcdClient, workerName, keepAliveTTL)) 728 }(ctx1, workerName1) 729 // wait worker ready 730 s.True(utils.WaitSomething(30, 100*time.Millisecond, func() bool { 731 w := s1.scheduler.GetWorkerBySource(source1.SourceName) 732 return w != nil 733 }), true) 734 735 // mock worker get status relay not started 736 mockWorkerClient := pbmock.NewMockWorkerClient(ctrl) 737 mockRelayQueryStatus(mockWorkerClient, source1.SourceName, workerName1, pb.Stage_InvalidStage) 738 s1.scheduler.SetWorkerClientForTest(workerName1, newMockRPCClient(mockWorkerClient)) 739 740 // get source status again,source should be bound by worker1,but relay not started 741 result = testutil.NewRequest().Get(source1StatusURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 742 s.Equal(http.StatusOK, result.Code()) 743 s.NoError(result.UnmarshalBodyToObject(&source1Status)) 744 s.Equal(source1.SourceName, source1Status.Data[0].SourceName) 745 s.Equal(workerName1, source1Status.Data[0].WorkerName) // worker1 is bound 746 s.Nil(source1Status.Data[0].RelayStatus) // not start relay 747 s.Equal(1, source1Status.Total) 748 749 // list source with status 750 result = testutil.NewRequest().Get(baseURL+"?with_status=true").GoWithHTTPHandler(s.T(), s1.openapiHandles) 751 // check http status code 752 s.Equal(http.StatusOK, result.Code()) 753 s.NoError(result.UnmarshalBodyToObject(&resultListSource)) 754 s.Len(resultListSource.Data, 1) 755 s.Equal(1, resultListSource.Total) 756 s.Equal(source1.SourceName, resultListSource.Data[0].SourceName) 757 statusList := *resultListSource.Data[0].StatusList 758 s.Len(statusList, 1) 759 status := statusList[0] 760 s.Equal(workerName1, status.WorkerName) 761 s.Nil(status.RelayStatus) 762 763 // start relay 764 enableRelayURL := fmt.Sprintf("%s/relay/enable", source1URL) 765 result = testutil.NewRequest().Post(enableRelayURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 766 // check http status code 767 s.Equal(http.StatusOK, result.Code()) 768 relayWorkers, err := s1.scheduler.GetRelayWorkers(source1Name) 769 s.NoError(err) 770 s.Len(relayWorkers, 1) 771 772 // mock worker get status relay started 773 mockWorkerClient = pbmock.NewMockWorkerClient(ctrl) 774 mockRelayQueryStatus(mockWorkerClient, source1.SourceName, workerName1, pb.Stage_Running) 775 s1.scheduler.SetWorkerClientForTest(workerName1, newMockRPCClient(mockWorkerClient)) 776 // get source status again, relay status should not be nil 777 result = testutil.NewRequest().Get(source1StatusURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 778 s.Equal(http.StatusOK, result.Code()) 779 s.NoError(result.UnmarshalBodyToObject(&source1Status)) 780 s.Equal(pb.Stage_Running.String(), source1Status.Data[0].RelayStatus.Stage) 781 782 // mock worker get status meet error 783 mockWorkerClient = pbmock.NewMockWorkerClient(ctrl) 784 mockRelayQueryStatus(mockWorkerClient, source1.SourceName, workerName1, pb.Stage_Paused) 785 s1.scheduler.SetWorkerClientForTest(workerName1, newMockRPCClient(mockWorkerClient)) 786 // get source status again, error message should not be nil 787 result = testutil.NewRequest().Get(source1StatusURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 788 s.Equal(http.StatusOK, result.Code()) 789 s.NoError(result.UnmarshalBodyToObject(&source1Status)) 790 s.Regexp("some error happened", *source1Status.Data[0].ErrorMsg) 791 s.Equal(workerName1, source1Status.Data[0].WorkerName) 792 793 // test list source and filter by enable-relay 794 result = testutil.NewRequest().Get(baseURL+"?enable_relay=true").GoWithHTTPHandler(s.T(), s1.openapiHandles) 795 s.Equal(http.StatusOK, result.Code()) 796 s.NoError(result.UnmarshalBodyToObject(&resultListSource)) 797 s.Len(resultListSource.Data, 1) 798 result = testutil.NewRequest().Get(baseURL+"?enable_relay=false").GoWithHTTPHandler(s.T(), s1.openapiHandles) 799 s.Equal(http.StatusOK, result.Code()) 800 s.NoError(result.UnmarshalBodyToObject(&resultListSource)) 801 s.Len(resultListSource.Data, 0) 802 803 // purge relay 804 purgeRelay := fmt.Sprintf("%s/relay/purge", source1URL) 805 purgeRelayReq := openapi.PurgeRelayRequest{RelayBinlogName: "binlog.001"} 806 mockWorkerClient = pbmock.NewMockWorkerClient(ctrl) 807 mockPurgeRelay(mockWorkerClient) 808 s1.scheduler.SetWorkerClientForTest(workerName1, newMockRPCClient(mockWorkerClient)) 809 result = testutil.NewRequest().Post(purgeRelay).WithJsonBody(purgeRelayReq).GoWithHTTPHandler(s.T(), s1.openapiHandles) 810 s.Equal(http.StatusOK, result.Code()) 811 812 // test disable relay 813 disableRelayURL := fmt.Sprintf("%s/relay/disable", source1URL) 814 disableRelayReq := openapi.DisableRelayRequest{} 815 result = testutil.NewRequest().Post(disableRelayURL).WithJsonBody(disableRelayReq).GoWithHTTPHandler(s.T(), s1.openapiHandles) 816 s.Equal(http.StatusOK, result.Code()) 817 relayWorkers, err = s1.scheduler.GetRelayWorkers(source1Name) 818 s.NoError(err) 819 s.Len(relayWorkers, 0) 820 821 // mock worker get status relay already stopped 822 mockWorkerClient = pbmock.NewMockWorkerClient(ctrl) 823 mockRelayQueryStatus(mockWorkerClient, source1.SourceName, workerName1, pb.Stage_InvalidStage) 824 s1.scheduler.SetWorkerClientForTest(workerName1, newMockRPCClient(mockWorkerClient)) 825 // get source status again 826 result = testutil.NewRequest().Get(source1StatusURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 827 s.Equal(http.StatusOK, result.Code()) 828 source1Status = openapi.GetSourceStatusResponse{} // reset 829 s.NoError(result.UnmarshalBodyToObject(&source1Status)) 830 s.Equal(source1.SourceName, source1Status.Data[0].SourceName) 831 s.Equal(workerName1, source1Status.Data[0].WorkerName) // worker1 is bound 832 s.Nil(source1Status.Data[0].RelayStatus) // not start relay 833 s.Equal(1, source1Status.Total) 834 835 // delete source with --force 836 result = testutil.NewRequest().Delete(fmt.Sprintf("%s/%s?force=true", baseURL, source1.SourceName)).GoWithHTTPHandler(s.T(), s1.openapiHandles) 837 // check http status code 838 s.Equal(http.StatusNoContent, result.Code()) 839 840 // delete again will failed 841 result = testutil.NewRequest().Delete(fmt.Sprintf("%s/%s", baseURL, source1.SourceName)).GoWithHTTPHandler(s.T(), s1.openapiHandles) 842 s.Equal(http.StatusBadRequest, result.Code()) 843 var errResp2 openapi.ErrorWithMessage 844 s.NoError(result.UnmarshalBodyToObject(&errResp2)) 845 s.Equal(int(terror.ErrSchedulerSourceCfgNotExist.Code()), errResp2.ErrorCode) 846 847 // list source 848 result = testutil.NewRequest().Get(baseURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 849 // check http status code 850 s.Equal(http.StatusOK, result.Code()) 851 var resultListSource2 openapi.GetSourceListResponse 852 s.NoError(result.UnmarshalBodyToObject(&resultListSource2)) 853 s.Len(resultListSource2.Data, 0) 854 s.Equal(0, resultListSource2.Total) 855 856 // create with no password 857 sourceNoPassword := source1 858 sourceNoPassword.Password = nil 859 createReqNoPassword := openapi.CreateSourceRequest{Source: sourceNoPassword} 860 result = testutil.NewRequest().Post(baseURL).WithJsonBody(createReqNoPassword).GoWithHTTPHandler(s.T(), s1.openapiHandles) 861 s.Equal(http.StatusCreated, result.Code()) 862 s.NoError(result.UnmarshalBodyToObject(&resultSource)) 863 s.Nil(resultSource.Password) 864 865 // update to have password 866 sourceHasPassword := source1 867 updateReqHasPassword := openapi.UpdateSourceRequest{Source: sourceHasPassword} 868 result = testutil.NewRequest().Put(source1URL).WithJsonBody(updateReqHasPassword).GoWithHTTPHandler(s.T(), s1.openapiHandles) 869 s.Equal(http.StatusOK, result.Code()) 870 s.NoError(result.UnmarshalBodyToObject(&source1FromHTTP)) 871 s.Equal(source1FromHTTP.Password, sourceHasPassword.Password) 872 873 // update with no password, will use old password 874 updateReqNoPassword := openapi.UpdateSourceRequest{Source: sourceNoPassword} 875 result = testutil.NewRequest().Put(source1URL).WithJsonBody(updateReqNoPassword).GoWithHTTPHandler(s.T(), s1.openapiHandles) 876 s.Equal(http.StatusOK, result.Code()) 877 s.NoError(result.UnmarshalBodyToObject(&source1FromHTTP)) 878 s.Nil(source1FromHTTP.Password) 879 // password is old 880 conf := s1.scheduler.GetSourceCfgByID(source1FromHTTP.SourceName) 881 s.NotNil(conf) 882 s.Equal(*sourceHasPassword.Password, conf.From.Password) 883 884 // delete source with --force 885 result = testutil.NewRequest().Delete(fmt.Sprintf("%s/%s?force=true", baseURL, source1.SourceName)).GoWithHTTPHandler(s.T(), s1.openapiHandles) 886 s.Equal(http.StatusNoContent, result.Code()) 887 } 888 889 func (s *OpenAPIViewSuite) testImportTaskTemplate(task *openapi.Task, s1 *Server) { 890 // test batch import task config 891 taskBatchImportURL := "/api/v1/tasks/templates/import" 892 req := openapi.TaskTemplateRequest{Overwrite: false} 893 result := testutil.NewRequest().Post(taskBatchImportURL).WithJsonBody(req).GoWithHTTPHandler(s.T(), s1.openapiHandles) 894 s.Equal(http.StatusAccepted, result.Code()) 895 var resp openapi.TaskTemplateResponse 896 s.NoError(result.UnmarshalBodyToObject(&resp)) 897 s.Len(resp.SuccessTaskList, 1) 898 s.Equal(task.Name, resp.SuccessTaskList[0]) 899 s.Len(resp.FailedTaskList, 0) 900 901 // import again without overwrite will fail 902 result = testutil.NewRequest().Post(taskBatchImportURL).WithJsonBody(req).GoWithHTTPHandler(s.T(), s1.openapiHandles) 903 s.Equal(http.StatusAccepted, result.Code()) 904 s.NoError(result.UnmarshalBodyToObject(&resp)) 905 s.Len(resp.SuccessTaskList, 0) 906 s.Len(resp.FailedTaskList, 1) 907 s.Equal(task.Name, resp.FailedTaskList[0].TaskName) 908 909 // import again with overwrite will success 910 req.Overwrite = true 911 result = testutil.NewRequest().Post(taskBatchImportURL).WithJsonBody(req).GoWithHTTPHandler(s.T(), s1.openapiHandles) 912 s.NoError(result.UnmarshalBodyToObject(&resp)) 913 s.Len(resp.SuccessTaskList, 1) 914 s.Equal(task.Name, resp.SuccessTaskList[0]) 915 s.Len(resp.FailedTaskList, 0) 916 } 917 918 func (s *OpenAPIViewSuite) testSourceOperationWithTask(source *openapi.Source, task *openapi.Task, s1 *Server) { 919 source1URL := fmt.Sprintf("/api/v1/sources/%s", source.SourceName) 920 disableSource1URL := fmt.Sprintf("%s/disable", source1URL) 921 enableSource1URL := fmt.Sprintf("%s/enable", source1URL) 922 transferSource1URL := fmt.Sprintf("%s/transfer", source1URL) 923 924 // disable 925 result := testutil.NewRequest().Post(disableSource1URL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 926 s.Equal(http.StatusOK, result.Code()) 927 s.Equal(pb.Stage_Stopped, s1.scheduler.GetExpectSubTaskStage(task.Name, source1Name).Expect) 928 929 // enable again 930 result = testutil.NewRequest().Post(enableSource1URL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 931 s.Equal(http.StatusOK, result.Code()) 932 s.Equal(pb.Stage_Running, s1.scheduler.GetExpectSubTaskStage(task.Name, source1Name).Expect) 933 934 // test transfer failed,success transfer is tested in IT test 935 req := openapi.WorkerNameRequest{WorkerName: "not exist"} 936 result = testutil.NewRequest().Post(transferSource1URL).WithJsonBody(req).GoWithHTTPHandler(s.T(), s1.openapiHandles) 937 s.Equal(http.StatusBadRequest, result.Code()) 938 var resp openapi.ErrorWithMessage 939 s.NoError(result.UnmarshalBodyToObject(&resp)) 940 s.Equal(int(terror.ErrSchedulerWorkerNotExist.Code()), resp.ErrorCode) 941 } 942 943 func (s *OpenAPIViewSuite) TestTaskAPI() { 944 ctx, cancel := context.WithCancel(context.Background()) 945 s1 := setupTestServer(ctx, s.T()) 946 ctrl := gomock.NewController(s.T()) 947 defer func() { 948 cancel() 949 s1.Close() 950 ctrl.Finish() 951 }() 952 953 dbCfg := config.GetDBConfigForTest() 954 source1 := openapi.Source{ 955 Enable: true, 956 SourceName: source1Name, 957 EnableGtid: false, 958 Host: dbCfg.Host, 959 Password: &dbCfg.Password, 960 Port: dbCfg.Port, 961 User: dbCfg.User, 962 } 963 // create source 964 sourceURL := "/api/v1/sources" 965 createSourceReq := openapi.CreateSourceRequest{Source: source1} 966 result := testutil.NewRequest().Post(sourceURL).WithJsonBody(createSourceReq).GoWithHTTPHandler(s.T(), s1.openapiHandles) 967 // check http status code 968 s.Equal(http.StatusCreated, result.Code()) 969 970 // add mock worker, the unbound sources should be bound before starting workers 971 ctx1, cancel1 := context.WithCancel(ctx) 972 defer cancel1() 973 workerName1 := "worker-1" 974 s.NoError(s1.scheduler.AddWorker(workerName1, "172.16.10.72:8262")) 975 go func(ctx context.Context, workerName string) { 976 s.NoError(ha.KeepAlive(ctx, s1.etcdClient, workerName, keepAliveTTL)) 977 }(ctx1, workerName1) 978 // wait worker ready 979 s.True(utils.WaitSomething(30, 100*time.Millisecond, func() bool { 980 w := s1.scheduler.GetWorkerBySource(source1.SourceName) 981 return w != nil 982 }), true) 983 984 // create task 985 taskURL := "/api/v1/tasks" 986 987 task, err := fixtures.GenNoShardOpenAPITaskForTest() 988 s.NoError(err) 989 // use a valid target db 990 task.TargetConfig.Host = dbCfg.Host 991 task.TargetConfig.Port = dbCfg.Port 992 task.TargetConfig.User = dbCfg.User 993 task.TargetConfig.Password = dbCfg.Password 994 995 // create task 996 createTaskReq := openapi.CreateTaskRequest{Task: task} 997 result = testutil.NewRequest().Post(taskURL).WithJsonBody(createTaskReq).GoWithHTTPHandler(s.T(), s1.openapiHandles) 998 s.Equal(http.StatusCreated, result.Code()) 999 var createTaskResp openapi.OperateTaskResponse 1000 s.NoError(result.UnmarshalBodyToObject(&createTaskResp)) 1001 s.Equal(createTaskResp.Task.Name, task.Name) 1002 subTaskM := s1.scheduler.GetSubTaskCfgsByTask(task.Name) 1003 s.Len(subTaskM, 1) 1004 s.Equal(task.Name, subTaskM[source1Name].Name) 1005 1006 // get task 1007 task1URL := fmt.Sprintf("%s/%s", taskURL, task.Name) 1008 var task1FromHTTP openapi.Task 1009 result = testutil.NewRequest().Get(task1URL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 1010 s.Equal(http.StatusOK, result.Code()) 1011 s.NoError(result.UnmarshalBodyToObject(&task1FromHTTP)) 1012 s.Equal(task1FromHTTP.Name, task.Name) 1013 1014 // update a task 1015 s.NoError(failpoint.Enable("github.com/pingcap/tiflow/dm/master/scheduler/operateCheckSubtasksCanUpdate", `return("success")`)) 1016 clone := task 1017 batch := 1000 1018 clone.SourceConfig.IncrMigrateConf.ReplBatch = &batch 1019 updateReq := openapi.UpdateTaskRequest{Task: clone} 1020 result = testutil.NewRequest().Put(task1URL).WithJsonBody(updateReq).GoWithHTTPHandler(s.T(), s1.openapiHandles) 1021 s.Equal(http.StatusOK, result.Code()) 1022 var updateResp openapi.OperateTaskResponse 1023 s.NoError(result.UnmarshalBodyToObject(&updateResp)) 1024 s.EqualValues(updateResp.Task.SourceConfig.IncrMigrateConf.ReplBatch, clone.SourceConfig.IncrMigrateConf.ReplBatch) 1025 s.NoError(failpoint.Disable("github.com/pingcap/tiflow/dm/master/scheduler/operateCheckSubtasksCanUpdate")) 1026 1027 // list tasks 1028 result = testutil.NewRequest().Get(taskURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 1029 s.Equal(http.StatusOK, result.Code()) 1030 var resultTaskList openapi.GetTaskListResponse 1031 s.NoError(result.UnmarshalBodyToObject(&resultTaskList)) 1032 s.Equal(1, resultTaskList.Total) 1033 s.Equal(task.Name, resultTaskList.Data[0].Name) 1034 1035 s.testImportTaskTemplate(&task, s1) 1036 1037 // start task 1038 startTaskURL := fmt.Sprintf("%s/%s/start", taskURL, task.Name) 1039 result = testutil.NewRequest().Post(startTaskURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 1040 s.Equal(http.StatusOK, result.Code()) 1041 s.Equal(pb.Stage_Running, s1.scheduler.GetExpectSubTaskStage(task.Name, source1Name).Expect) 1042 1043 // get task status 1044 mockWorkerClient := pbmock.NewMockWorkerClient(ctrl) 1045 mockTaskQueryStatus(mockWorkerClient, task.Name, source1.SourceName, workerName1, false) 1046 s1.scheduler.SetWorkerClientForTest(workerName1, newMockRPCClient(mockWorkerClient)) 1047 taskStatusURL := fmt.Sprintf("%s/%s/status", taskURL, task.Name) 1048 result = testutil.NewRequest().Get(taskStatusURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 1049 s.Equal(http.StatusOK, result.Code()) 1050 var resultTaskStatus openapi.GetTaskStatusResponse 1051 s.NoError(result.UnmarshalBodyToObject(&resultTaskStatus)) 1052 s.Equal(1, resultTaskStatus.Total) // only 1 subtask 1053 s.Equal(task.Name, resultTaskStatus.Data[0].Name) 1054 s.Equal(openapi.TaskStageRunning, resultTaskStatus.Data[0].Stage) 1055 s.Equal(workerName1, resultTaskStatus.Data[0].WorkerName) 1056 s.Equal(float64(0), resultTaskStatus.Data[0].DumpStatus.CompletedTables) 1057 s.Equal(int64(1), resultTaskStatus.Data[0].DumpStatus.TotalTables) 1058 s.Equal(float64(10), resultTaskStatus.Data[0].DumpStatus.EstimateTotalRows) 1059 1060 // get task status with source name 1061 taskStatusURL = fmt.Sprintf("%s/%s/status?source_name_list=%s", taskURL, task.Name, source1Name) 1062 result = testutil.NewRequest().Get(taskStatusURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 1063 s.Equal(http.StatusOK, result.Code()) 1064 var resultTaskStatusWithStatus openapi.GetTaskStatusResponse 1065 s.NoError(result.UnmarshalBodyToObject(&resultTaskStatusWithStatus)) 1066 s.EqualValues(resultTaskStatus, resultTaskStatusWithStatus) 1067 1068 // list task with status 1069 result = testutil.NewRequest().Get(taskURL+"?with_status=true").GoWithHTTPHandler(s.T(), s1.openapiHandles) 1070 s.Equal(http.StatusOK, result.Code()) 1071 var resultListTask openapi.GetTaskListResponse 1072 s.NoError(result.UnmarshalBodyToObject(&resultListTask)) 1073 s.Len(resultListTask.Data, 1) 1074 s.Equal(1, resultListTask.Total) 1075 s.NotNil(resultListTask.Data[0].StatusList) 1076 statusList := *resultListTask.Data[0].StatusList 1077 status := statusList[0] 1078 s.Equal(workerName1, status.WorkerName) 1079 s.Equal(task.Name, status.Name) 1080 1081 // list with filter 1082 result = testutil.NewRequest().Get(taskURL+"?stage=Stopped").GoWithHTTPHandler(s.T(), s1.openapiHandles) 1083 s.Equal(http.StatusOK, result.Code()) 1084 resultListTask = openapi.GetTaskListResponse{} // reset 1085 s.NoError(result.UnmarshalBodyToObject(&resultListTask)) 1086 s.Len(resultListTask.Data, 0) 1087 1088 result = testutil.NewRequest().Get(taskURL+"?stage=Running").GoWithHTTPHandler(s.T(), s1.openapiHandles) 1089 s.Equal(http.StatusOK, result.Code()) 1090 resultListTask = openapi.GetTaskListResponse{} // reset 1091 s.NoError(result.UnmarshalBodyToObject(&resultListTask)) 1092 s.Len(resultListTask.Data, 1) 1093 1094 result = testutil.NewRequest().Get(taskURL+"?stage=Running&source_name_list=notsource").GoWithHTTPHandler(s.T(), s1.openapiHandles) 1095 s.Equal(http.StatusOK, result.Code()) 1096 resultListTask = openapi.GetTaskListResponse{} // reset 1097 s.NoError(result.UnmarshalBodyToObject(&resultListTask)) 1098 s.Len(resultListTask.Data, 0) 1099 1100 result = testutil.NewRequest().Get(taskURL+"?stage=Running&source_name_list="+source1Name).GoWithHTTPHandler(s.T(), s1.openapiHandles) 1101 s.Equal(http.StatusOK, result.Code()) 1102 resultListTask = openapi.GetTaskListResponse{} // reset 1103 s.NoError(result.UnmarshalBodyToObject(&resultListTask)) 1104 s.Len(resultListTask.Data, 1) 1105 1106 // get task with status 1107 result = testutil.NewRequest().Get(task1URL+"?with_status=true").GoWithHTTPHandler(s.T(), s1.openapiHandles) 1108 s.Equal(http.StatusOK, result.Code()) 1109 s.NoError(result.UnmarshalBodyToObject(&task1FromHTTP)) 1110 s.Equal(task1FromHTTP.Name, task.Name) 1111 statusList = *task1FromHTTP.StatusList 1112 s.Len(statusList, 1) 1113 s.Equal(workerName1, statusList[0].WorkerName) 1114 s.Equal(task.Name, statusList[0].Name) 1115 1116 // test some error happened on worker 1117 mockWorkerClient = pbmock.NewMockWorkerClient(ctrl) 1118 mockTaskQueryStatus(mockWorkerClient, task.Name, source1.SourceName, workerName1, true) 1119 s1.scheduler.SetWorkerClientForTest(workerName1, newMockRPCClient(mockWorkerClient)) 1120 result = testutil.NewRequest().Get(taskURL+"?with_status=true").GoWithHTTPHandler(s.T(), s1.openapiHandles) 1121 s.Equal(http.StatusOK, result.Code()) 1122 s.NoError(result.UnmarshalBodyToObject(&resultListTask)) 1123 s.Len(resultListTask.Data, 1) 1124 s.Equal(1, resultListTask.Total) 1125 s.NotNil(resultListTask.Data[0].StatusList) 1126 statusList = *resultListTask.Data[0].StatusList 1127 s.Len(statusList, 1) 1128 status = statusList[0] 1129 s.NotNil(status.ErrorMsg) 1130 1131 // test convertTaskConfig 1132 convertReq := openapi.ConverterTaskRequest{} 1133 convertResp := openapi.ConverterTaskResponse{} 1134 convertURL := fmt.Sprintf("%s/%s", taskURL, "converters") 1135 result = testutil.NewRequest().Post(convertURL).WithJsonBody(convertReq).GoWithHTTPHandler(s.T(), s1.openapiHandles) 1136 s.Equal(http.StatusBadRequest, result.Code()) // not valid req 1137 1138 // from task to taskConfig 1139 convertReq.Task = &task 1140 result = testutil.NewRequest().Post(convertURL).WithJsonBody(convertReq).GoWithHTTPHandler(s.T(), s1.openapiHandles) 1141 s.Equal(http.StatusOK, result.Code()) 1142 s.NoError(result.UnmarshalBodyToObject(&convertResp)) 1143 s.NotNil(convertResp.Task) 1144 s.NotNil(convertResp.TaskConfigFile) 1145 taskConfigFile := convertResp.TaskConfigFile 1146 1147 // from taskCfg to task 1148 convertReq.Task = nil 1149 convertReq.TaskConfigFile = &taskConfigFile 1150 result = testutil.NewRequest().Post(convertURL).WithJsonBody(convertReq).GoWithHTTPHandler(s.T(), s1.openapiHandles) 1151 s.Equal(http.StatusOK, result.Code()) 1152 s.NoError(result.UnmarshalBodyToObject(&convertResp)) 1153 s.NotNil(convertResp.Task) 1154 s.NotNil(convertResp.TaskConfigFile) 1155 taskConfigFile2 := convertResp.TaskConfigFile 1156 s.Equal(taskConfigFile2, taskConfigFile) 1157 1158 s.testSourceOperationWithTask(&source1, &task, s1) 1159 1160 // stop task 1161 stopTaskURL := fmt.Sprintf("%s/%s/stop", taskURL, task.Name) 1162 stopTaskReq := openapi.StopTaskRequest{} 1163 result = testutil.NewRequest().Post(stopTaskURL).WithJsonBody(stopTaskReq).GoWithHTTPHandler(s.T(), s1.openapiHandles) 1164 s.Equal(http.StatusOK, result.Code()) 1165 s.Equal(pb.Stage_Stopped, s1.scheduler.GetExpectSubTaskStage(task.Name, source1Name).Expect) 1166 1167 // delete task 1168 result = testutil.NewRequest().Delete(task1URL+"?force=true").GoWithHTTPHandler(s.T(), s1.openapiHandles) 1169 s.Equal(http.StatusNoContent, result.Code()) 1170 subTaskM = s1.scheduler.GetSubTaskCfgsByTask(task.Name) 1171 s.Len(subTaskM, 0) 1172 1173 // list tasks 1174 result = testutil.NewRequest().Get(taskURL).GoWithHTTPHandler(s.T(), s1.openapiHandles) 1175 s.Equal(http.StatusOK, result.Code()) 1176 resultListTask = openapi.GetTaskListResponse{} // reset 1177 s.NoError(result.UnmarshalBodyToObject(&resultTaskList)) 1178 s.Equal(0, resultTaskList.Total) 1179 } 1180 1181 func TestOpenAPIViewSuite(t *testing.T) { 1182 suite.Run(t, new(OpenAPIViewSuite)) 1183 }