github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/pkg/storage/utils_test.go (about) 1 // Copyright 2022 PingCAP, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package storage 15 16 import ( 17 "context" 18 "errors" 19 "os" 20 "path" 21 "path/filepath" 22 "strings" 23 "testing" 24 25 "github.com/aws/aws-sdk-go/aws" 26 "github.com/aws/aws-sdk-go/aws/awserr" 27 "github.com/aws/aws-sdk-go/aws/request" 28 "github.com/aws/aws-sdk-go/service/s3" 29 backuppb "github.com/pingcap/kvproto/pkg/brpb" 30 "github.com/pingcap/tidb/br/pkg/mock" 31 "github.com/pingcap/tidb/br/pkg/storage" 32 "github.com/stretchr/testify/require" 33 "go.uber.org/mock/gomock" 34 ) 35 36 func TestIsS3OrLocalDisk(t *testing.T) { 37 cases := []struct { 38 path string 39 s3 bool 40 local bool 41 }{ 42 { 43 path: "", 44 s3: false, 45 local: false, 46 }, 47 { 48 path: "1invalid:", 49 s3: false, 50 local: false, 51 }, 52 { 53 path: "file:///tmp/storage", 54 s3: false, 55 local: true, 56 }, 57 { 58 path: "/tmp/storage", 59 s3: false, 60 local: true, 61 }, 62 { 63 path: "./tmp/storage", 64 s3: false, 65 local: true, 66 }, 67 { 68 path: "tmp/storage", 69 s3: false, 70 local: true, 71 }, 72 { 73 path: "s3:///bucket/more/prefix", 74 s3: true, 75 local: false, 76 }, 77 { 78 path: "s3://bucket2/prefix", 79 s3: true, 80 local: false, 81 }, 82 { 83 path: "s3://bucket3/prefix/path?endpoint=https://127.0.0.1:9000&force_path_style=0&SSE=aws:kms&sse-kms-key-id=TestKey&xyz=abc", 84 s3: true, 85 local: false, 86 }, 87 { 88 // git secrets will report error when it's a real AK, so we use a truncated one 89 path: "s3://bucket4/prefix/path?access-key=NXN7IOSAAKDEEOLF&secret-access-key=nRE/7Dt+PaIbYKrK/ExCiX=XMLPNw", 90 s3: true, 91 local: false, 92 }, 93 } 94 95 for _, c := range cases { 96 require.Equal(t, c.s3, IsS3Path(c.path)) 97 require.Equal(t, c.local, IsLocalDiskPath(c.path)) 98 } 99 } 100 101 func TestIsS3AndAdjustAndTrimPath(t *testing.T) { 102 testPaths := []struct { 103 isURLFormat bool 104 rawPath string 105 expectPath string 106 }{ 107 {true, "file:///tmp/storage", "file:///tmp/storage_placeholder"}, 108 {false, "/tmp/storage", "/tmp/storage_placeholder"}, 109 {false, "./tmp/storage", "./tmp/storage_placeholder"}, 110 {false, "./tmp/storage/", "./tmp/storage_placeholder"}, 111 {false, "tmp/storage", "tmp/storage_placeholder"}, 112 {true, "s3:///bucket/more/prefix", "s3:///bucket/more/prefix_placeholder"}, 113 {true, "s3://bucket2/prefix", "s3://bucket2/prefix_placeholder"}, 114 {true, "s3://bucket2/prefix/", "s3://bucket2/prefix_placeholder"}, 115 { 116 true, "s3://bucket3/prefix/path?endpoint=https://127.0.0.1:9000&force_path_style=0&SSE=aws:kms&sse-kms-key-id=TestKey&xyz=abc", 117 "s3://bucket3/prefix/path_placeholder?endpoint=https://127.0.0.1:9000&force_path_style=0&SSE=aws:kms&sse-kms-key-id=TestKey&xyz=abc", 118 }, 119 { 120 // git secrets will report error when it's a real AK, so we use a truncated one 121 true, "s3://bucket3/prefix/path?access-key=NXN7IOSAAKDEEOLF&secret-access-key=nRE/7Dt+PaIbYKrK/ExCiX=XMLPNw", 122 "s3://bucket3/prefix/path_placeholder?access-key=NXN7IOSAAKDEEOLF&secret-access-key=nRE/7Dt%2BPaIbYKrK/ExCiX=XMLPNw", 123 }, 124 } 125 126 testUniqueIDs := []struct { 127 test string 128 expect string 129 }{ 130 {"mysql-replica-01", "mysql-replica-01"}, 131 {"t-Ë!s`t", "t-%C3%8B%21s%60t"}, 132 {"abc???bcd", "abc%3F%3F%3Fbcd"}, 133 {"ab?c/b%cËd", "ab%3Fc/b%25c%C3%8Bd"}, 134 {"a%2F%3Fbc", "a%252F%253Fbc"}, 135 } 136 137 testSeparators := []string{".", string(filepath.Separator)} 138 139 for _, testUniqueID := range testUniqueIDs { 140 for _, testPath := range testPaths { 141 for _, testSeparator := range testSeparators { 142 // "" 143 res, err := AdjustPath("", "") 144 require.NoError(t, err) 145 require.Equal(t, "", res) 146 res, err = AdjustPath(testPath.rawPath, "") 147 require.NoError(t, err) 148 require.Equal(t, testPath.rawPath, res) 149 res, err = AdjustPath("", testSeparator+testUniqueID.test) 150 require.NoError(t, err) 151 require.Equal(t, "", res) 152 // error 153 _, err = AdjustPath("1invalid:", testSeparator+testUniqueID.test) 154 require.Error(t, err) 155 require.Regexp(t, "parse (.*)1invalid:(.*): first path segment in URL cannot contain colon*", err.Error()) 156 // normal 157 var expectPath string 158 if testPath.isURLFormat { 159 expectPath = strings.ReplaceAll(testPath.expectPath, "_placeholder", testSeparator+testUniqueID.expect) 160 } else { 161 expectPath = strings.ReplaceAll(testPath.expectPath, "_placeholder", testSeparator+testUniqueID.test) 162 } 163 res, err = AdjustPath(testPath.rawPath, testSeparator+testUniqueID.test) 164 require.NoError(t, err) 165 require.Equal(t, expectPath, res) 166 // repeat 167 res, err = AdjustPath(expectPath, testSeparator+testUniqueID.test) 168 require.NoError(t, err) 169 require.Equal(t, expectPath, res) 170 // trim 171 res, err = TrimPath(expectPath, testSeparator+testUniqueID.test) 172 require.NoError(t, err) 173 require.Equal(t, strings.ReplaceAll(testPath.expectPath, "_placeholder", ""), res) 174 } 175 } 176 } 177 } 178 179 type s3Suite struct { 180 controller *gomock.Controller 181 s3 *mock.MockS3API 182 storage *storage.S3Storage 183 } 184 185 func createS3Suite(c gomock.TestReporter) (s *s3Suite, clean func()) { 186 s = new(s3Suite) 187 s.controller = gomock.NewController(c) 188 s.s3 = mock.NewMockS3API(s.controller) 189 s.storage = storage.NewS3StorageForTest( 190 s.s3, 191 &backuppb.S3{ 192 Region: "us-west-2", 193 Bucket: "bucket", 194 Prefix: "prefix/", 195 Acl: "acl", 196 Sse: "sse", 197 StorageClass: "sc", 198 }, 199 ) 200 201 clean = func() { 202 s.controller.Finish() 203 } 204 205 return 206 } 207 208 func TestCollectDirFilesAndRemove(t *testing.T) { 209 fileNames := []string{"schema.sql", "table.sql"} 210 211 // test local 212 localDir := t.TempDir() 213 for _, fileName := range fileNames { 214 f, err := os.Create(path.Join(localDir, fileName)) 215 require.NoError(t, err) 216 err = f.Close() 217 require.NoError(t, err) 218 } 219 localRes, err := CollectDirFiles(context.Background(), localDir, nil) 220 require.NoError(t, err) 221 for _, fileName := range fileNames { 222 _, ok := localRes[fileName] 223 require.True(t, ok) 224 } 225 226 // current dir 227 pwd, err := os.Getwd() 228 require.NoError(t, err) 229 tempDir, err := os.MkdirTemp(pwd, "TestCollectDirFiles") 230 require.NoError(t, err) 231 defer os.RemoveAll(tempDir) 232 for _, fileName := range fileNames { 233 f, err1 := os.Create(path.Join(tempDir, fileName)) 234 require.NoError(t, err1) 235 err1 = f.Close() 236 require.NoError(t, err1) 237 } 238 localRes, err = CollectDirFiles(context.Background(), "./"+path.Base(tempDir), nil) 239 require.NoError(t, err) 240 for _, fileName := range fileNames { 241 _, ok := localRes[fileName] 242 require.True(t, ok) 243 } 244 245 // test s3 246 s, clean := createS3Suite(t) 247 defer clean() 248 ctx := aws.BackgroundContext() 249 250 objects := make([]*s3.Object, 0, len(fileNames)) 251 for _, fileName := range fileNames { 252 object := &s3.Object{ 253 Key: aws.String(path.Join("prefix", fileName)), 254 Size: aws.Int64(100), 255 } 256 objects = append(objects, object) 257 } 258 259 s.s3.EXPECT(). 260 ListObjectsWithContext(ctx, gomock.Any()). 261 DoAndReturn(func(_ context.Context, input *s3.ListObjectsInput, opt ...request.Option) (*s3.ListObjectsOutput, error) { 262 require.Equal(t, "bucket", aws.StringValue(input.Bucket)) 263 require.Equal(t, "prefix/", aws.StringValue(input.Prefix)) 264 require.Equal(t, "", aws.StringValue(input.Marker)) 265 require.Equal(t, int64(1000), aws.Int64Value(input.MaxKeys)) 266 require.Equal(t, "", aws.StringValue(input.Delimiter)) 267 return &s3.ListObjectsOutput{ 268 IsTruncated: aws.Bool(false), 269 Contents: objects, 270 }, nil 271 }) 272 273 localRes, err = CollectDirFiles(context.Background(), "", s.storage) 274 require.NoError(t, err) 275 for _, fileName := range fileNames { 276 _, ok := localRes[fileName] 277 require.True(t, ok) 278 } 279 } 280 281 func TestRemoveAll(t *testing.T) { 282 fileNames := []string{"schema.sql", "table.sql"} 283 284 // test local 285 localDir := t.TempDir() 286 defer os.RemoveAll(localDir) 287 for _, fileName := range fileNames { 288 f, err := os.Create(path.Join(localDir, fileName)) 289 require.NoError(t, err) 290 err = f.Close() 291 require.NoError(t, err) 292 } 293 err := RemoveAll(context.Background(), localDir, nil) 294 require.NoError(t, err) 295 _, err = os.Stat(localDir) 296 require.True(t, os.IsNotExist(err)) 297 298 // test s3 299 s, clean := createS3Suite(t) 300 defer clean() 301 ctx := aws.BackgroundContext() 302 303 objects := make([]*s3.Object, 0, len(fileNames)) 304 for _, fileName := range fileNames { 305 object := &s3.Object{ 306 Key: aws.String(path.Join("prefix", fileName)), 307 Size: aws.Int64(100), 308 } 309 objects = append(objects, object) 310 } 311 312 firstCall := s.s3.EXPECT(). 313 ListObjectsWithContext(ctx, gomock.Any()). 314 DoAndReturn(func(_ context.Context, input *s3.ListObjectsInput, opt ...request.Option) (*s3.ListObjectsOutput, error) { 315 require.Equal(t, "bucket", aws.StringValue(input.Bucket)) 316 require.Equal(t, "prefix/", aws.StringValue(input.Prefix)) 317 require.Equal(t, "", aws.StringValue(input.Marker)) 318 require.Equal(t, int64(1000), aws.Int64Value(input.MaxKeys)) 319 require.Equal(t, "", aws.StringValue(input.Delimiter)) 320 return &s3.ListObjectsOutput{ 321 IsTruncated: aws.Bool(false), 322 Contents: objects, 323 }, nil 324 }) 325 secondCall := s.s3.EXPECT(). 326 DeleteObjectWithContext(ctx, gomock.Any()). 327 DoAndReturn(func(_ context.Context, input *s3.DeleteObjectInput, opt ...request.Option) (*s3.DeleteObjectInput, error) { 328 require.Equal(t, "bucket", aws.StringValue(input.Bucket)) 329 require.True(t, aws.StringValue(input.Key) == "prefix/schema.sql" || aws.StringValue(input.Key) == "prefix/table.sql") 330 return &s3.DeleteObjectInput{}, nil 331 }).After(firstCall) 332 thirdCall := s.s3.EXPECT(). 333 DeleteObjectWithContext(ctx, gomock.Any()). 334 DoAndReturn(func(_ context.Context, input *s3.DeleteObjectInput, opt ...request.Option) (*s3.DeleteObjectInput, error) { 335 require.Equal(t, "bucket", aws.StringValue(input.Bucket)) 336 require.True(t, aws.StringValue(input.Key) == "prefix/schema.sql" || aws.StringValue(input.Key) == "prefix/table.sql") 337 return &s3.DeleteObjectInput{}, nil 338 }).After(secondCall) 339 fourthCall := s.s3.EXPECT(). 340 DeleteObjectWithContext(ctx, gomock.Any()). 341 DoAndReturn(func(_ context.Context, input *s3.DeleteObjectInput, opt ...request.Option) (*s3.DeleteObjectInput, error) { 342 require.Equal(t, "bucket", aws.StringValue(input.Bucket)) 343 require.Equal(t, "prefix/", aws.StringValue(input.Key)) 344 return &s3.DeleteObjectInput{}, nil 345 }).After(thirdCall) 346 347 s.s3.EXPECT(). 348 HeadObjectWithContext(ctx, gomock.Any()). 349 Return(nil, awserr.New(s3.ErrCodeNoSuchKey, "no such key", nil)).After(fourthCall) 350 351 err = RemoveAll(context.Background(), "", s.storage) 352 require.NoError(t, err) 353 354 exists, err := s.storage.FileExists(ctx, "") 355 require.NoError(t, err) 356 require.False(t, exists) 357 } 358 359 func TestIsNotExistError(t *testing.T) { 360 // test local 361 localDir := t.TempDir() 362 defer os.RemoveAll(localDir) 363 _, err := os.Open(path.Join(localDir, "test.log")) 364 require.Error(t, err) 365 res := IsNotExistError(err) 366 require.True(t, res) 367 368 // test s3 369 s, clean := createS3Suite(t) 370 defer clean() 371 ctx := aws.BackgroundContext() 372 373 s.s3.EXPECT(). 374 GetObjectWithContext(ctx, gomock.Any()). 375 Return(nil, awserr.New(s3.ErrCodeNoSuchKey, "no such key", nil)) 376 377 _, err = s.storage.ReadFile(ctx, "test.log") 378 require.Error(t, err) 379 res = IsNotExistError(err) 380 require.True(t, res) 381 382 // test other local error 383 _, err = os.ReadFile(localDir) 384 require.Error(t, err) 385 res = IsNotExistError(err) 386 require.False(t, res) 387 388 // test other s3 error 389 s.s3.EXPECT(). 390 GetObjectWithContext(ctx, gomock.Any()). 391 Return(nil, errors.New("just some unrelated error")) 392 393 _, err = s.storage.ReadFile(ctx, "test.log") 394 require.Error(t, err) 395 res = IsNotExistError(err) 396 require.False(t, res) 397 }