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  }