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 }