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  }