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 }