github.com/matrixorigin/matrixone@v1.2.0/pkg/fileservice/minio_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  	"crypto/tls"
    20  	"crypto/x509"
    21  	"errors"
    22  	"io"
    23  	"net"
    24  	"net/http"
    25  	"net/url"
    26  	"os"
    27  	gotrace "runtime/trace"
    28  	"strings"
    29  	"time"
    30  
    31  	"github.com/matrixorigin/matrixone/pkg/common/moerr"
    32  	"github.com/matrixorigin/matrixone/pkg/logutil"
    33  	"github.com/matrixorigin/matrixone/pkg/perfcounter"
    34  	"github.com/matrixorigin/matrixone/pkg/util/trace"
    35  	"github.com/minio/minio-go/v7"
    36  	"github.com/minio/minio-go/v7/pkg/credentials"
    37  	"go.uber.org/zap"
    38  )
    39  
    40  type MinioSDK struct {
    41  	name            string
    42  	bucket          string
    43  	core            *minio.Core
    44  	client          *minio.Client
    45  	perfCounterSets []*perfcounter.CounterSet
    46  	listMaxKeys     int
    47  }
    48  
    49  func NewMinioSDK(
    50  	ctx context.Context,
    51  	args ObjectStorageArguments,
    52  	perfCounterSets []*perfcounter.CounterSet,
    53  ) (*MinioSDK, error) {
    54  
    55  	if err := args.validate(); err != nil {
    56  		return nil, err
    57  	}
    58  
    59  	options := new(minio.Options)
    60  
    61  	// credentials
    62  	var credentialProviders []credentials.Provider
    63  	if args.shouldLoadDefaultCredentials() {
    64  		credentialProviders = append(credentialProviders,
    65  			// aws env
    66  			new(credentials.EnvAWS),
    67  			// minio env
    68  			new(credentials.EnvMinio),
    69  		)
    70  	}
    71  	if args.KeyID != "" && args.KeySecret != "" {
    72  		// static
    73  		credentialProviders = append(credentialProviders, &credentials.Static{
    74  			Value: credentials.Value{
    75  				AccessKeyID:     args.KeyID,
    76  				SecretAccessKey: args.KeySecret,
    77  				SessionToken:    args.SessionToken,
    78  				SignerType:      credentials.SignatureV4,
    79  			},
    80  		})
    81  	}
    82  	if args.RoleARN != "" {
    83  		// assume role
    84  		credentialProviders = append(credentialProviders, &credentials.STSAssumeRole{
    85  			Options: credentials.STSAssumeRoleOptions{
    86  				AccessKey:       args.KeyID,
    87  				SecretKey:       args.KeySecret,
    88  				RoleARN:         args.RoleARN,
    89  				RoleSessionName: args.ExternalID,
    90  			},
    91  		})
    92  	}
    93  
    94  	// special treatments for 天翼云
    95  	if strings.Contains(args.Endpoint, "ctyunapi.cn") {
    96  		if args.KeyID == "" {
    97  			// try to fetch one
    98  			creds := credentials.NewChainCredentials(credentialProviders)
    99  			value, err := creds.Get()
   100  			if err != nil {
   101  				return nil, err
   102  			}
   103  			args.KeyID = value.AccessKeyID
   104  			args.KeySecret = value.SecretAccessKey
   105  			args.SessionToken = value.SessionToken
   106  		}
   107  		credentialProviders = []credentials.Provider{
   108  			&credentials.Static{
   109  				Value: credentials.Value{
   110  					AccessKeyID:     args.KeyID,
   111  					SecretAccessKey: args.KeySecret,
   112  					SessionToken:    args.SessionToken,
   113  					SignerType:      credentials.SignatureV2,
   114  				},
   115  			},
   116  		}
   117  	}
   118  
   119  	options.Creds = credentials.NewChainCredentials(credentialProviders)
   120  
   121  	// region
   122  	if args.Region != "" {
   123  		options.Region = args.Region
   124  	}
   125  
   126  	// transport
   127  	dialer := &net.Dialer{
   128  		KeepAlive: 5 * time.Second,
   129  	}
   130  	transport := &http.Transport{
   131  		Proxy:                 http.ProxyFromEnvironment,
   132  		DialContext:           dialer.DialContext,
   133  		MaxIdleConns:          100,
   134  		IdleConnTimeout:       180 * time.Second,
   135  		MaxIdleConnsPerHost:   100,
   136  		MaxConnsPerHost:       1000,
   137  		TLSHandshakeTimeout:   3 * time.Second,
   138  		ExpectContinueTimeout: 1 * time.Second,
   139  		ForceAttemptHTTP2:     true,
   140  	}
   141  	if len(args.CertFiles) > 0 {
   142  		// custom certs
   143  		pool, err := x509.SystemCertPool()
   144  		if err != nil {
   145  			panic(err)
   146  		}
   147  		for _, path := range args.CertFiles {
   148  			content, err := os.ReadFile(path)
   149  			if err != nil {
   150  				logutil.Info("load cert file error",
   151  					zap.Any("err", err),
   152  				)
   153  				// ignore
   154  				continue
   155  			}
   156  			logutil.Info("file service: load cert file",
   157  				zap.Any("path", path),
   158  			)
   159  			pool.AppendCertsFromPEM(content)
   160  		}
   161  		tlsConfig := &tls.Config{
   162  			InsecureSkipVerify: true,
   163  			RootCAs:            pool,
   164  		}
   165  		transport.TLSClientConfig = tlsConfig
   166  	}
   167  	options.Transport = transport
   168  
   169  	// endpoint
   170  	isSecure, err := minioValidateEndpoint(&args)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  	options.Secure = isSecure
   175  
   176  	client, err := minio.New(args.Endpoint, options)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  	core, err := minio.NewCore(args.Endpoint, options)
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  
   185  	logutil.Info("new object storage",
   186  		zap.Any("sdk", "minio"),
   187  		zap.Any("arguments", args),
   188  	)
   189  
   190  	if !args.NoBucketValidation {
   191  		// validate
   192  		ok, err := client.BucketExists(ctx, args.Bucket)
   193  		if err != nil {
   194  			return nil, err
   195  		}
   196  		if !ok {
   197  			return nil, moerr.NewInternalErrorNoCtx(
   198  				"bad s3 config, no such bucket or no permissions: %v",
   199  				args.Bucket,
   200  			)
   201  		}
   202  	}
   203  
   204  	return &MinioSDK{
   205  		name:            args.Name,
   206  		bucket:          args.Bucket,
   207  		client:          client,
   208  		core:            core,
   209  		perfCounterSets: perfCounterSets,
   210  	}, nil
   211  }
   212  
   213  var _ ObjectStorage = new(MinioSDK)
   214  
   215  func (a *MinioSDK) List(
   216  	ctx context.Context,
   217  	prefix string,
   218  	fn func(bool, string, int64) (bool, error),
   219  ) error {
   220  
   221  	if err := ctx.Err(); err != nil {
   222  		return err
   223  	}
   224  
   225  	var marker string
   226  
   227  loop1:
   228  	for {
   229  		result, err := a.listObjects(ctx, prefix, marker)
   230  		if err != nil {
   231  			return err
   232  		}
   233  
   234  		for _, obj := range result.Contents {
   235  			more, err := fn(false, obj.Key, obj.Size)
   236  			if err != nil {
   237  				return err
   238  			}
   239  			if !more {
   240  				break loop1
   241  			}
   242  		}
   243  
   244  		for _, prefix := range result.CommonPrefixes {
   245  			more, err := fn(true, prefix.Prefix, 0)
   246  			if err != nil {
   247  				return err
   248  			}
   249  			if !more {
   250  				break loop1
   251  			}
   252  		}
   253  
   254  		if !result.IsTruncated {
   255  			break
   256  		}
   257  		marker = result.Marker
   258  	}
   259  
   260  	return nil
   261  }
   262  
   263  func (a *MinioSDK) Stat(
   264  	ctx context.Context,
   265  	key string,
   266  ) (
   267  	size int64,
   268  	err error,
   269  ) {
   270  
   271  	if err := ctx.Err(); err != nil {
   272  		return 0, err
   273  	}
   274  
   275  	info, err := a.statObject(
   276  		ctx,
   277  		key,
   278  	)
   279  	if err != nil {
   280  		return
   281  	}
   282  
   283  	size = info.Size
   284  
   285  	return
   286  }
   287  
   288  func (a *MinioSDK) Exists(
   289  	ctx context.Context,
   290  	key string,
   291  ) (
   292  	bool,
   293  	error,
   294  ) {
   295  
   296  	if err := ctx.Err(); err != nil {
   297  		return false, err
   298  	}
   299  
   300  	_, err := a.statObject(
   301  		ctx,
   302  		key,
   303  	)
   304  	if err != nil {
   305  		if a.is404(err) {
   306  			return false, nil
   307  		}
   308  		return false, err
   309  	}
   310  
   311  	return true, nil
   312  }
   313  
   314  func (a *MinioSDK) Write(
   315  	ctx context.Context,
   316  	key string,
   317  	r io.Reader,
   318  	size int64,
   319  	expire *time.Time,
   320  ) (
   321  	err error,
   322  ) {
   323  
   324  	_, err = a.putObject(
   325  		ctx,
   326  		key,
   327  		r,
   328  		size,
   329  		expire,
   330  	)
   331  	if err != nil {
   332  		return err
   333  	}
   334  
   335  	return
   336  }
   337  
   338  func (a *MinioSDK) Read(
   339  	ctx context.Context,
   340  	key string,
   341  	min *int64,
   342  	max *int64,
   343  ) (
   344  	r io.ReadCloser,
   345  	err error,
   346  ) {
   347  
   348  	defer func() {
   349  		if a.is404(err) {
   350  			err = moerr.NewFileNotFoundNoCtx(key)
   351  		}
   352  	}()
   353  
   354  	if max == nil {
   355  		// read to end
   356  		r, err := a.getObject(
   357  			ctx,
   358  			key,
   359  			min,
   360  			nil,
   361  		)
   362  		if err != nil {
   363  			return nil, err
   364  		}
   365  		return r, nil
   366  	}
   367  
   368  	r, err = a.getObject(
   369  		ctx,
   370  		key,
   371  		min,
   372  		max,
   373  	)
   374  	if err != nil {
   375  		return nil, err
   376  	}
   377  	return &readCloser{
   378  		r:         io.LimitReader(r, int64(*max-*min)),
   379  		closeFunc: r.Close,
   380  	}, nil
   381  }
   382  
   383  func (a *MinioSDK) Delete(
   384  	ctx context.Context,
   385  	keys ...string,
   386  ) (
   387  	err error,
   388  ) {
   389  
   390  	if err := ctx.Err(); err != nil {
   391  		return err
   392  	}
   393  
   394  	if len(keys) == 0 {
   395  		return nil
   396  	}
   397  	if len(keys) == 1 {
   398  		return a.deleteSingle(ctx, keys[0])
   399  	}
   400  
   401  	for i := 0; i < len(keys); i += 1000 {
   402  		end := i + 1000
   403  		if end > len(keys) {
   404  			end = len(keys)
   405  		}
   406  		if _, err := a.deleteObjects(ctx, keys[i:end]...); err != nil {
   407  			return err
   408  		}
   409  	}
   410  
   411  	return nil
   412  }
   413  
   414  func (a *MinioSDK) deleteSingle(ctx context.Context, key string) error {
   415  	ctx, span := trace.Start(ctx, "MinioSDK.deleteSingle")
   416  	defer span.End()
   417  
   418  	_, err := a.deleteObject(
   419  		ctx,
   420  		key,
   421  	)
   422  	if err != nil {
   423  		return err
   424  	}
   425  
   426  	return nil
   427  }
   428  
   429  func (a *MinioSDK) listObjects(ctx context.Context, prefix string, marker string) (minio.ListBucketResult, error) {
   430  	ctx, task := gotrace.NewTask(ctx, "MinioSDK.listObjects")
   431  	defer task.End()
   432  	perfcounter.Update(ctx, func(counter *perfcounter.CounterSet) {
   433  		counter.FileService.S3.List.Add(1)
   434  	}, a.perfCounterSets...)
   435  	return DoWithRetry(
   436  		"s3 list objects",
   437  		func() (minio.ListBucketResult, error) {
   438  			return a.core.ListObjects(
   439  				a.bucket,
   440  				prefix,
   441  				marker,
   442  				"/",
   443  				a.listMaxKeys,
   444  			)
   445  		},
   446  		maxRetryAttemps,
   447  		IsRetryableError,
   448  	)
   449  }
   450  
   451  func (a *MinioSDK) statObject(ctx context.Context, key string) (minio.ObjectInfo, error) {
   452  	ctx, task := gotrace.NewTask(ctx, "MinioSDK.statObject")
   453  	defer task.End()
   454  	perfcounter.Update(ctx, func(counter *perfcounter.CounterSet) {
   455  		counter.FileService.S3.Head.Add(1)
   456  	}, a.perfCounterSets...)
   457  	return DoWithRetry(
   458  		"s3 head object",
   459  		func() (minio.ObjectInfo, error) {
   460  			return a.client.StatObject(
   461  				ctx,
   462  				a.bucket,
   463  				key,
   464  				minio.StatObjectOptions{},
   465  			)
   466  		},
   467  		maxRetryAttemps,
   468  		IsRetryableError,
   469  	)
   470  }
   471  
   472  func (a *MinioSDK) putObject(
   473  	ctx context.Context,
   474  	key string,
   475  	r io.Reader,
   476  	size int64,
   477  	expire *time.Time,
   478  ) (minio.UploadInfo, error) {
   479  	ctx, task := gotrace.NewTask(ctx, "MinioSDK.putObject")
   480  	defer task.End()
   481  	perfcounter.Update(ctx, func(counter *perfcounter.CounterSet) {
   482  		counter.FileService.S3.Put.Add(1)
   483  	}, a.perfCounterSets...)
   484  	// not retryable because Reader may be half consumed
   485  	//TODO set expire
   486  	return a.client.PutObject(
   487  		ctx,
   488  		a.bucket,
   489  		key,
   490  		r,
   491  		size,
   492  		minio.PutObjectOptions{},
   493  	)
   494  }
   495  
   496  func (a *MinioSDK) getObject(ctx context.Context, key string, min *int64, max *int64) (io.ReadCloser, error) {
   497  	ctx, task := gotrace.NewTask(ctx, "MinioSDK.getObject")
   498  	defer task.End()
   499  	perfcounter.Update(ctx, func(counter *perfcounter.CounterSet) {
   500  		counter.FileService.S3.Get.Add(1)
   501  	}, a.perfCounterSets...)
   502  	r, err := newRetryableReader(
   503  		func(offset int64) (io.ReadCloser, error) {
   504  			obj, err := DoWithRetry(
   505  				"s3 get object",
   506  				func() (obj *minio.Object, err error) {
   507  					return a.client.GetObject(ctx, a.bucket, key, minio.GetObjectOptions{})
   508  				},
   509  				maxRetryAttemps,
   510  				IsRetryableError,
   511  			)
   512  			if err != nil {
   513  				return nil, err
   514  			}
   515  			if offset > 0 {
   516  				if _, err := obj.Seek(offset, 0); err != nil {
   517  					return nil, err
   518  				}
   519  			}
   520  			return obj, nil
   521  		},
   522  		*min,
   523  		IsRetryableError,
   524  	)
   525  	if err != nil {
   526  		return nil, err
   527  	}
   528  	return r, nil
   529  }
   530  
   531  func (a *MinioSDK) deleteObject(ctx context.Context, key string) (any, error) {
   532  	ctx, task := gotrace.NewTask(ctx, "MinioSDK.deleteObject")
   533  	defer task.End()
   534  	perfcounter.Update(ctx, func(counter *perfcounter.CounterSet) {
   535  		counter.FileService.S3.Delete.Add(1)
   536  	}, a.perfCounterSets...)
   537  	return DoWithRetry(
   538  		"s3 delete object",
   539  		func() (any, error) {
   540  			if err := a.client.RemoveObject(ctx, a.bucket, key, minio.RemoveObjectOptions{}); err != nil {
   541  				return nil, err
   542  			}
   543  			return nil, nil
   544  		},
   545  		maxRetryAttemps,
   546  		IsRetryableError,
   547  	)
   548  }
   549  
   550  func (a *MinioSDK) deleteObjects(ctx context.Context, keys ...string) (any, error) {
   551  	ctx, task := gotrace.NewTask(ctx, "MinioSDK.deleteObjects")
   552  	defer task.End()
   553  	perfcounter.Update(ctx, func(counter *perfcounter.CounterSet) {
   554  		counter.FileService.S3.DeleteMulti.Add(1)
   555  	}, a.perfCounterSets...)
   556  	return DoWithRetry(
   557  		"s3 delete objects",
   558  		func() (any, error) {
   559  			objsCh := make(chan minio.ObjectInfo)
   560  			errCh := a.client.RemoveObjects(ctx, a.bucket, objsCh, minio.RemoveObjectsOptions{})
   561  			for _, key := range keys {
   562  				objsCh <- minio.ObjectInfo{
   563  					Key: key,
   564  				}
   565  			}
   566  			for err := range errCh {
   567  				return nil, err.Err
   568  			}
   569  			return nil, nil
   570  		},
   571  		maxRetryAttemps,
   572  		IsRetryableError,
   573  	)
   574  }
   575  
   576  func (a *MinioSDK) is404(err error) bool {
   577  	if err == nil {
   578  		return false
   579  	}
   580  	var resp minio.ErrorResponse
   581  	if !errors.As(err, &resp) {
   582  		return false
   583  	}
   584  	return resp.Code == "NoSuchKey"
   585  }
   586  
   587  func minioValidateEndpoint(args *ObjectStorageArguments) (isSecure bool, err error) {
   588  	if args.Endpoint == "" {
   589  		return false, nil
   590  	}
   591  
   592  	endpointURL, err := url.Parse(args.Endpoint)
   593  	if err != nil {
   594  		return false, err
   595  	}
   596  	isSecure = endpointURL.Scheme == "https"
   597  	endpointURL.Scheme = ""
   598  	args.Endpoint = endpointURL.String()
   599  	args.Endpoint = strings.TrimLeft(args.Endpoint, "/")
   600  
   601  	return
   602  }