github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/ais/backend/aws.go (about) 1 //go:build aws 2 3 // Package backend contains implementation of various backend providers. 4 /* 5 * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved. 6 */ 7 package backend 8 9 import ( 10 "context" 11 "errors" 12 "fmt" 13 "io" 14 "net/http" 15 "os" 16 "strconv" 17 "strings" 18 "sync" 19 "time" 20 21 aiss3 "github.com/NVIDIA/aistore/ais/s3" 22 "github.com/NVIDIA/aistore/api/apc" 23 "github.com/NVIDIA/aistore/api/env" 24 "github.com/NVIDIA/aistore/cmn" 25 "github.com/NVIDIA/aistore/cmn/cos" 26 "github.com/NVIDIA/aistore/cmn/debug" 27 "github.com/NVIDIA/aistore/cmn/feat" 28 "github.com/NVIDIA/aistore/cmn/nlog" 29 "github.com/NVIDIA/aistore/core" 30 "github.com/NVIDIA/aistore/core/meta" 31 "github.com/NVIDIA/aistore/memsys" 32 "github.com/aws/aws-sdk-go-v2/aws" 33 awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" 34 "github.com/aws/aws-sdk-go-v2/config" 35 s3manager "github.com/aws/aws-sdk-go-v2/feature/s3/manager" 36 "github.com/aws/aws-sdk-go-v2/service/s3" 37 "github.com/aws/aws-sdk-go-v2/service/s3/types" 38 "github.com/aws/smithy-go" 39 ) 40 41 type ( 42 s3bp struct { 43 t core.TargetPut 44 mm *memsys.MMSA 45 base 46 } 47 sessConf struct { 48 bck *cmn.Bck 49 region string 50 } 51 ) 52 53 var ( 54 // map[string]*s3.Client, with one s3.Client a.k.a. "svc" 55 // per (profile, region, endpoint) triplet 56 clients sync.Map 57 58 s3Endpoint string 59 awsProfile string 60 ) 61 62 // interface guard 63 var _ core.Backend = (*s3bp)(nil) 64 65 // environment variables => static defaults that can still be overridden via bck.Props.Extra.AWS 66 // in addition to these two (below), default bucket region = env.AwsDefaultRegion() 67 func NewAWS(t core.TargetPut) (core.Backend, error) { 68 s3Endpoint = os.Getenv(env.AWS.Endpoint) 69 awsProfile = os.Getenv(env.AWS.Profile) 70 return &s3bp{ 71 t: t, 72 mm: t.PageMM(), 73 base: base{apc.AWS}, 74 }, nil 75 } 76 77 // as core.Backend -------------------------------------------------------------- 78 79 // 80 // HEAD BUCKET 81 // 82 83 const gotBucketLocation = "got_bucket_location" 84 85 func (*s3bp) HeadBucket(_ context.Context, bck *meta.Bck) (bckProps cos.StrKVs, ecode int, _ error) { 86 var ( 87 cloudBck = bck.RemoteBck() 88 sessConf = sessConf{bck: cloudBck} 89 ) 90 svc, err := sessConf.s3client("") 91 if err != nil { 92 return nil, 0, err 93 } 94 if cmn.Rom.FastV(5, cos.SmoduleBackend) { 95 nlog.Infoln("[head_bucket]", cloudBck.Name) 96 } 97 if sessConf.region == "" { 98 var region string 99 if region, err = getBucketLocation(svc, cloudBck.Name); err != nil { 100 ecode, err = awsErrorToAISError(err, cloudBck, "") 101 return nil, ecode, err 102 } 103 if cmn.Rom.FastV(4, cos.SmoduleBackend) { 104 nlog.Infoln("get-bucket-location", cloudBck.Name, "region", region) 105 } 106 svc, err = sessConf.s3client(gotBucketLocation) 107 debug.AssertNoErr(err) 108 } 109 110 // NOTE: return a few assorted fields, specifically to fill-in vendor-specific `cmn.ExtraProps` 111 bckProps = make(cos.StrKVs, 4) 112 bckProps[apc.HdrBackendProvider] = apc.AWS 113 bckProps[apc.HdrS3Region] = sessConf.region 114 bckProps[apc.HdrS3Endpoint] = "" 115 if bck.Props != nil { 116 bckProps[apc.HdrS3Endpoint] = bck.Props.Extra.AWS.Endpoint 117 } 118 versioned, errV := getBucketVersioning(svc, cloudBck) 119 if errV != nil { 120 ecode, err = awsErrorToAISError(errV, cloudBck, "") 121 return nil, ecode, err 122 } 123 bckProps[apc.HdrBucketVerEnabled] = strconv.FormatBool(versioned) 124 return bckProps, 0, nil 125 } 126 127 // 128 // LIST OBJECTS via INVENTORY 129 // 130 131 // when successful, returns w/ rlock held and inventory's (lom, lmfh) in the context; 132 // otherwise, always unlocks and frees 133 func (s3bp *s3bp) GetBucketInv(bck *meta.Bck, ctx *core.LsoInvCtx) (int, error) { 134 debug.Assert(ctx != nil && ctx.Lom == nil) 135 var ( 136 cloudBck = bck.RemoteBck() 137 sessConf = sessConf{bck: cloudBck} 138 ) 139 svc, err := sessConf.s3client("[get_bucket_inv]") 140 if err != nil { 141 return 0, err 142 } 143 144 // one bucket, one inventory, one statically defined name 145 prefix, objName := aiss3.InvPrefObjname(bck.Bucket(), ctx.Name, ctx.ID) 146 lom := core.AllocLOM(objName) 147 if err = lom.InitBck(bck.Bucket()); err != nil { 148 core.FreeLOM(lom) 149 return 0, err 150 } 151 if !lom.TryLock(false) { 152 err = cmn.NewErrBusy(invTag, lom.Cname(), "likely getting updated") 153 core.FreeLOM(lom) 154 return 0, err 155 } 156 157 lsV2resp, csv, manifest, ecode, err := s3bp.initInventory(cloudBck, svc, ctx, prefix) 158 if err != nil { 159 lom.Unlock(false) 160 core.FreeLOM(lom) 161 return ecode, err 162 } 163 ctx.Lom = lom 164 mtime, usable := checkInvLom(csv.mtime, ctx) 165 if usable { 166 if ctx.Lmfh, err = ctx.Lom.OpenFile(); err != nil { 167 lom.Unlock(false) 168 core.FreeLOM(lom) 169 ctx.Lom = nil 170 return 0, _errInv("usable-inv-open", err) 171 } 172 173 return 0, nil // w/ rlock 174 } 175 176 // rlock -> wlock 177 178 lom.Unlock(false) 179 err = cmn.NewErrBusy(invTag, lom.Cname(), "timed out waiting to acquire write access") // prelim 180 sleep, total := time.Second, invBusyTimeout 181 for total >= 0 { 182 if lom.TryLock(true) { 183 err = nil 184 break 185 } 186 time.Sleep(sleep) 187 total -= sleep 188 } 189 if err != nil { 190 core.FreeLOM(lom) 191 ctx.Lom = nil 192 return 0, err // busy 193 } 194 195 // acquired wlock: check for write/write race 196 197 finfo, err := os.Stat(ctx.Lom.FQN) 198 if err == nil { 199 newMtime := finfo.ModTime() 200 if newMtime.Sub(mtime) > time.Hour { 201 // updated by smbd else 202 // reload the lom and return 203 ctx.Lom.Uncache() 204 _, usable = checkInvLom(newMtime, ctx) 205 debug.Assert(usable) 206 207 // wlock --> rlock must succeed 208 lom.Unlock(true) 209 lom.Lock(false) 210 211 if ctx.Lmfh, err = ctx.Lom.OpenFile(); err != nil { 212 lom.Unlock(false) 213 core.FreeLOM(lom) 214 ctx.Lom = nil 215 return 0, _errInv("reload-inv-open", err) 216 } 217 return 0, nil // ok 218 } 219 } 220 221 // still under wlock: cleanup old, read and write as ctx.Lom 222 223 cleanupOldInventory(cloudBck, svc, lsV2resp, csv, manifest) 224 225 err = s3bp.getInventory(cloudBck, ctx, csv) 226 227 // wlock --> rlock 228 229 lom.Unlock(true) 230 231 if err != nil { 232 core.FreeLOM(lom) 233 ctx.Lom = nil 234 return 0, err 235 } 236 237 lom.Lock(false) // must succeed 238 if ctx.Lmfh, err = ctx.Lom.OpenFile(); err != nil { 239 lom.Unlock(false) 240 core.FreeLOM(lom) 241 ctx.Lom = nil 242 return 0, _errInv("get-inv-open", err) 243 } 244 245 return 0, nil // ok 246 } 247 248 // using local(ized) .csv 249 func (s3bp *s3bp) ListObjectsInv(bck *meta.Bck, msg *apc.LsoMsg, lst *cmn.LsoRes, ctx *core.LsoInvCtx) (err error) { 250 debug.Assert(ctx.Lom != nil && ctx.Lmfh != nil, ctx.Lom, " ", ctx.Lmfh) 251 252 cloudBck := bck.RemoteBck() 253 254 if ctx.SGL == nil { 255 if ctx.EOF { 256 debug.Assert(false) // (unlikely) 257 goto none 258 } 259 ctx.SGL = s3bp.mm.NewSGL(invPageSGL, memsys.DefaultBuf2Size) 260 } else if l := ctx.SGL.Len(); l > 0 && l < invSwapSGL && !ctx.EOF { 261 // swap SGLs 262 sgl := s3bp.mm.NewSGL(invPageSGL, memsys.DefaultBuf2Size) 263 written, err := io.Copy(sgl, ctx.SGL) // buffering not needed - gets executed via sgl WriteTo() 264 debug.AssertNoErr(err) 265 debug.Assert(written == l && sgl.Len() == l, written, " vs ", l, " vs ", sgl.Len()) 266 ctx.SGL.Free() 267 ctx.SGL = sgl 268 } 269 err = s3bp.listInventory(cloudBck, ctx, msg, lst) 270 271 if err == nil || err == io.EOF { 272 return nil 273 } 274 none: 275 lst.Entries = lst.Entries[:0] 276 return err 277 } 278 279 // 280 // LIST OBJECTS 281 // 282 283 // NOTE: obtaining versioning info is extremely slow - to avoid timeouts, imposing a hard limit on the page size 284 const versionedPageSize = 20 285 286 func (*s3bp) ListObjects(bck *meta.Bck, msg *apc.LsoMsg, lst *cmn.LsoRes) (ecode int, _ error) { 287 var ( 288 h = cmn.BackendHelpers.Amazon 289 cloudBck = bck.RemoteBck() 290 sessConf = sessConf{bck: cloudBck} 291 versioning bool 292 ) 293 svc, err := sessConf.s3client("[list_objects]") 294 if err != nil { 295 return 0, err 296 } 297 params := &s3.ListObjectsV2Input{Bucket: aws.String(cloudBck.Name)} 298 if prefix := msg.Prefix; prefix != "" { 299 if msg.IsFlagSet(apc.LsNoRecursion) { 300 // NOTE: important to indicate subdirectory with trailing '/' 301 if cos.IsLastB(prefix, '/') { 302 params.Delimiter = aws.String("/") 303 } 304 } 305 params.Prefix = aws.String(prefix) 306 } 307 if msg.ContinuationToken != "" { 308 params.ContinuationToken = aws.String(msg.ContinuationToken) 309 } 310 311 versioning = bck.Props != nil && bck.Props.Versioning.Enabled && msg.WantProp(apc.GetPropsVersion) 312 msg.PageSize = calcPageSize(msg.PageSize, bck.MaxPageSize()) 313 if versioning { 314 msg.PageSize = min(versionedPageSize, msg.PageSize) 315 } 316 params.MaxKeys = aws.Int32(int32(msg.PageSize)) 317 318 resp, err := svc.ListObjectsV2(context.Background(), params) 319 if err != nil { 320 if cmn.Rom.FastV(4, cos.SmoduleBackend) { 321 nlog.Infoln("list_objects", cloudBck.Name, err) 322 } 323 ecode, err = awsErrorToAISError(err, cloudBck, "") 324 return ecode, err 325 } 326 327 var ( 328 custom cos.StrKVs 329 l = len(resp.Contents) 330 wantCustom = msg.WantProp(apc.GetPropsCustom) 331 ) 332 for i := len(lst.Entries); i < l; i++ { 333 lst.Entries = append(lst.Entries, &cmn.LsoEnt{}) // add missing empty 334 } 335 if wantCustom { 336 custom = make(cos.StrKVs, 2) // reuse 337 } 338 for i, obj := range resp.Contents { 339 entry := lst.Entries[i] 340 entry.Name = *obj.Key 341 entry.Size = *obj.Size 342 if msg.IsFlagSet(apc.LsNameOnly) || msg.IsFlagSet(apc.LsNameSize) { 343 continue 344 } 345 if v, ok := h.EncodeCksum(obj.ETag); ok { 346 entry.Checksum = v 347 } 348 if wantCustom { 349 custom[cmn.ETag] = entry.Checksum 350 mtime := *(obj.LastModified) 351 custom[cmn.LastModified] = fmtTime(mtime) 352 entry.Custom = cmn.CustomMD2S(custom) 353 } 354 } 355 lst.Entries = lst.Entries[:l] 356 357 if *resp.IsTruncated { 358 lst.ContinuationToken = *resp.NextContinuationToken 359 } 360 361 if len(lst.Entries) == 0 || !versioning { 362 if cmn.Rom.FastV(4, cos.SmoduleBackend) { 363 nlog.Infoln("[list_objects]", cloudBck.Name, len(lst.Entries)) 364 } 365 return 0, nil 366 } 367 368 // [slow path] for each already listed object: 369 // - set the `ListObjectVersionsInput.Prefix` to the object's full name 370 // - get the versions and lookup the latest one 371 var ( 372 verParams = &s3.ListObjectVersionsInput{Bucket: aws.String(cloudBck.Name)} 373 num int 374 ) 375 for _, entry := range lst.Entries { 376 verParams.Prefix = aws.String(entry.Name) 377 verResp, err := svc.ListObjectVersions(context.Background(), verParams) 378 if err != nil { 379 return awsErrorToAISError(err, cloudBck, "") 380 } 381 for _, vers := range verResp.Versions { 382 if latest := *(vers.IsLatest); !latest { 383 continue 384 } 385 if key := *(vers.Key); key == entry.Name { 386 v, ok := h.EncodeVersion(vers.VersionId) 387 debug.Assert(ok, entry.Name+": "+*(vers.VersionId)) 388 entry.Version = v 389 num++ 390 } 391 } 392 } 393 if cmn.Rom.FastV(4, cos.SmoduleBackend) { 394 nlog.Infoln("[list_objects]", cloudBck.Name, len(lst.Entries), num) 395 } 396 return 0, nil 397 } 398 399 // 400 // LIST BUCKETS 401 // 402 403 func (*s3bp) ListBuckets(cmn.QueryBcks) (bcks cmn.Bcks, ecode int, _ error) { 404 var ( 405 sessConf sessConf 406 result *s3.ListBucketsOutput 407 ) 408 svc, err := sessConf.s3client("") 409 if err != nil { 410 ecode, err = awsErrorToAISError(err, &cmn.Bck{Provider: apc.AWS}, "") 411 return nil, ecode, err 412 } 413 result, err = svc.ListBuckets(context.Background(), &s3.ListBucketsInput{}) 414 if err != nil { 415 ecode, err = awsErrorToAISError(err, &cmn.Bck{Provider: apc.AWS}, "") 416 return nil, ecode, err 417 } 418 419 bcks = make(cmn.Bcks, len(result.Buckets)) 420 for idx, bck := range result.Buckets { 421 if cmn.Rom.FastV(4, cos.SmoduleBackend) { 422 nlog.Infoln("[bucket_names]", aws.ToString(bck.Name), "created", *bck.CreationDate) 423 } 424 bcks[idx] = cmn.Bck{ 425 Name: aws.ToString(bck.Name), 426 Provider: apc.AWS, 427 } 428 } 429 return bcks, 0, nil 430 } 431 432 // 433 // HEAD OBJECT 434 // 435 436 func (*s3bp) HeadObj(_ context.Context, lom *core.LOM, oreq *http.Request) (oa *cmn.ObjAttrs, ecode int, err error) { 437 var ( 438 svc *s3.Client 439 headOutput *s3.HeadObjectOutput 440 h = cmn.BackendHelpers.Amazon 441 cloudBck = lom.Bck().RemoteBck() 442 sessConf = sessConf{bck: cloudBck} 443 ) 444 445 if lom.IsFeatureSet(feat.S3PresignedRequest) && oreq != nil { 446 q := oreq.URL.Query() // TODO: optimize-out 447 pts := aiss3.NewPresignedReq(oreq, lom, nil, q) 448 resp, err := pts.Do(core.T.DataClient()) 449 if err != nil { 450 return nil, resp.StatusCode, err 451 } 452 if resp != nil { 453 oa = resp.ObjAttrs() 454 goto exit 455 } 456 } 457 458 svc, err = sessConf.s3client("[head_object]") 459 if err != nil { 460 return 461 } 462 headOutput, err = svc.HeadObject(context.Background(), &s3.HeadObjectInput{ 463 Bucket: aws.String(cloudBck.Name), 464 Key: aws.String(lom.ObjName), 465 }) 466 if err != nil { 467 ecode, err = awsErrorToAISError(err, cloudBck, lom.ObjName) 468 return 469 } 470 oa = &cmn.ObjAttrs{} 471 oa.CustomMD = make(cos.StrKVs, 6) 472 oa.SetCustomKey(cmn.SourceObjMD, apc.AWS) 473 oa.Size = *headOutput.ContentLength 474 if v, ok := h.EncodeVersion(headOutput.VersionId); ok { 475 lom.SetCustomKey(cmn.VersionObjMD, v) 476 oa.Ver = v 477 } 478 if v, ok := h.EncodeCksum(headOutput.ETag); ok { 479 oa.SetCustomKey(cmn.ETag, v) 480 // assuming SSE-S3 or plaintext encryption 481 // from https://docs.aws.amazon.com/AmazonS3/latest/API/API_Object.html: 482 // - "The entity tag is a hash of the object. The ETag reflects changes only 483 // to the contents of an object, not its metadata." 484 // - "The ETag may or may not be an MD5 digest of the object data. Whether or 485 // not it is depends on how the object was created and how it is encrypted..." 486 if !cmn.IsS3MultipartEtag(v) { 487 oa.SetCustomKey(cmn.MD5ObjMD, v) 488 } 489 } 490 491 // AIS custom (see also: PutObject, GetObjReader) 492 if cksumType, ok := headOutput.Metadata[cos.S3MetadataChecksumType]; ok { 493 if cksumValue, ok := headOutput.Metadata[cos.S3MetadataChecksumVal]; ok { 494 oa.SetCksum(cksumType, cksumValue) 495 } 496 } 497 498 // unlike other custom attrs, "Content-Type" is not getting stored w/ LOM 499 // - only shown via list-objects and HEAD when not present 500 if v := headOutput.ContentType; v != nil { 501 oa.SetCustomKey(cos.HdrContentType, *v) 502 } 503 if v := headOutput.LastModified; v != nil { 504 mtime := *(headOutput.LastModified) 505 if oa.Atime == 0 { 506 oa.Atime = mtime.UnixNano() 507 } 508 oa.SetCustomKey(cmn.LastModified, fmtTime(mtime)) 509 } 510 511 exit: 512 if cmn.Rom.FastV(5, cos.SmoduleBackend) { 513 nlog.Infoln("[head_object]", cloudBck.Cname(lom.ObjName)) 514 } 515 return 516 } 517 518 // 519 // GET OBJECT 520 // 521 522 func (s3bp *s3bp) GetObj(ctx context.Context, lom *core.LOM, owt cmn.OWT, oreq *http.Request) (int, error) { 523 var res core.GetReaderResult 524 525 if lom.IsFeatureSet(feat.S3PresignedRequest) && oreq != nil { 526 q := oreq.URL.Query() // TODO: optimize-out 527 pts := aiss3.NewPresignedReq(oreq, lom, nil, q) 528 resp, err := pts.DoReader(core.T.DataClient()) 529 if err != nil { 530 res = core.GetReaderResult{Err: err, ErrCode: resp.StatusCode} 531 goto finalize 532 } 533 if resp != nil { 534 res = core.GetReaderResult{ 535 R: resp.BodyR, 536 Size: resp.Size, 537 ErrCode: resp.StatusCode, 538 } 539 goto finalize 540 } 541 } 542 543 res = s3bp.GetObjReader(ctx, lom, 0, 0) 544 545 finalize: 546 if res.Err != nil { 547 return res.ErrCode, res.Err 548 } 549 params := allocPutParams(res, owt) 550 err := s3bp.t.PutObject(lom, params) 551 core.FreePutParams(params) 552 if cmn.Rom.FastV(5, cos.SmoduleBackend) { 553 nlog.Infoln("[get_object]", lom.String(), err) 554 } 555 return 0, err 556 } 557 558 func (*s3bp) GetObjReader(ctx context.Context, lom *core.LOM, offset, length int64) (res core.GetReaderResult) { 559 var ( 560 obj *s3.GetObjectOutput 561 cloudBck = lom.Bck().RemoteBck() 562 sessConf = sessConf{bck: cloudBck} 563 input = s3.GetObjectInput{ 564 Bucket: aws.String(cloudBck.Name), 565 Key: aws.String(lom.ObjName), 566 } 567 ) 568 svc, err := sessConf.s3client("[get_obj_reader]") 569 if err != nil { 570 res.Err = err 571 return 572 } 573 if length > 0 { 574 rng := cmn.MakeRangeHdr(offset, length) 575 input.Range = aws.String(rng) 576 obj, err = svc.GetObject(ctx, &input) 577 if err != nil { 578 res.ErrCode, res.Err = awsErrorToAISError(err, cloudBck, lom.ObjName) 579 if res.ErrCode == http.StatusRequestedRangeNotSatisfiable { 580 res.Err = cmn.NewErrRangeNotSatisfiable(res.Err, nil, 0) 581 } 582 return res 583 } 584 } else { 585 obj, err = svc.GetObject(ctx, &input) 586 if err != nil { 587 res.ErrCode, res.Err = awsErrorToAISError(err, cloudBck, lom.ObjName) 588 return res 589 } 590 // custom metadata 591 lom.SetCustomKey(cmn.SourceObjMD, apc.AWS) 592 593 res.ExpCksum = _getCustom(lom, obj) 594 595 md := obj.Metadata 596 if cksumType, ok := md[cos.S3MetadataChecksumType]; ok { 597 if cksumValue, ok := md[cos.S3MetadataChecksumVal]; ok { 598 cksum := cos.NewCksum(cksumType, cksumValue) 599 lom.SetCksum(cksum) 600 res.ExpCksum = cksum // precedence over md5 (<= ETag) 601 } 602 } 603 } 604 605 res.R = obj.Body 606 res.Size = *obj.ContentLength 607 return res 608 } 609 610 func _getCustom(lom *core.LOM, obj *s3.GetObjectOutput) (md5 *cos.Cksum) { 611 h := cmn.BackendHelpers.Amazon 612 if v, ok := h.EncodeVersion(obj.VersionId); ok { 613 lom.SetVersion(v) 614 lom.SetCustomKey(cmn.VersionObjMD, v) 615 } 616 // see ETag/MD5 NOTE above 617 if v, ok := h.EncodeCksum(obj.ETag); ok { 618 lom.SetCustomKey(cmn.ETag, v) 619 if !cmn.IsS3MultipartEtag(v) { 620 md5 = cos.NewCksum(cos.ChecksumMD5, v) 621 lom.SetCustomKey(cmn.MD5ObjMD, v) 622 } 623 } 624 mtime := *(obj.LastModified) 625 lom.SetCustomKey(cmn.LastModified, fmtTime(mtime)) 626 return 627 } 628 629 // 630 // PUT OBJECT 631 // 632 633 func (*s3bp) PutObj(r io.ReadCloser, lom *core.LOM, oreq *http.Request) (ecode int, err error) { 634 var ( 635 svc *s3.Client 636 uploader *s3manager.Uploader 637 uploadOutput *s3manager.UploadOutput 638 h = cmn.BackendHelpers.Amazon 639 cksumType, cksumValue = lom.Checksum().Get() 640 cloudBck = lom.Bck().RemoteBck() 641 sessConf = sessConf{bck: cloudBck} 642 md = make(map[string]string, 2) 643 ) 644 if lom.IsFeatureSet(feat.S3PresignedRequest) && oreq != nil { 645 q := oreq.URL.Query() // TODO: optimize-out 646 pts := aiss3.NewPresignedReq(oreq, lom, r, q) 647 resp, err := pts.Do(core.T.DataClient()) 648 if err != nil { 649 return resp.StatusCode, err 650 } 651 if resp != nil { 652 uploadOutput = &s3manager.UploadOutput{ 653 ETag: aws.String(resp.Header.Get(cos.HdrETag)), 654 } 655 goto exit 656 } 657 } 658 659 svc, err = sessConf.s3client("[put_object]") 660 if err != nil { 661 return 662 } 663 664 md[cos.S3MetadataChecksumType] = cksumType 665 md[cos.S3MetadataChecksumVal] = cksumValue 666 667 uploader = s3manager.NewUploader(svc) 668 uploadOutput, err = uploader.Upload(context.Background(), &s3.PutObjectInput{ 669 Bucket: aws.String(cloudBck.Name), 670 Key: aws.String(lom.ObjName), 671 Body: r, 672 Metadata: md, 673 }) 674 if err != nil { 675 ecode, err = awsErrorToAISError(err, cloudBck, lom.ObjName) 676 cos.Close(r) 677 return 678 } 679 680 exit: 681 // compare with setCustomS3() above 682 if v, ok := h.EncodeVersion(uploadOutput.VersionID); ok { 683 lom.SetCustomKey(cmn.VersionObjMD, v) 684 lom.SetVersion(v) 685 } 686 if v, ok := h.EncodeCksum(uploadOutput.ETag); ok { 687 lom.SetCustomKey(cmn.ETag, v) 688 // see ETag/MD5 NOTE above 689 if !cmn.IsS3MultipartEtag(v) { 690 lom.SetCustomKey(cmn.MD5ObjMD, v) 691 } 692 } 693 if cmn.Rom.FastV(5, cos.SmoduleBackend) { 694 nlog.Infoln("[put_object]", lom.String()) 695 } 696 cos.Close(r) 697 return 698 } 699 700 // 701 // DELETE OBJECT 702 // 703 704 func (*s3bp) DeleteObj(lom *core.LOM) (ecode int, err error) { 705 var ( 706 svc *s3.Client 707 cloudBck = lom.Bck().RemoteBck() 708 sessConf = sessConf{bck: cloudBck} 709 ) 710 svc, err = sessConf.s3client("[delete_object]") 711 if err != nil { 712 return 713 } 714 _, err = svc.DeleteObject(context.Background(), &s3.DeleteObjectInput{ 715 Bucket: aws.String(cloudBck.Name), 716 Key: aws.String(lom.ObjName), 717 }) 718 if err != nil { 719 ecode, err = awsErrorToAISError(err, cloudBck, lom.ObjName) 720 return 721 } 722 if cmn.Rom.FastV(5, cos.SmoduleBackend) { 723 nlog.Infoln("[delete_object]", lom.String()) 724 } 725 return 726 } 727 728 // 729 // static helpers 730 // 731 732 // newClient creates new S3 client on a per-region basis or, more precisely, 733 // per (region, endpoint) pair - and note that s3 endpoint is per-bucket configurable. 734 // If the client already exists newClient simply returns it. 735 // From S3 SDK: 736 // "S3 methods are safe to use concurrently. It is not safe to modify mutate 737 // any of the struct's properties though." 738 func (sessConf *sessConf) s3client(tag string) (*s3.Client, error) { 739 var ( 740 endpoint = s3Endpoint 741 profile = awsProfile 742 ) 743 if sessConf.bck != nil && sessConf.bck.Props != nil { 744 if sessConf.region == "" { 745 sessConf.region = sessConf.bck.Props.Extra.AWS.CloudRegion 746 } 747 if sessConf.bck.Props.Extra.AWS.Endpoint != "" { 748 endpoint = sessConf.bck.Props.Extra.AWS.Endpoint 749 } 750 if sessConf.bck.Props.Extra.AWS.Profile != "" { 751 profile = sessConf.bck.Props.Extra.AWS.Profile 752 } 753 } 754 755 cid := _cid(profile, sessConf.region, endpoint) 756 asvc, loaded := clients.Load(cid) 757 if loaded { 758 svc, ok := asvc.(*s3.Client) 759 debug.Assert(ok) 760 return svc, nil 761 } 762 763 // slow path 764 cfg, err := loadConfig(endpoint, profile) 765 if err != nil { 766 return nil, err 767 } 768 769 svc := s3.NewFromConfig(cfg, sessConf.options) 770 771 // NOTE: 772 // - gotBucketLocation special case 773 // - otherwise, not caching s3 client for an unknown or missing region 774 if sessConf.region == "" && tag != gotBucketLocation { 775 if tag != "" && cmn.Rom.FastV(4, cos.SmoduleBackend) { 776 nlog.Warningln(tag, "no region for bucket", sessConf.bck.Cname("")) 777 } 778 return svc, nil 779 } 780 781 // cache (without recomputing _cid and possibly an empty region) 782 if cmn.Rom.FastV(4, cos.SmoduleBackend) { 783 nlog.Infoln("add s3client for tuple (profile, region, endpoint):", cid) 784 } 785 clients.Store(cid, svc) // race or no race, no particular reason to do LoadOrStore 786 return svc, nil 787 } 788 789 func (sessConf *sessConf) options(options *s3.Options) { 790 if sessConf.region != "" { 791 options.Region = sessConf.region 792 } else { 793 sessConf.region = options.Region 794 } 795 if bck := sessConf.bck; bck != nil { 796 if bck.Props != nil { 797 options.UsePathStyle = bck.Props.Features.IsSet(feat.S3UsePathStyle) 798 } else { 799 options.UsePathStyle = cmn.Rom.Features().IsSet(feat.S3UsePathStyle) 800 } 801 } 802 } 803 804 func _cid(profile, region, endpoint string) string { 805 sb := &strings.Builder{} 806 if profile != "" { 807 sb.WriteString(profile) 808 } 809 sb.WriteByte('#') 810 if region != "" { 811 sb.WriteString(region) 812 } 813 sb.WriteByte('#') 814 if endpoint != "" { 815 sb.WriteString(endpoint) 816 } 817 return sb.String() 818 } 819 820 // loadConfig create config using default creds from ~/.aws/credentials and environment variables. 821 func loadConfig(endpoint, profile string) (aws.Config, error) { 822 // NOTE: The AWS SDK for Go v2, uses lower case header maps by default. 823 cfg, err := config.LoadDefaultConfig( 824 context.Background(), 825 config.WithHTTPClient(cmn.NewClient(cmn.TransportArgs{})), 826 config.WithSharedConfigProfile(profile), 827 ) 828 if err != nil { 829 return cfg, err 830 } 831 if endpoint != "" { 832 cfg.BaseEndpoint = aws.String(endpoint) 833 } 834 return cfg, nil 835 } 836 837 func getBucketVersioning(svc *s3.Client, bck *cmn.Bck) (enabled bool, errV error) { 838 input := &s3.GetBucketVersioningInput{Bucket: aws.String(bck.Name)} 839 result, err := svc.GetBucketVersioning(context.Background(), input) 840 if err != nil { 841 return false, err 842 } 843 enabled = result.Status == types.BucketVersioningStatusEnabled 844 return 845 } 846 847 func getBucketLocation(svc *s3.Client, bckName string) (region string, err error) { 848 resp, err := svc.GetBucketLocation(context.Background(), &s3.GetBucketLocationInput{ 849 Bucket: aws.String(bckName), 850 }) 851 if err != nil { 852 return 853 } 854 region = string(resp.LocationConstraint) 855 if region == "" { 856 region = env.AwsDefaultRegion() // env "AWS_REGION" or "us-east-1" - in that order 857 } 858 return 859 } 860 861 // For reference see https://github.com/aws/aws-sdk-go-v2/issues/1110#issuecomment-1054643716. 862 func awsErrorToAISError(awsError error, bck *cmn.Bck, objName string) (int, error) { 863 if cmn.Rom.FastV(5, cos.SmoduleBackend) { 864 nlog.InfoDepth(1, "begin "+aiss3.ErrPrefix+" =========================") 865 nlog.InfoDepth(1, awsError) 866 nlog.InfoDepth(1, "end "+aiss3.ErrPrefix+" ===========================") 867 } 868 869 var reqErr smithy.APIError 870 if !errors.As(awsError, &reqErr) { 871 return http.StatusInternalServerError, _awsErr(awsError, "") 872 } 873 874 switch reqErr.(type) { 875 case *types.NoSuchBucket: 876 return http.StatusNotFound, cmn.NewErrRemoteBckNotFound(bck) 877 case *types.NoSuchKey: 878 e := fmt.Errorf("%s[%s: %s]", aiss3.ErrPrefix, reqErr.ErrorCode(), bck.Cname(objName)) 879 return http.StatusNotFound, e 880 default: 881 var ( 882 rspErr *awshttp.ResponseError 883 code = reqErr.ErrorCode() 884 ) 885 if errors.As(awsError, &rspErr) { 886 return rspErr.HTTPStatusCode(), _awsErr(awsError, code) 887 } 888 889 return http.StatusBadRequest, _awsErr(awsError, code) 890 } 891 } 892 893 // Strip original AWS error to its essentials: type code and error message 894 // See also: 895 // * ais/s3/err.go WriteErr() that (NOTE) relies on the formatting below 896 // * aws-sdk-go/aws/awserr/types.go 897 func _awsErr(awsError error, code string) error { 898 var ( 899 msg = awsError.Error() 900 origErrMsg = awsError.Error() 901 ) 902 // Strip extra information 903 if idx := strings.Index(msg, "\n\t"); idx > 0 { 904 msg = msg[:idx] 905 } 906 // ...but preserve original error information. 907 if idx := strings.Index(origErrMsg, "\ncaused"); idx > 0 { 908 // `idx+1` because we want to remove `\n`. 909 msg += " (" + origErrMsg[idx+1:] + ")" 910 } 911 if code != "" { 912 if i := strings.Index(msg, code+": "); i > 0 { 913 msg = msg[i:] 914 } 915 } 916 return errors.New(aiss3.ErrPrefix + "[" + strings.TrimSuffix(msg, ".") + "]") 917 }