github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/cdc/server/server_test.go (about) 1 // Copyright 2020 PingCAP, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package server 15 16 import ( 17 "context" 18 "crypto/tls" 19 "encoding/json" 20 "fmt" 21 "net/http" 22 "net/url" 23 "os" 24 "os/user" 25 "path/filepath" 26 "runtime" 27 "strings" 28 "sync" 29 "testing" 30 "time" 31 32 "github.com/golang/mock/gomock" 33 "github.com/pingcap/tiflow/cdc/capture" 34 "github.com/pingcap/tiflow/cdc/model" 35 "github.com/pingcap/tiflow/pkg/config" 36 cerrors "github.com/pingcap/tiflow/pkg/errors" 37 "github.com/pingcap/tiflow/pkg/etcd" 38 mock_etcd "github.com/pingcap/tiflow/pkg/etcd/mock" 39 "github.com/pingcap/tiflow/pkg/httputil" 40 "github.com/pingcap/tiflow/pkg/retry" 41 "github.com/pingcap/tiflow/pkg/security" 42 "github.com/pingcap/tiflow/pkg/util" 43 "github.com/stretchr/testify/require" 44 "github.com/tikv/pd/pkg/utils/tempurl" 45 clientv3 "go.etcd.io/etcd/client/v3" 46 "go.etcd.io/etcd/server/v3/embed" 47 "golang.org/x/sync/errgroup" 48 ) 49 50 type testServer struct { 51 server *server 52 e *embed.Etcd 53 clientURL *url.URL 54 ctx context.Context 55 cancel context.CancelFunc 56 errg *errgroup.Group 57 } 58 59 func newServer(t *testing.T) *testServer { 60 var err error 61 dir := t.TempDir() 62 s := &testServer{} 63 s.clientURL, s.e, err = etcd.SetupEmbedEtcd(dir) 64 require.Nil(t, err) 65 66 pdEndpoints := []string{ 67 "http://" + s.clientURL.Host, 68 "http://invalid-pd-host:2379", 69 } 70 server, err := New(pdEndpoints) 71 require.Nil(t, err) 72 require.NotNil(t, server) 73 s.server = server 74 75 s.ctx, s.cancel = context.WithCancel(context.Background()) 76 client, err := clientv3.New(clientv3.Config{ 77 Endpoints: s.server.pdEndpoints, 78 Context: s.ctx, 79 DialTimeout: 5 * time.Second, 80 }) 81 require.Nil(t, err) 82 83 etcdClient, err := etcd.NewCDCEtcdClient(s.ctx, client, etcd.DefaultCDCClusterID) 84 require.Nil(t, err) 85 s.server.etcdClient = etcdClient 86 87 s.errg = util.HandleErrWithErrGroup(s.ctx, s.e.Err(), func(e error) { t.Log(e) }) 88 return s 89 } 90 91 func (s *testServer) close(t *testing.T) { 92 s.server.Close() 93 s.e.Close() 94 s.cancel() 95 err := s.errg.Wait() 96 if err != nil { 97 t.Errorf("Error group error: %s", err) 98 } 99 } 100 101 func TestServerBasic(t *testing.T) { 102 t.Parallel() 103 s := newServer(t) 104 defer s.close(t) 105 testSetUpDataDir(t, s) 106 } 107 108 func testSetUpDataDir(t *testing.T, s *testServer) { 109 conf := config.GetGlobalServerConfig() 110 // DataDir is not set, and no changefeed exist, use the default 111 conf.DataDir = "" 112 err := s.server.setUpDir(s.ctx) 113 require.Nil(t, err) 114 require.Equal(t, defaultDataDir, conf.DataDir) 115 require.Equal(t, filepath.Join(defaultDataDir, config.DefaultSortDir), conf.Sorter.SortDir) 116 117 // DataDir is not set, but has existed changefeed, use the one with the largest available space 118 conf.DataDir = "" 119 dir := t.TempDir() 120 err = s.server.etcdClient.SaveChangeFeedInfo(s.ctx, 121 &model.ChangeFeedInfo{SortDir: dir}, model.DefaultChangeFeedID("a")) 122 require.Nil(t, err) 123 124 err = s.server.etcdClient.SaveChangeFeedInfo(s.ctx, 125 &model.ChangeFeedInfo{}, model.DefaultChangeFeedID("b")) 126 require.Nil(t, err) 127 128 err = s.server.setUpDir(s.ctx) 129 require.Nil(t, err) 130 131 require.Equal(t, dir, conf.DataDir) 132 require.Equal(t, filepath.Join(dir, config.DefaultSortDir), conf.Sorter.SortDir) 133 134 conf.DataDir = t.TempDir() 135 // DataDir has been set, just use it 136 err = s.server.setUpDir(s.ctx) 137 require.Nil(t, err) 138 require.NotEqual(t, "", conf.DataDir) 139 require.Equal(t, filepath.Join(conf.DataDir, config.DefaultSortDir), conf.Sorter.SortDir) 140 } 141 142 func TestCheckDir(t *testing.T) { 143 me, err := user.Current() 144 require.Nil(t, err) 145 if me.Name == "root" || runtime.GOOS == "windows" { 146 t.Skip("test case is running as a superuser or in windows") 147 } 148 149 dir := t.TempDir() 150 _, err = checkDir(dir) 151 require.Nil(t, err) 152 153 readOnly := filepath.Join(dir, "readOnly") 154 err = os.MkdirAll(readOnly, 0o400) 155 require.Nil(t, err) 156 _, err = checkDir(readOnly) 157 require.Error(t, err) 158 159 writeOnly := filepath.Join(dir, "writeOnly") 160 err = os.MkdirAll(writeOnly, 0o200) 161 require.Nil(t, err) 162 _, err = checkDir(writeOnly) 163 require.Error(t, err) 164 165 file := filepath.Join(dir, "file") 166 f, err := os.Create(file) 167 require.Nil(t, err) 168 require.Nil(t, f.Close()) 169 _, err = checkDir(file) 170 require.Error(t, err) 171 } 172 173 const retryTime = 20 174 175 func TestServerTLSWithoutCommonName(t *testing.T) { 176 addr := tempurl.Alloc()[len("http://"):] 177 // Do not specify common name 178 _, securityCfg, err := security.NewServerCredential4Test("") 179 require.Nil(t, err) 180 conf := config.GetDefaultServerConfig() 181 conf.Addr = addr 182 conf.AdvertiseAddr = addr 183 conf.Security = securityCfg 184 config.StoreGlobalServerConfig(conf) 185 186 server, err := New([]string{"https://127.0.0.1:2379"}) 187 cp := capture.NewCapture4Test(nil) 188 ctrl := gomock.NewController(t) 189 etcdClient := mock_etcd.NewMockCDCEtcdClient(ctrl) 190 etcdClient.EXPECT().GetClusterID().Return("abcd").AnyTimes() 191 cp.EtcdClient = etcdClient 192 server.capture = cp 193 require.Nil(t, err) 194 err = server.startStatusHTTP(server.tcpServer.HTTP1Listener()) 195 require.Nil(t, err) 196 defer func() { 197 require.Nil(t, server.statusServer.Close()) 198 }() 199 200 statusURL := fmt.Sprintf("https://%s/api/v1/status", addr) 201 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 202 defer cancel() 203 204 var wg sync.WaitGroup 205 wg.Add(1) 206 go func() { 207 defer wg.Done() 208 err := server.tcpServer.Run(ctx) 209 require.ErrorContains(t, err, "ErrTCPServerClosed") 210 }() 211 212 // test cli sends request without a cert will success 213 err = retry.Do(ctx, func() error { 214 tr := &http.Transport{ 215 TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 216 } 217 cli := httputil.NewTestClient(tr) 218 resp, err := cli.Get(ctx, statusURL) 219 if err != nil { 220 return err 221 } 222 defer resp.Body.Close() 223 decoder := json.NewDecoder(resp.Body) 224 captureInfo := &model.CaptureInfo{} 225 err = decoder.Decode(captureInfo) 226 require.Nil(t, err) 227 info, err := server.capture.Info() 228 require.Nil(t, err) 229 require.Equal(t, info.ID, captureInfo.ID) 230 return nil 231 }, retry.WithMaxTries(retryTime), retry.WithBackoffBaseDelay(50), 232 retry.WithIsRetryableErr(cerrors.IsRetryableError)) 233 require.Nil(t, err) 234 235 // test cli sends request with a cert will success 236 err = retry.Do(ctx, func() error { 237 cli, err := httputil.NewClient(securityCfg) 238 require.Nil(t, err) 239 resp, err := cli.Get(ctx, statusURL) 240 if err != nil { 241 return err 242 } 243 decoder := json.NewDecoder(resp.Body) 244 captureInfo := &model.CaptureInfo{} 245 err = decoder.Decode(captureInfo) 246 require.Nil(t, err) 247 info, err := server.capture.Info() 248 require.Nil(t, err) 249 require.Equal(t, info.ID, captureInfo.ID) 250 resp.Body.Close() 251 return nil 252 }, retry.WithMaxTries(retryTime), retry.WithBackoffBaseDelay(50), 253 retry.WithIsRetryableErr(cerrors.IsRetryableError)) 254 require.Nil(t, err) 255 256 cancel() 257 wg.Wait() 258 } 259 260 func TestServerTLSWithCommonNameAndRotate(t *testing.T) { 261 addr := tempurl.Alloc()[len("http://"):] 262 // specify a common name 263 ca, securityCfg, err := security.NewServerCredential4Test("server") 264 securityCfg.CertAllowedCN = append(securityCfg.CertAllowedCN, "client1") 265 require.Nil(t, err) 266 267 conf := config.GetDefaultServerConfig() 268 conf.Addr = addr 269 conf.AdvertiseAddr = addr 270 conf.Security = securityCfg 271 config.StoreGlobalServerConfig(conf) 272 273 server, err := New([]string{"https://127.0.0.1:2379"}) 274 cp := capture.NewCapture4Test(nil) 275 ctrl := gomock.NewController(t) 276 etcdClient := mock_etcd.NewMockCDCEtcdClient(ctrl) 277 etcdClient.EXPECT().GetClusterID().Return("abcd").AnyTimes() 278 cp.EtcdClient = etcdClient 279 server.capture = cp 280 require.Nil(t, err) 281 err = server.startStatusHTTP(server.tcpServer.HTTP1Listener()) 282 require.Nil(t, err) 283 defer func() { 284 require.Nil(t, server.statusServer.Close()) 285 }() 286 287 statusURL := fmt.Sprintf("https://%s/api/v1/status", addr) 288 ctx, cancel := context.WithTimeout(context.Background(), 40*time.Second) 289 defer cancel() 290 291 var wg sync.WaitGroup 292 wg.Add(1) 293 go func() { 294 defer wg.Done() 295 err := server.tcpServer.Run(ctx) 296 require.ErrorContains(t, err, "ErrTCPServerClosed") 297 }() 298 299 // test cli sends request without a cert will fail 300 err = retry.Do(ctx, func() error { 301 tr := &http.Transport{ 302 TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 303 } 304 cli := httputil.NewTestClient(tr) 305 require.Nil(t, err) 306 resp, err := cli.Get(ctx, statusURL) 307 if err != nil { 308 return err 309 } 310 decoder := json.NewDecoder(resp.Body) 311 captureInfo := &model.CaptureInfo{} 312 err = decoder.Decode(captureInfo) 313 require.Nil(t, err) 314 info, err := server.capture.Info() 315 require.Nil(t, err) 316 require.Equal(t, info.ID, captureInfo.ID) 317 resp.Body.Close() 318 return nil 319 }, retry.WithMaxTries(retryTime), retry.WithBackoffBaseDelay(50), 320 retry.WithIsRetryableErr(cerrors.IsRetryableError)) 321 require.True(t, 322 strings.Contains(err.Error(), "remote error: tls: bad certificate") || 323 strings.Contains(err.Error(), "remote error: tls: certificate required"), 324 "bad err: %s", err.Error(), 325 ) 326 327 testTlSClient := func(securityCfg *security.Credential) error { 328 return retry.Do(ctx, func() error { 329 cli, err := httputil.NewClient(securityCfg) 330 require.Nil(t, err) 331 resp, err := cli.Get(ctx, statusURL) 332 if err != nil { 333 return err 334 } 335 decoder := json.NewDecoder(resp.Body) 336 captureInfo := &model.CaptureInfo{} 337 err = decoder.Decode(captureInfo) 338 require.Nil(t, err) 339 info, err := server.capture.Info() 340 require.Nil(t, err) 341 require.Equal(t, info.ID, captureInfo.ID) 342 resp.Body.Close() 343 return nil 344 }, retry.WithMaxTries(retryTime), retry.WithBackoffBaseDelay(50), 345 retry.WithIsRetryableErr(cerrors.IsRetryableError)) 346 } 347 348 // test cli sends request with a cert will success 349 350 // test peer success 351 require.NoError(t, testTlSClient(securityCfg)) 352 353 // test rotate 354 serverCert, serverkey, err := ca.GenerateCerts("rotate") 355 require.NoError(t, err) 356 err = os.WriteFile(securityCfg.CertPath, serverCert, 0o600) 357 require.NoError(t, err) 358 err = os.WriteFile(securityCfg.KeyPath, serverkey, 0o600) 359 require.NoError(t, err) 360 // peer fail due to invalid common name `rotate` 361 require.ErrorContains(t, testTlSClient(securityCfg), "client certificate authentication failed") 362 363 cert, key, err := ca.GenerateCerts("client1") 364 require.NoError(t, err) 365 certPath, err := security.WriteFile("ticdc-test-client-cert", cert) 366 require.NoError(t, err) 367 keyPath, err := security.WriteFile("ticdc-test-client-key", key) 368 require.NoError(t, err) 369 require.NoError(t, testTlSClient(&security.Credential{ 370 CAPath: securityCfg.CAPath, 371 CertPath: certPath, 372 KeyPath: keyPath, 373 CertAllowedCN: []string{"rotate"}, 374 })) 375 376 cancel() 377 wg.Wait() 378 }