github.com/matrixorigin/matrixone@v1.2.0/pkg/fileservice/aliyun_sdk.go (about)

     1  // Copyright 2023 Matrix Origin
     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  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package fileservice
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"net/http"
    23  	gotrace "runtime/trace"
    24  	"strconv"
    25  	"time"
    26  
    27  	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
    28  	"github.com/aliyun/alibaba-cloud-sdk-go/services/sts"
    29  	"github.com/aliyun/aliyun-oss-go-sdk/oss"
    30  	"github.com/aliyun/credentials-go/credentials"
    31  	awscredentials "github.com/aws/aws-sdk-go/aws/credentials"
    32  	"github.com/matrixorigin/matrixone/pkg/common/moerr"
    33  	"github.com/matrixorigin/matrixone/pkg/logutil"
    34  	"github.com/matrixorigin/matrixone/pkg/perfcounter"
    35  	"github.com/matrixorigin/matrixone/pkg/util/trace"
    36  	"go.uber.org/zap"
    37  )
    38  
    39  type AliyunSDK struct {
    40  	name            string
    41  	bucket          *oss.Bucket
    42  	perfCounterSets []*perfcounter.CounterSet
    43  	listMaxKeys     int
    44  }
    45  
    46  var (
    47  	aliyunCredentialExpireDuration = time.Minute * 30
    48  )
    49  
    50  func NewAliyunSDK(
    51  	ctx context.Context,
    52  	args ObjectStorageArguments,
    53  	perfCounterSets []*perfcounter.CounterSet,
    54  ) (_ *AliyunSDK, err error) {
    55  	defer catch(&err)
    56  
    57  	if err := args.validate(); err != nil {
    58  		return nil, err
    59  	}
    60  
    61  	opts := []oss.ClientOption{}
    62  	if args.Region != "" {
    63  		opts = append(opts, oss.Region(args.Region))
    64  	}
    65  	if args.SecurityToken != "" {
    66  		opts = append(opts, oss.SecurityToken(args.SecurityToken))
    67  	}
    68  	credentialsProvider, err := args.credentialProviderForAliyunSDK(ctx)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  	if credentialsProvider != nil {
    73  		opts = append(opts, oss.SetCredentialsProvider(credentialsProvider))
    74  	}
    75  
    76  	client, err := oss.New(
    77  		args.Endpoint,
    78  		"", "",
    79  		opts...,
    80  	)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	logutil.Info("new object storage",
    86  		zap.Any("sdk", "aliyun"),
    87  		zap.Any("arguments", args),
    88  	)
    89  
    90  	if !args.NoBucketValidation {
    91  		// validate bucket
    92  		_, err := client.GetBucketInfo(args.Bucket)
    93  		if err != nil {
    94  			return nil, err
    95  		}
    96  	}
    97  
    98  	bucket, err := client.Bucket(args.Bucket)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	return &AliyunSDK{
   104  		name:            args.Name,
   105  		bucket:          bucket,
   106  		perfCounterSets: perfCounterSets,
   107  	}, nil
   108  }
   109  
   110  var _ ObjectStorage = new(AliyunSDK)
   111  
   112  func (a *AliyunSDK) List(
   113  	ctx context.Context,
   114  	prefix string,
   115  	fn func(bool, string, int64) (bool, error),
   116  ) error {
   117  
   118  	if err := ctx.Err(); err != nil {
   119  		return err
   120  	}
   121  
   122  	var cont string
   123  
   124  loop1:
   125  	for {
   126  		result, err := a.listObjects(ctx, prefix, cont)
   127  		if err != nil {
   128  			return err
   129  		}
   130  
   131  		for _, obj := range result.Objects {
   132  			more, err := fn(false, obj.Key, obj.Size)
   133  			if err != nil {
   134  				return err
   135  			}
   136  			if !more {
   137  				break loop1
   138  			}
   139  		}
   140  
   141  		for _, prefix := range result.CommonPrefixes {
   142  			more, err := fn(true, prefix, 0)
   143  			if err != nil {
   144  				return err
   145  			}
   146  			if !more {
   147  				break loop1
   148  			}
   149  		}
   150  
   151  		if !result.IsTruncated {
   152  			break
   153  		}
   154  		cont = result.NextContinuationToken
   155  	}
   156  
   157  	return nil
   158  }
   159  
   160  func (a *AliyunSDK) Stat(
   161  	ctx context.Context,
   162  	key string,
   163  ) (
   164  	size int64,
   165  	err error,
   166  ) {
   167  
   168  	defer func() {
   169  		if a.is404(err) {
   170  			err = moerr.NewFileNotFoundNoCtx(key)
   171  		}
   172  	}()
   173  
   174  	if err := ctx.Err(); err != nil {
   175  		return 0, err
   176  	}
   177  
   178  	info, err := a.statObject(
   179  		ctx,
   180  		key,
   181  	)
   182  	if err != nil {
   183  		return
   184  	}
   185  
   186  	if str := info.Get(oss.HTTPHeaderContentLength); str != "" {
   187  		size, err = strconv.ParseInt(str, 10, 64)
   188  		if err != nil {
   189  			return
   190  		}
   191  	}
   192  
   193  	return
   194  }
   195  
   196  func (a *AliyunSDK) Exists(
   197  	ctx context.Context,
   198  	key string,
   199  ) (
   200  	bool,
   201  	error,
   202  ) {
   203  
   204  	if err := ctx.Err(); err != nil {
   205  		return false, err
   206  	}
   207  
   208  	_, err := a.statObject(
   209  		ctx,
   210  		key,
   211  	)
   212  	if err != nil {
   213  		if a.is404(err) {
   214  			return false, nil
   215  		}
   216  		return false, err
   217  	}
   218  
   219  	return true, nil
   220  }
   221  
   222  func (a *AliyunSDK) Write(
   223  	ctx context.Context,
   224  	key string,
   225  	r io.Reader,
   226  	size int64,
   227  	expire *time.Time,
   228  ) (
   229  	err error,
   230  ) {
   231  
   232  	err = a.putObject(
   233  		ctx,
   234  		key,
   235  		r,
   236  		size,
   237  		expire,
   238  	)
   239  	if err != nil {
   240  		return err
   241  	}
   242  
   243  	return
   244  }
   245  
   246  func (a *AliyunSDK) Read(
   247  	ctx context.Context,
   248  	key string,
   249  	min *int64,
   250  	max *int64,
   251  ) (
   252  	r io.ReadCloser,
   253  	err error,
   254  ) {
   255  
   256  	defer func() {
   257  		if a.is404(err) {
   258  			err = moerr.NewFileNotFoundNoCtx(key)
   259  		}
   260  	}()
   261  
   262  	if max == nil {
   263  		// read to end
   264  		r, err := a.getObject(
   265  			ctx,
   266  			key,
   267  			min,
   268  			nil,
   269  		)
   270  		if err != nil {
   271  			return nil, err
   272  		}
   273  		return r, nil
   274  	}
   275  
   276  	r, err = a.getObject(
   277  		ctx,
   278  		key,
   279  		min,
   280  		max,
   281  	)
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  	return &readCloser{
   286  		r:         io.LimitReader(r, int64(*max-*min)),
   287  		closeFunc: r.Close,
   288  	}, nil
   289  }
   290  
   291  func (a *AliyunSDK) Delete(
   292  	ctx context.Context,
   293  	keys ...string,
   294  ) (
   295  	err error,
   296  ) {
   297  
   298  	if err := ctx.Err(); err != nil {
   299  		return err
   300  	}
   301  
   302  	if len(keys) == 0 {
   303  		return nil
   304  	}
   305  	if len(keys) == 1 {
   306  		return a.deleteSingle(ctx, keys[0])
   307  	}
   308  
   309  	for i := 0; i < len(keys); i += 1000 {
   310  		end := i + 1000
   311  		if end > len(keys) {
   312  			end = len(keys)
   313  		}
   314  		if _, err := a.deleteObjects(ctx, keys[i:end]...); err != nil {
   315  			return err
   316  		}
   317  	}
   318  
   319  	return nil
   320  }
   321  
   322  func (a *AliyunSDK) deleteSingle(ctx context.Context, key string) error {
   323  	ctx, span := trace.Start(ctx, "AliyunSDK.deleteSingle")
   324  	defer span.End()
   325  
   326  	_, err := a.deleteObject(
   327  		ctx,
   328  		key,
   329  	)
   330  	if err != nil {
   331  		return err
   332  	}
   333  
   334  	return nil
   335  }
   336  
   337  func (a *AliyunSDK) listObjects(ctx context.Context, prefix string, cont string) (oss.ListObjectsResultV2, error) {
   338  	ctx, task := gotrace.NewTask(ctx, "AliyunSDK.listObjects")
   339  	defer task.End()
   340  	perfcounter.Update(ctx, func(counter *perfcounter.CounterSet) {
   341  		counter.FileService.S3.List.Add(1)
   342  	}, a.perfCounterSets...)
   343  	opts := []oss.Option{
   344  		oss.WithContext(ctx),
   345  		oss.Delimiter("/"),
   346  	}
   347  	if prefix != "" {
   348  		opts = append(opts, oss.Prefix(prefix))
   349  	}
   350  	if cont != "" {
   351  		opts = append(opts, oss.ContinuationToken(cont))
   352  	}
   353  	if a.listMaxKeys > 0 {
   354  		opts = append(opts, oss.MaxKeys(a.listMaxKeys))
   355  	}
   356  	return DoWithRetry(
   357  		"s3 list objects",
   358  		func() (oss.ListObjectsResultV2, error) {
   359  			return a.bucket.ListObjectsV2(opts...)
   360  		},
   361  		maxRetryAttemps,
   362  		IsRetryableError,
   363  	)
   364  }
   365  
   366  func (a *AliyunSDK) statObject(ctx context.Context, key string) (http.Header, error) {
   367  	ctx, task := gotrace.NewTask(ctx, "AliyunSDK.statObject")
   368  	defer task.End()
   369  	perfcounter.Update(ctx, func(counter *perfcounter.CounterSet) {
   370  		counter.FileService.S3.Head.Add(1)
   371  	}, a.perfCounterSets...)
   372  	return DoWithRetry(
   373  		"s3 head object",
   374  		func() (http.Header, error) {
   375  			return a.bucket.GetObjectMeta(
   376  				key,
   377  				oss.WithContext(ctx),
   378  			)
   379  		},
   380  		maxRetryAttemps,
   381  		IsRetryableError,
   382  	)
   383  }
   384  
   385  func (a *AliyunSDK) putObject(
   386  	ctx context.Context,
   387  	key string,
   388  	r io.Reader,
   389  	size int64,
   390  	expire *time.Time,
   391  ) (err error) {
   392  	defer catch(&err)
   393  	ctx, task := gotrace.NewTask(ctx, "AliyunSDK.putObject")
   394  	defer task.End()
   395  	perfcounter.Update(ctx, func(counter *perfcounter.CounterSet) {
   396  		counter.FileService.S3.Put.Add(1)
   397  	}, a.perfCounterSets...)
   398  	// not retryable because Reader may be half consumed
   399  	opts := []oss.Option{
   400  		oss.WithContext(ctx),
   401  	}
   402  	if expire != nil {
   403  		opts = append(opts, oss.Expires(*expire))
   404  	}
   405  	return a.bucket.PutObject(
   406  		key,
   407  		r,
   408  		opts...,
   409  	)
   410  }
   411  
   412  func (a *AliyunSDK) getObject(ctx context.Context, key string, min *int64, max *int64) (io.ReadCloser, error) {
   413  	ctx, task := gotrace.NewTask(ctx, "AliyunSDK.getObject")
   414  	defer task.End()
   415  	perfcounter.Update(ctx, func(counter *perfcounter.CounterSet) {
   416  		counter.FileService.S3.Get.Add(1)
   417  	}, a.perfCounterSets...)
   418  	r, err := newRetryableReader(
   419  		func(offset int64) (io.ReadCloser, error) {
   420  			opts := []oss.Option{
   421  				oss.WithContext(ctx),
   422  			}
   423  			var rang string
   424  			if max != nil {
   425  				rang = fmt.Sprintf("%d-%d", offset, *max)
   426  			} else {
   427  				rang = fmt.Sprintf("%d-", offset)
   428  			}
   429  			opts = append(opts, oss.NormalizedRange(rang))
   430  			opts = append(opts, oss.RangeBehavior("standard"))
   431  			r, err := DoWithRetry(
   432  				"s3 get object",
   433  				func() (io.ReadCloser, error) {
   434  					return a.bucket.GetObject(
   435  						key,
   436  						opts...,
   437  					)
   438  				},
   439  				maxRetryAttemps,
   440  				IsRetryableError,
   441  			)
   442  			if err != nil {
   443  				return nil, err
   444  			}
   445  			return r, nil
   446  		},
   447  		*min,
   448  		IsRetryableError,
   449  	)
   450  	if err != nil {
   451  		return nil, err
   452  	}
   453  	return r, nil
   454  }
   455  
   456  func (a *AliyunSDK) deleteObject(ctx context.Context, key string) (bool, error) {
   457  	ctx, task := gotrace.NewTask(ctx, "AliyunSDK.deleteObject")
   458  	defer task.End()
   459  	perfcounter.Update(ctx, func(counter *perfcounter.CounterSet) {
   460  		counter.FileService.S3.Delete.Add(1)
   461  	}, a.perfCounterSets...)
   462  	return DoWithRetry[bool](
   463  		"s3 delete object",
   464  		func() (bool, error) {
   465  			if err := a.bucket.DeleteObject(
   466  				key,
   467  				oss.WithContext(ctx),
   468  			); err != nil {
   469  				return false, err
   470  			}
   471  			return true, nil
   472  		},
   473  		maxRetryAttemps,
   474  		IsRetryableError,
   475  	)
   476  }
   477  
   478  func (a *AliyunSDK) deleteObjects(ctx context.Context, keys ...string) (bool, error) {
   479  	ctx, task := gotrace.NewTask(ctx, "AliyunSDK.deleteObjects")
   480  	defer task.End()
   481  	perfcounter.Update(ctx, func(counter *perfcounter.CounterSet) {
   482  		counter.FileService.S3.DeleteMulti.Add(1)
   483  	}, a.perfCounterSets...)
   484  	return DoWithRetry[bool](
   485  		"s3 delete objects",
   486  		func() (bool, error) {
   487  			_, err := a.bucket.DeleteObjects(
   488  				keys,
   489  				oss.WithContext(ctx),
   490  			)
   491  			if err != nil {
   492  				return false, err
   493  			}
   494  			return true, nil
   495  		},
   496  		maxRetryAttemps,
   497  		IsRetryableError,
   498  	)
   499  }
   500  
   501  func (a *AliyunSDK) is404(err error) bool {
   502  	if err == nil {
   503  		return false
   504  	}
   505  	var ossErr oss.ServiceError
   506  	if errors.As(err, &ossErr) {
   507  		if ossErr.Code == "NoSuchKey" {
   508  			return true
   509  		}
   510  	}
   511  	return false
   512  }
   513  
   514  func (o ObjectStorageArguments) credentialProviderForAliyunSDK(
   515  	ctx context.Context,
   516  ) (ret oss.CredentialsProvider, err error) {
   517  
   518  	defer func() {
   519  		if err != nil {
   520  			return
   521  		}
   522  		// chain assume role provider
   523  		if o.RoleARN != "" {
   524  			logutil.Info("with role arn")
   525  			upstream := ret
   526  			ret = &aliyunAssumeRoleCredentialsProvider{
   527  				args:     o,
   528  				upstream: upstream,
   529  			}
   530  		}
   531  	}()
   532  
   533  	config := new(credentials.Config)
   534  
   535  	// access key
   536  	if o.KeyID != "" && o.KeySecret != "" {
   537  
   538  		if o.SecurityToken != "" {
   539  			// sts
   540  			config.SetType("sts")
   541  			config.SetAccessKeyId(o.KeyID)
   542  			config.SetAccessKeySecret(o.KeySecret)
   543  			config.SetSecurityToken(o.SecurityToken)
   544  
   545  		} else {
   546  			// static
   547  			config.SetType("access_key")
   548  			config.SetAccessKeyId(o.KeyID)
   549  			config.SetAccessKeySecret(o.KeySecret)
   550  		}
   551  
   552  	} else if o.RAMRole != "" {
   553  		// ecs ram role
   554  		config.SetType("ecs_ram_role")
   555  		config.SetRoleName(o.RAMRole)
   556  
   557  	} else if o.BearerToken != "" {
   558  		// bearer token
   559  		config.SetType("bearer")
   560  		config.SetBearerToken(o.BearerToken)
   561  	}
   562  
   563  	if config.Type == nil {
   564  
   565  		if !o.shouldLoadDefaultCredentials() {
   566  			return nil, moerr.NewInvalidInputNoCtx(
   567  				"no valid credentials",
   568  			)
   569  		}
   570  
   571  		// check aws env
   572  		awsCredentials := awscredentials.NewEnvCredentials()
   573  		_, err = awsCredentials.Get()
   574  		if err == nil {
   575  			logutil.Info("using aws env credentials")
   576  			return aliyunCredentialsProviderFunc(func() (string, string, string) {
   577  				v, err := awsCredentials.Get()
   578  				if err != nil {
   579  					throw(err)
   580  				}
   581  				return v.AccessKeyID, v.SecretAccessKey, v.SessionToken
   582  			}), nil
   583  		}
   584  
   585  		// default chain
   586  		logutil.Info("using default credential chain")
   587  		provider, err := credentials.NewCredential(nil)
   588  		if err != nil {
   589  			return nil, err
   590  		}
   591  		ret := toOSSCredentialProvider(provider)
   592  		return ret, nil
   593  	}
   594  
   595  	// from config
   596  	logutil.Info("credential from config",
   597  		zap.Any("type", *config.Type),
   598  	)
   599  	provider, err := credentials.NewCredential(config)
   600  	if err != nil {
   601  		return nil, err
   602  	}
   603  	return toOSSCredentialProvider(provider), nil
   604  
   605  }
   606  
   607  type aliyunCredentialsProviderFunc func() (string, string, string)
   608  
   609  var _ oss.CredentialsProvider = aliyunCredentialsProviderFunc(nil)
   610  
   611  func (a aliyunCredentialsProviderFunc) GetCredentials() oss.Credentials {
   612  	id, secret, token := a()
   613  	return &aliyunCredential{
   614  		KeyID:         id,
   615  		KeySecret:     secret,
   616  		SecurityToken: token,
   617  	}
   618  }
   619  
   620  type aliyunCredential struct {
   621  	KeyID         string
   622  	KeySecret     string
   623  	SecurityToken string
   624  }
   625  
   626  var _ oss.Credentials = aliyunCredential{}
   627  
   628  func (a aliyunCredential) GetAccessKeyID() string {
   629  	return a.KeyID
   630  }
   631  
   632  func (a aliyunCredential) GetAccessKeySecret() string {
   633  	return a.KeySecret
   634  }
   635  
   636  func (a aliyunCredential) GetSecurityToken() string {
   637  	return a.SecurityToken
   638  }
   639  
   640  func toOSSCredentialProvider(
   641  	provider credentials.Credential,
   642  ) oss.CredentialsProvider {
   643  	return &ossCredentialProvider{
   644  		upstream: provider,
   645  	}
   646  }
   647  
   648  type ossCredentialProvider struct {
   649  	upstream credentials.Credential
   650  }
   651  
   652  var _ oss.CredentialsProvider = new(ossCredentialProvider)
   653  
   654  func (o *ossCredentialProvider) GetCredentials() oss.Credentials {
   655  	return o
   656  }
   657  
   658  var _ oss.Credentials = new(ossCredentialProvider)
   659  
   660  func (o *ossCredentialProvider) GetAccessKeyID() string {
   661  	ret, err := o.upstream.GetAccessKeyId()
   662  	if err != nil {
   663  		throw(err)
   664  	}
   665  	return *ret
   666  }
   667  
   668  func (o *ossCredentialProvider) GetAccessKeySecret() string {
   669  	ret, err := o.upstream.GetAccessKeySecret()
   670  	if err != nil {
   671  		throw(err)
   672  	}
   673  	return *ret
   674  }
   675  
   676  func (o *ossCredentialProvider) GetSecurityToken() string {
   677  	ret, err := o.upstream.GetSecurityToken()
   678  	if err != nil {
   679  		throw(err)
   680  	}
   681  	return *ret
   682  }
   683  
   684  type aliyunAssumeRoleCredentialsProvider struct {
   685  	args       ObjectStorageArguments
   686  	upstream   oss.CredentialsProvider
   687  	credential aliyunCredential
   688  	validUntil time.Time
   689  }
   690  
   691  var _ oss.CredentialsProvider = new(aliyunAssumeRoleCredentialsProvider)
   692  
   693  func (a *aliyunAssumeRoleCredentialsProvider) GetCredentials() oss.Credentials {
   694  	if err := a.refresh(); err != nil {
   695  		throw(err)
   696  	}
   697  	return a.credential
   698  }
   699  
   700  func (a *aliyunAssumeRoleCredentialsProvider) refresh() error {
   701  	if time.Until(a.validUntil) > time.Minute*5 {
   702  		return nil
   703  	}
   704  
   705  	credential := a.upstream.GetCredentials()
   706  	var client *sts.Client
   707  	var err error
   708  	if securityToken := credential.GetSecurityToken(); securityToken != "" {
   709  		client, err = sts.NewClientWithStsToken(
   710  			a.args.Region,
   711  			credential.GetAccessKeyID(),
   712  			credential.GetAccessKeySecret(),
   713  			securityToken,
   714  		)
   715  	} else {
   716  		client, err = sts.NewClientWithAccessKey(
   717  			a.args.Region,
   718  			credential.GetAccessKeyID(),
   719  			credential.GetAccessKeySecret(),
   720  		)
   721  	}
   722  	if err != nil {
   723  		return err
   724  	}
   725  
   726  	req := sts.CreateAssumeRoleRequest()
   727  	req.Scheme = "https"
   728  	req.RoleSessionName = a.args.RoleSessionName
   729  	req.DurationSeconds = requests.NewInteger(int(aliyunCredentialExpireDuration / time.Second))
   730  	req.ExternalId = a.args.ExternalID
   731  	req.RoleArn = a.args.RoleARN
   732  
   733  	resp, err := client.AssumeRole(req)
   734  	if err != nil {
   735  		return err
   736  	}
   737  
   738  	expire, err := time.Parse(time.RFC3339, resp.Credentials.Expiration)
   739  	if err != nil {
   740  		logutil.Warn("bad expire time from response",
   741  			zap.Any("time", resp.Credentials.Expiration),
   742  		)
   743  		a.validUntil = time.Now().Add(aliyunCredentialExpireDuration)
   744  	} else {
   745  		a.validUntil = expire
   746  	}
   747  	a.credential.KeyID = resp.Credentials.AccessKeyId
   748  	a.credential.KeySecret = resp.Credentials.AccessKeySecret
   749  	a.credential.SecurityToken = resp.Credentials.SecurityToken
   750  
   751  	return nil
   752  }