github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/bucket-lifecycle.go (about) 1 // Copyright (c) 2015-2024 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "context" 22 "encoding/xml" 23 "errors" 24 "fmt" 25 "io" 26 "net/http" 27 "strconv" 28 "strings" 29 "sync" 30 "sync/atomic" 31 "time" 32 33 "github.com/google/uuid" 34 "github.com/minio/madmin-go/v3" 35 "github.com/minio/minio-go/v7/pkg/tags" 36 "github.com/minio/minio/internal/amztime" 37 sse "github.com/minio/minio/internal/bucket/encryption" 38 "github.com/minio/minio/internal/bucket/lifecycle" 39 "github.com/minio/minio/internal/event" 40 xhttp "github.com/minio/minio/internal/http" 41 "github.com/minio/minio/internal/logger" 42 "github.com/minio/minio/internal/s3select" 43 xnet "github.com/minio/pkg/v2/net" 44 "github.com/zeebo/xxh3" 45 ) 46 47 const ( 48 // Disabled means the lifecycle rule is inactive 49 Disabled = "Disabled" 50 // TransitionStatus status of transition 51 TransitionStatus = "transition-status" 52 // TransitionedObjectName name of transitioned object 53 TransitionedObjectName = "transitioned-object" 54 // TransitionedVersionID is version of remote object 55 TransitionedVersionID = "transitioned-versionID" 56 // TransitionTier name of transition storage class 57 TransitionTier = "transition-tier" 58 ) 59 60 // LifecycleSys - Bucket lifecycle subsystem. 61 type LifecycleSys struct{} 62 63 // Get - gets lifecycle config associated to a given bucket name. 64 func (sys *LifecycleSys) Get(bucketName string) (lc *lifecycle.Lifecycle, err error) { 65 lc, _, err = globalBucketMetadataSys.GetLifecycleConfig(bucketName) 66 return lc, err 67 } 68 69 // NewLifecycleSys - creates new lifecycle system. 70 func NewLifecycleSys() *LifecycleSys { 71 return &LifecycleSys{} 72 } 73 74 func ilmTrace(startTime time.Time, duration time.Duration, oi ObjectInfo, event string) madmin.TraceInfo { 75 return madmin.TraceInfo{ 76 TraceType: madmin.TraceILM, 77 Time: startTime, 78 NodeName: globalLocalNodeName, 79 FuncName: event, 80 Duration: duration, 81 Path: pathJoin(oi.Bucket, oi.Name), 82 Error: "", 83 Message: getSource(4), 84 Custom: map[string]string{"version-id": oi.VersionID}, 85 } 86 } 87 88 func (sys *LifecycleSys) trace(oi ObjectInfo) func(event string) { 89 startTime := time.Now() 90 return func(event string) { 91 duration := time.Since(startTime) 92 if globalTrace.NumSubscribers(madmin.TraceILM) > 0 { 93 globalTrace.Publish(ilmTrace(startTime, duration, oi, event)) 94 } 95 } 96 } 97 98 type expiryTask struct { 99 objInfo ObjectInfo 100 event lifecycle.Event 101 src lcEventSrc 102 } 103 104 // expiryStats records metrics related to ILM expiry activities 105 type expiryStats struct { 106 missedExpiryTasks atomic.Int64 107 missedFreeVersTasks atomic.Int64 108 missedTierJournalTasks atomic.Int64 109 workers atomic.Int32 110 } 111 112 // MissedTasks returns the number of ILM expiry tasks that were missed since 113 // there were no available workers. 114 func (e *expiryStats) MissedTasks() int64 { 115 return e.missedExpiryTasks.Load() 116 } 117 118 // MissedFreeVersTasks returns the number of free version collection tasks that 119 // were missed since there were no available workers. 120 func (e *expiryStats) MissedFreeVersTasks() int64 { 121 return e.missedFreeVersTasks.Load() 122 } 123 124 // MissedTierJournalTasks returns the number of tasks to remove tiered objects 125 // that were missed since there were no available workers. 126 func (e *expiryStats) MissedTierJournalTasks() int64 { 127 return e.missedTierJournalTasks.Load() 128 } 129 130 // NumWorkers returns the number of active workers executing one of ILM expiry 131 // tasks or free version collection tasks. 132 func (e *expiryStats) NumWorkers() int32 { 133 return e.workers.Load() 134 } 135 136 type expiryOp interface { 137 OpHash() uint64 138 } 139 140 type freeVersionTask struct { 141 ObjectInfo 142 } 143 144 func (f freeVersionTask) OpHash() uint64 { 145 return xxh3.HashString(f.TransitionedObject.Tier + f.TransitionedObject.Name) 146 } 147 148 func (n newerNoncurrentTask) OpHash() uint64 { 149 return xxh3.HashString(n.bucket + n.versions[0].ObjectV.ObjectName) 150 } 151 152 func (j jentry) OpHash() uint64 { 153 return xxh3.HashString(j.TierName + j.ObjName) 154 } 155 156 func (e expiryTask) OpHash() uint64 { 157 return xxh3.HashString(e.objInfo.Bucket + e.objInfo.Name) 158 } 159 160 // expiryState manages all ILM related expiration activities. 161 type expiryState struct { 162 mu sync.RWMutex 163 workers atomic.Pointer[[]chan expiryOp] 164 165 ctx context.Context 166 objAPI ObjectLayer 167 168 stats expiryStats 169 } 170 171 // PendingTasks returns the number of pending ILM expiry tasks. 172 func (es *expiryState) PendingTasks() int { 173 w := es.workers.Load() 174 if w == nil || len(*w) == 0 { 175 return 0 176 } 177 var tasks int 178 for _, wrkr := range *w { 179 tasks += len(wrkr) 180 } 181 return tasks 182 } 183 184 // enqueueTierJournalEntry enqueues a tier journal entry referring to a remote 185 // object corresponding to a 'replaced' object versions. This applies only to 186 // non-versioned or version suspended buckets. 187 func (es *expiryState) enqueueTierJournalEntry(je jentry) { 188 wrkr := es.getWorkerCh(je.OpHash()) 189 if wrkr == nil { 190 es.stats.missedTierJournalTasks.Add(1) 191 return 192 } 193 select { 194 case <-GlobalContext.Done(): 195 case wrkr <- je: 196 default: 197 es.stats.missedTierJournalTasks.Add(1) 198 } 199 } 200 201 // enqueueFreeVersion enqueues a free version to be deleted 202 func (es *expiryState) enqueueFreeVersion(oi ObjectInfo) { 203 task := freeVersionTask{ObjectInfo: oi} 204 wrkr := es.getWorkerCh(task.OpHash()) 205 if wrkr == nil { 206 es.stats.missedFreeVersTasks.Add(1) 207 return 208 } 209 select { 210 case <-GlobalContext.Done(): 211 case wrkr <- task: 212 default: 213 es.stats.missedFreeVersTasks.Add(1) 214 } 215 } 216 217 // enqueueByDays enqueues object versions expired by days for expiry. 218 func (es *expiryState) enqueueByDays(oi ObjectInfo, event lifecycle.Event, src lcEventSrc) { 219 task := expiryTask{objInfo: oi, event: event, src: src} 220 wrkr := es.getWorkerCh(task.OpHash()) 221 if wrkr == nil { 222 es.stats.missedExpiryTasks.Add(1) 223 return 224 } 225 select { 226 case <-GlobalContext.Done(): 227 case wrkr <- task: 228 default: 229 es.stats.missedExpiryTasks.Add(1) 230 } 231 } 232 233 // enqueueByNewerNoncurrent enqueues object versions expired by 234 // NewerNoncurrentVersions limit for expiry. 235 func (es *expiryState) enqueueByNewerNoncurrent(bucket string, versions []ObjectToDelete, lcEvent lifecycle.Event) { 236 if len(versions) == 0 { 237 return 238 } 239 240 task := newerNoncurrentTask{bucket: bucket, versions: versions, event: lcEvent} 241 wrkr := es.getWorkerCh(task.OpHash()) 242 if wrkr == nil { 243 es.stats.missedExpiryTasks.Add(1) 244 return 245 } 246 select { 247 case <-GlobalContext.Done(): 248 case wrkr <- task: 249 default: 250 es.stats.missedExpiryTasks.Add(1) 251 } 252 } 253 254 // globalExpiryState is the per-node instance which manages all ILM expiry tasks. 255 var globalExpiryState *expiryState 256 257 // newExpiryState creates an expiryState with buffered channels allocated for 258 // each ILM expiry task type. 259 func newExpiryState(ctx context.Context, objAPI ObjectLayer, n int) *expiryState { 260 es := &expiryState{ 261 ctx: ctx, 262 objAPI: objAPI, 263 } 264 workers := make([]chan expiryOp, 0, n) 265 es.workers.Store(&workers) 266 es.ResizeWorkers(n) 267 return es 268 } 269 270 func (es *expiryState) getWorkerCh(h uint64) chan<- expiryOp { 271 w := es.workers.Load() 272 if w == nil || len(*w) == 0 { 273 return nil 274 } 275 workers := *w 276 return workers[h%uint64(len(workers))] 277 } 278 279 func (es *expiryState) ResizeWorkers(n int) { 280 // Lock to avoid multiple resizes to happen at the same time. 281 es.mu.Lock() 282 defer es.mu.Unlock() 283 var workers []chan expiryOp 284 if v := es.workers.Load(); v != nil { 285 // Copy to new array. 286 workers = append(workers, *v...) 287 } 288 289 if n == len(workers) || n < 1 { 290 return 291 } 292 293 for len(workers) < n { 294 input := make(chan expiryOp, 10000) 295 workers = append(workers, input) 296 go es.Worker(input) 297 es.stats.workers.Add(1) 298 } 299 300 for len(workers) > n { 301 worker := workers[len(workers)-1] 302 workers = workers[:len(workers)-1] 303 worker <- expiryOp(nil) 304 es.stats.workers.Add(-1) 305 } 306 // Atomically replace workers. 307 es.workers.Store(&workers) 308 } 309 310 // Worker handles 4 types of expiration tasks. 311 // 1. Expiry of objects, includes regular and transitioned objects 312 // 2. Expiry of noncurrent versions due to NewerNoncurrentVersions 313 // 3. Expiry of free-versions, for remote objects of transitioned object which have been expired since. 314 // 4. Expiry of remote objects corresponding to objects in a 315 // non-versioned/version suspended buckets 316 func (es *expiryState) Worker(input <-chan expiryOp) { 317 for { 318 select { 319 case <-es.ctx.Done(): 320 return 321 case v, ok := <-input: 322 if !ok { 323 return 324 } 325 if v == nil { 326 // ResizeWorkers signaling worker to quit 327 return 328 } 329 switch v := v.(type) { 330 case expiryTask: 331 if v.objInfo.TransitionedObject.Status != "" { 332 applyExpiryOnTransitionedObject(es.ctx, es.objAPI, v.objInfo, v.event, v.src) 333 } else { 334 applyExpiryOnNonTransitionedObjects(es.ctx, es.objAPI, v.objInfo, v.event, v.src) 335 } 336 case newerNoncurrentTask: 337 deleteObjectVersions(es.ctx, es.objAPI, v.bucket, v.versions, v.event) 338 case jentry: 339 logger.LogIf(es.ctx, deleteObjectFromRemoteTier(es.ctx, v.ObjName, v.VersionID, v.TierName)) 340 case freeVersionTask: 341 oi := v.ObjectInfo 342 traceFn := globalLifecycleSys.trace(oi) 343 if !oi.TransitionedObject.FreeVersion { 344 // nothing to be done 345 return 346 } 347 348 ignoreNotFoundErr := func(err error) error { 349 switch { 350 case isErrVersionNotFound(err), isErrObjectNotFound(err): 351 return nil 352 } 353 return err 354 } 355 // Remove the remote object 356 err := deleteObjectFromRemoteTier(es.ctx, oi.TransitionedObject.Name, oi.TransitionedObject.VersionID, oi.TransitionedObject.Tier) 357 if ignoreNotFoundErr(err) != nil { 358 logger.LogIf(es.ctx, err) 359 return 360 } 361 362 // Remove this free version 363 _, err = es.objAPI.DeleteObject(es.ctx, oi.Bucket, oi.Name, ObjectOptions{ 364 VersionID: oi.VersionID, 365 InclFreeVersions: true, 366 }) 367 if err == nil { 368 auditLogLifecycle(es.ctx, oi, ILMFreeVersionDelete, nil, traceFn) 369 } 370 if ignoreNotFoundErr(err) != nil { 371 logger.LogIf(es.ctx, err) 372 } 373 default: 374 logger.LogIf(es.ctx, fmt.Errorf("Invalid work type - %v", v)) 375 } 376 } 377 } 378 } 379 380 func initBackgroundExpiry(ctx context.Context, objectAPI ObjectLayer) { 381 globalExpiryState = newExpiryState(ctx, objectAPI, globalILMConfig.getExpirationWorkers()) 382 } 383 384 // newerNoncurrentTask encapsulates arguments required by worker to expire objects 385 // by NewerNoncurrentVersions 386 type newerNoncurrentTask struct { 387 bucket string 388 versions []ObjectToDelete 389 event lifecycle.Event 390 } 391 392 type transitionTask struct { 393 objInfo ObjectInfo 394 src lcEventSrc 395 event lifecycle.Event 396 } 397 398 type transitionState struct { 399 transitionCh chan transitionTask 400 401 ctx context.Context 402 objAPI ObjectLayer 403 mu sync.Mutex 404 numWorkers int 405 killCh chan struct{} 406 407 activeTasks atomic.Int64 408 missedImmediateTasks atomic.Int64 409 410 lastDayMu sync.RWMutex 411 lastDayStats map[string]*lastDayTierStats 412 } 413 414 func (t *transitionState) queueTransitionTask(oi ObjectInfo, event lifecycle.Event, src lcEventSrc) { 415 task := transitionTask{objInfo: oi, event: event, src: src} 416 select { 417 case <-t.ctx.Done(): 418 case t.transitionCh <- task: 419 default: 420 switch src { 421 case lcEventSrc_s3PutObject, lcEventSrc_s3CopyObject, lcEventSrc_s3CompleteMultipartUpload: 422 // Update missed immediate tasks only for incoming requests. 423 t.missedImmediateTasks.Add(1) 424 } 425 } 426 } 427 428 var globalTransitionState *transitionState 429 430 // newTransitionState returns a transitionState object ready to be initialized 431 // via its Init method. 432 func newTransitionState(ctx context.Context) *transitionState { 433 return &transitionState{ 434 transitionCh: make(chan transitionTask, 100000), 435 ctx: ctx, 436 killCh: make(chan struct{}), 437 lastDayStats: make(map[string]*lastDayTierStats), 438 } 439 } 440 441 // Init initializes t with given objAPI and instantiates the configured number 442 // of transition workers. 443 func (t *transitionState) Init(objAPI ObjectLayer) { 444 n := globalAPIConfig.getTransitionWorkers() 445 // Prefer ilm.transition_workers over now deprecated api.transition_workers 446 if tw := globalILMConfig.getTransitionWorkers(); tw > 0 { 447 n = tw 448 } 449 t.mu.Lock() 450 defer t.mu.Unlock() 451 452 t.objAPI = objAPI 453 t.updateWorkers(n) 454 } 455 456 // PendingTasks returns the number of ILM transition tasks waiting for a worker 457 // goroutine. 458 func (t *transitionState) PendingTasks() int { 459 return len(t.transitionCh) 460 } 461 462 // ActiveTasks returns the number of active (ongoing) ILM transition tasks. 463 func (t *transitionState) ActiveTasks() int64 { 464 return t.activeTasks.Load() 465 } 466 467 // MissedImmediateTasks returns the number of tasks - deferred to scanner due 468 // to tasks channel being backlogged. 469 func (t *transitionState) MissedImmediateTasks() int64 { 470 return t.missedImmediateTasks.Load() 471 } 472 473 // worker waits for transition tasks 474 func (t *transitionState) worker(objectAPI ObjectLayer) { 475 for { 476 select { 477 case <-t.killCh: 478 return 479 case <-t.ctx.Done(): 480 return 481 case task, ok := <-t.transitionCh: 482 if !ok { 483 return 484 } 485 t.activeTasks.Add(1) 486 if err := transitionObject(t.ctx, objectAPI, task.objInfo, newLifecycleAuditEvent(task.src, task.event)); err != nil { 487 if !isErrVersionNotFound(err) && !isErrObjectNotFound(err) && !xnet.IsNetworkOrHostDown(err, false) { 488 if !strings.Contains(err.Error(), "use of closed network connection") { 489 logger.LogIf(t.ctx, fmt.Errorf("Transition to %s failed for %s/%s version:%s with %w", 490 task.event.StorageClass, task.objInfo.Bucket, task.objInfo.Name, task.objInfo.VersionID, err)) 491 } 492 } 493 } else { 494 ts := tierStats{ 495 TotalSize: uint64(task.objInfo.Size), 496 NumVersions: 1, 497 } 498 if task.objInfo.IsLatest { 499 ts.NumObjects = 1 500 } 501 t.addLastDayStats(task.event.StorageClass, ts) 502 } 503 t.activeTasks.Add(-1) 504 } 505 } 506 } 507 508 func (t *transitionState) addLastDayStats(tier string, ts tierStats) { 509 t.lastDayMu.Lock() 510 defer t.lastDayMu.Unlock() 511 512 if _, ok := t.lastDayStats[tier]; !ok { 513 t.lastDayStats[tier] = &lastDayTierStats{} 514 } 515 t.lastDayStats[tier].addStats(ts) 516 } 517 518 func (t *transitionState) getDailyAllTierStats() DailyAllTierStats { 519 t.lastDayMu.RLock() 520 defer t.lastDayMu.RUnlock() 521 522 res := make(DailyAllTierStats, len(t.lastDayStats)) 523 for tier, st := range t.lastDayStats { 524 res[tier] = st.clone() 525 } 526 return res 527 } 528 529 // UpdateWorkers at the end of this function leaves n goroutines waiting for 530 // transition tasks 531 func (t *transitionState) UpdateWorkers(n int) { 532 t.mu.Lock() 533 defer t.mu.Unlock() 534 if t.objAPI == nil { // Init hasn't been called yet. 535 return 536 } 537 t.updateWorkers(n) 538 } 539 540 func (t *transitionState) updateWorkers(n int) { 541 for t.numWorkers < n { 542 go t.worker(t.objAPI) 543 t.numWorkers++ 544 } 545 546 for t.numWorkers > n { 547 go func() { t.killCh <- struct{}{} }() 548 t.numWorkers-- 549 } 550 } 551 552 var errInvalidStorageClass = errors.New("invalid storage class") 553 554 func validateTransitionTier(lc *lifecycle.Lifecycle) error { 555 for _, rule := range lc.Rules { 556 if rule.Transition.StorageClass != "" { 557 if valid := globalTierConfigMgr.IsTierValid(rule.Transition.StorageClass); !valid { 558 return errInvalidStorageClass 559 } 560 } 561 if rule.NoncurrentVersionTransition.StorageClass != "" { 562 if valid := globalTierConfigMgr.IsTierValid(rule.NoncurrentVersionTransition.StorageClass); !valid { 563 return errInvalidStorageClass 564 } 565 } 566 } 567 return nil 568 } 569 570 // enqueueTransitionImmediate enqueues obj for transition if eligible. 571 // This is to be called after a successful upload of an object (version). 572 func enqueueTransitionImmediate(obj ObjectInfo, src lcEventSrc) { 573 if lc, err := globalLifecycleSys.Get(obj.Bucket); err == nil { 574 switch event := lc.Eval(obj.ToLifecycleOpts()); event.Action { 575 case lifecycle.TransitionAction, lifecycle.TransitionVersionAction: 576 globalTransitionState.queueTransitionTask(obj, event, src) 577 } 578 } 579 } 580 581 // expireTransitionedObject handles expiry of transitioned/restored objects 582 // (versions) in one of the following situations: 583 // 584 // 1. when a restored (via PostRestoreObject API) object expires. 585 // 2. when a transitioned object expires (based on an ILM rule). 586 func expireTransitionedObject(ctx context.Context, objectAPI ObjectLayer, oi *ObjectInfo, lcEvent lifecycle.Event, src lcEventSrc) error { 587 traceFn := globalLifecycleSys.trace(*oi) 588 opts := ObjectOptions{ 589 Versioned: globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name), 590 Expiration: ExpirationOptions{Expire: true}, 591 } 592 if lcEvent.Action.DeleteVersioned() { 593 opts.VersionID = oi.VersionID 594 } 595 tags := newLifecycleAuditEvent(src, lcEvent).Tags() 596 if lcEvent.Action.DeleteRestored() { 597 // delete locally restored copy of object or object version 598 // from the source, while leaving metadata behind. The data on 599 // transitioned tier lies untouched and still accessible 600 opts.Transition.ExpireRestored = true 601 _, err := objectAPI.DeleteObject(ctx, oi.Bucket, oi.Name, opts) 602 if err == nil { 603 // TODO consider including expiry of restored object to events we 604 // notify. 605 auditLogLifecycle(ctx, *oi, ILMExpiry, tags, traceFn) 606 } 607 return err 608 } 609 610 // Delete remote object from warm-tier 611 err := deleteObjectFromRemoteTier(ctx, oi.TransitionedObject.Name, oi.TransitionedObject.VersionID, oi.TransitionedObject.Tier) 612 if err == nil { 613 // Skip adding free version since we successfully deleted the 614 // remote object 615 opts.SkipFreeVersion = true 616 } else { 617 logger.LogIf(ctx, err) 618 } 619 620 // Now, delete object from hot-tier namespace 621 if _, err := objectAPI.DeleteObject(ctx, oi.Bucket, oi.Name, opts); err != nil { 622 return err 623 } 624 625 // Send audit for the lifecycle delete operation 626 defer auditLogLifecycle(ctx, *oi, ILMExpiry, tags, traceFn) 627 628 eventName := event.ObjectRemovedDelete 629 if oi.DeleteMarker { 630 eventName = event.ObjectRemovedDeleteMarkerCreated 631 } 632 objInfo := ObjectInfo{ 633 Name: oi.Name, 634 VersionID: oi.VersionID, 635 DeleteMarker: oi.DeleteMarker, 636 } 637 // Notify object deleted event. 638 sendEvent(eventArgs{ 639 EventName: eventName, 640 BucketName: oi.Bucket, 641 Object: objInfo, 642 UserAgent: "Internal: [ILM-Expiry]", 643 Host: globalLocalNodeName, 644 }) 645 646 return nil 647 } 648 649 // generate an object name for transitioned object 650 func genTransitionObjName(bucket string) (string, error) { 651 u, err := uuid.NewRandom() 652 if err != nil { 653 return "", err 654 } 655 us := u.String() 656 hash := xxh3.HashString(pathJoin(globalDeploymentID(), bucket)) 657 obj := fmt.Sprintf("%s/%s/%s/%s", strconv.FormatUint(hash, 16), us[0:2], us[2:4], us) 658 return obj, nil 659 } 660 661 // transition object to target specified by the transition ARN. When an object is transitioned to another 662 // storage specified by the transition ARN, the metadata is left behind on source cluster and original content 663 // is moved to the transition tier. Note that in the case of encrypted objects, entire encrypted stream is moved 664 // to the transition tier without decrypting or re-encrypting. 665 func transitionObject(ctx context.Context, objectAPI ObjectLayer, oi ObjectInfo, lae lcAuditEvent) (err error) { 666 defer func() { 667 if err != nil { 668 return 669 } 670 globalScannerMetrics.timeILM(lae.Action)(1) 671 }() 672 673 opts := ObjectOptions{ 674 Transition: TransitionOptions{ 675 Status: lifecycle.TransitionPending, 676 Tier: lae.StorageClass, 677 ETag: oi.ETag, 678 }, 679 LifecycleAuditEvent: lae, 680 VersionID: oi.VersionID, 681 Versioned: globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name), 682 VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name), 683 MTime: oi.ModTime, 684 } 685 return objectAPI.TransitionObject(ctx, oi.Bucket, oi.Name, opts) 686 } 687 688 type auditTierOp struct { 689 Tier string `json:"tier"` 690 TimeToResponseNS int64 `json:"timeToResponseNS"` 691 OutputBytes int64 `json:"tx,omitempty"` 692 Error string `json:"error,omitempty"` 693 } 694 695 func auditTierActions(ctx context.Context, tier string, bytes int64) func(err error) { 696 startTime := time.Now() 697 return func(err error) { 698 // Record only when audit targets configured. 699 if len(logger.AuditTargets()) == 0 { 700 return 701 } 702 703 op := auditTierOp{ 704 Tier: tier, 705 OutputBytes: bytes, 706 } 707 708 if err == nil { 709 since := time.Since(startTime) 710 op.TimeToResponseNS = since.Nanoseconds() 711 globalTierMetrics.Observe(tier, since) 712 globalTierMetrics.logSuccess(tier) 713 } else { 714 op.Error = err.Error() 715 globalTierMetrics.logFailure(tier) 716 } 717 718 logger.GetReqInfo(ctx).AppendTags("tierStats", op) 719 } 720 } 721 722 // getTransitionedObjectReader returns a reader from the transitioned tier. 723 func getTransitionedObjectReader(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, oi ObjectInfo, opts ObjectOptions) (gr *GetObjectReader, err error) { 724 tgtClient, err := globalTierConfigMgr.getDriver(oi.TransitionedObject.Tier) 725 if err != nil { 726 return nil, fmt.Errorf("transition storage class not configured") 727 } 728 729 fn, off, length, err := NewGetObjectReader(rs, oi, opts) 730 if err != nil { 731 return nil, ErrorRespToObjectError(err, bucket, object) 732 } 733 gopts := WarmBackendGetOpts{} 734 735 // get correct offsets for object 736 if off >= 0 && length >= 0 { 737 gopts.startOffset = off 738 gopts.length = length 739 } 740 741 timeTierAction := auditTierActions(ctx, oi.TransitionedObject.Tier, length) 742 reader, err := tgtClient.Get(ctx, oi.TransitionedObject.Name, remoteVersionID(oi.TransitionedObject.VersionID), gopts) 743 if err != nil { 744 return nil, err 745 } 746 closer := func() { 747 timeTierAction(reader.Close()) 748 } 749 return fn(reader, h, closer) 750 } 751 752 // RestoreRequestType represents type of restore. 753 type RestoreRequestType string 754 755 const ( 756 // SelectRestoreRequest specifies select request. This is the only valid value 757 SelectRestoreRequest RestoreRequestType = "SELECT" 758 ) 759 760 // Encryption specifies encryption setting on restored bucket 761 type Encryption struct { 762 EncryptionType sse.Algorithm `xml:"EncryptionType"` 763 KMSContext string `xml:"KMSContext,omitempty"` 764 KMSKeyID string `xml:"KMSKeyId,omitempty"` 765 } 766 767 // MetadataEntry denotes name and value. 768 type MetadataEntry struct { 769 Name string `xml:"Name"` 770 Value string `xml:"Value"` 771 } 772 773 // S3Location specifies s3 location that receives result of a restore object request 774 type S3Location struct { 775 BucketName string `xml:"BucketName,omitempty"` 776 Encryption Encryption `xml:"Encryption,omitempty"` 777 Prefix string `xml:"Prefix,omitempty"` 778 StorageClass string `xml:"StorageClass,omitempty"` 779 Tagging *tags.Tags `xml:"Tagging,omitempty"` 780 UserMetadata []MetadataEntry `xml:"UserMetadata"` 781 } 782 783 // OutputLocation specifies bucket where object needs to be restored 784 type OutputLocation struct { 785 S3 S3Location `xml:"S3,omitempty"` 786 } 787 788 // IsEmpty returns true if output location not specified. 789 func (o *OutputLocation) IsEmpty() bool { 790 return o.S3.BucketName == "" 791 } 792 793 // SelectParameters specifies sql select parameters 794 type SelectParameters struct { 795 s3select.S3Select 796 } 797 798 // IsEmpty returns true if no select parameters set 799 func (sp *SelectParameters) IsEmpty() bool { 800 return sp == nil 801 } 802 803 var selectParamsXMLName = "SelectParameters" 804 805 // UnmarshalXML - decodes XML data. 806 func (sp *SelectParameters) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { 807 // Essentially the same as S3Select barring the xml name. 808 if start.Name.Local == selectParamsXMLName { 809 start.Name = xml.Name{Space: "", Local: "SelectRequest"} 810 } 811 return sp.S3Select.UnmarshalXML(d, start) 812 } 813 814 // RestoreObjectRequest - xml to restore a transitioned object 815 type RestoreObjectRequest struct { 816 XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ RestoreRequest" json:"-"` 817 Days int `xml:"Days,omitempty"` 818 Type RestoreRequestType `xml:"Type,omitempty"` 819 Tier string `xml:"Tier"` 820 Description string `xml:"Description,omitempty"` 821 SelectParameters *SelectParameters `xml:"SelectParameters,omitempty"` 822 OutputLocation OutputLocation `xml:"OutputLocation,omitempty"` 823 } 824 825 // Maximum 2MiB size per restore object request. 826 const maxRestoreObjectRequestSize = 2 << 20 827 828 // parseRestoreRequest parses RestoreObjectRequest from xml 829 func parseRestoreRequest(reader io.Reader) (*RestoreObjectRequest, error) { 830 req := RestoreObjectRequest{} 831 if err := xml.NewDecoder(io.LimitReader(reader, maxRestoreObjectRequestSize)).Decode(&req); err != nil { 832 return nil, err 833 } 834 return &req, nil 835 } 836 837 // validate a RestoreObjectRequest as per AWS S3 spec https://docs.aws.amazon.com/AmazonS3/latest/API/API_RestoreObject.html 838 func (r *RestoreObjectRequest) validate(ctx context.Context, objAPI ObjectLayer) error { 839 if r.Type != SelectRestoreRequest && !r.SelectParameters.IsEmpty() { 840 return fmt.Errorf("Select parameters can only be specified with SELECT request type") 841 } 842 if r.Type == SelectRestoreRequest && r.SelectParameters.IsEmpty() { 843 return fmt.Errorf("SELECT restore request requires select parameters to be specified") 844 } 845 846 if r.Type != SelectRestoreRequest && !r.OutputLocation.IsEmpty() { 847 return fmt.Errorf("OutputLocation required only for SELECT request type") 848 } 849 if r.Type == SelectRestoreRequest && r.OutputLocation.IsEmpty() { 850 return fmt.Errorf("OutputLocation required for SELECT requests") 851 } 852 853 if r.Days != 0 && r.Type == SelectRestoreRequest { 854 return fmt.Errorf("Days cannot be specified with SELECT restore request") 855 } 856 if r.Days == 0 && r.Type != SelectRestoreRequest { 857 return fmt.Errorf("restoration days should be at least 1") 858 } 859 // Check if bucket exists. 860 if !r.OutputLocation.IsEmpty() { 861 if _, err := objAPI.GetBucketInfo(ctx, r.OutputLocation.S3.BucketName, BucketOptions{}); err != nil { 862 return err 863 } 864 if r.OutputLocation.S3.Prefix == "" { 865 return fmt.Errorf("Prefix is a required parameter in OutputLocation") 866 } 867 if r.OutputLocation.S3.Encryption.EncryptionType != xhttp.AmzEncryptionAES { 868 return NotImplemented{} 869 } 870 } 871 return nil 872 } 873 874 // postRestoreOpts returns ObjectOptions with version-id from the POST restore object request for a given bucket and object. 875 func postRestoreOpts(ctx context.Context, r *http.Request, bucket, object string) (opts ObjectOptions, err error) { 876 versioned := globalBucketVersioningSys.PrefixEnabled(bucket, object) 877 versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object) 878 vid := strings.TrimSpace(r.Form.Get(xhttp.VersionID)) 879 if vid != "" && vid != nullVersionID { 880 _, err := uuid.Parse(vid) 881 if err != nil { 882 logger.LogIf(ctx, err) 883 return opts, InvalidVersionID{ 884 Bucket: bucket, 885 Object: object, 886 VersionID: vid, 887 } 888 } 889 if !versioned && !versionSuspended { 890 return opts, InvalidArgument{ 891 Bucket: bucket, 892 Object: object, 893 Err: fmt.Errorf("version-id specified %s but versioning is not enabled on %s", opts.VersionID, bucket), 894 } 895 } 896 } 897 return ObjectOptions{ 898 Versioned: versioned, 899 VersionSuspended: versionSuspended, 900 VersionID: vid, 901 }, nil 902 } 903 904 // set ObjectOptions for PUT call to restore temporary copy of transitioned data 905 func putRestoreOpts(bucket, object string, rreq *RestoreObjectRequest, objInfo ObjectInfo) (putOpts ObjectOptions) { 906 meta := make(map[string]string) 907 sc := rreq.OutputLocation.S3.StorageClass 908 if sc == "" { 909 sc = objInfo.StorageClass 910 } 911 meta[strings.ToLower(xhttp.AmzStorageClass)] = sc 912 913 if rreq.Type == SelectRestoreRequest { 914 for _, v := range rreq.OutputLocation.S3.UserMetadata { 915 if !stringsHasPrefixFold(v.Name, "x-amz-meta") { 916 meta["x-amz-meta-"+v.Name] = v.Value 917 continue 918 } 919 meta[v.Name] = v.Value 920 } 921 if tags := rreq.OutputLocation.S3.Tagging.String(); tags != "" { 922 meta[xhttp.AmzObjectTagging] = tags 923 } 924 if rreq.OutputLocation.S3.Encryption.EncryptionType != "" { 925 meta[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionAES 926 } 927 return ObjectOptions{ 928 Versioned: globalBucketVersioningSys.PrefixEnabled(bucket, object), 929 VersionSuspended: globalBucketVersioningSys.PrefixSuspended(bucket, object), 930 UserDefined: meta, 931 } 932 } 933 for k, v := range objInfo.UserDefined { 934 meta[k] = v 935 } 936 if len(objInfo.UserTags) != 0 { 937 meta[xhttp.AmzObjectTagging] = objInfo.UserTags 938 } 939 // Set restore object status 940 restoreExpiry := lifecycle.ExpectedExpiryTime(time.Now().UTC(), rreq.Days) 941 meta[xhttp.AmzRestore] = completedRestoreObj(restoreExpiry).String() 942 return ObjectOptions{ 943 Versioned: globalBucketVersioningSys.PrefixEnabled(bucket, object), 944 VersionSuspended: globalBucketVersioningSys.PrefixSuspended(bucket, object), 945 UserDefined: meta, 946 VersionID: objInfo.VersionID, 947 MTime: objInfo.ModTime, 948 Expires: objInfo.Expires, 949 } 950 } 951 952 var errRestoreHDRMalformed = fmt.Errorf("x-amz-restore header malformed") 953 954 // IsRemote returns true if this object version's contents are in its remote 955 // tier. 956 func (fi FileInfo) IsRemote() bool { 957 if fi.TransitionStatus != lifecycle.TransitionComplete { 958 return false 959 } 960 return !isRestoredObjectOnDisk(fi.Metadata) 961 } 962 963 // IsRemote returns true if this object version's contents are in its remote 964 // tier. 965 func (oi ObjectInfo) IsRemote() bool { 966 if oi.TransitionedObject.Status != lifecycle.TransitionComplete { 967 return false 968 } 969 return !isRestoredObjectOnDisk(oi.UserDefined) 970 } 971 972 // restoreObjStatus represents a restore-object's status. It can be either 973 // ongoing or completed. 974 type restoreObjStatus struct { 975 ongoing bool 976 expiry time.Time 977 } 978 979 // ongoingRestoreObj constructs restoreObjStatus for an ongoing restore-object. 980 func ongoingRestoreObj() restoreObjStatus { 981 return restoreObjStatus{ 982 ongoing: true, 983 } 984 } 985 986 // completeRestoreObj constructs restoreObjStatus for a completed restore-object with given expiry. 987 func completedRestoreObj(expiry time.Time) restoreObjStatus { 988 return restoreObjStatus{ 989 ongoing: false, 990 expiry: expiry.UTC(), 991 } 992 } 993 994 // String returns x-amz-restore compatible representation of r. 995 func (r restoreObjStatus) String() string { 996 if r.Ongoing() { 997 return `ongoing-request="true"` 998 } 999 return fmt.Sprintf(`ongoing-request="false", expiry-date="%s"`, r.expiry.Format(http.TimeFormat)) 1000 } 1001 1002 // Expiry returns expiry of restored object and true if restore-object has completed. 1003 // Otherwise returns zero value of time.Time and false. 1004 func (r restoreObjStatus) Expiry() (time.Time, bool) { 1005 if r.Ongoing() { 1006 return time.Time{}, false 1007 } 1008 return r.expiry, true 1009 } 1010 1011 // Ongoing returns true if restore-object is ongoing. 1012 func (r restoreObjStatus) Ongoing() bool { 1013 return r.ongoing 1014 } 1015 1016 // OnDisk returns true if restored object contents exist in MinIO. Otherwise returns false. 1017 // The restore operation could be in one of the following states, 1018 // - in progress (no content on MinIO's disks yet) 1019 // - completed 1020 // - completed but expired (again, no content on MinIO's disks) 1021 func (r restoreObjStatus) OnDisk() bool { 1022 if expiry, ok := r.Expiry(); ok && time.Now().UTC().Before(expiry) { 1023 // completed 1024 return true 1025 } 1026 return false // in progress or completed but expired 1027 } 1028 1029 // parseRestoreObjStatus parses restoreHdr from AmzRestore header. If the value is valid it returns a 1030 // restoreObjStatus value with the status and expiry (if any). Otherwise returns 1031 // the empty value and an error indicating the parse failure. 1032 func parseRestoreObjStatus(restoreHdr string) (restoreObjStatus, error) { 1033 tokens := strings.SplitN(restoreHdr, ",", 2) 1034 progressTokens := strings.SplitN(tokens[0], "=", 2) 1035 if len(progressTokens) != 2 { 1036 return restoreObjStatus{}, errRestoreHDRMalformed 1037 } 1038 if strings.TrimSpace(progressTokens[0]) != "ongoing-request" { 1039 return restoreObjStatus{}, errRestoreHDRMalformed 1040 } 1041 1042 switch progressTokens[1] { 1043 case "true", `"true"`: // true without double quotes is deprecated in Feb 2022 1044 if len(tokens) == 1 { 1045 return ongoingRestoreObj(), nil 1046 } 1047 case "false", `"false"`: // false without double quotes is deprecated in Feb 2022 1048 if len(tokens) != 2 { 1049 return restoreObjStatus{}, errRestoreHDRMalformed 1050 } 1051 expiryTokens := strings.SplitN(tokens[1], "=", 2) 1052 if len(expiryTokens) != 2 { 1053 return restoreObjStatus{}, errRestoreHDRMalformed 1054 } 1055 if strings.TrimSpace(expiryTokens[0]) != "expiry-date" { 1056 return restoreObjStatus{}, errRestoreHDRMalformed 1057 } 1058 expiry, err := amztime.ParseHeader(strings.Trim(expiryTokens[1], `"`)) 1059 if err != nil { 1060 return restoreObjStatus{}, errRestoreHDRMalformed 1061 } 1062 return completedRestoreObj(expiry), nil 1063 } 1064 return restoreObjStatus{}, errRestoreHDRMalformed 1065 } 1066 1067 // isRestoredObjectOnDisk returns true if the restored object is on disk. Note 1068 // this function must be called only if object version's transition status is 1069 // complete. 1070 func isRestoredObjectOnDisk(meta map[string]string) (onDisk bool) { 1071 if restoreHdr, ok := meta[xhttp.AmzRestore]; ok { 1072 if restoreStatus, err := parseRestoreObjStatus(restoreHdr); err == nil { 1073 return restoreStatus.OnDisk() 1074 } 1075 } 1076 return onDisk 1077 } 1078 1079 // ToLifecycleOpts returns lifecycle.ObjectOpts value for oi. 1080 func (oi ObjectInfo) ToLifecycleOpts() lifecycle.ObjectOpts { 1081 return lifecycle.ObjectOpts{ 1082 Name: oi.Name, 1083 UserTags: oi.UserTags, 1084 VersionID: oi.VersionID, 1085 ModTime: oi.ModTime, 1086 Size: oi.Size, 1087 IsLatest: oi.IsLatest, 1088 NumVersions: oi.NumVersions, 1089 DeleteMarker: oi.DeleteMarker, 1090 SuccessorModTime: oi.SuccessorModTime, 1091 RestoreOngoing: oi.RestoreOngoing, 1092 RestoreExpires: oi.RestoreExpires, 1093 TransitionStatus: oi.TransitionedObject.Status, 1094 } 1095 }