github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/bucket/lifecycle/lifecycle.go (about)

     1  // Copyright (c) 2015-2021 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 lifecycle
    19  
    20  import (
    21  	"encoding/xml"
    22  	"fmt"
    23  	"io"
    24  	"net/http"
    25  	"sort"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/google/uuid"
    30  	xhttp "github.com/minio/minio/internal/http"
    31  )
    32  
    33  var (
    34  	errLifecycleTooManyRules = Errorf("Lifecycle configuration allows a maximum of 1000 rules")
    35  	errLifecycleNoRule       = Errorf("Lifecycle configuration should have at least one rule")
    36  	errLifecycleDuplicateID  = Errorf("Rule ID must be unique. Found same ID for more than one rule")
    37  	errXMLNotWellFormed      = Errorf("The XML you provided was not well-formed or did not validate against our published schema")
    38  )
    39  
    40  const (
    41  	// TransitionComplete marks completed transition
    42  	TransitionComplete = "complete"
    43  	// TransitionPending - transition is yet to be attempted
    44  	TransitionPending = "pending"
    45  )
    46  
    47  // Action represents a delete action or other transition
    48  // actions that will be implemented later.
    49  type Action int
    50  
    51  //go:generate stringer -type Action $GOFILE
    52  
    53  const (
    54  	// NoneAction means no action required after evaluating lifecycle rules
    55  	NoneAction Action = iota
    56  	// DeleteAction means the object needs to be removed after evaluating lifecycle rules
    57  	DeleteAction
    58  	// DeleteVersionAction deletes a particular version
    59  	DeleteVersionAction
    60  	// TransitionAction transitions a particular object after evaluating lifecycle transition rules
    61  	TransitionAction
    62  	// TransitionVersionAction transitions a particular object version after evaluating lifecycle transition rules
    63  	TransitionVersionAction
    64  	// DeleteRestoredAction means the temporarily restored object needs to be removed after evaluating lifecycle rules
    65  	DeleteRestoredAction
    66  	// DeleteRestoredVersionAction deletes a particular version that was temporarily restored
    67  	DeleteRestoredVersionAction
    68  	// DeleteAllVersionsAction deletes all versions when an object expires
    69  	DeleteAllVersionsAction
    70  
    71  	// ActionCount must be the last action and shouldn't be used as a regular action.
    72  	ActionCount
    73  )
    74  
    75  // DeleteRestored - Returns true if action demands delete on restored objects
    76  func (a Action) DeleteRestored() bool {
    77  	return a == DeleteRestoredAction || a == DeleteRestoredVersionAction
    78  }
    79  
    80  // DeleteVersioned - Returns true if action demands delete on a versioned object
    81  func (a Action) DeleteVersioned() bool {
    82  	return a == DeleteVersionAction || a == DeleteRestoredVersionAction
    83  }
    84  
    85  // DeleteAll - Returns true if the action demands deleting all versions of an object
    86  func (a Action) DeleteAll() bool {
    87  	return a == DeleteAllVersionsAction
    88  }
    89  
    90  // Delete - Returns true if action demands delete on all objects (including restored)
    91  func (a Action) Delete() bool {
    92  	if a.DeleteRestored() {
    93  		return true
    94  	}
    95  	return a == DeleteVersionAction || a == DeleteAction || a == DeleteAllVersionsAction
    96  }
    97  
    98  // Lifecycle - Configuration for bucket lifecycle.
    99  type Lifecycle struct {
   100  	XMLName         xml.Name   `xml:"LifecycleConfiguration"`
   101  	Rules           []Rule     `xml:"Rule"`
   102  	ExpiryUpdatedAt *time.Time `xml:"ExpiryUpdatedAt,omitempty"`
   103  }
   104  
   105  // HasTransition returns 'true' if lifecycle document has Transition enabled.
   106  func (lc Lifecycle) HasTransition() bool {
   107  	for _, rule := range lc.Rules {
   108  		if rule.Transition.IsEnabled() {
   109  			return true
   110  		}
   111  	}
   112  	return false
   113  }
   114  
   115  // HasExpiry returns 'true' if lifecycle document has Expiry enabled.
   116  func (lc Lifecycle) HasExpiry() bool {
   117  	for _, rule := range lc.Rules {
   118  		if !rule.Expiration.IsNull() || !rule.NoncurrentVersionExpiration.IsNull() {
   119  			return true
   120  		}
   121  	}
   122  	return false
   123  }
   124  
   125  // UnmarshalXML - decodes XML data.
   126  func (lc *Lifecycle) UnmarshalXML(d *xml.Decoder, start xml.StartElement) (err error) {
   127  	switch start.Name.Local {
   128  	case "LifecycleConfiguration", "BucketLifecycleConfiguration":
   129  	default:
   130  		return xml.UnmarshalError(fmt.Sprintf("expected element type <LifecycleConfiguration>/<BucketLifecycleConfiguration> but have <%s>",
   131  			start.Name.Local))
   132  	}
   133  	for {
   134  		// Read tokens from the XML document in a stream.
   135  		t, err := d.Token()
   136  		if err != nil {
   137  			if err == io.EOF {
   138  				break
   139  			}
   140  			return err
   141  		}
   142  
   143  		if se, ok := t.(xml.StartElement); ok {
   144  			switch se.Name.Local {
   145  			case "Rule":
   146  				var r Rule
   147  				if err = d.DecodeElement(&r, &se); err != nil {
   148  					return err
   149  				}
   150  				lc.Rules = append(lc.Rules, r)
   151  			case "ExpiryUpdatedAt":
   152  				var t time.Time
   153  				if err = d.DecodeElement(&t, &start); err != nil {
   154  					return err
   155  				}
   156  				lc.ExpiryUpdatedAt = &t
   157  			default:
   158  				return xml.UnmarshalError(fmt.Sprintf("expected element type <Rule> but have <%s>", se.Name.Local))
   159  			}
   160  		}
   161  	}
   162  	return nil
   163  }
   164  
   165  // HasActiveRules - returns whether lc has active rules at any level below or at prefix.
   166  func (lc Lifecycle) HasActiveRules(prefix string) bool {
   167  	if len(lc.Rules) == 0 {
   168  		return false
   169  	}
   170  	for _, rule := range lc.Rules {
   171  		if rule.Status == Disabled {
   172  			continue
   173  		}
   174  
   175  		if len(prefix) > 0 && len(rule.GetPrefix()) > 0 {
   176  			// we can skip this rule if it doesn't match the tested
   177  			// prefix.
   178  			if !strings.HasPrefix(prefix, rule.GetPrefix()) && !strings.HasPrefix(rule.GetPrefix(), prefix) {
   179  				continue
   180  			}
   181  		}
   182  
   183  		if rule.NoncurrentVersionExpiration.NoncurrentDays > 0 {
   184  			return true
   185  		}
   186  		if rule.NoncurrentVersionExpiration.NewerNoncurrentVersions > 0 {
   187  			return true
   188  		}
   189  		if !rule.NoncurrentVersionTransition.IsNull() {
   190  			return true
   191  		}
   192  		if !rule.Expiration.IsDateNull() && rule.Expiration.Date.Before(time.Now().UTC()) {
   193  			return true
   194  		}
   195  		if !rule.Expiration.IsDaysNull() {
   196  			return true
   197  		}
   198  		if rule.Expiration.DeleteMarker.val {
   199  			return true
   200  		}
   201  		if !rule.Transition.IsDateNull() && rule.Transition.Date.Before(time.Now().UTC()) {
   202  			return true
   203  		}
   204  		if !rule.Transition.IsNull() { // this allows for Transition.Days to be zero.
   205  			return true
   206  		}
   207  
   208  	}
   209  	return false
   210  }
   211  
   212  // ParseLifecycleConfigWithID - parses for a Lifecycle config and assigns
   213  // unique id to rules with empty ID.
   214  func ParseLifecycleConfigWithID(r io.Reader) (*Lifecycle, error) {
   215  	var lc Lifecycle
   216  	if err := xml.NewDecoder(r).Decode(&lc); err != nil {
   217  		return nil, err
   218  	}
   219  	// assign a unique id for rules with empty ID
   220  	for i := range lc.Rules {
   221  		if lc.Rules[i].ID == "" {
   222  			lc.Rules[i].ID = uuid.New().String()
   223  		}
   224  	}
   225  	return &lc, nil
   226  }
   227  
   228  // ParseLifecycleConfig - parses data in given reader to Lifecycle.
   229  func ParseLifecycleConfig(reader io.Reader) (*Lifecycle, error) {
   230  	var lc Lifecycle
   231  	if err := xml.NewDecoder(reader).Decode(&lc); err != nil {
   232  		return nil, err
   233  	}
   234  	return &lc, nil
   235  }
   236  
   237  // Validate - validates the lifecycle configuration
   238  func (lc Lifecycle) Validate() error {
   239  	// Lifecycle config can't have more than 1000 rules
   240  	if len(lc.Rules) > 1000 {
   241  		return errLifecycleTooManyRules
   242  	}
   243  	// Lifecycle config should have at least one rule
   244  	if len(lc.Rules) == 0 {
   245  		return errLifecycleNoRule
   246  	}
   247  
   248  	// Validate all the rules in the lifecycle config
   249  	for _, r := range lc.Rules {
   250  		if err := r.Validate(); err != nil {
   251  			return err
   252  		}
   253  	}
   254  	// Make sure Rule ID is unique
   255  	for i := range lc.Rules {
   256  		if i == len(lc.Rules)-1 {
   257  			break
   258  		}
   259  		otherRules := lc.Rules[i+1:]
   260  		for _, otherRule := range otherRules {
   261  			if lc.Rules[i].ID == otherRule.ID {
   262  				return errLifecycleDuplicateID
   263  			}
   264  		}
   265  	}
   266  	return nil
   267  }
   268  
   269  // FilterRules returns the rules filtered by the status, prefix and tags
   270  func (lc Lifecycle) FilterRules(obj ObjectOpts) []Rule {
   271  	if obj.Name == "" {
   272  		return nil
   273  	}
   274  	var rules []Rule
   275  	for _, rule := range lc.Rules {
   276  		if rule.Status == Disabled {
   277  			continue
   278  		}
   279  		if !strings.HasPrefix(obj.Name, rule.GetPrefix()) {
   280  			continue
   281  		}
   282  		if !obj.DeleteMarker && !rule.Filter.TestTags(obj.UserTags) {
   283  			continue
   284  		}
   285  		if !obj.DeleteMarker && !rule.Filter.BySize(obj.Size) {
   286  			continue
   287  		}
   288  		rules = append(rules, rule)
   289  	}
   290  	return rules
   291  }
   292  
   293  // ObjectOpts provides information to deduce the lifecycle actions
   294  // which can be triggered on the resultant object.
   295  type ObjectOpts struct {
   296  	Name             string
   297  	UserTags         string
   298  	ModTime          time.Time
   299  	Size             int64
   300  	VersionID        string
   301  	IsLatest         bool
   302  	DeleteMarker     bool
   303  	NumVersions      int
   304  	SuccessorModTime time.Time
   305  	TransitionStatus string
   306  	RestoreOngoing   bool
   307  	RestoreExpires   time.Time
   308  }
   309  
   310  // ExpiredObjectDeleteMarker returns true if an object version referred to by o
   311  // is the only version remaining and is a delete marker. It returns false
   312  // otherwise.
   313  func (o ObjectOpts) ExpiredObjectDeleteMarker() bool {
   314  	return o.DeleteMarker && o.NumVersions == 1
   315  }
   316  
   317  // Event contains a lifecycle action with associated info
   318  type Event struct {
   319  	Action                  Action
   320  	RuleID                  string
   321  	Due                     time.Time
   322  	NoncurrentDays          int
   323  	NewerNoncurrentVersions int
   324  	StorageClass            string
   325  }
   326  
   327  // Eval returns the lifecycle event applicable now.
   328  func (lc Lifecycle) Eval(obj ObjectOpts) Event {
   329  	return lc.eval(obj, time.Now().UTC())
   330  }
   331  
   332  // eval returns the lifecycle event applicable at the given now. If now is the
   333  // zero value of time.Time, it returns the upcoming lifecycle event.
   334  func (lc Lifecycle) eval(obj ObjectOpts, now time.Time) Event {
   335  	var events []Event
   336  	if obj.ModTime.IsZero() {
   337  		return Event{}
   338  	}
   339  
   340  	// Handle expiry of restored object; NB Restored Objects have expiry set on
   341  	// them as part of RestoreObject API. They aren't governed by lifecycle
   342  	// rules.
   343  	if !obj.RestoreExpires.IsZero() && now.After(obj.RestoreExpires) {
   344  		action := DeleteRestoredAction
   345  		if !obj.IsLatest {
   346  			action = DeleteRestoredVersionAction
   347  		}
   348  
   349  		events = append(events, Event{
   350  			Action: action,
   351  			Due:    now,
   352  		})
   353  	}
   354  
   355  	for _, rule := range lc.FilterRules(obj) {
   356  		if obj.IsLatest && rule.Expiration.DeleteAll.val {
   357  			if !rule.Expiration.IsDaysNull() {
   358  				// Specifying the Days tag will automatically perform all versions cleanup
   359  				// once the latest object is old enough to satisfy the age criteria.
   360  				// This is a MinIO only extension.
   361  				if expectedExpiry := ExpectedExpiryTime(obj.ModTime, int(rule.Expiration.Days)); now.IsZero() || now.After(expectedExpiry) {
   362  					events = append(events, Event{
   363  						Action: DeleteAllVersionsAction,
   364  						RuleID: rule.ID,
   365  						Due:    expectedExpiry,
   366  					})
   367  					// No other conflicting actions apply to an all version expired object.
   368  					break
   369  				}
   370  			}
   371  		}
   372  
   373  		if obj.ExpiredObjectDeleteMarker() {
   374  			if rule.Expiration.DeleteMarker.val {
   375  				// Indicates whether MinIO will remove a delete marker with no noncurrent versions.
   376  				// Only latest marker is removed. If set to true, the delete marker will be expired;
   377  				// if set to false the policy takes no action. This cannot be specified with Days or
   378  				// Date in a Lifecycle Expiration Policy.
   379  				events = append(events, Event{
   380  					Action: DeleteVersionAction,
   381  					RuleID: rule.ID,
   382  					Due:    now,
   383  				})
   384  				// No other conflicting actions apply to an expired object delete marker
   385  				break
   386  			}
   387  
   388  			if !rule.Expiration.IsDaysNull() {
   389  				// Specifying the Days tag will automatically perform ExpiredObjectDeleteMarker cleanup
   390  				// once delete markers are old enough to satisfy the age criteria.
   391  				// https://docs.aws.amazon.com/AmazonS3/latest/userguide/lifecycle-configuration-examples.html
   392  				if expectedExpiry := ExpectedExpiryTime(obj.ModTime, int(rule.Expiration.Days)); now.IsZero() || now.After(expectedExpiry) {
   393  					events = append(events, Event{
   394  						Action: DeleteVersionAction,
   395  						RuleID: rule.ID,
   396  						Due:    expectedExpiry,
   397  					})
   398  					// No other conflicting actions apply to an expired object delete marker
   399  					break
   400  				}
   401  			}
   402  		}
   403  
   404  		// Skip rules with newer noncurrent versions specified. These rules are
   405  		// not handled at an individual version level. eval applies only to a
   406  		// specific version.
   407  		if !obj.IsLatest && rule.NoncurrentVersionExpiration.NewerNoncurrentVersions > 0 {
   408  			continue
   409  		}
   410  
   411  		if !obj.IsLatest && !rule.NoncurrentVersionExpiration.IsDaysNull() {
   412  			// Non current versions should be deleted if their age exceeds non current days configuration
   413  			// https://docs.aws.amazon.com/AmazonS3/latest/dev/intro-lifecycle-rules.html#intro-lifecycle-rules-actions
   414  			if expectedExpiry := ExpectedExpiryTime(obj.SuccessorModTime, int(rule.NoncurrentVersionExpiration.NoncurrentDays)); now.IsZero() || now.After(expectedExpiry) {
   415  				events = append(events, Event{
   416  					Action: DeleteVersionAction,
   417  					RuleID: rule.ID,
   418  					Due:    expectedExpiry,
   419  				})
   420  			}
   421  		}
   422  
   423  		if !obj.IsLatest && !rule.NoncurrentVersionTransition.IsNull() {
   424  			if !obj.DeleteMarker && obj.TransitionStatus != TransitionComplete {
   425  				// Non current versions should be transitioned if their age exceeds non current days configuration
   426  				// https://docs.aws.amazon.com/AmazonS3/latest/dev/intro-lifecycle-rules.html#intro-lifecycle-rules-actions
   427  				if due, ok := rule.NoncurrentVersionTransition.NextDue(obj); ok && (now.IsZero() || now.After(due)) {
   428  					events = append(events, Event{
   429  						Action:       TransitionVersionAction,
   430  						RuleID:       rule.ID,
   431  						Due:          due,
   432  						StorageClass: rule.NoncurrentVersionTransition.StorageClass,
   433  					})
   434  				}
   435  			}
   436  		}
   437  
   438  		// Remove the object or simply add a delete marker (once) in a versioned bucket
   439  		if obj.IsLatest && !obj.DeleteMarker {
   440  			switch {
   441  			case !rule.Expiration.IsDateNull():
   442  				if now.IsZero() || now.After(rule.Expiration.Date.Time) {
   443  					events = append(events, Event{
   444  						Action: DeleteAction,
   445  						RuleID: rule.ID,
   446  						Due:    rule.Expiration.Date.Time,
   447  					})
   448  				}
   449  			case !rule.Expiration.IsDaysNull():
   450  				if expectedExpiry := ExpectedExpiryTime(obj.ModTime, int(rule.Expiration.Days)); now.IsZero() || now.After(expectedExpiry) {
   451  					events = append(events, Event{
   452  						Action: DeleteAction,
   453  						RuleID: rule.ID,
   454  						Due:    expectedExpiry,
   455  					})
   456  				}
   457  			}
   458  
   459  			if obj.TransitionStatus != TransitionComplete {
   460  				if due, ok := rule.Transition.NextDue(obj); ok && (now.IsZero() || now.After(due)) {
   461  					events = append(events, Event{
   462  						Action:       TransitionAction,
   463  						RuleID:       rule.ID,
   464  						Due:          due,
   465  						StorageClass: rule.Transition.StorageClass,
   466  					})
   467  				}
   468  			}
   469  		}
   470  	}
   471  
   472  	if len(events) > 0 {
   473  		sort.Slice(events, func(i, j int) bool {
   474  			// Prefer Expiration over Transition for both current
   475  			// and noncurrent versions when,
   476  			// - now is past the expected time to action
   477  			// - expected time to action is the same for both actions
   478  			if now.After(events[i].Due) && now.After(events[j].Due) || events[i].Due.Equal(events[j].Due) {
   479  				switch events[i].Action {
   480  				case DeleteAction, DeleteVersionAction:
   481  					return true
   482  				}
   483  				switch events[j].Action {
   484  				case DeleteAction, DeleteVersionAction:
   485  					return false
   486  				}
   487  				return true
   488  			}
   489  
   490  			// Prefer earlier occurring event
   491  			return events[i].Due.Before(events[j].Due)
   492  		})
   493  		return events[0]
   494  	}
   495  
   496  	return Event{
   497  		Action: NoneAction,
   498  	}
   499  }
   500  
   501  // ExpectedExpiryTime calculates the expiry, transition or restore date/time based on a object modtime.
   502  // The expected transition or restore time is always a midnight time following the object
   503  // modification time plus the number of transition/restore days.
   504  //
   505  //	e.g. If the object modtime is `Thu May 21 13:42:50 GMT 2020` and the object should
   506  //	    transition in 1 day, then the expected transition time is `Fri, 23 May 2020 00:00:00 GMT`
   507  func ExpectedExpiryTime(modTime time.Time, days int) time.Time {
   508  	if days == 0 {
   509  		return modTime
   510  	}
   511  	t := modTime.UTC().Add(time.Duration(days+1) * 24 * time.Hour)
   512  	return t.Truncate(24 * time.Hour)
   513  }
   514  
   515  // SetPredictionHeaders sets time to expiry and transition headers on w for a
   516  // given obj.
   517  func (lc Lifecycle) SetPredictionHeaders(w http.ResponseWriter, obj ObjectOpts) {
   518  	event := lc.eval(obj, time.Time{})
   519  	switch event.Action {
   520  	case DeleteAction, DeleteVersionAction, DeleteAllVersionsAction:
   521  		w.Header()[xhttp.AmzExpiration] = []string{
   522  			fmt.Sprintf(`expiry-date="%s", rule-id="%s"`, event.Due.Format(http.TimeFormat), event.RuleID),
   523  		}
   524  	case TransitionAction, TransitionVersionAction:
   525  		w.Header()[xhttp.MinIOTransition] = []string{
   526  			fmt.Sprintf(`transition-date="%s", rule-id="%s"`, event.Due.Format(http.TimeFormat), event.RuleID),
   527  		}
   528  	}
   529  }
   530  
   531  // NoncurrentVersionsExpirationLimit returns the number of noncurrent versions
   532  // to be retained from the first applicable rule per S3 behavior.
   533  func (lc Lifecycle) NoncurrentVersionsExpirationLimit(obj ObjectOpts) Event {
   534  	for _, rule := range lc.FilterRules(obj) {
   535  		if rule.NoncurrentVersionExpiration.NewerNoncurrentVersions == 0 {
   536  			continue
   537  		}
   538  		return Event{
   539  			Action:                  DeleteVersionAction,
   540  			RuleID:                  rule.ID,
   541  			NoncurrentDays:          int(rule.NoncurrentVersionExpiration.NoncurrentDays),
   542  			NewerNoncurrentVersions: rule.NoncurrentVersionExpiration.NewerNoncurrentVersions,
   543  		}
   544  	}
   545  	return Event{}
   546  }