storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/pkg/bucket/lifecycle/lifecycle.go (about)

     1  /*
     2   * MinIO Cloud Storage, (C) 2019 MinIO, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package lifecycle
    18  
    19  import (
    20  	"encoding/xml"
    21  	"fmt"
    22  	"io"
    23  	"strings"
    24  	"time"
    25  )
    26  
    27  var (
    28  	errLifecycleTooManyRules = Errorf("Lifecycle configuration allows a maximum of 1000 rules")
    29  	errLifecycleNoRule       = Errorf("Lifecycle configuration should have at least one rule")
    30  	errLifecycleDuplicateID  = Errorf("Lifecycle configuration has rule with the same ID. Rule ID must be unique.")
    31  	errXMLNotWellFormed      = Errorf("The XML you provided was not well-formed or did not validate against our published schema")
    32  )
    33  
    34  const (
    35  	// TransitionComplete marks completed transition
    36  	TransitionComplete = "complete"
    37  	// TransitionPending - transition is yet to be attempted
    38  	TransitionPending = "pending"
    39  )
    40  
    41  // Action represents a delete action or other transition
    42  // actions that will be implemented later.
    43  type Action int
    44  
    45  //go:generate stringer -type Action $GOFILE
    46  
    47  const (
    48  	// NoneAction means no action required after evaluating lifecycle rules
    49  	NoneAction Action = iota
    50  	// DeleteAction means the object needs to be removed after evaluating lifecycle rules
    51  	DeleteAction
    52  	// DeleteVersionAction deletes a particular version
    53  	DeleteVersionAction
    54  	// TransitionAction transitions a particular object after evaluating lifecycle transition rules
    55  	TransitionAction
    56  	//TransitionVersionAction transitions a particular object version after evaluating lifecycle transition rules
    57  	TransitionVersionAction
    58  	// DeleteRestoredAction means the temporarily restored object needs to be removed after evaluating lifecycle rules
    59  	DeleteRestoredAction
    60  	// DeleteRestoredVersionAction deletes a particular version that was temporarily restored
    61  	DeleteRestoredVersionAction
    62  )
    63  
    64  // Lifecycle - Configuration for bucket lifecycle.
    65  type Lifecycle struct {
    66  	XMLName xml.Name `xml:"LifecycleConfiguration"`
    67  	Rules   []Rule   `xml:"Rule"`
    68  }
    69  
    70  // UnmarshalXML - decodes XML data.
    71  func (lc *Lifecycle) UnmarshalXML(d *xml.Decoder, start xml.StartElement) (err error) {
    72  	switch start.Name.Local {
    73  	case "LifecycleConfiguration", "BucketLifecycleConfiguration":
    74  	default:
    75  		return xml.UnmarshalError(fmt.Sprintf("expected element type <LifecycleConfiguration>/<BucketLifecycleConfiguration> but have <%s>",
    76  			start.Name.Local))
    77  	}
    78  	for {
    79  		// Read tokens from the XML document in a stream.
    80  		t, err := d.Token()
    81  		if err != nil {
    82  			if err == io.EOF {
    83  				break
    84  			}
    85  			return err
    86  		}
    87  
    88  		switch se := t.(type) {
    89  		case xml.StartElement:
    90  			switch se.Name.Local {
    91  			case "Rule":
    92  				var r Rule
    93  				if err = d.DecodeElement(&r, &se); err != nil {
    94  					return err
    95  				}
    96  				lc.Rules = append(lc.Rules, r)
    97  			default:
    98  				return xml.UnmarshalError(fmt.Sprintf("expected element type <Rule> but have <%s>", se.Name.Local))
    99  			}
   100  		}
   101  	}
   102  	return nil
   103  }
   104  
   105  // HasActiveRules - returns whether policy has active rules for.
   106  // Optionally a prefix can be supplied.
   107  // If recursive is specified the function will also return true if any level below the
   108  // prefix has active rules. If no prefix is specified recursive is effectively true.
   109  func (lc Lifecycle) HasActiveRules(prefix string, recursive bool) bool {
   110  	if len(lc.Rules) == 0 {
   111  		return false
   112  	}
   113  	for _, rule := range lc.Rules {
   114  		if rule.Status == Disabled {
   115  			continue
   116  		}
   117  
   118  		if len(prefix) > 0 && len(rule.GetPrefix()) > 0 {
   119  			if !recursive {
   120  				// If not recursive, incoming prefix must be in rule prefix
   121  				if !strings.HasPrefix(prefix, rule.GetPrefix()) {
   122  					continue
   123  				}
   124  			}
   125  			if recursive {
   126  				// If recursive, we can skip this rule if it doesn't match the tested prefix.
   127  				if !strings.HasPrefix(prefix, rule.GetPrefix()) && !strings.HasPrefix(rule.GetPrefix(), prefix) {
   128  					continue
   129  				}
   130  			}
   131  		}
   132  
   133  		if rule.NoncurrentVersionExpiration.NoncurrentDays > 0 {
   134  			return true
   135  		}
   136  		if rule.NoncurrentVersionTransition.NoncurrentDays > 0 {
   137  			return true
   138  		}
   139  		if rule.Expiration.IsNull() && rule.Transition.IsNull() {
   140  			continue
   141  		}
   142  		if !rule.Expiration.IsDateNull() && rule.Expiration.Date.Before(time.Now()) {
   143  			return true
   144  		}
   145  		if !rule.Transition.IsDateNull() && rule.Transition.Date.Before(time.Now()) {
   146  			return true
   147  		}
   148  		if !rule.Expiration.IsDaysNull() || !rule.Transition.IsDaysNull() {
   149  			return true
   150  		}
   151  	}
   152  	return false
   153  }
   154  
   155  // ParseLifecycleConfig - parses data in given reader to Lifecycle.
   156  func ParseLifecycleConfig(reader io.Reader) (*Lifecycle, error) {
   157  	var lc Lifecycle
   158  	if err := xml.NewDecoder(reader).Decode(&lc); err != nil {
   159  		return nil, err
   160  	}
   161  	return &lc, nil
   162  }
   163  
   164  // Validate - validates the lifecycle configuration
   165  func (lc Lifecycle) Validate() error {
   166  	// Lifecycle config can't have more than 1000 rules
   167  	if len(lc.Rules) > 1000 {
   168  		return errLifecycleTooManyRules
   169  	}
   170  	// Lifecycle config should have at least one rule
   171  	if len(lc.Rules) == 0 {
   172  		return errLifecycleNoRule
   173  	}
   174  	// Validate all the rules in the lifecycle config
   175  	for _, r := range lc.Rules {
   176  		if err := r.Validate(); err != nil {
   177  			return err
   178  		}
   179  	}
   180  	// Make sure Rule ID is unique
   181  	for i := range lc.Rules {
   182  		if i == len(lc.Rules)-1 {
   183  			break
   184  		}
   185  		otherRules := lc.Rules[i+1:]
   186  		for _, otherRule := range otherRules {
   187  			if lc.Rules[i].ID == otherRule.ID {
   188  				return errLifecycleDuplicateID
   189  			}
   190  		}
   191  	}
   192  	return nil
   193  }
   194  
   195  // FilterActionableRules returns the rules actions that need to be executed
   196  // after evaluating prefix/tag filtering
   197  func (lc Lifecycle) FilterActionableRules(obj ObjectOpts) []Rule {
   198  	if obj.Name == "" {
   199  		return nil
   200  	}
   201  	var rules []Rule
   202  	for _, rule := range lc.Rules {
   203  		if rule.Status == Disabled {
   204  			continue
   205  		}
   206  		if !strings.HasPrefix(obj.Name, rule.GetPrefix()) {
   207  			continue
   208  		}
   209  		// Indicates whether MinIO will remove a delete marker with no
   210  		// noncurrent versions. If set to true, the delete marker will
   211  		// be expired; if set to false the policy takes no action. This
   212  		// cannot be specified with Days or Date in a Lifecycle
   213  		// Expiration Policy.
   214  		if rule.Expiration.DeleteMarker.val {
   215  			rules = append(rules, rule)
   216  			continue
   217  		}
   218  		// The NoncurrentVersionExpiration action requests MinIO to expire
   219  		// noncurrent versions of objects x days after the objects become
   220  		// noncurrent.
   221  		if !rule.NoncurrentVersionExpiration.IsDaysNull() {
   222  			rules = append(rules, rule)
   223  			continue
   224  		}
   225  		// The NoncurrentVersionTransition action requests MinIO to transition
   226  		// noncurrent versions of objects x days after the objects become
   227  		// noncurrent.
   228  		if !rule.NoncurrentVersionTransition.IsDaysNull() {
   229  			rules = append(rules, rule)
   230  			continue
   231  		}
   232  
   233  		if rule.Filter.TestTags(strings.Split(obj.UserTags, "&")) {
   234  			rules = append(rules, rule)
   235  		}
   236  		if !rule.Transition.IsNull() {
   237  			rules = append(rules, rule)
   238  		}
   239  	}
   240  	return rules
   241  }
   242  
   243  // ObjectOpts provides information to deduce the lifecycle actions
   244  // which can be triggered on the resultant object.
   245  type ObjectOpts struct {
   246  	Name             string
   247  	UserTags         string
   248  	ModTime          time.Time
   249  	VersionID        string
   250  	IsLatest         bool
   251  	DeleteMarker     bool
   252  	NumVersions      int
   253  	SuccessorModTime time.Time
   254  	TransitionStatus string
   255  	RestoreOngoing   bool
   256  	RestoreExpires   time.Time
   257  }
   258  
   259  // ExpiredObjectDeleteMarker returns true if an object version referred to by o
   260  // is the only version remaining and is a delete marker. It returns false
   261  // otherwise.
   262  func (o ObjectOpts) ExpiredObjectDeleteMarker() bool {
   263  	return o.DeleteMarker && o.NumVersions == 1
   264  }
   265  
   266  // ComputeAction returns the action to perform by evaluating all lifecycle rules
   267  // against the object name and its modification time.
   268  func (lc Lifecycle) ComputeAction(obj ObjectOpts) Action {
   269  	var action = NoneAction
   270  	if obj.ModTime.IsZero() {
   271  		return action
   272  	}
   273  
   274  	for _, rule := range lc.FilterActionableRules(obj) {
   275  		if obj.ExpiredObjectDeleteMarker() && rule.Expiration.DeleteMarker.val {
   276  			// Indicates whether MinIO will remove a delete marker with no noncurrent versions.
   277  			// Only latest marker is removed. If set to true, the delete marker will be expired;
   278  			// if set to false the policy takes no action. This cannot be specified with Days or
   279  			// Date in a Lifecycle Expiration Policy.
   280  			return DeleteVersionAction
   281  		}
   282  
   283  		if !rule.NoncurrentVersionExpiration.IsDaysNull() {
   284  			if obj.VersionID != "" && !obj.IsLatest && !obj.SuccessorModTime.IsZero() {
   285  				// Non current versions should be deleted if their age exceeds non current days configuration
   286  				// https://docs.aws.amazon.com/AmazonS3/latest/dev/intro-lifecycle-rules.html#intro-lifecycle-rules-actions
   287  				if time.Now().After(ExpectedExpiryTime(obj.SuccessorModTime, int(rule.NoncurrentVersionExpiration.NoncurrentDays))) {
   288  					return DeleteVersionAction
   289  				}
   290  			}
   291  
   292  			if obj.VersionID != "" && obj.ExpiredObjectDeleteMarker() {
   293  				// From https: //docs.aws.amazon.com/AmazonS3/latest/dev/lifecycle-configuration-examples.html :
   294  				//   The NoncurrentVersionExpiration action in the same Lifecycle configuration removes noncurrent objects X days
   295  				//   after they become noncurrent. Thus, in this example, all object versions are permanently removed X days after
   296  				//   object creation. You will have expired object delete markers, but Amazon S3 detects and removes the expired
   297  				//   object delete markers for you.
   298  				if time.Now().After(ExpectedExpiryTime(obj.ModTime, int(rule.NoncurrentVersionExpiration.NoncurrentDays))) {
   299  					return DeleteVersionAction
   300  				}
   301  			}
   302  		}
   303  
   304  		if !rule.NoncurrentVersionTransition.IsDaysNull() {
   305  			if obj.VersionID != "" && !obj.IsLatest && !obj.SuccessorModTime.IsZero() && !obj.DeleteMarker && obj.TransitionStatus != TransitionComplete {
   306  				// Non current versions should be deleted if their age exceeds non current days configuration
   307  				// https://docs.aws.amazon.com/AmazonS3/latest/dev/intro-lifecycle-rules.html#intro-lifecycle-rules-actions
   308  				if time.Now().After(ExpectedExpiryTime(obj.SuccessorModTime, int(rule.NoncurrentVersionTransition.NoncurrentDays))) {
   309  					return TransitionVersionAction
   310  				}
   311  			}
   312  		}
   313  
   314  		// Remove the object or simply add a delete marker (once) in a versioned bucket
   315  		if obj.VersionID == "" || obj.IsLatest && !obj.DeleteMarker {
   316  			switch {
   317  			case !rule.Expiration.IsDateNull():
   318  				if time.Now().UTC().After(rule.Expiration.Date.Time) {
   319  					return DeleteAction
   320  				}
   321  			case !rule.Expiration.IsDaysNull():
   322  				if time.Now().UTC().After(ExpectedExpiryTime(obj.ModTime, int(rule.Expiration.Days))) {
   323  					return DeleteAction
   324  				}
   325  			}
   326  
   327  			if obj.TransitionStatus != TransitionComplete {
   328  				switch {
   329  				case !rule.Transition.IsDateNull():
   330  					if time.Now().UTC().After(rule.Transition.Date.Time) {
   331  						action = TransitionAction
   332  					}
   333  				case !rule.Transition.IsDaysNull():
   334  					if time.Now().UTC().After(ExpectedExpiryTime(obj.ModTime, int(rule.Transition.Days))) {
   335  						action = TransitionAction
   336  					}
   337  				}
   338  			}
   339  			if !obj.RestoreExpires.IsZero() && time.Now().After(obj.RestoreExpires) {
   340  				if obj.VersionID != "" {
   341  					action = DeleteRestoredVersionAction
   342  				} else {
   343  					action = DeleteRestoredAction
   344  				}
   345  			}
   346  
   347  		}
   348  	}
   349  	return action
   350  }
   351  
   352  // ExpectedExpiryTime calculates the expiry, transition or restore date/time based on a object modtime.
   353  // The expected transition or restore time is always a midnight time following the the object
   354  // modification time plus the number of transition/restore days.
   355  //   e.g. If the object modtime is `Thu May 21 13:42:50 GMT 2020` and the object should
   356  //       transition in 1 day, then the expected transition time is `Fri, 23 May 2020 00:00:00 GMT`
   357  func ExpectedExpiryTime(modTime time.Time, days int) time.Time {
   358  	t := modTime.UTC().Add(time.Duration(days+1) * 24 * time.Hour)
   359  	return t.Truncate(24 * time.Hour)
   360  }
   361  
   362  // PredictExpiryTime returns the expiry date/time of a given object
   363  // after evaluting the current lifecycle document.
   364  func (lc Lifecycle) PredictExpiryTime(obj ObjectOpts) (string, time.Time) {
   365  	if obj.DeleteMarker {
   366  		// We don't need to send any x-amz-expiration for delete marker.
   367  		return "", time.Time{}
   368  	}
   369  
   370  	var finalExpiryDate time.Time
   371  	var finalExpiryRuleID string
   372  
   373  	// Iterate over all actionable rules and find the earliest
   374  	// expiration date and its associated rule ID.
   375  	for _, rule := range lc.FilterActionableRules(obj) {
   376  		if !rule.NoncurrentVersionExpiration.IsDaysNull() && !obj.IsLatest && obj.VersionID != "" {
   377  			return rule.ID, ExpectedExpiryTime(obj.SuccessorModTime, int(rule.NoncurrentVersionExpiration.NoncurrentDays))
   378  		}
   379  
   380  		if !rule.Expiration.IsDateNull() {
   381  			if finalExpiryDate.IsZero() || finalExpiryDate.After(rule.Expiration.Date.Time) {
   382  				finalExpiryRuleID = rule.ID
   383  				finalExpiryDate = rule.Expiration.Date.Time
   384  			}
   385  		}
   386  		if !rule.Expiration.IsDaysNull() {
   387  			expectedExpiry := ExpectedExpiryTime(obj.ModTime, int(rule.Expiration.Days))
   388  			if finalExpiryDate.IsZero() || finalExpiryDate.After(expectedExpiry) {
   389  				finalExpiryRuleID = rule.ID
   390  				finalExpiryDate = expectedExpiry
   391  			}
   392  		}
   393  	}
   394  	return finalExpiryRuleID, finalExpiryDate
   395  }