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  }